[CLD-7421][CLD-7420] Deprecate Self Serve: First Pass (#26668)
* Deprecate Self Serve: First Pass * Fix ci * Fix more ci * Remmove outdated server tests * Fix a missed spot opening purchase modal in Self Hosted * Fix i18n * Clean up some more server code, fix webapp test * Fix alignment of button * Fix linter * Fix i18n server side * Add back translation * Remove client functions * Put back client functions --------- Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
@@ -45,8 +45,6 @@ import BillingHistory, {searchableStrings as billingHistorySearchableStrings} fr
|
||||
import BillingSubscriptions, {searchableStrings as billingSubscriptionSearchableStrings} from './billing/billing_subscriptions';
|
||||
import CompanyInfo, {searchableStrings as billingCompanyInfoSearchableStrings} from './billing/company_info';
|
||||
import CompanyInfoEdit from './billing/company_info_edit';
|
||||
import PaymentInfo, {searchableStrings as billingPaymentInfoSearchableStrings} from './billing/payment_info';
|
||||
import PaymentInfoEdit from './billing/payment_info_edit';
|
||||
import BleveSettings, {searchableStrings as bleveSearchableStrings} from './bleve_settings';
|
||||
import BrandImageSetting from './brand_image_setting/brand_image_setting';
|
||||
import ClusterSettings, {searchableStrings as clusterSearchableStrings} from './cluster_settings';
|
||||
@@ -385,30 +383,6 @@ const AdminDefinition: AdminDefinitionType = {
|
||||
isHidden: it.not(it.licensedForFeature('Cloud')),
|
||||
isDisabled: it.not(it.userHasWritePermissionOnResource('billing')),
|
||||
},
|
||||
payment_info: {
|
||||
url: 'billing/payment_info',
|
||||
title: defineMessage({id: 'admin.sidebar.payment_info', defaultMessage: 'Payment Information'}),
|
||||
isHidden: it.any(
|
||||
it.hidePaymentInfo,
|
||||
|
||||
// cloud only view
|
||||
it.not(it.licensedForFeature('Cloud')),
|
||||
),
|
||||
searchableStrings: billingPaymentInfoSearchableStrings,
|
||||
schema: {
|
||||
id: 'PaymentInfo',
|
||||
component: PaymentInfo,
|
||||
},
|
||||
isDisabled: it.not(it.userHasWritePermissionOnResource('billing')),
|
||||
},
|
||||
payment_info_edit: {
|
||||
url: 'billing/payment_info_edit',
|
||||
schema: {
|
||||
id: 'PaymentInfoEdit',
|
||||
component: PaymentInfoEdit,
|
||||
},
|
||||
isDisabled: it.not(it.userHasWritePermissionOnResource('billing')),
|
||||
},
|
||||
},
|
||||
},
|
||||
reporting: {
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {unixTimestampFromNow} from 'tests/helpers/date';
|
||||
import {renderWithContext} from 'tests/react_testing_utils';
|
||||
import {CloudProducts} from 'utils/constants';
|
||||
|
||||
import {CloudAnnualRenewalBanner} from './billing_subscriptions';
|
||||
|
||||
describe('CloudAnnualRenewalBanner', () => {
|
||||
const initialState = {
|
||||
entities: {
|
||||
general: {
|
||||
license: {
|
||||
IsLicensed: 'true',
|
||||
Cloud: 'true',
|
||||
},
|
||||
},
|
||||
users: {
|
||||
currentUserId: 'current_user_id',
|
||||
profiles: {
|
||||
current_user_id: {roles: 'system_admin'},
|
||||
},
|
||||
},
|
||||
cloud: {
|
||||
subscription: {
|
||||
product_id: 'test_prod_1',
|
||||
trial_end_at: 1652807380,
|
||||
is_free_trial: 'false',
|
||||
cancel_at: 1652807380,
|
||||
},
|
||||
products: {
|
||||
test_prod_1: {
|
||||
id: 'test_prod_1',
|
||||
sku: CloudProducts.STARTER,
|
||||
price_per_seat: 0,
|
||||
},
|
||||
test_prod_2: {
|
||||
id: 'test_prod_2',
|
||||
sku: CloudProducts.ENTERPRISE,
|
||||
price_per_seat: 0,
|
||||
},
|
||||
test_prod_3: {
|
||||
id: 'test_prod_3',
|
||||
sku: CloudProducts.PROFESSIONAL,
|
||||
price_per_seat: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it('should not render if subscription is not available', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.cloud.subscription = null;
|
||||
|
||||
const {queryByText} = renderWithContext(<CloudAnnualRenewalBanner/>, state);
|
||||
|
||||
expect(queryByText(/Your annual subscription expires in/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render with correct title and buttons', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.cloud.subscription = {
|
||||
...state.entities.cloud.subscription,
|
||||
end_at: unixTimestampFromNow(30),
|
||||
};
|
||||
const {getByText} = renderWithContext(<CloudAnnualRenewalBanner/>, state);
|
||||
|
||||
expect(getByText(/Your annual subscription expires in 30 days. Please renew now to avoid any disruption/)).toBeInTheDocument();
|
||||
expect(getByText(/Renew/)).toBeInTheDocument();
|
||||
expect(getByText(/Contact Sales/)).toBeInTheDocument();
|
||||
|
||||
const renewButton = getByText(/Renew/);
|
||||
renewButton.click();
|
||||
});
|
||||
|
||||
it('should render with danger mode if expiration is within 7 days', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.cloud.subscription = {
|
||||
...state.entities.cloud.subscription,
|
||||
end_at: unixTimestampFromNow(4),
|
||||
};
|
||||
const {getByText, getByTestId} = renderWithContext(<CloudAnnualRenewalBanner/>, state);
|
||||
|
||||
expect(getByText(/Your annual subscription expires in 4 days. Please renew now to avoid any disruption/)).toBeInTheDocument();
|
||||
expect(getByText(/Renew/)).toBeInTheDocument();
|
||||
expect(getByText(/Contact Sales/)).toBeInTheDocument();
|
||||
expect(getByTestId('cloud_annual_renewal_alert_banner_danger')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render with with different title when end_at time has passed', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.cloud.subscription = {
|
||||
...state.entities.cloud.subscription,
|
||||
end_at: unixTimestampFromNow(-5),
|
||||
cancel_at: unixTimestampFromNow(5),
|
||||
};
|
||||
const {getByText, getByTestId} = renderWithContext(<CloudAnnualRenewalBanner/>, state);
|
||||
|
||||
expect(getByText(/Your subscription has expired. Your workspace will be deleted in 5 days. Please renew now to avoid any disruption/)).toBeInTheDocument();
|
||||
expect(getByText(/Renew/)).toBeInTheDocument();
|
||||
expect(getByText(/Contact Sales/)).toBeInTheDocument();
|
||||
expect(getByTestId('cloud_annual_renewal_alert_banner_danger')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -2,16 +2,10 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import BlockableLink from 'components/admin_console/blockable_link';
|
||||
import type {ModeType} from 'components/alert_banner';
|
||||
import AlertBanner from 'components/alert_banner';
|
||||
import useGetSubscription from 'components/common/hooks/useGetSubscription';
|
||||
import useOpenCloudPurchaseModal from 'components/common/hooks/useOpenCloudPurchaseModal';
|
||||
import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink';
|
||||
|
||||
import {daysToCancellation, daysToExpiration} from 'utils/cloud_utils';
|
||||
|
||||
export const creditCardExpiredBanner = (setShowCreditCardBanner: (value: boolean) => void) => {
|
||||
return (
|
||||
@@ -59,61 +53,3 @@ export const paymentFailedBanner = () => {
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const CloudAnnualRenewalBanner = () => {
|
||||
const openPurchaseModal = useOpenCloudPurchaseModal({});
|
||||
const subscription = useGetSubscription();
|
||||
const {formatMessage} = useIntl();
|
||||
const [openSalesLink] = useOpenSalesLink();
|
||||
if (!subscription || !subscription.cancel_at || (subscription.will_renew === 'true' && !subscription.delinquent_since)) {
|
||||
return null;
|
||||
}
|
||||
const daysUntilExpiration = daysToExpiration(subscription);
|
||||
const daysUntilCancelation = daysToCancellation(subscription);
|
||||
const renewButton = (
|
||||
<button
|
||||
className='btn btn-primary'
|
||||
onClick={() => openPurchaseModal({})}
|
||||
>
|
||||
{formatMessage({id: 'cloud_annual_renewal.banner.buttonText.renew', defaultMessage: 'Renew'})}
|
||||
</button>
|
||||
);
|
||||
|
||||
const contactSalesButton = (
|
||||
<button
|
||||
className='btn btn-tertiary'
|
||||
onClick={openSalesLink}
|
||||
>
|
||||
{formatMessage({id: 'cloud_annual_renewal.banner.buttonText.contactSales', defaultMessage: 'Contact Sales'})}
|
||||
</button>
|
||||
);
|
||||
|
||||
const alertBannerProps = {
|
||||
mode: 'info' as ModeType,
|
||||
title: (<>{formatMessage({id: 'billing_subscriptions.cloud_annual_renewal_alert_banner_title', defaultMessage: 'Your annual subscription expires in {days} days. Please renew now to avoid any disruption'}, {days: daysUntilExpiration})}</>),
|
||||
actionButtonLeft: renewButton,
|
||||
actionButtonRight: contactSalesButton,
|
||||
message: <></>,
|
||||
};
|
||||
|
||||
// If outside the 60 day window or on a trial, don't show this banner.
|
||||
if (daysUntilExpiration > 60 || subscription.is_free_trial === 'true') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (daysUntilExpiration <= 7) {
|
||||
alertBannerProps.mode = 'danger';
|
||||
}
|
||||
|
||||
if (daysUntilExpiration <= 0) {
|
||||
alertBannerProps.title = <>{formatMessage({id: 'billing_subscriptions.cloud_annual_renewal_alert_banner_title_expired', defaultMessage: 'Your subscription has expired. Your workspace will be deleted in {days} days. Please renew now to avoid any disruption'}, {days: daysUntilCancelation})}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<AlertBanner
|
||||
id={'cloud_annual_renewal_alert_banner_' + alertBannerProps.mode}
|
||||
{...alertBannerProps}
|
||||
/>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import React, {useEffect} from 'react';
|
||||
import {FormattedMessage, defineMessages} from 'react-intl';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
|
||||
@@ -11,7 +11,6 @@ import {getCloudSubscription, getCloudProducts, getCloudCustomer} from 'mattermo
|
||||
import {
|
||||
getSubscriptionProduct,
|
||||
getCloudSubscription as selectCloudSubscription,
|
||||
getCloudCustomer as selectCloudCustomer,
|
||||
getCloudErrors,
|
||||
} from 'mattermost-redux/selectors/entities/cloud';
|
||||
|
||||
@@ -19,29 +18,16 @@ import {pageVisited} from 'actions/telemetry_actions';
|
||||
|
||||
import CloudTrialBanner from 'components/admin_console/billing/billing_subscriptions/cloud_trial_banner';
|
||||
import CloudFetchError from 'components/cloud_fetch_error';
|
||||
import useGetLimits from 'components/common/hooks/useGetLimits';
|
||||
import useOpenCloudPurchaseModal from 'components/common/hooks/useOpenCloudPurchaseModal';
|
||||
import useOpenPricingModal from 'components/common/hooks/useOpenPricingModal';
|
||||
import AdminHeader from 'components/widgets/admin_console/admin_header';
|
||||
|
||||
import {isCustomerCardExpired} from 'utils/cloud_utils';
|
||||
import {
|
||||
TrialPeriodDays,
|
||||
} from 'utils/constants';
|
||||
import {useQuery} from 'utils/http_utils';
|
||||
import {hasSomeLimits} from 'utils/limits';
|
||||
import {getRemainingDaysFromFutureTimestamp} from 'utils/utils';
|
||||
|
||||
import {
|
||||
CloudAnnualRenewalBanner,
|
||||
creditCardExpiredBanner,
|
||||
paymentFailedBanner,
|
||||
} from './billing_subscriptions';
|
||||
import CancelSubscription from './cancel_subscription';
|
||||
import ContactSalesCard from './contact_sales_card';
|
||||
import LimitReachedBanner from './limit_reached_banner';
|
||||
import Limits from './limits';
|
||||
import {ToPaidNudgeBanner} from './to_paid_plan_nudge_banner';
|
||||
|
||||
import BillingSummary from '../billing_summary';
|
||||
import PlanDetails from '../plan_details';
|
||||
@@ -59,18 +45,11 @@ export const searchableStrings = [
|
||||
const BillingSubscriptions = () => {
|
||||
const dispatch = useDispatch();
|
||||
const subscription = useSelector(selectCloudSubscription);
|
||||
const [cloudLimits] = useGetLimits();
|
||||
const errorLoadingData = useSelector((state: GlobalState) => {
|
||||
const errors = getCloudErrors(state);
|
||||
return Boolean(errors.limits || errors.subscription || errors.customer || errors.products);
|
||||
});
|
||||
|
||||
const isCardExpired = isCustomerCardExpired(useSelector(selectCloudCustomer));
|
||||
|
||||
const trialEndDate = subscription?.trial_end_at || 0;
|
||||
|
||||
const [showCreditCardBanner, setShowCreditCardBanner] = useState(true);
|
||||
|
||||
const query = useQuery();
|
||||
const actionQueryParam = query.get('action');
|
||||
|
||||
@@ -78,13 +57,6 @@ const BillingSubscriptions = () => {
|
||||
|
||||
const openPricingModal = useOpenPricingModal();
|
||||
|
||||
const openCloudPurchaseModal = useOpenCloudPurchaseModal({});
|
||||
|
||||
// show the upgrade section when is a free tier customer
|
||||
const onUpgradeMattermostCloud = (callerInfo: string) => {
|
||||
openCloudPurchaseModal({trackingLocation: callerInfo});
|
||||
};
|
||||
|
||||
let isFreeTrial = false;
|
||||
let daysLeftOnTrial = 0;
|
||||
if (subscription?.is_free_trial === 'true') {
|
||||
@@ -103,23 +75,11 @@ const BillingSubscriptions = () => {
|
||||
|
||||
pageVisited('cloud_admin', 'pageview_billing_subscription');
|
||||
|
||||
if (actionQueryParam === 'show_purchase_modal') {
|
||||
onUpgradeMattermostCloud('billing_subscriptions_external_direct_link');
|
||||
}
|
||||
|
||||
if (actionQueryParam === 'show_pricing_modal') {
|
||||
openPricingModal({trackingLocation: 'billing_subscriptions_external_direct_link'});
|
||||
}
|
||||
|
||||
if (actionQueryParam === 'show_delinquency_modal') {
|
||||
openCloudPurchaseModal({trackingLocation: 'billing_subscriptions_external_direct_link'});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const shouldShowPaymentFailedBanner = () => {
|
||||
return subscription?.last_invoice?.status === 'failed';
|
||||
};
|
||||
|
||||
// handle not loaded yet here, failed to load handled below
|
||||
if ((!subscription || !product) && !errorLoadingData) {
|
||||
return null;
|
||||
@@ -134,15 +94,6 @@ const BillingSubscriptions = () => {
|
||||
<div className='admin-console__content'>
|
||||
{errorLoadingData && <CloudFetchError/>}
|
||||
{!errorLoadingData && <>
|
||||
<LimitReachedBanner
|
||||
product={product}
|
||||
/>
|
||||
{shouldShowPaymentFailedBanner() && paymentFailedBanner()}
|
||||
{<CloudAnnualRenewalBanner/>}
|
||||
{<ToPaidNudgeBanner/>}
|
||||
{showCreditCardBanner &&
|
||||
isCardExpired &&
|
||||
creditCardExpiredBanner(setShowCreditCardBanner)}
|
||||
{isFreeTrial && <CloudTrialBanner trialEndDate={trialEndDate}/>}
|
||||
<div className='BillingSubscriptions__topWrapper'>
|
||||
<PlanDetails
|
||||
@@ -152,19 +103,13 @@ const BillingSubscriptions = () => {
|
||||
<BillingSummary
|
||||
isFreeTrial={isFreeTrial}
|
||||
daysLeftOnTrial={daysLeftOnTrial}
|
||||
onUpgradeMattermostCloud={onUpgradeMattermostCloud}
|
||||
/>
|
||||
</div>
|
||||
{hasSomeLimits(cloudLimits) && !isFreeTrial ? (
|
||||
<Limits/>
|
||||
) : (
|
||||
<ContactSalesCard
|
||||
isFreeTrial={isFreeTrial}
|
||||
subscriptionPlan={product?.sku}
|
||||
onUpgradeMattermostCloud={openPricingModal}
|
||||
/>
|
||||
)}
|
||||
<CancelSubscription/>
|
||||
<ContactSalesCard
|
||||
isFreeTrial={isFreeTrial}
|
||||
subscriptionPlan={product?.sku}
|
||||
onUpgradeMattermostCloud={openPricingModal}
|
||||
/>
|
||||
</>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
@import 'utils/mixins';
|
||||
|
||||
.LimitReachedBanner {
|
||||
&__actions {
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
&__primary {
|
||||
font-size: 12px;
|
||||
|
||||
@include primary-button;
|
||||
|
||||
&:hover {
|
||||
color: var(--button-color);
|
||||
}
|
||||
}
|
||||
|
||||
&__contact-sales {
|
||||
margin-left: 4px;
|
||||
font-size: 12px;
|
||||
|
||||
@include tertiary-button;
|
||||
|
||||
&:hover {
|
||||
color: var(--button-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import type {GlobalState} from '@mattermost/types/store';
|
||||
import type {UserProfile, UsersState} from '@mattermost/types/users';
|
||||
|
||||
import {Preferences} from 'mattermost-redux/constants';
|
||||
import {getPreferenceKey} from 'mattermost-redux/utils/preference_utils';
|
||||
|
||||
import * as useGetUsageDeltas from 'components/common/hooks/useGetUsageDeltas';
|
||||
import * as useOpenCloudPurchaseModal from 'components/common/hooks/useOpenCloudPurchaseModal';
|
||||
import * as useOpenPricingModal from 'components/common/hooks/useOpenPricingModal';
|
||||
import * as useOpenSalesLink from 'components/common/hooks/useOpenSalesLink';
|
||||
import * as useSaveBool from 'components/common/hooks/useSavePreferences';
|
||||
|
||||
import {fireEvent, renderWithContext, screen} from 'tests/react_testing_utils';
|
||||
import {CloudProducts} from 'utils/constants';
|
||||
|
||||
import LimitReachedBanner from './limit_reached_banner';
|
||||
|
||||
const upgradeCloudKey = getPreferenceKey(Preferences.CATEGORY_UPGRADE_CLOUD, Preferences.SYSTEM_CONSOLE_LIMIT_REACHED);
|
||||
|
||||
const state: GlobalState = {
|
||||
entities: {
|
||||
users: {
|
||||
currentUserId: 'userid',
|
||||
profiles: {
|
||||
userid: {} as UserProfile,
|
||||
},
|
||||
} as unknown as UsersState,
|
||||
preferences: {
|
||||
myPreferences: {
|
||||
[upgradeCloudKey]: {value: 'false'},
|
||||
},
|
||||
},
|
||||
cloud: {
|
||||
limits: {
|
||||
},
|
||||
products: {
|
||||
},
|
||||
},
|
||||
general: {
|
||||
license: {
|
||||
},
|
||||
config: {
|
||||
},
|
||||
},
|
||||
},
|
||||
} as GlobalState;
|
||||
|
||||
const base = {
|
||||
id: '',
|
||||
name: '',
|
||||
description: '',
|
||||
price_per_seat: 0,
|
||||
add_ons: [],
|
||||
product_family: '',
|
||||
billing_scheme: '',
|
||||
recurring_interval: '',
|
||||
cross_sells_to: '',
|
||||
};
|
||||
|
||||
const free = {...base, sku: CloudProducts.STARTER};
|
||||
const enterprise = {...base, sku: CloudProducts.ENTERPRISE};
|
||||
|
||||
const noLimitReached = {
|
||||
files: {
|
||||
totalStorage: -1,
|
||||
totalStorageLoaded: true,
|
||||
},
|
||||
messages: {
|
||||
history: -1,
|
||||
historyLoaded: true,
|
||||
},
|
||||
boards: {
|
||||
cards: -1,
|
||||
cardsLoaded: true,
|
||||
},
|
||||
teams: {
|
||||
active: -1,
|
||||
cloudArchived: -1,
|
||||
teamsLoaded: true,
|
||||
},
|
||||
integrations: {
|
||||
enabled: -1,
|
||||
enabledLoaded: true,
|
||||
},
|
||||
};
|
||||
const someLimitReached = {
|
||||
...noLimitReached,
|
||||
integrations: {
|
||||
...noLimitReached.integrations,
|
||||
enabled: 1,
|
||||
},
|
||||
};
|
||||
|
||||
const titleFree = /Upgrade to one of our paid plans to avoid/;
|
||||
const titleProfessional = /Upgrade to Enterprise to avoid Professional plan/;
|
||||
|
||||
function makeSpies() {
|
||||
const mockUseOpenSalesLink = jest.spyOn(useOpenSalesLink, 'default');
|
||||
const mockUseGetUsageDeltas = jest.spyOn(useGetUsageDeltas, 'default');
|
||||
const mockUseOpenCloudPurchaseModal = jest.spyOn(useOpenCloudPurchaseModal, 'default');
|
||||
const mockUseOpenPricingModal = jest.spyOn(useOpenPricingModal, 'default');
|
||||
const mockUseSaveBool = jest.spyOn(useSaveBool, 'useSaveBool');
|
||||
return {
|
||||
useOpenSalesLink: mockUseOpenSalesLink,
|
||||
useGetUsageDeltas: mockUseGetUsageDeltas,
|
||||
useOpenCloudPurchaseModal: mockUseOpenCloudPurchaseModal,
|
||||
useOpenPricingModal: mockUseOpenPricingModal,
|
||||
useSaveBool: mockUseSaveBool,
|
||||
};
|
||||
}
|
||||
|
||||
describe('limits_reached_banner', () => {
|
||||
test('does not render when product is enterprise', () => {
|
||||
const spies = makeSpies();
|
||||
spies.useGetUsageDeltas.mockReturnValue(someLimitReached);
|
||||
|
||||
renderWithContext(<LimitReachedBanner product={enterprise}/>, state);
|
||||
|
||||
expect(screen.queryByText(titleFree)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(titleProfessional)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('does not render when banner was dismissed', () => {
|
||||
const myState = {
|
||||
...state,
|
||||
entities: {
|
||||
...state.entities,
|
||||
preferences: {
|
||||
...state.entities.preferences,
|
||||
myPreferences: {
|
||||
...state.entities.preferences.myPreferences,
|
||||
[upgradeCloudKey]: {value: 'true'},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const spies = makeSpies();
|
||||
spies.useGetUsageDeltas.mockReturnValue(someLimitReached);
|
||||
|
||||
renderWithContext(<LimitReachedBanner product={enterprise}/>, myState);
|
||||
|
||||
expect(screen.queryByText(titleFree)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(titleProfessional)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('does not render when no limit reached', () => {
|
||||
const spies = makeSpies();
|
||||
spies.useGetUsageDeltas.mockReturnValue(noLimitReached);
|
||||
|
||||
renderWithContext(<LimitReachedBanner product={free}/>, state);
|
||||
|
||||
expect(screen.queryByText(titleFree)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(titleProfessional)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders free banner', () => {
|
||||
const spies = makeSpies();
|
||||
const mockOpenPricingModal = jest.fn();
|
||||
spies.useOpenPricingModal.mockReturnValue(mockOpenPricingModal);
|
||||
spies.useGetUsageDeltas.mockReturnValue(someLimitReached);
|
||||
|
||||
renderWithContext(<LimitReachedBanner product={free}/>, state);
|
||||
|
||||
screen.getByText(titleFree);
|
||||
expect(screen.queryByText(titleProfessional)).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByText('View plans'));
|
||||
|
||||
expect(mockOpenPricingModal).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('clicking Contact Sales opens sales link', () => {
|
||||
const spies = makeSpies();
|
||||
const mockOpenSalesLink = jest.fn();
|
||||
spies.useOpenSalesLink.mockReturnValue([mockOpenSalesLink, '']);
|
||||
spies.useGetUsageDeltas.mockReturnValue(someLimitReached);
|
||||
|
||||
renderWithContext(<LimitReachedBanner product={free}/>, state);
|
||||
|
||||
screen.getByText(titleFree);
|
||||
expect(screen.queryByText(titleProfessional)).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByText('Contact sales'));
|
||||
|
||||
expect(mockOpenSalesLink).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,105 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useIntl, FormattedMessage} from 'react-intl';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import type {Product} from '@mattermost/types/cloud';
|
||||
|
||||
import {Preferences} from 'mattermost-redux/constants';
|
||||
import {getHasDismissedSystemConsoleLimitReached} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import AlertBanner from 'components/alert_banner';
|
||||
import useGetUsageDeltas from 'components/common/hooks/useGetUsageDeltas';
|
||||
import useOpenPricingModal from 'components/common/hooks/useOpenPricingModal';
|
||||
import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink';
|
||||
import {useSaveBool} from 'components/common/hooks/useSavePreferences';
|
||||
|
||||
import {CloudProducts} from 'utils/constants';
|
||||
import {anyUsageDeltaExceededLimit} from 'utils/limits';
|
||||
|
||||
import './limit_reached_banner.scss';
|
||||
|
||||
interface Props {
|
||||
product?: Product;
|
||||
}
|
||||
|
||||
const LimitReachedBanner = (props: Props) => {
|
||||
const intl = useIntl();
|
||||
const someLimitExceeded = anyUsageDeltaExceededLimit(useGetUsageDeltas());
|
||||
|
||||
const hasDismissedBanner = useSelector(getHasDismissedSystemConsoleLimitReached);
|
||||
|
||||
const [openSalesLink] = useOpenSalesLink();
|
||||
const openPricingModal = useOpenPricingModal();
|
||||
const saveBool = useSaveBool();
|
||||
if (hasDismissedBanner || !someLimitExceeded || !props.product || (props.product.sku !== CloudProducts.STARTER)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const title = (
|
||||
<FormattedMessage
|
||||
id='workspace_limits.banner_upgrade.free'
|
||||
defaultMessage='Upgrade to one of our paid plans to avoid {planName} plan data limits'
|
||||
values={{
|
||||
planName: props.product.name,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const description = (
|
||||
<FormattedMessage
|
||||
id='workspace_limits.banner_upgrade_reason.free'
|
||||
defaultMessage='Your workspace has exceeded {planName} plan data limits. Upgrade to a paid plan for additional capacity.'
|
||||
values={{
|
||||
planName: props.product.name,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const upgradeMessage = {
|
||||
id: 'workspace_limits.modals.view_plans',
|
||||
defaultMessage: 'View plans',
|
||||
};
|
||||
|
||||
const upgradeAction = () => openPricingModal({trackingLocation: 'limit_reached_banner'});
|
||||
|
||||
const onDismiss = () => {
|
||||
saveBool({
|
||||
category: Preferences.CATEGORY_UPGRADE_CLOUD,
|
||||
name: Preferences.SYSTEM_CONSOLE_LIMIT_REACHED,
|
||||
value: true,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<AlertBanner
|
||||
mode='danger'
|
||||
title={title}
|
||||
message={description}
|
||||
onDismiss={onDismiss}
|
||||
className='LimitReachedBanner'
|
||||
>
|
||||
<div className='LimitReachedBanner__actions'>
|
||||
<button
|
||||
onClick={upgradeAction}
|
||||
className='btn LimitReachedBanner__primary'
|
||||
>
|
||||
{intl.formatMessage(upgradeMessage)}
|
||||
</button>
|
||||
<button
|
||||
onClick={openSalesLink}
|
||||
className='btn LimitReachedBanner__contact-sales'
|
||||
>
|
||||
{intl.formatMessage({
|
||||
id: 'admin.license.trialCard.contactSales',
|
||||
defaultMessage: 'Contact sales',
|
||||
})}
|
||||
</button>
|
||||
</div>
|
||||
</AlertBanner>
|
||||
);
|
||||
};
|
||||
|
||||
export default LimitReachedBanner;
|
||||
@@ -6,7 +6,7 @@ import React from 'react';
|
||||
import {renderWithContext, screen} from 'tests/react_testing_utils';
|
||||
import {CloudProducts} from 'utils/constants';
|
||||
|
||||
import {ToPaidNudgeBanner, ToPaidPlanBannerDismissable} from './to_paid_plan_nudge_banner';
|
||||
import {ToPaidPlanBannerDismissable} from './to_paid_plan_nudge_banner';
|
||||
|
||||
const initialState = {
|
||||
views: {
|
||||
@@ -171,68 +171,3 @@ describe('ToPaidPlanBannerDismissable', () => {
|
||||
expect(() => screen.getByTestId('cloud-free-deprecation-announcement-bar')).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ToPaidNudgeBanner', () => {
|
||||
test('should show only for cloud free', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.cloud = {
|
||||
subscription: {
|
||||
product_id: 'prod_starter',
|
||||
is_free_trial: 'false',
|
||||
trial_end_at: 1,
|
||||
},
|
||||
products: {
|
||||
prod_starter: {
|
||||
id: 'prod_starter',
|
||||
sku: CloudProducts.STARTER,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithContext(<ToPaidNudgeBanner/>, state, {useMockedStore: true});
|
||||
|
||||
screen.getByTestId('cloud-free-deprecation-alert-banner');
|
||||
});
|
||||
|
||||
test('should NOT show for cloud professional', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.cloud = {
|
||||
subscription: {
|
||||
product_id: 'prod_pro',
|
||||
is_free_trial: 'false',
|
||||
trial_end_at: 1,
|
||||
},
|
||||
products: {
|
||||
prod_pro: {
|
||||
id: 'prod_pro',
|
||||
sku: CloudProducts.PROFESSIONAL,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithContext(<ToPaidNudgeBanner/>, state, {useMockedStore: true});
|
||||
|
||||
expect(() => screen.getByTestId('cloud-free-deprecation-alert-banner')).toThrow();
|
||||
});
|
||||
|
||||
test('should NOT show for cloud enterprise', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.cloud = {
|
||||
subscription: {
|
||||
product_id: 'prod_ent',
|
||||
is_free_trial: 'false',
|
||||
trial_end_at: 1,
|
||||
},
|
||||
products: {
|
||||
prod_ent: {
|
||||
id: 'prod_ent',
|
||||
sku: CloudProducts.ENTERPRISE,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithContext(<ToPaidNudgeBanner/>, state, {useMockedStore: true});
|
||||
|
||||
expect(() => screen.getByTestId('cloud-free-deprecation-alert-banner')).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import moment from 'moment';
|
||||
import React, {useEffect} from 'react';
|
||||
import {useIntl, FormattedMessage} from 'react-intl';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
|
||||
import type {GlobalState} from '@mattermost/types/store';
|
||||
@@ -13,11 +13,8 @@ import {getSubscriptionProduct as selectSubscriptionProduct} from 'mattermost-re
|
||||
import {deprecateCloudFree, get as getPreference} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentUser, isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import AlertBanner from 'components/alert_banner';
|
||||
import AnnouncementBar from 'components/announcement_bar/default_announcement_bar';
|
||||
import useOpenCloudPurchaseModal from 'components/common/hooks/useOpenCloudPurchaseModal';
|
||||
import useOpenPricingModal from 'components/common/hooks/useOpenPricingModal';
|
||||
import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink';
|
||||
|
||||
import {AnnouncementBarTypes, CloudBanners, CloudProducts, Preferences} from 'utils/constants';
|
||||
import {t} from 'utils/i18n';
|
||||
@@ -174,73 +171,3 @@ export const ToPaidPlanBannerDismissable = () => {
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ToPaidNudgeBanner = () => {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const [openSalesLink] = useOpenSalesLink();
|
||||
const openPurchaseModal = useOpenCloudPurchaseModal({});
|
||||
|
||||
const product = useSelector(selectSubscriptionProduct);
|
||||
const cloudFreeDeprecated = useSelector(deprecateCloudFree);
|
||||
const currentProductStarter = product?.sku === CloudProducts.STARTER;
|
||||
|
||||
if (!cloudFreeDeprecated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!currentProductStarter) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = moment(Date.now());
|
||||
const cloudFreeEndDate = moment(cloudFreeCloseMoment, 'YYYYMMDD');
|
||||
const daysToCloudFreeEnd = cloudFreeEndDate.diff(now, 'days');
|
||||
|
||||
const title = (
|
||||
<FormattedMessage
|
||||
id='cloud_billing.nudge_to_paid.title'
|
||||
defaultMessage='Upgrade to paid plan to keep your workspace'
|
||||
/>
|
||||
);
|
||||
|
||||
const description = (
|
||||
<FormattedMessage
|
||||
id='cloud_billing.nudge_to_paid.description'
|
||||
defaultMessage='Cloud Free will be deprecated in {days} days. Upgrade to a paid plan or contact sales.'
|
||||
values={{days: daysToCloudFreeEnd < 0 ? 0 : daysToCloudFreeEnd}}
|
||||
/>
|
||||
);
|
||||
|
||||
const viewPlansAction = (
|
||||
<button
|
||||
onClick={() => openPurchaseModal({trackingLocation: 'to_paid_plan_nudge_banner'})}
|
||||
className='btn ToPaidNudgeBanner__primary'
|
||||
>
|
||||
{formatMessage({id: 'cloud_billing.nudge_to_paid.learn_more', defaultMessage: 'Upgrade'})}
|
||||
</button>
|
||||
);
|
||||
|
||||
const contactSalesAction = (
|
||||
<button
|
||||
onClick={openSalesLink}
|
||||
className='btn ToPaidNudgeBanner__secondary'
|
||||
>
|
||||
{formatMessage({id: 'cloud_billing.nudge_to_paid.contact_sales', defaultMessage: 'Contact sales'})}
|
||||
</button>
|
||||
);
|
||||
|
||||
const bannerMode = (daysToCloudFreeEnd <= 10) ? 'danger' : 'info';
|
||||
|
||||
return (
|
||||
<AlertBanner
|
||||
id='cloud-free-deprecation-alert-banner'
|
||||
mode={bannerMode}
|
||||
title={title}
|
||||
message={description}
|
||||
className='ToYearlyNudgeBanner'
|
||||
actionButtonLeft={viewPlansAction}
|
||||
actionButtonRight={contactSalesAction}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import React from 'react';
|
||||
import {FormattedDate, FormattedMessage, FormattedNumber, defineMessages} from 'react-intl';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import {CheckCircleOutlineIcon, CheckIcon, ClockOutlineIcon} from '@mattermost/compass-icons/components';
|
||||
import {CheckCircleOutlineIcon} from '@mattermost/compass-icons/components';
|
||||
import type {Invoice, InvoiceLineItem, Product} from '@mattermost/types/cloud';
|
||||
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
@@ -15,6 +15,7 @@ import {openModal} from 'actions/views/modals';
|
||||
|
||||
import BlockableLink from 'components/admin_console/blockable_link';
|
||||
import CloudInvoicePreview from 'components/cloud_invoice_preview';
|
||||
import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink';
|
||||
import EmptyBillingHistorySvg from 'components/common/svg_images_components/empty_billing_history_svg';
|
||||
import UpgradeSvg from 'components/common/svg_images_components/upgrade_svg';
|
||||
import ExternalLink from 'components/external_link';
|
||||
@@ -65,118 +66,81 @@ export const noBillingHistory = (
|
||||
</div>
|
||||
);
|
||||
|
||||
export const freeTrial = (onUpgradeMattermostCloud: (callerInfo: string) => void, daysLeftOnTrial: number, reverseTrial: boolean) => (
|
||||
<div className='UpgradeMattermostCloud'>
|
||||
<div className='UpgradeMattermostCloud__image'>
|
||||
<UpgradeSvg
|
||||
height={167}
|
||||
width={234}
|
||||
type FreeTrialProps = {
|
||||
daysLeftOnTrial: number;
|
||||
}
|
||||
|
||||
export const FreeTrial = ({daysLeftOnTrial}: FreeTrialProps) => {
|
||||
const [openSalesLink] = useOpenSalesLink();
|
||||
return (
|
||||
<div className='UpgradeMattermostCloud'>
|
||||
<div className='UpgradeMattermostCloud__image'>
|
||||
<UpgradeSvg
|
||||
height={167}
|
||||
width={234}
|
||||
/>
|
||||
</div>
|
||||
<div className='UpgradeMattermostCloud__title'>
|
||||
{daysLeftOnTrial > TrialPeriodDays.TRIAL_1_DAY &&
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscription.freeTrial.title'
|
||||
defaultMessage={'You\'re currently on a free trial'}
|
||||
/>
|
||||
}
|
||||
{(daysLeftOnTrial === TrialPeriodDays.TRIAL_1_DAY || daysLeftOnTrial === TrialPeriodDays.TRIAL_0_DAYS) &&
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscription.freeTrial.lastDay.title'
|
||||
defaultMessage={'Your free trial ends today'}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
<div className='UpgradeMattermostCloud__description'>
|
||||
{daysLeftOnTrial > TrialPeriodDays.TRIAL_WARNING_THRESHOLD &&
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscription.freeTrial.description'
|
||||
defaultMessage='Your free trial will expire in {daysLeftOnTrial} days. Add your payment information to continue after the trial ends.'
|
||||
values={{daysLeftOnTrial}}
|
||||
/>
|
||||
}
|
||||
{(daysLeftOnTrial > TrialPeriodDays.TRIAL_1_DAY && daysLeftOnTrial <= TrialPeriodDays.TRIAL_WARNING_THRESHOLD) &&
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscription.freeTrial.lessThan3Days.description'
|
||||
defaultMessage='Your free trial will end in {daysLeftOnTrial, number} {daysLeftOnTrial, plural, one {day} other {days}}. Add payment information to continue enjoying the benefits of Cloud Professional.'
|
||||
values={{daysLeftOnTrial}}
|
||||
/>
|
||||
}
|
||||
{(daysLeftOnTrial === TrialPeriodDays.TRIAL_1_DAY || daysLeftOnTrial === TrialPeriodDays.TRIAL_0_DAYS) &&
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscription.freeTrial.lastDay.description'
|
||||
defaultMessage='Your free trial has ended. Add payment information to continue enjoying the benefits of Cloud Professional.'
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => openSalesLink()}
|
||||
className='UpgradeMattermostCloud__upgradeButton'
|
||||
>
|
||||
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscription.privateCloudCard.contactSales'
|
||||
defaultMessage='Contact Sales'
|
||||
/>
|
||||
|
||||
</button>
|
||||
</div>);
|
||||
};
|
||||
|
||||
export const getPaymentStatus = () => {
|
||||
return (
|
||||
<div className='BillingSummary__lastInvoice-headerStatus paid'>
|
||||
<CheckCircleOutlineIcon/> {' '}
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscriptions.billing_summary.lastInvoice.paid'
|
||||
defaultMessage='Paid'
|
||||
/>
|
||||
</div>
|
||||
<div className='UpgradeMattermostCloud__title'>
|
||||
{daysLeftOnTrial > TrialPeriodDays.TRIAL_1_DAY &&
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscription.freeTrial.title'
|
||||
defaultMessage={'You\'re currently on a free trial'}
|
||||
/>
|
||||
}
|
||||
{(daysLeftOnTrial === TrialPeriodDays.TRIAL_1_DAY || daysLeftOnTrial === TrialPeriodDays.TRIAL_0_DAYS) &&
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscription.freeTrial.lastDay.title'
|
||||
defaultMessage={'Your free trial ends today'}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
<div className='UpgradeMattermostCloud__description'>
|
||||
{daysLeftOnTrial > TrialPeriodDays.TRIAL_WARNING_THRESHOLD &&
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscription.freeTrial.description'
|
||||
defaultMessage='Your free trial will expire in {daysLeftOnTrial} days. Add your payment information to continue after the trial ends.'
|
||||
values={{daysLeftOnTrial}}
|
||||
/>
|
||||
}
|
||||
{(daysLeftOnTrial > TrialPeriodDays.TRIAL_1_DAY && daysLeftOnTrial <= TrialPeriodDays.TRIAL_WARNING_THRESHOLD) &&
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscription.freeTrial.lessThan3Days.description'
|
||||
defaultMessage='Your free trial will end in {daysLeftOnTrial, number} {daysLeftOnTrial, plural, one {day} other {days}}. Add payment information to continue enjoying the benefits of Cloud Professional.'
|
||||
values={{daysLeftOnTrial}}
|
||||
/>
|
||||
}
|
||||
{(daysLeftOnTrial === TrialPeriodDays.TRIAL_1_DAY || daysLeftOnTrial === TrialPeriodDays.TRIAL_0_DAYS) &&
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscription.freeTrial.lastDay.description'
|
||||
defaultMessage='Your free trial has ended. Add payment information to continue enjoying the benefits of Cloud Professional.'
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => onUpgradeMattermostCloud('billing_summary_free_trial_upgrade_button')}
|
||||
className='UpgradeMattermostCloud__upgradeButton'
|
||||
>
|
||||
{
|
||||
reverseTrial ? (
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscription.cloudTrial.purchaseButton'
|
||||
defaultMessage='Purchase Now'
|
||||
/>
|
||||
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscription.cloudTrial.subscribeButton'
|
||||
defaultMessage='Upgrade Now'
|
||||
/>
|
||||
)
|
||||
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const getPaymentStatus = (status: string, willRenew?: boolean) => {
|
||||
if (willRenew) {
|
||||
return (
|
||||
<div className='BillingSummary__lastInvoice-headerStatus paid'>
|
||||
<CheckIcon/> {' '}
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscriptions.billing_summary.lastInvoice.approved'
|
||||
defaultMessage='Approved'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
switch (status.toLowerCase()) {
|
||||
case 'failed':
|
||||
return (
|
||||
<div className='BillingSummary__lastInvoice-headerStatus failed'>
|
||||
<i className='icon icon-alert-outline'/> {' '}
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscriptions.billing_summary.lastInvoice.failed'
|
||||
defaultMessage='Failed'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case 'paid':
|
||||
return (
|
||||
<div className='BillingSummary__lastInvoice-headerStatus paid'>
|
||||
<CheckCircleOutlineIcon/> {' '}
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscriptions.billing_summary.lastInvoice.paid'
|
||||
defaultMessage='Paid'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<div className='BillingSummary__lastInvoice-headerStatus pending'>
|
||||
<ClockOutlineIcon/> {' '}
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscriptions.billing_summary.lastInvoice.pending'
|
||||
defaultMessage='Pending'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
type InvoiceInfoProps = {
|
||||
@@ -185,10 +149,9 @@ type InvoiceInfoProps = {
|
||||
fullCharges: InvoiceLineItem[];
|
||||
partialCharges: InvoiceLineItem[];
|
||||
hasMore?: number;
|
||||
willRenew?: boolean;
|
||||
}
|
||||
|
||||
export const InvoiceInfo = ({invoice, product, fullCharges, partialCharges, hasMore, willRenew}: InvoiceInfoProps) => {
|
||||
export const InvoiceInfo = ({invoice, product, fullCharges, partialCharges, hasMore}: InvoiceInfoProps) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const isUpcomingInvoice = invoice?.status.toLowerCase() === 'upcoming';
|
||||
@@ -225,7 +188,7 @@ export const InvoiceInfo = ({invoice, product, fullCharges, partialCharges, hasM
|
||||
<div className='BillingSummary__lastInvoice-headerTitle'>
|
||||
{title()}
|
||||
</div>
|
||||
{getPaymentStatus(invoice.status, willRenew)}
|
||||
{getPaymentStatus()}
|
||||
</div>
|
||||
<div className='BillingSummary__lastInvoice-date'>
|
||||
<FormattedDate
|
||||
|
||||
@@ -2,83 +2,30 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import {getSubscriptionProduct, checkHadPriorTrial, getCloudSubscription} from 'mattermost-redux/selectors/entities/cloud';
|
||||
import {cloudReverseTrial} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {buildInvoiceSummaryPropsFromLineItems} from 'utils/cloud_utils';
|
||||
import {CloudProducts} from 'utils/constants';
|
||||
|
||||
import {
|
||||
noBillingHistory,
|
||||
InvoiceInfo,
|
||||
freeTrial,
|
||||
FreeTrial,
|
||||
} from './billing_summary';
|
||||
import {tryEnterpriseCard, UpgradeToProfessionalCard} from './upsell_card';
|
||||
|
||||
import './billing_summary.scss';
|
||||
|
||||
type BillingSummaryProps = {
|
||||
isFreeTrial: boolean;
|
||||
daysLeftOnTrial: number;
|
||||
onUpgradeMattermostCloud: (callerInfo: string) => void;
|
||||
}
|
||||
|
||||
const BillingSummary = ({isFreeTrial, daysLeftOnTrial, onUpgradeMattermostCloud}: BillingSummaryProps) => {
|
||||
const subscription = useSelector(getCloudSubscription);
|
||||
const product = useSelector(getSubscriptionProduct);
|
||||
const reverseTrial = useSelector(cloudReverseTrial);
|
||||
|
||||
export default function BillingSummary({isFreeTrial, daysLeftOnTrial}: BillingSummaryProps) {
|
||||
let body = noBillingHistory;
|
||||
|
||||
const isPreTrial = subscription?.is_free_trial === 'false' && subscription?.trial_end_at === 0;
|
||||
const hasPriorTrial = useSelector(checkHadPriorTrial);
|
||||
const isStarterPreTrial = product?.sku === CloudProducts.STARTER && isPreTrial;
|
||||
const isStarterPostTrial = product?.sku === CloudProducts.STARTER && hasPriorTrial;
|
||||
|
||||
if (isStarterPreTrial && reverseTrial) {
|
||||
body = <UpgradeToProfessionalCard/>;
|
||||
} else if (isStarterPreTrial) {
|
||||
body = tryEnterpriseCard;
|
||||
} else if (isStarterPostTrial) {
|
||||
body = <UpgradeToProfessionalCard/>;
|
||||
} else if (isFreeTrial) {
|
||||
body = freeTrial(onUpgradeMattermostCloud, daysLeftOnTrial, reverseTrial);
|
||||
} else if (subscription?.last_invoice && !subscription?.upcoming_invoice) {
|
||||
const invoice = subscription.last_invoice;
|
||||
const fullCharges = invoice.line_items.filter((item) => item.type === 'full');
|
||||
const partialCharges = invoice.line_items.filter((item) => item.type === 'partial');
|
||||
|
||||
body = (
|
||||
<InvoiceInfo
|
||||
invoice={invoice}
|
||||
product={product}
|
||||
fullCharges={fullCharges}
|
||||
partialCharges={partialCharges}
|
||||
/>
|
||||
);
|
||||
} else if (subscription?.upcoming_invoice) {
|
||||
const invoice = subscription.upcoming_invoice;
|
||||
const {fullCharges, partialCharges, hasMore} = buildInvoiceSummaryPropsFromLineItems(invoice.line_items);
|
||||
|
||||
body = (
|
||||
<InvoiceInfo
|
||||
invoice={invoice}
|
||||
product={product}
|
||||
fullCharges={fullCharges}
|
||||
partialCharges={partialCharges}
|
||||
hasMore={hasMore}
|
||||
willRenew={subscription?.will_renew === 'true'}
|
||||
/>
|
||||
);
|
||||
if (isFreeTrial) {
|
||||
// eslint-disable-next-line new-cap
|
||||
body = FreeTrial({daysLeftOnTrial});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='BillingSummary'>
|
||||
{body}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default BillingSummary;
|
||||
|
||||
@@ -6,7 +6,6 @@ import React from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import CloudStartTrialButton from 'components/cloud_start_trial/cloud_start_trial_btn';
|
||||
import useOpenCloudPurchaseModal from 'components/common/hooks/useOpenCloudPurchaseModal';
|
||||
import WomanUpArrowsAndCloudsSvg from 'components/common/svg_images_components/woman_up_arrows_and_clouds_svg';
|
||||
import StartTrialCaution from 'components/pricing_modal/start_trial_caution';
|
||||
|
||||
@@ -31,9 +30,6 @@ const enterpriseAdvantages = [
|
||||
},
|
||||
];
|
||||
|
||||
// Currently, these are the same. In the future, they may diverge.
|
||||
const professionalAdvantages = enterpriseAdvantages;
|
||||
|
||||
interface Props {
|
||||
advantages: Message[];
|
||||
title: Message;
|
||||
@@ -149,26 +145,6 @@ export const tryEnterpriseCard = (
|
||||
/>
|
||||
);
|
||||
|
||||
export const UpgradeToProfessionalCard = () => {
|
||||
const openPurchaseModal = useOpenCloudPurchaseModal({});
|
||||
return (
|
||||
<UpsellCard
|
||||
title={{
|
||||
id: t('admin.billing.subscriptions.billing_summary.upgrade_professional'),
|
||||
defaultMessage: 'Upgrade to the Professional Plan',
|
||||
}}
|
||||
cta={{
|
||||
id: t('admin.billing.subscriptions.billing_summary.upgrade_professional.cta'),
|
||||
defaultMessage: 'Upgrade',
|
||||
}}
|
||||
ctaAction={() => openPurchaseModal({trackingLocation: 'billing_summary_upsell_professional_card'})}
|
||||
ctaPrimary={true}
|
||||
andMore={true}
|
||||
advantages={professionalAdvantages}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ExploreEnterpriseCard = () => {
|
||||
return (
|
||||
<UpsellCard
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
.PaymentInfo .AlertBanner {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {FormattedMessage, defineMessages} from 'react-intl';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
|
||||
import type {GlobalState} from '@mattermost/types/store';
|
||||
|
||||
import {getCloudCustomer} from 'mattermost-redux/actions/cloud';
|
||||
import {getCloudErrors} from 'mattermost-redux/selectors/entities/cloud';
|
||||
|
||||
import {pageVisited} from 'actions/telemetry_actions';
|
||||
|
||||
import AlertBanner from 'components/alert_banner';
|
||||
import CloudFetchError from 'components/cloud_fetch_error';
|
||||
import AdminHeader from 'components/widgets/admin_console/admin_header';
|
||||
|
||||
import PaymentInfoDisplay from './payment_info_display';
|
||||
|
||||
import './payment_info.scss';
|
||||
|
||||
type Props = Record<string, never>;
|
||||
|
||||
const messages = defineMessages({
|
||||
title: {id: 'admin.billing.payment_info.title', defaultMessage: 'Payment Information'},
|
||||
});
|
||||
|
||||
export const searchableStrings = [
|
||||
messages.title,
|
||||
];
|
||||
|
||||
const PaymentInfo: React.FC<Props> = () => {
|
||||
const dispatch = useDispatch();
|
||||
const {customer: customerError} = useSelector(getCloudErrors);
|
||||
|
||||
const isCardAboutToExpire = useSelector((state: GlobalState) => {
|
||||
const {customer} = state.entities.cloud;
|
||||
if (!customer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const expiryYear = customer.payment_method.exp_year;
|
||||
|
||||
// If not expiry year, or its 0, it's not expired (because it probably isn't set)
|
||||
if (!expiryYear) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// This works because we store the expiry month as the actual 1-12 base month, but Date uses a 0-11 base month
|
||||
// But credit cards expire at the end of their expiry month, so we can just use that number.
|
||||
const lastExpiryDate = new Date(expiryYear, customer.payment_method.exp_month, 1);
|
||||
const currentDatePlus10Days = new Date();
|
||||
currentDatePlus10Days.setDate(currentDatePlus10Days.getDate() + 10);
|
||||
return lastExpiryDate <= currentDatePlus10Days;
|
||||
});
|
||||
|
||||
const [showCreditCardBanner, setShowCreditCardBanner] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getCloudCustomer());
|
||||
|
||||
pageVisited('cloud_admin', 'pageview_billing_payment_info');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className='wrapper--fixed PaymentInfo'>
|
||||
<AdminHeader>
|
||||
<FormattedMessage {...messages.title}/>
|
||||
</AdminHeader>
|
||||
<div className='admin-console__wrapper'>
|
||||
<div className='admin-console__content'>
|
||||
{showCreditCardBanner && isCardAboutToExpire && (
|
||||
<AlertBanner
|
||||
mode='info'
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='admin.billing.payment_info.creditCardAboutToExpire'
|
||||
defaultMessage='Your credit card is about to expire'
|
||||
/>
|
||||
}
|
||||
message={
|
||||
<FormattedMessage
|
||||
id='admin.billing.payment_info.creditCardAboutToExpire.description'
|
||||
defaultMessage='Please update your payment information to avoid any disruption.'
|
||||
/>
|
||||
}
|
||||
onDismiss={() => setShowCreditCardBanner(false)}
|
||||
/>
|
||||
)}
|
||||
{customerError ? <CloudFetchError/> : <PaymentInfoDisplay/>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PaymentInfo;
|
||||
@@ -1,149 +0,0 @@
|
||||
.PaymentInfoDisplay {
|
||||
border: 1px solid rgba(var(--sys-center-channel-color-rgb), 0.08);
|
||||
border-radius: 4px;
|
||||
background-color: var(--sys-center-channel-bg);
|
||||
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.08);
|
||||
color: var(--sys-center-channel-color);
|
||||
}
|
||||
|
||||
.PaymentInfoDisplay__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 28px 32px 24px 32px;
|
||||
border-bottom: 1px solid rgba(var(--sys-center-channel-color-rgb), 0.08);
|
||||
}
|
||||
|
||||
.PaymentInfoDisplay__headerText-top {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.PaymentInfoDisplay__headerText-bottom {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.PaymentInfoDisplay__addInfo {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.PaymentInfoDisplay__addInfoButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
background: var(--sys-button-bg);
|
||||
color: var(--sys-button-color);
|
||||
|
||||
&:hover:not(.disabled) {
|
||||
background: linear-gradient(0deg, rgba(var(--sys-center-channel-color-rgb), 0.16), rgba(var(--sys-center-channel-color-rgb), 0.16)), var(--sys-button-bg);
|
||||
color: var(--sys-button-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: linear-gradient(0deg, rgba(var(--sys-center-channel-color-rgb), 0.32), rgba(var(--sys-center-channel-color-rgb), 0.32)), var(--sys-button-bg);
|
||||
color: var(--sys-button-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: inset 0 0 0 2px var(--sys-sidebar-text-active-border);
|
||||
color: var(--sys-button-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background: rgba(var(--sys-center-channel-color-rgb), 0.08);
|
||||
color: rgba(var(--sys-center-channel-color-rgb), 0.32);
|
||||
}
|
||||
|
||||
> i {
|
||||
font-size: 14.4px;
|
||||
line-height: 17px;
|
||||
|
||||
&::before {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
> span {
|
||||
margin-left: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 9px;
|
||||
}
|
||||
}
|
||||
|
||||
.PaymentInfoDisplay__noPaymentInfo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 48px;
|
||||
}
|
||||
|
||||
.PaymentInfoDisplay__noPaymentInfo-message {
|
||||
margin-top: 24px;
|
||||
color: var(--sys-center-channel-color);
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.PaymentInfoDisplay__noPaymentInfo-link {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 12px;
|
||||
color: var(--sys-button-bg);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.PaymentInfoDisplay__paymentInfo {
|
||||
display: flex;
|
||||
padding: 28px 45px 30px 32px;
|
||||
color: var(--sys-center-channel-color);
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
|
||||
.CardImage {
|
||||
max-width: 55px;
|
||||
max-height: 37px;
|
||||
}
|
||||
}
|
||||
|
||||
.PaymentInfoDisplay__paymentInfo-name {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.PaymentInfoDisplay__paymentInfo-addressTitle {
|
||||
margin-top: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.PaymentInfoDisplay__paymentInfo-address > div {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.PaymentInfoDisplay__paymentInfo-edit {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.PaymentInfoDisplay__paymentInfo-editButton {
|
||||
padding: 2px;
|
||||
border-radius: 4px;
|
||||
margin-left: 12px;
|
||||
color: rgba(var(--sys-center-channel-color-rgb), 0.75);
|
||||
font-size: 18px;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: rgba(var(--sys-center-channel-color-rgb), 0.08);
|
||||
color: rgba(var(--sys-center-channel-color-rgb), 0.75);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.PaymentInfoDisplay__paymentInfo-cardInfo::first-letter {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import {trackEvent} from 'actions/telemetry_actions';
|
||||
|
||||
import BlockableLink from 'components/admin_console/blockable_link';
|
||||
import useGetSubscription from 'components/common/hooks/useGetSubscription';
|
||||
import useOpenCloudPurchaseModal from 'components/common/hooks/useOpenCloudPurchaseModal';
|
||||
import CreditCardSvg from 'components/common/svg_images_components/credit_card_svg';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import PaymentDetails from './payment_details';
|
||||
|
||||
import './payment_info_display.scss';
|
||||
|
||||
const addInfoButton = (
|
||||
<div className='PaymentInfoDisplay__addInfo'>
|
||||
<BlockableLink
|
||||
to='/admin_console/billing/payment_info_edit'
|
||||
className='PaymentInfoDisplay__addInfoButton'
|
||||
onClick={() => trackEvent('cloud_admin', 'click_add_credit_card')}
|
||||
>
|
||||
<i className='icon icon-plus'/>
|
||||
<FormattedMessage
|
||||
id='admin.billing.payment_info.add'
|
||||
defaultMessage='Add a Credit Card'
|
||||
/>
|
||||
</BlockableLink>
|
||||
</div>
|
||||
);
|
||||
|
||||
const noPaymentInfoSection = (
|
||||
<div className='PaymentInfoDisplay__noPaymentInfo'>
|
||||
<CreditCardSvg
|
||||
width={280}
|
||||
height={190}
|
||||
/>
|
||||
<div className='PaymentInfoDisplay__noPaymentInfo-message'>
|
||||
<FormattedMessage
|
||||
id='admin.billing.payment_info_display.noPaymentInfo'
|
||||
defaultMessage='There are currently no credit cards on file.'
|
||||
/>
|
||||
</div>
|
||||
<BlockableLink
|
||||
to='/admin_console/billing/payment_info_edit'
|
||||
className='PaymentInfoDisplay__noPaymentInfo-link'
|
||||
onClick={() => trackEvent('cloud_admin', 'click_add_credit_card')}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.billing.payment_info.add'
|
||||
defaultMessage='Add a Credit Card'
|
||||
/>
|
||||
</BlockableLink>
|
||||
</div>
|
||||
);
|
||||
|
||||
const PaymentInfoDisplay: React.FC = () => {
|
||||
const paymentInfo = useSelector((state: GlobalState) => state.entities.cloud.customer);
|
||||
const subscription = useGetSubscription();
|
||||
const openPurchaseModal = useOpenCloudPurchaseModal({});
|
||||
if (!paymentInfo || !subscription) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let body = noPaymentInfoSection;
|
||||
|
||||
if (paymentInfo?.payment_method && paymentInfo?.billing_address) {
|
||||
body = (
|
||||
<div className='PaymentInfoDisplay__paymentInfo'>
|
||||
<PaymentDetails/>
|
||||
<div className='PaymentInfoDisplay__paymentInfo-edit'>
|
||||
{subscription.delinquent_since ? (
|
||||
<div
|
||||
className='PaymentInfoDisplay__paymentInfo-editButton'
|
||||
onClick={() => openPurchaseModal({trackingLocation: 'edit_payment_info'})}
|
||||
>
|
||||
<i className='icon icon-pencil-outline'/>
|
||||
</div>
|
||||
) : (
|
||||
<BlockableLink
|
||||
to='/admin_console/billing/payment_info_edit'
|
||||
className='PaymentInfoDisplay__paymentInfo-editButton'
|
||||
>
|
||||
<i className='icon icon-pencil-outline'/>
|
||||
</BlockableLink>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='PaymentInfoDisplay'>
|
||||
<div className='PaymentInfoDisplay__header'>
|
||||
<div className='PaymentInfoDisplay__headerText'>
|
||||
<div className='PaymentInfoDisplay__headerText-top'>
|
||||
<FormattedMessage
|
||||
id='admin.billing.payment_info_display.savedPaymentDetails'
|
||||
defaultMessage='Your saved payment details'
|
||||
/>
|
||||
</div>
|
||||
<div className='PaymentInfoDisplay__headerText-bottom'>
|
||||
<FormattedMessage
|
||||
id='admin.billing.payment_info_display.allCardsAccepted'
|
||||
defaultMessage='All major credit cards are accepted.'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!(paymentInfo?.payment_method && paymentInfo?.billing_address) && addInfoButton}
|
||||
</div>
|
||||
<div className='PaymentInfoDisplay__body'>
|
||||
{body}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PaymentInfoDisplay;
|
||||
@@ -1,57 +0,0 @@
|
||||
.PaymentInfoEdit__card {
|
||||
padding: 28px 32px;
|
||||
border: 1px solid rgba(var(--sys-center-channel-color-rgb), 0.08);
|
||||
border-radius: 4px;
|
||||
margin-top: 20px;
|
||||
background-color: var(--sys-center-channel-bg);
|
||||
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.PaymentInfoEdit__paymentForm {
|
||||
max-width: 480px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
.section-title:not(:first-child) {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
color: var(--sys-center-channel-text);
|
||||
}
|
||||
|
||||
.Input_fieldset {
|
||||
background: var(--sys-center-channel-bg);
|
||||
}
|
||||
|
||||
&.Input_fieldset:focus-within {
|
||||
box-shadow: inset 0 0 0 2px var(--sys-button-bg);
|
||||
}
|
||||
|
||||
&.Input_fieldset___error {
|
||||
box-shadow: inset 0 0 0 1px var(--sys-error-text);
|
||||
color: var(--sys-error-text);
|
||||
}
|
||||
|
||||
&.Input_fieldset___error:focus-within {
|
||||
box-shadow: inset 0 0 0 2px var(--sys-error-text);
|
||||
color: var(--sys-error-text);
|
||||
}
|
||||
|
||||
.Input::placeholder {
|
||||
color: var(--sys-center-channel-color);
|
||||
}
|
||||
}
|
||||
|
||||
.PaymentInfoEdit__error {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--sys-error-text);
|
||||
font-size: 12px;
|
||||
|
||||
i {
|
||||
margin-right: 4px;
|
||||
font-size: 14.4px;
|
||||
}
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Elements} from '@stripe/react-stripe-js';
|
||||
import type {Stripe} from '@stripe/stripe-js';
|
||||
import {loadStripe} from '@stripe/stripe-js/pure'; // https://github.com/stripe/stripe-js#importing-loadstripe-without-side-effects
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
import {useHistory} from 'react-router-dom';
|
||||
|
||||
import {getCloudCustomer} from 'mattermost-redux/actions/cloud';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {completeStripeAddPaymentMethod} from 'actions/cloud';
|
||||
import {isCwsMockMode} from 'selectors/cloud';
|
||||
|
||||
import BlockableLink from 'components/admin_console/blockable_link';
|
||||
import AlertBanner from 'components/alert_banner';
|
||||
import ExternalLink from 'components/external_link';
|
||||
import FormattedMarkdownMessage from 'components/formatted_markdown_message';
|
||||
import PaymentForm from 'components/payment_form/payment_form';
|
||||
import {STRIPE_CSS_SRC, getStripePublicKey} from 'components/payment_form/stripe';
|
||||
import SaveButton from 'components/save_button';
|
||||
import AdminHeader from 'components/widgets/admin_console/admin_header';
|
||||
|
||||
import {CloudLinks} from 'utils/constants';
|
||||
|
||||
import {areBillingDetailsValid} from 'types/cloud/sku';
|
||||
import type {BillingDetails} from 'types/cloud/sku';
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import './payment_info_edit.scss';
|
||||
|
||||
let stripePromise: Promise<Stripe | null>;
|
||||
|
||||
const PaymentInfoEdit: React.FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
const history = useHistory();
|
||||
|
||||
const cwsMockMode = useSelector(isCwsMockMode);
|
||||
const paymentInfo = useSelector((state: GlobalState) => state.entities.cloud.customer);
|
||||
const theme = useSelector(getTheme);
|
||||
|
||||
const [showCreditCardWarning, setShowCreditCardWarning] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isValid, setIsValid] = useState<boolean | undefined>(undefined);
|
||||
const [isServerError, setIsServerError] = useState(false);
|
||||
const [billingDetails, setBillingDetails] = useState<BillingDetails>({
|
||||
address: paymentInfo?.billing_address?.line1 || '',
|
||||
address2: paymentInfo?.billing_address?.line2 || '',
|
||||
city: paymentInfo?.billing_address?.city || '',
|
||||
state: paymentInfo?.billing_address?.state || '',
|
||||
country: paymentInfo?.billing_address?.country || '',
|
||||
postalCode: paymentInfo?.billing_address?.postal_code || '',
|
||||
name: '',
|
||||
card: {} as any,
|
||||
});
|
||||
|
||||
const stripePublicKey = useSelector((state: GlobalState) => getStripePublicKey(state));
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getCloudCustomer());
|
||||
}, []);
|
||||
|
||||
const onPaymentInput = (billing: BillingDetails) => {
|
||||
setIsServerError(false);
|
||||
setIsValid(areBillingDetailsValid(billing));
|
||||
setBillingDetails(billing);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setIsSaving(true);
|
||||
const setPaymentMethod = completeStripeAddPaymentMethod((await stripePromise)!, billingDetails!, cwsMockMode);
|
||||
const success = await setPaymentMethod();
|
||||
|
||||
if (success) {
|
||||
history.push('/admin_console/billing/payment_info');
|
||||
} else {
|
||||
setIsServerError(true);
|
||||
}
|
||||
|
||||
setIsSaving(false);
|
||||
};
|
||||
|
||||
if (!stripePromise) {
|
||||
stripePromise = loadStripe(stripePublicKey);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='wrapper--fixed PaymentInfoEdit'>
|
||||
<AdminHeader withBackButton={true}>
|
||||
<div>
|
||||
<BlockableLink
|
||||
to='/admin_console/billing/payment_info'
|
||||
className='fa fa-angle-left back'
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.billing.payment_info_edit.title'
|
||||
defaultMessage='Edit Payment Information'
|
||||
/>
|
||||
</div>
|
||||
</AdminHeader>
|
||||
<div className='admin-console__wrapper'>
|
||||
<div className='admin-console__content'>
|
||||
{showCreditCardWarning &&
|
||||
<AlertBanner
|
||||
mode='info'
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='admin.billing.payment_info_edit.creditCardWarningTitle'
|
||||
defaultMessage='NOTE: Your card will not be charged at this time'
|
||||
/>
|
||||
}
|
||||
message={
|
||||
<>
|
||||
<FormattedMarkdownMessage
|
||||
id='admin.billing.payment_info_edit.creditCardWarningDescription'
|
||||
defaultMessage='Your credit card will be charged based on the number of users you have at the end of the monthly billing cycle. '
|
||||
/>
|
||||
<ExternalLink
|
||||
location='payment_info_edit'
|
||||
href={CloudLinks.BILLING_DOCS}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscription.planDetails.howBillingWorks'
|
||||
defaultMessage='See how billing works'
|
||||
/>
|
||||
</ExternalLink>
|
||||
</>
|
||||
}
|
||||
onDismiss={() => setShowCreditCardWarning(false)}
|
||||
/>
|
||||
}
|
||||
<div className='PaymentInfoEdit__card'>
|
||||
<Elements
|
||||
options={{fonts: [{cssSrc: STRIPE_CSS_SRC}]}}
|
||||
stripe={stripePromise}
|
||||
>
|
||||
<PaymentForm
|
||||
className='PaymentInfoEdit__paymentForm'
|
||||
onInputChange={onPaymentInput}
|
||||
initialBillingDetails={billingDetails}
|
||||
theme={theme}
|
||||
/>
|
||||
</Elements>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='admin-console-save'>
|
||||
<SaveButton
|
||||
saving={isSaving}
|
||||
disabled={!billingDetails || !isValid}
|
||||
onClick={handleSubmit}
|
||||
defaultMessage={(
|
||||
<FormattedMessage
|
||||
id='admin.billing.payment_info_edit.save'
|
||||
defaultMessage='Save credit card'
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<BlockableLink
|
||||
className='cancel-button'
|
||||
to='/admin_console/billing/payment_info'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.billing.payment_info_edit.cancel'
|
||||
defaultMessage='Cancel'
|
||||
/>
|
||||
</BlockableLink>
|
||||
{isValid === false &&
|
||||
<span className='PaymentInfoEdit__error'>
|
||||
<i className='icon icon-alert-outline'/>
|
||||
<FormattedMessage
|
||||
id='admin.billing.payment_info_edit.formError'
|
||||
defaultMessage='There are errors in the form above'
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
{isServerError &&
|
||||
<span className='PaymentInfoEdit__error'>
|
||||
<i className='icon icon-alert-outline'/>
|
||||
<FormattedMessage
|
||||
id='admin.billing.payment_info_edit.serverError'
|
||||
defaultMessage='Something went wrong while saving payment infomation'
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PaymentInfoEdit;
|
||||
@@ -12,17 +12,15 @@ import {trackEvent} from 'actions/telemetry_actions';
|
||||
|
||||
import {EmbargoedEntityTrialError} from 'components/admin_console/license_settings/trial_banner/trial_banner';
|
||||
import AlertBanner from 'components/alert_banner';
|
||||
import ContactUsButton from 'components/announcement_bar/contact_sales/contact_us';
|
||||
import PurchaseLink from 'components/announcement_bar/purchase_link/purchase_link';
|
||||
import CloudStartTrialButton from 'components/cloud_start_trial/cloud_start_trial_btn';
|
||||
import ExternalLink from 'components/external_link';
|
||||
import FormattedMarkdownMessage from 'components/formatted_markdown_message';
|
||||
import StartTrialBtn from 'components/learn_more_trial_modal/start_trial_btn';
|
||||
import PurchaseModal from 'components/purchase_modal';
|
||||
import LoadingSpinner from 'components/widgets/loading/loading_spinner';
|
||||
|
||||
import {FREEMIUM_TO_ENTERPRISE_TRIAL_LENGTH_DAYS} from 'utils/cloud_utils';
|
||||
import {ModalIdentifiers, TELEMETRY_CATEGORIES, AboutLinks, LicenseLinks, LicenseSkus} from 'utils/constants';
|
||||
import {TELEMETRY_CATEGORIES, AboutLinks, LicenseLinks, LicenseSkus} from 'utils/constants';
|
||||
import {goToMattermostContactSalesForm} from 'utils/contact_support_sales';
|
||||
import * as Utils from 'utils/utils';
|
||||
|
||||
@@ -82,23 +80,6 @@ export default class FeatureDiscovery extends React.PureComponent<Props, State>
|
||||
this.props.actions.getPrevTrialLicense();
|
||||
}
|
||||
|
||||
openUpgradeModal = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
trackEvent(
|
||||
TELEMETRY_CATEGORIES.CLOUD_ADMIN,
|
||||
'click_subscribe_from_feature_discovery',
|
||||
);
|
||||
|
||||
this.props.actions.openModal({
|
||||
modalId: ModalIdentifiers.CLOUD_PURCHASE,
|
||||
dialogType: PurchaseModal,
|
||||
dialogProps: {
|
||||
callerCTA: 'feature_discovery_subscribe_button',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
contactSalesFunc = () => {
|
||||
const {customer, isCloud} = this.props;
|
||||
const customerEmail = customer?.email || '';
|
||||
@@ -157,9 +138,6 @@ export default class FeatureDiscovery extends React.PureComponent<Props, State>
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ContactUsButton
|
||||
eventID='post_trial_contact_sales'
|
||||
/>
|
||||
</>
|
||||
|
||||
</div>
|
||||
@@ -172,7 +150,6 @@ export default class FeatureDiscovery extends React.PureComponent<Props, State>
|
||||
isCloudTrial,
|
||||
hadPrevCloudTrial,
|
||||
isPaidSubscription,
|
||||
minimumSKURequiredForFeature,
|
||||
} = this.props;
|
||||
|
||||
const canRequestCloudFreeTrial = isCloud && !isCloudTrial && !hadPrevCloudTrial && !isPaidSubscription;
|
||||
@@ -217,42 +194,6 @@ export default class FeatureDiscovery extends React.PureComponent<Props, State>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
} else if (hadPrevCloudTrial) {
|
||||
// if it is cloud, but this account already had a free trial, then the cta button must be Upgrade now
|
||||
ctaPrimaryButton = (
|
||||
<button
|
||||
className='btn btn-primary'
|
||||
data-testid='featureDiscovery_primaryCallToAction'
|
||||
onClick={this.openUpgradeModal}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.ldap_feature_discovery_cloud.call_to_action.primary'
|
||||
defaultMessage='Upgrade now'
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
|
||||
if (minimumSKURequiredForFeature === LicenseSkus.Enterprise) {
|
||||
ctaPrimaryButton = (
|
||||
<button
|
||||
className='btn btn-primary'
|
||||
data-testid='featureDiscovery_primaryCallToAction'
|
||||
onClick={() => {
|
||||
if (isCloud) {
|
||||
trackEvent(TELEMETRY_CATEGORIES.CLOUD_ADMIN, 'click_enterprise_contact_sales_feature_discovery');
|
||||
} else {
|
||||
trackEvent(TELEMETRY_CATEGORIES.SELF_HOSTED_ADMIN, 'click_enterprise_contact_sales_feature_discovery');
|
||||
}
|
||||
this.contactSalesFunc();
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.ldap_feature_discovery_cloud.call_to_action.primary_sales'
|
||||
defaultMessage='Contact sales'
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,23 +5,16 @@ import classNames from 'classnames';
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import type {RefObject} from 'react';
|
||||
import {FormattedDate, FormattedMessage, FormattedNumber, FormattedTime, defineMessages, useIntl} from 'react-intl';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import type {ClientLicense} from '@mattermost/types/config';
|
||||
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/admin';
|
||||
|
||||
import {trackEvent} from 'actions/telemetry_actions';
|
||||
import {getExpandSeatsLink} from 'selectors/cloud';
|
||||
|
||||
import useCanSelfHostedExpand from 'components/common/hooks/useCanSelfHostedExpand';
|
||||
import useControlSelfHostedExpansionModal from 'components/common/hooks/useControlSelfHostedExpansionModal';
|
||||
import useOpenPricingModal from 'components/common/hooks/useOpenPricingModal';
|
||||
import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink';
|
||||
import Tag from 'components/widgets/tag/tag';
|
||||
|
||||
import {FileTypes, TELEMETRY_CATEGORIES} from 'utils/constants';
|
||||
import {useQuery} from 'utils/http_utils';
|
||||
import {FileTypes} from 'utils/constants';
|
||||
import {calculateOverageUserActivated} from 'utils/overage_team';
|
||||
import {getSkuDisplayName} from 'utils/subscription';
|
||||
import {getRemainingDaysFromFutureTimestamp, toTitleCase} from 'utils/utils';
|
||||
@@ -63,20 +56,7 @@ const EnterpriseEditionLeftPanel = ({
|
||||
const {formatMessage} = useIntl();
|
||||
const [unsanitizedLicense, setUnsanitizedLicense] = useState(license);
|
||||
const openPricingModal = useOpenPricingModal();
|
||||
const canExpand = useCanSelfHostedExpand();
|
||||
const selfHostedExpansionModal = useControlSelfHostedExpansionModal({trackingLocation: 'license_settings_add_seats'});
|
||||
const expandableLink = useSelector(getExpandSeatsLink);
|
||||
const isSelfHostedPurchaseEnabled = useSelector(getConfig)?.ServiceSettings?.SelfHostedPurchase;
|
||||
|
||||
const query = useQuery();
|
||||
const actionQueryParam = query.get('action');
|
||||
|
||||
useEffect(() => {
|
||||
if (actionQueryParam === 'show_expansion_modal' && canExpand && isSelfHostedPurchaseEnabled) {
|
||||
selfHostedExpansionModal.open();
|
||||
query.set('action', '');
|
||||
}
|
||||
}, []);
|
||||
const [openContactSales] = useOpenSalesLink();
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchUnSanitizedLicense() {
|
||||
@@ -106,15 +86,6 @@ const EnterpriseEditionLeftPanel = ({
|
||||
</button>
|
||||
);
|
||||
|
||||
const handleClickAddSeats = () => {
|
||||
trackEvent(TELEMETRY_CATEGORIES.SELF_HOSTED_EXPANSION, 'add_seats_clicked');
|
||||
if (!isSelfHostedPurchaseEnabled || !canExpand) {
|
||||
window.open(expandableLink(unsanitizedLicense.Id), '_blank');
|
||||
} else {
|
||||
selfHostedExpansionModal.open();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className='EnterpriseEditionLeftPanel'
|
||||
@@ -155,17 +126,15 @@ const EnterpriseEditionLeftPanel = ({
|
||||
<div className='licenseInformation'>
|
||||
<div className='license-details-top'>
|
||||
<span className='title'>{'License details'}</span>
|
||||
{canExpand &&
|
||||
<button
|
||||
className='add-seats-button btn btn-primary'
|
||||
onClick={handleClickAddSeats}
|
||||
>
|
||||
<FormattedMessage
|
||||
id={'admin.license.enterpriseEdition.add.seats'}
|
||||
defaultMessage='+ Add seats'
|
||||
/>
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
className='add-seats-button btn btn-primary'
|
||||
onClick={openContactSales}
|
||||
>
|
||||
<FormattedMessage
|
||||
id={'admin.license.enterpriseEdition.add.seats'}
|
||||
defaultMessage='+ Add seats'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
{
|
||||
renderLicenseContent(
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
.purchase_buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
|
||||
button {
|
||||
@@ -54,7 +54,7 @@
|
||||
|
||||
&.contact-us {
|
||||
width: 130px;
|
||||
margin-left: 15px;
|
||||
margin-left: 0px !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import React, {memo} from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import ContactUsButton from 'components/announcement_bar/contact_sales/contact_us';
|
||||
import PurchaseLink from 'components/announcement_bar/purchase_link/purchase_link';
|
||||
import WomanUpArrowsAndCloudsSvg from 'components/common/svg_images_components/woman_up_arrows_and_clouds_svg';
|
||||
|
||||
const StarterRightPanel = () => {
|
||||
@@ -44,15 +43,6 @@ const StarterRightPanel = () => {
|
||||
})}
|
||||
</div>
|
||||
<div className='purchase_buttons'>
|
||||
<PurchaseLink
|
||||
eventID='post_trial_purchase_license'
|
||||
buttonTextElement={
|
||||
<FormattedMessage
|
||||
id='admin.license.trialCard.purchase'
|
||||
defaultMessage='Purchase'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ContactUsButton
|
||||
eventID='post_trial_contact_sales'
|
||||
/>
|
||||
|
||||
Ссылка в новой задаче
Block a user