mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
[CLD-6537] Cloud Annual Renewals - Banners Implementation (#25267)
* Initial implementation of Cloud Annual Renewal flow - system banners for admins and end users * Actually add the file * Add AlertBanner to system console billing_subscriptions page * Fixes for non-admin banner and pipeline * Updates to coincide with the CWS side of thigns * Add a feature flag for the 60 day experience to ensure banners don't show accidentally * Fix tests --------- Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
parent
95670abcea
commit
8b07655a65
@ -96,20 +96,26 @@ func getSubscription(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
ProductID: subscription.ProductID,
|
||||
IsFreeTrial: subscription.IsFreeTrial,
|
||||
TrialEndAt: subscription.TrialEndAt,
|
||||
EndAt: subscription.EndAt,
|
||||
CancelAt: subscription.CancelAt,
|
||||
DelinquentSince: subscription.DelinquentSince,
|
||||
CustomerID: "",
|
||||
AddOns: []string{},
|
||||
StartAt: 0,
|
||||
EndAt: 0,
|
||||
CreateAt: 0,
|
||||
Seats: 0,
|
||||
Status: "",
|
||||
DNS: "",
|
||||
LastInvoice: &model.Invoice{},
|
||||
DelinquentSince: subscription.DelinquentSince,
|
||||
BillingType: "",
|
||||
}
|
||||
}
|
||||
|
||||
if !c.App.Config().FeatureFlags.CloudAnnualRenewals {
|
||||
subscription.WillRenew = ""
|
||||
subscription.CancelAt = nil
|
||||
}
|
||||
|
||||
json, err := json.Marshal(subscription)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("Api4.getSubscription", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
|
@ -154,7 +154,7 @@ func Test_GetSubscription(t *testing.T) {
|
||||
ProductID: "SomeProductId",
|
||||
AddOns: []string{},
|
||||
StartAt: 0,
|
||||
EndAt: 0,
|
||||
EndAt: 2000000000,
|
||||
CreateAt: 0,
|
||||
Seats: 0,
|
||||
IsFreeTrial: "true",
|
||||
|
@ -180,6 +180,8 @@ type Subscription struct {
|
||||
OriginallyLicensedSeats int `json:"originally_licensed_seats"`
|
||||
ComplianceBlocked string `json:"compliance_blocked"`
|
||||
BillingType string `json:"billing_type"`
|
||||
CancelAt *int64 `json:"cancel_at"`
|
||||
WillRenew string `json:"will_renew"`
|
||||
}
|
||||
|
||||
// Subscription History model represents true up event in a yearly subscription
|
||||
|
@ -46,6 +46,8 @@ type FeatureFlags struct {
|
||||
|
||||
CloudIPFiltering bool
|
||||
ConsumePostHook bool
|
||||
|
||||
CloudAnnualRenewals bool
|
||||
}
|
||||
|
||||
func (f *FeatureFlags) SetDefaults() {
|
||||
@ -63,6 +65,7 @@ func (f *FeatureFlags) SetDefaults() {
|
||||
f.StreamlinedMarketplace = true
|
||||
f.CloudIPFiltering = false
|
||||
f.ConsumePostHook = false
|
||||
f.CloudAnnualRenewals = false
|
||||
}
|
||||
|
||||
// ToMap returns the feature flags as a map[string]string
|
||||
|
@ -0,0 +1,108 @@
|
||||
// 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 31 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 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();
|
||||
});
|
||||
|
||||
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 6 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,10 +2,16 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {FormattedMessage, useIntl} 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 {daysToExpiration} from 'utils/cloud_utils';
|
||||
|
||||
export const creditCardExpiredBanner = (setShowCreditCardBanner: (value: boolean) => void) => {
|
||||
return (
|
||||
@ -53,3 +59,57 @@ export const paymentFailedBanner = () => {
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const CloudAnnualRenewalBanner = () => {
|
||||
// TODO: Update with renewal modal
|
||||
const openPurchaseModal = useOpenCloudPurchaseModal({});
|
||||
const subscription = useGetSubscription();
|
||||
const {formatMessage} = useIntl();
|
||||
const [openSalesLink] = useOpenSalesLink();
|
||||
if (!subscription || !subscription.cancel_at) {
|
||||
return null;
|
||||
}
|
||||
const daysUntilExpiration = daysToExpiration(subscription?.end_at * 1000);
|
||||
const daysUntilCancelation = daysToExpiration(subscription?.cancel_at * 1000);
|
||||
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 (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}
|
||||
/>
|
||||
|
||||
);
|
||||
};
|
||||
|
@ -37,6 +37,7 @@ import {hasSomeLimits} from 'utils/limits';
|
||||
import {getRemainingDaysFromFutureTimestamp} from 'utils/utils';
|
||||
|
||||
import {
|
||||
CloudAnnualRenewalBanner,
|
||||
creditCardExpiredBanner,
|
||||
paymentFailedBanner,
|
||||
} from './billing_subscriptions';
|
||||
@ -138,6 +139,7 @@ const BillingSubscriptions = () => {
|
||||
product={product}
|
||||
/>
|
||||
{shouldShowPaymentFailedBanner() && paymentFailedBanner()}
|
||||
{<CloudAnnualRenewalBanner/>}
|
||||
{<ToYearlyNudgeBanner/>}
|
||||
{<ToPaidNudgeBanner/>}
|
||||
{showCreditCardBanner &&
|
||||
|
@ -9,6 +9,7 @@ import {ToPaidPlanBannerDismissable} from 'components/admin_console/billing/bill
|
||||
import {ToYearlyNudgeBannerDismissable} from 'components/admin_console/billing/billing_subscriptions/to_yearly_nudge_banner';
|
||||
import withGetCloudSubscription from 'components/common/hocs/cloud/with_get_cloud_subscription';
|
||||
|
||||
import CloudAnnualRenewalAnnouncementBar from './cloud_annual_renewal';
|
||||
import CloudDelinquencyAnnouncementBar from './cloud_delinquency';
|
||||
import CloudTrialAnnouncementBar from './cloud_trial_announcement_bar';
|
||||
import CloudTrialEndAnnouncementBar from './cloud_trial_ended_announcement_bar';
|
||||
@ -70,6 +71,7 @@ class AnnouncementBarController extends React.PureComponent<Props> {
|
||||
let cloudTrialAnnouncementBar = null;
|
||||
let cloudTrialEndAnnouncementBar = null;
|
||||
let cloudDelinquencyAnnouncementBar = null;
|
||||
let cloudRenewalAnnouncementBar = null;
|
||||
let notifyAdminDowngradeDelinquencyBar = null;
|
||||
let toYearlyNudgeBannerDismissable = null;
|
||||
let toPaidPlanNudgeBannerDismissable = null;
|
||||
@ -86,6 +88,9 @@ class AnnouncementBarController extends React.PureComponent<Props> {
|
||||
cloudDelinquencyAnnouncementBar = (
|
||||
<CloudDelinquencyAnnouncementBar/>
|
||||
);
|
||||
cloudRenewalAnnouncementBar = (
|
||||
<CloudAnnualRenewalAnnouncementBar/>
|
||||
);
|
||||
notifyAdminDowngradeDelinquencyBar = (
|
||||
<NotifyAdminDowngradeDelinquencyBar/>
|
||||
);
|
||||
@ -108,6 +113,7 @@ class AnnouncementBarController extends React.PureComponent<Props> {
|
||||
{cloudTrialAnnouncementBar}
|
||||
{cloudTrialEndAnnouncementBar}
|
||||
{cloudDelinquencyAnnouncementBar}
|
||||
{cloudRenewalAnnouncementBar}
|
||||
{notifyAdminDowngradeDelinquencyBar}
|
||||
{toYearlyNudgeBannerDismissable}
|
||||
{toPaidPlanNudgeBannerDismissable}
|
||||
|
@ -0,0 +1,374 @@
|
||||
// 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 {CloudBanners, CloudProducts, Preferences} from 'utils/constants';
|
||||
|
||||
import CloudAnnualRenewalAnnouncementBar, {getCurrentYearAsString} from './index';
|
||||
|
||||
describe('components/announcement_bar/cloud_delinquency', () => {
|
||||
const initialState = {
|
||||
views: {
|
||||
announcementBar: {
|
||||
announcementBarState: {
|
||||
announcementBarCount: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
entities: {
|
||||
admin: {
|
||||
config: {
|
||||
FeatureFlags: {
|
||||
CloudAnnualRenewals: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
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: null,
|
||||
},
|
||||
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 show banner when feature flag is disabled time set', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.general.config = {
|
||||
...state.entities.admin.config,
|
||||
CloudAnnualRenewals: false,
|
||||
};
|
||||
|
||||
const {queryByText} = renderWithContext(
|
||||
<CloudAnnualRenewalAnnouncementBar/>,
|
||||
state,
|
||||
);
|
||||
|
||||
expect(queryByText('Your annual subscription expires in')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should not show banner when no cancel_at time set', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.cloud.subscription = {
|
||||
...state.entities.cloud.subscription,
|
||||
cancel_at: null,
|
||||
};
|
||||
|
||||
const {queryByText} = renderWithContext(
|
||||
<CloudAnnualRenewalAnnouncementBar/>,
|
||||
state,
|
||||
);
|
||||
|
||||
expect(queryByText('Your annual subscription expires in')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should show 60 day banner to admin when cancel_at time is set accordingly', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.cloud.subscription = {
|
||||
...state.entities.cloud.subscription,
|
||||
cancel_at: unixTimestampFromNow(69),
|
||||
end_at: unixTimestampFromNow(55),
|
||||
};
|
||||
|
||||
const {getByText} = renderWithContext(
|
||||
<CloudAnnualRenewalAnnouncementBar/>,
|
||||
state,
|
||||
);
|
||||
|
||||
expect(getByText('Your annual subscription expires in 56 days. Please renew to avoid any disruption.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should NOT show 60 day banner to non-admin when cancel_at time is set accordingly', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.cloud.subscription = {
|
||||
...state.entities.cloud.subscription,
|
||||
cancel_at: unixTimestampFromNow(69),
|
||||
end_at: unixTimestampFromNow(55),
|
||||
};
|
||||
|
||||
state.entities.users = {
|
||||
currentUserId: 'current_user_id',
|
||||
profiles: {
|
||||
current_user_id: {roles: 'user'},
|
||||
},
|
||||
};
|
||||
|
||||
const {queryByText} = renderWithContext(
|
||||
<CloudAnnualRenewalAnnouncementBar/>,
|
||||
state,
|
||||
);
|
||||
|
||||
expect(queryByText('Your annual subscription expires in')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should show 30 day banner to admin when cancel_at time is set accordingly', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.cloud.subscription = {
|
||||
...state.entities.cloud.subscription,
|
||||
cancel_at: unixTimestampFromNow(69),
|
||||
end_at: unixTimestampFromNow(25),
|
||||
};
|
||||
|
||||
const {getByText} = renderWithContext(
|
||||
<CloudAnnualRenewalAnnouncementBar/>,
|
||||
state,
|
||||
);
|
||||
|
||||
expect(getByText('Your annual subscription expires in 26 days. Please renew to avoid any disruption.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should NOT show 30 day banner to non-admin when cancel_at time is set accordingly', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.cloud.subscription = {
|
||||
...state.entities.cloud.subscription,
|
||||
cancel_at: unixTimestampFromNow(69),
|
||||
end_at: unixTimestampFromNow(25),
|
||||
};
|
||||
|
||||
state.entities.users = {
|
||||
currentUserId: 'current_user_id',
|
||||
profiles: {
|
||||
current_user_id: {roles: 'user'},
|
||||
},
|
||||
};
|
||||
|
||||
const {queryByText} = renderWithContext(
|
||||
<CloudAnnualRenewalAnnouncementBar/>,
|
||||
state,
|
||||
);
|
||||
|
||||
expect(queryByText('Your annual subscription expires in')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should show 7 day banner to admin when cancel_at time is set accordingly', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.cloud.subscription = {
|
||||
...state.entities.cloud.subscription,
|
||||
cancel_at: unixTimestampFromNow(69),
|
||||
end_at: unixTimestampFromNow(5),
|
||||
};
|
||||
|
||||
const {getByText} = renderWithContext(
|
||||
<CloudAnnualRenewalAnnouncementBar/>,
|
||||
state,
|
||||
);
|
||||
|
||||
expect(getByText('Your annual subscription expires in 6 days. Failure to renew will result in your workspace being deleted.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should NOT show 7 day banner to non admin when cancel_at time is set accordingly', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.cloud.subscription = {
|
||||
...state.entities.cloud.subscription,
|
||||
cancel_at: unixTimestampFromNow(69),
|
||||
end_at: unixTimestampFromNow(5),
|
||||
};
|
||||
|
||||
state.entities.users = {
|
||||
currentUserId: 'current_user_id',
|
||||
profiles: {
|
||||
current_user_id: {roles: 'user'},
|
||||
},
|
||||
};
|
||||
|
||||
const {queryByText} = renderWithContext(
|
||||
<CloudAnnualRenewalAnnouncementBar/>,
|
||||
state,
|
||||
);
|
||||
|
||||
expect(queryByText('Your annual subscription expires in')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should NOT show 7 day banner to admin when delinquent_since is set', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.cloud.subscription = {
|
||||
...state.entities.cloud.subscription,
|
||||
cancel_at: unixTimestampFromNow(69),
|
||||
end_at: unixTimestampFromNow(5),
|
||||
delinquent_since: unixTimestampFromNow(5),
|
||||
};
|
||||
|
||||
const {queryByText} = renderWithContext(
|
||||
<CloudAnnualRenewalAnnouncementBar/>,
|
||||
state,
|
||||
);
|
||||
|
||||
expect(queryByText('Your annual subscription expires in 6 days. Failure to renew will result in your workspace being deleted.')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should NOT show 60 day banner to admin when they dismissed the banner', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.cloud.subscription = {
|
||||
...state.entities.cloud.subscription,
|
||||
cancel_at: unixTimestampFromNow(69),
|
||||
end_at: unixTimestampFromNow(55),
|
||||
};
|
||||
|
||||
state.entities.preferences = {
|
||||
myPreferences: {
|
||||
[`${Preferences.CLOUD_ANNUAL_RENEWAL_BANNER}--${CloudBanners.ANNUAL_RENEWAL_60_DAY}_${getCurrentYearAsString()}`]: {
|
||||
user_id: 'rq7fq4hfjp8ifywsfwk114545a',
|
||||
category: 'cloud_annual_renewal_banner',
|
||||
name: 'annual_renewal_60_day_2023',
|
||||
value: 'true',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const {queryByText} = renderWithContext(
|
||||
<CloudAnnualRenewalAnnouncementBar/>,
|
||||
state,
|
||||
);
|
||||
|
||||
expect(queryByText('Your annual subscription expires in')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should NOT show 30 day banner to admin when they dismissed the banner', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.cloud.subscription = {
|
||||
...state.entities.cloud.subscription,
|
||||
cancel_at: unixTimestampFromNow(69),
|
||||
end_at: unixTimestampFromNow(25),
|
||||
};
|
||||
|
||||
state.entities.preferences = {
|
||||
myPreferences: {
|
||||
[`${Preferences.CLOUD_ANNUAL_RENEWAL_BANNER}--${CloudBanners.ANNUAL_RENEWAL_30_DAY}_${getCurrentYearAsString()}`]: {
|
||||
user_id: 'rq7fq4hfjp8ifywsfwk114545a',
|
||||
category: 'cloud_annual_renewal_banner',
|
||||
name: 'annual_renewal_30_day_2023',
|
||||
value: 'true',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const {queryByText} = renderWithContext(
|
||||
<CloudAnnualRenewalAnnouncementBar/>,
|
||||
state,
|
||||
);
|
||||
|
||||
expect(queryByText('Your annual subscription expires in')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should show 60 day banner to admin in 2023 when they dismissed the banner in 2022', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.cloud.subscription = {
|
||||
...state.entities.cloud.subscription,
|
||||
cancel_at: unixTimestampFromNow(69),
|
||||
end_at: unixTimestampFromNow(55),
|
||||
};
|
||||
|
||||
state.entities.preferences = {
|
||||
myPreferences: {
|
||||
[`${Preferences.CLOUD_ANNUAL_RENEWAL_BANNER}--${CloudBanners.ANNUAL_RENEWAL_60_DAY}_2022`]: {
|
||||
user_id: 'rq7fq4hfjp8ifywsfwk114545a',
|
||||
category: 'cloud_annual_renewal_banner',
|
||||
name: 'annual_renewal_60_day_2022',
|
||||
value: 'true',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const {queryByText} = renderWithContext(
|
||||
<CloudAnnualRenewalAnnouncementBar/>,
|
||||
state,
|
||||
);
|
||||
|
||||
expect(queryByText('Your annual subscription expires in 56 days. Please renew to avoid any disruption.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should show 30 day banner to admin in 2023 when they dismissed the banner in 2022', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.cloud.subscription = {
|
||||
...state.entities.cloud.subscription,
|
||||
cancel_at: unixTimestampFromNow(69),
|
||||
end_at: unixTimestampFromNow(25),
|
||||
};
|
||||
|
||||
state.entities.preferences = {
|
||||
myPreferences: {
|
||||
[`${Preferences.CLOUD_ANNUAL_RENEWAL_BANNER}--${CloudBanners.ANNUAL_RENEWAL_30_DAY}_2022`]: {
|
||||
user_id: 'rq7fq4hfjp8ifywsfwk114545a',
|
||||
category: 'cloud_annual_renewal_banner',
|
||||
name: 'annual_renewal_30_day_2022',
|
||||
value: 'true',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const {queryByText} = renderWithContext(
|
||||
<CloudAnnualRenewalAnnouncementBar/>,
|
||||
state,
|
||||
);
|
||||
|
||||
expect(queryByText('Your annual subscription expires in 26 days. Please renew to avoid any disruption.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should NOT show any banner if renewal date is more than 60 days away"', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.cloud.subscription = {
|
||||
...state.entities.cloud.subscription,
|
||||
cancel_at: unixTimestampFromNow(69),
|
||||
end_at: unixTimestampFromNow(75),
|
||||
};
|
||||
|
||||
const {queryByText} = renderWithContext(
|
||||
<CloudAnnualRenewalAnnouncementBar/>,
|
||||
state,
|
||||
);
|
||||
|
||||
expect(queryByText('Your annual subscription expires in')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should NOT show any banner if within renewal period but will_renew is set to "true"', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.cloud.subscription = {
|
||||
...state.entities.cloud.subscription,
|
||||
cancel_at: unixTimestampFromNow(69),
|
||||
end_at: unixTimestampFromNow(25),
|
||||
will_renew: 'true',
|
||||
};
|
||||
|
||||
const {queryByText} = renderWithContext(
|
||||
<CloudAnnualRenewalAnnouncementBar/>,
|
||||
state,
|
||||
);
|
||||
|
||||
expect(queryByText('Your annual subscription expires in')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -0,0 +1,132 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useMemo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
|
||||
import {InformationOutlineIcon} from '@mattermost/compass-icons/components';
|
||||
|
||||
import {savePreferences} from 'mattermost-redux/actions/preferences';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/admin';
|
||||
import {get} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserId, isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {useDelinquencySubscription} from 'components/common/hooks/useDelinquencySubscription';
|
||||
import useGetSubscription from 'components/common/hooks/useGetSubscription';
|
||||
import useOpenCloudPurchaseModal from 'components/common/hooks/useOpenCloudPurchaseModal';
|
||||
|
||||
import {daysToExpiration} from 'utils/cloud_utils';
|
||||
import {Preferences, AnnouncementBarTypes, CloudBanners} from 'utils/constants';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import AnnouncementBar from '../default_announcement_bar';
|
||||
|
||||
const between = (x: number, min: number, max: number) => {
|
||||
return x >= min && x <= max;
|
||||
};
|
||||
|
||||
export const getCurrentYearAsString = () => {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
return year.toString();
|
||||
};
|
||||
|
||||
const CloudAnnualRenewalAnnouncementBar = () => {
|
||||
const subscription = useGetSubscription();
|
||||
|
||||
// TODO: Update with renewal modal
|
||||
const openPurchaseModal = useOpenCloudPurchaseModal({});
|
||||
const {formatMessage} = useIntl();
|
||||
const {isDelinquencySubscription} = useDelinquencySubscription();
|
||||
const isAdmin = useSelector(isCurrentUserSystemAdmin);
|
||||
const dispatch = useDispatch();
|
||||
const currentUserId = useSelector(getCurrentUserId);
|
||||
const hasDismissed60DayBanner = useSelector((state: GlobalState) => get(state, Preferences.CLOUD_ANNUAL_RENEWAL_BANNER, `${CloudBanners.ANNUAL_RENEWAL_60_DAY}_${getCurrentYearAsString()}`)) === 'true';
|
||||
const hasDismissed30DayBanner = useSelector((state: GlobalState) => get(state, Preferences.CLOUD_ANNUAL_RENEWAL_BANNER, `${CloudBanners.ANNUAL_RENEWAL_30_DAY}_${getCurrentYearAsString()}`)) === 'true';
|
||||
const cloudAnnualRenewalsEnabled = useSelector(getConfig).FeatureFlags?.CloudAnnualRenewals;
|
||||
|
||||
const daysUntilExpiration = useMemo(() => {
|
||||
if (!subscription || !subscription.end_at || !subscription.cancel_at) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return daysToExpiration(subscription.end_at * 1000);
|
||||
}, [subscription]);
|
||||
|
||||
const handleDismiss = (banner: string) => {
|
||||
// We store the preference name with the current year as a string appended to the end,
|
||||
// so that next renewal period we can show the banner again despite the user having dismissed it in the previous year
|
||||
dispatch(savePreferences(currentUserId, [{
|
||||
category: Preferences.CLOUD_ANNUAL_RENEWAL_BANNER,
|
||||
name: `${banner}_${getCurrentYearAsString()}`,
|
||||
user_id: currentUserId,
|
||||
value: 'true',
|
||||
}]));
|
||||
};
|
||||
|
||||
const getBanner = useMemo(() => {
|
||||
const defaultProps = {
|
||||
showLinkAsButton: true,
|
||||
isTallBanner: true,
|
||||
icon: <i className='icon icon-alert-outline'/>,
|
||||
modalButtonText: formatMessage({id: 'cloud_annual_renewal.banner.buttonText.renew', defaultMessage: 'Renew'}),
|
||||
modalButtonDefaultText: 'Renew',
|
||||
message: <></>,
|
||||
onButtonClick: openPurchaseModal,
|
||||
handleClose: () => { },
|
||||
showCloseButton: true,
|
||||
};
|
||||
let bannerProps = {
|
||||
...defaultProps,
|
||||
type: '',
|
||||
};
|
||||
if (between(daysUntilExpiration, 31, 60) && !hasDismissed60DayBanner) {
|
||||
if (hasDismissed60DayBanner) {
|
||||
return null;
|
||||
}
|
||||
bannerProps = {
|
||||
...defaultProps,
|
||||
message: (<>{formatMessage({id: 'cloud_annual_renewal.banner.message.60', defaultMessage: 'Your annual subscription expires in {days} days. Please renew to avoid any disruption.'}, {days: daysUntilExpiration})}</>),
|
||||
icon: (<InformationOutlineIcon size={18}/>),
|
||||
type: AnnouncementBarTypes.ANNOUNCEMENT,
|
||||
handleClose: () => handleDismiss(CloudBanners.ANNUAL_RENEWAL_60_DAY),
|
||||
};
|
||||
} else if (between(daysUntilExpiration, 8, 30)) {
|
||||
if (hasDismissed30DayBanner) {
|
||||
return null;
|
||||
}
|
||||
bannerProps = {
|
||||
...defaultProps,
|
||||
message: (<>{formatMessage({id: 'cloud_annual_renewal.banner.message.30', defaultMessage: 'Your annual subscription expires in {days} days. Please renew to avoid any disruption.'}, {days: daysUntilExpiration})}</>),
|
||||
icon: (<InformationOutlineIcon size={18}/>),
|
||||
type: AnnouncementBarTypes.ADVISOR,
|
||||
handleClose: () => handleDismiss(CloudBanners.ANNUAL_RENEWAL_30_DAY),
|
||||
};
|
||||
} else if (between(daysUntilExpiration, 0, 7) && !isDelinquencySubscription()) {
|
||||
bannerProps = {
|
||||
...defaultProps,
|
||||
message: (<>{formatMessage({id: 'cloud_annual_renewal.banner.message.7', defaultMessage: 'Your annual subscription expires in {days} days. Failure to renew will result in your workspace being deleted.'}, {days: daysUntilExpiration})}</>),
|
||||
icon: (<i className='icon icon-alert-outline'/>),
|
||||
type: AnnouncementBarTypes.CRITICAL,
|
||||
showCloseButton: false,
|
||||
};
|
||||
}
|
||||
|
||||
return <AnnouncementBar {...bannerProps}/>;
|
||||
}, [daysUntilExpiration, hasDismissed60DayBanner, hasDismissed30DayBanner]);
|
||||
|
||||
// Delinquent subscriptions will have a cancel_at time, but the banner is handled separately
|
||||
if (!cloudAnnualRenewalsEnabled || !subscription || !subscription.cancel_at || subscription.will_renew === 'true' || isDelinquencySubscription() || !isAdmin || daysUntilExpiration > 60) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{getBanner}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CloudAnnualRenewalAnnouncementBar;
|
@ -2,15 +2,16 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import * as reactRedux from 'react-redux';
|
||||
|
||||
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
|
||||
import mockStore from 'tests/test_store';
|
||||
import {renderWithContext} from 'tests/react_testing_utils';
|
||||
import {CloudProducts} from 'utils/constants';
|
||||
|
||||
import CloudDelinquencyAnnouncementBar from './index';
|
||||
|
||||
describe('components/announcement_bar/cloud_delinquency', () => {
|
||||
const now = new Date();
|
||||
const fiveDaysAgo = new Date(now.getTime() - (5 * 24 * 60 * 60 * 1000)).getTime() / 1000;
|
||||
const fiveDaysFromNow = new Date(now.getTime() + (5 * 24 * 60 * 60 * 1000)).getTime() / 1000;
|
||||
const initialState = {
|
||||
views: {
|
||||
announcementBar: {
|
||||
@ -37,7 +38,8 @@ describe('components/announcement_bar/cloud_delinquency', () => {
|
||||
product_id: 'test_prod_1',
|
||||
trial_end_at: 1652807380,
|
||||
is_free_trial: 'false',
|
||||
delinquent_since: 1652807380, // may 17 2022
|
||||
delinquent_since: fiveDaysAgo, // may 17 2022
|
||||
cancel_at: fiveDaysFromNow, // may 17 2022
|
||||
},
|
||||
products: {
|
||||
test_prod_1: {
|
||||
@ -67,20 +69,30 @@ describe('components/announcement_bar/cloud_delinquency', () => {
|
||||
delinquent_since: null,
|
||||
};
|
||||
|
||||
jest.useFakeTimers().setSystemTime(new Date('2022-06-20'));
|
||||
|
||||
const store = mockStore(state);
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<reactRedux.Provider store={store}>
|
||||
<CloudDelinquencyAnnouncementBar/>
|
||||
</reactRedux.Provider>,
|
||||
const {queryByText} = renderWithContext(
|
||||
<CloudDelinquencyAnnouncementBar/>,
|
||||
state,
|
||||
);
|
||||
|
||||
expect(wrapper.find('AnnouncementBar').exists()).toEqual(false);
|
||||
expect(queryByText('Your annual subscription has expired')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should not show banner when user is not admin', () => {
|
||||
it('Should not show banner when no cancel_at time is set', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.cloud.subscription = {
|
||||
...state.entities.cloud.subscription,
|
||||
cancel_at: null,
|
||||
};
|
||||
|
||||
const {queryByText} = renderWithContext(
|
||||
<CloudDelinquencyAnnouncementBar/>,
|
||||
state,
|
||||
);
|
||||
|
||||
expect(queryByText('Your annual subscription has expired')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should show banner when user is not admin, but should not show CTA', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
state.entities.users = {
|
||||
currentUserId: 'current_user_id',
|
||||
@ -89,44 +101,24 @@ describe('components/announcement_bar/cloud_delinquency', () => {
|
||||
},
|
||||
};
|
||||
|
||||
jest.useFakeTimers().setSystemTime(new Date('2022-06-20'));
|
||||
|
||||
const store = mockStore(state);
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<reactRedux.Provider store={store}>
|
||||
<CloudDelinquencyAnnouncementBar/>
|
||||
</reactRedux.Provider>,
|
||||
const {queryByText, getByText} = renderWithContext(
|
||||
<CloudDelinquencyAnnouncementBar/>,
|
||||
state,
|
||||
);
|
||||
|
||||
expect(wrapper.find('AnnouncementBar').exists()).toEqual(false);
|
||||
expect(getByText('Your annual subscription has expired. Please contact your System Admin to keep this workspace')).toBeInTheDocument();
|
||||
expect(queryByText('Update billing now')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should match snapshot when delinquent < 90 days', () => {
|
||||
it('Should show banner and CTA when user is admin', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
const store = mockStore(state);
|
||||
jest.useFakeTimers().setSystemTime(new Date('2022-06-20'));
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<reactRedux.Provider store={store}>
|
||||
<CloudDelinquencyAnnouncementBar/>
|
||||
</reactRedux.Provider>,
|
||||
const {getByText} = renderWithContext(
|
||||
<CloudDelinquencyAnnouncementBar/>,
|
||||
state,
|
||||
);
|
||||
|
||||
expect(wrapper.find('.announcement-bar-advisor').exists()).toEqual(true);
|
||||
});
|
||||
|
||||
it('Should match snapshot when delinquent > 90 days', () => {
|
||||
const state = JSON.parse(JSON.stringify(initialState));
|
||||
const store = mockStore(state);
|
||||
jest.useFakeTimers().setSystemTime(new Date('2022-12-20'));
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<reactRedux.Provider store={store}>
|
||||
<CloudDelinquencyAnnouncementBar/>
|
||||
</reactRedux.Provider>,
|
||||
);
|
||||
|
||||
expect(wrapper.find('.announcement-bar-critical').exists()).toEqual(true);
|
||||
expect(getByText('Your annual subscription has expired. Please renew now to keep this workspace')).toBeInTheDocument();
|
||||
expect(getByText('Update billing now')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
@ -2,11 +2,10 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
|
||||
import {isSystemAdmin} from 'mattermost-redux/utils/user_utils';
|
||||
import {isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {trackEvent} from 'actions/telemetry_actions';
|
||||
|
||||
@ -17,9 +16,6 @@ import useOpenCloudPurchaseModal from 'components/common/hooks/useOpenCloudPurch
|
||||
import {
|
||||
AnnouncementBarTypes, TELEMETRY_CATEGORIES,
|
||||
} from 'utils/constants';
|
||||
import {t} from 'utils/i18n';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import AnnouncementBar from '../default_announcement_bar';
|
||||
|
||||
@ -27,65 +23,43 @@ const CloudDelinquencyAnnouncementBar = () => {
|
||||
const subscription = useGetSubscription();
|
||||
const openPurchaseModal = useOpenCloudPurchaseModal({});
|
||||
const {isDelinquencySubscription} = useDelinquencySubscription();
|
||||
const currentUser = useSelector((state: GlobalState) =>
|
||||
getCurrentUser(state),
|
||||
);
|
||||
const {formatMessage} = useIntl();
|
||||
const isAdmin = useSelector(isCurrentUserSystemAdmin);
|
||||
|
||||
const getBannerType = () => {
|
||||
const delinquencyDate = new Date(
|
||||
(subscription?.delinquent_since || 0) * 1000,
|
||||
);
|
||||
|
||||
const oneDay = 24 * 60 * 60 * 1000; // hours*minutes*seconds*milliseconds
|
||||
const today = new Date();
|
||||
const diffDays = Math.round(
|
||||
Math.abs((today.valueOf() - delinquencyDate.valueOf()) / oneDay),
|
||||
);
|
||||
if (diffDays > 90) {
|
||||
return AnnouncementBarTypes.CRITICAL;
|
||||
}
|
||||
return AnnouncementBarTypes.ADVISOR;
|
||||
};
|
||||
|
||||
if (!isDelinquencySubscription() || !isSystemAdmin(currentUser.roles)) {
|
||||
if (!isDelinquencySubscription() || !subscription?.cancel_at) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bannerType = getBannerType();
|
||||
|
||||
let message = {
|
||||
id: t('cloud_delinquency.banner.title'),
|
||||
defaultMessage:
|
||||
'Update your billing information now to keep paid features.',
|
||||
let props = {
|
||||
message: (<>{formatMessage({id: 'cloud_annual_renewal_delinquency.banner.message', defaultMessage: 'Your annual subscription has expired. Please renew now to keep this workspace'})}</>),
|
||||
modalButtonText: formatMessage({id: 'cloud_delinquency.banner.buttonText', defaultMessage: 'Update billing now'}),
|
||||
modalButtonDefaultText: 'Update billing now',
|
||||
showLinkAsButton: true,
|
||||
isTallBanner: true,
|
||||
icon: <i className='icon icon-alert-outline'/>,
|
||||
showCTA: true,
|
||||
onButtonClick: () => {
|
||||
trackEvent(TELEMETRY_CATEGORIES.CLOUD_DELINQUENCY, 'click_update_billing');
|
||||
openPurchaseModal({
|
||||
trackingLocation:
|
||||
'cloud_delinquency_announcement_bar',
|
||||
});
|
||||
},
|
||||
type: AnnouncementBarTypes.CRITICAL,
|
||||
showCloseButton: false,
|
||||
};
|
||||
|
||||
// If critical banner, wording is different
|
||||
if (bannerType === AnnouncementBarTypes.CRITICAL) {
|
||||
message = {
|
||||
id: t('cloud_delinquency.post_downgrade_banner.title'),
|
||||
defaultMessage:
|
||||
'Update your billing information now to re-activate paid features.',
|
||||
if (!isAdmin) {
|
||||
props = {
|
||||
...props,
|
||||
message: (<>{formatMessage({id: 'cloud_annual_renewal_delinquency.banner.end_user.message', defaultMessage: 'Your annual subscription has expired. Please contact your System Admin to keep this workspace'})}</>),
|
||||
showCTA: false,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<AnnouncementBar
|
||||
type={bannerType}
|
||||
showCloseButton={false}
|
||||
onButtonClick={() => {
|
||||
trackEvent(TELEMETRY_CATEGORIES.CLOUD_DELINQUENCY, 'click_update_billing');
|
||||
openPurchaseModal({
|
||||
trackingLocation:
|
||||
'cloud_delinquency_announcement_bar',
|
||||
});
|
||||
}
|
||||
}
|
||||
modalButtonText={t('cloud_delinquency.banner.buttonText')}
|
||||
modalButtonDefaultText={'Update billing now'}
|
||||
message={<FormattedMessage {...message}/>}
|
||||
showLinkAsButton={true}
|
||||
isTallBanner={true}
|
||||
icon={<i className='icon icon-alert-outline'/>}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -2863,6 +2863,8 @@
|
||||
"backstage_sidebar.integrations.incoming_webhooks": "Incoming Webhooks",
|
||||
"backstage_sidebar.integrations.oauthApps": "OAuth 2.0 Applications",
|
||||
"backstage_sidebar.integrations.outgoing_webhooks": "Outgoing Webhooks",
|
||||
"billing_subscriptions.cloud_annual_renewal_alert_banner_title": "Your annual subscription expires in {days} days. Please renew now to avoid any disruption",
|
||||
"billing_subscriptions.cloud_annual_renewal_alert_banner_title_expired": "Your subscription has expired. Your workspace will be deleted in {days} days. Please renew now to avoid any disruption",
|
||||
"billing.subscription.info.mostRecentPaymentFailed": "Your most recent payment failed",
|
||||
"billing.subscription.info.mostRecentPaymentFailed.description.mostRecentPaymentFailed": "It looks your most recent payment failed because the credit card on your account has expired. Please <link>update your payment information</link> to avoid any disruption.",
|
||||
"bot.add.description": "Description",
|
||||
@ -3125,6 +3127,13 @@
|
||||
"claim.oauth_to_email.pwdNotMatch": "Passwords do not match.",
|
||||
"claim.oauth_to_email.switchTo": "Switch {type} to Email and Password",
|
||||
"claim.oauth_to_email.title": "Switch {type} Account to Email",
|
||||
"cloud_annual_renewal_delinquency.banner.end_user.message": "Your annual subscription has expired. Please contact your System Admin to keep this workspace",
|
||||
"cloud_annual_renewal_delinquency.banner.message": "Your annual subscription has expired. Please renew now to keep this workspace",
|
||||
"cloud_annual_renewal.banner.buttonText.contactSales": "Contact Sales",
|
||||
"cloud_annual_renewal.banner.buttonText.renew": "Renew",
|
||||
"cloud_annual_renewal.banner.message.30": "Your annual subscription expires in {days} days. Please renew to avoid any disruption.",
|
||||
"cloud_annual_renewal.banner.message.60": "Your annual subscription expires in {days} days. Please renew to avoid any disruption.",
|
||||
"cloud_annual_renewal.banner.message.7": "Your annual subscription expires in {days} days. Failure to renew will result in your workspace being deleted.",
|
||||
"cloud_archived.error.access": "Permalink belongs to a message that has been archived because of {planName} limits. Upgrade to access message again.",
|
||||
"cloud_archived.error.title": "Message Archived",
|
||||
"cloud_billing_history_modal.title": "Invoice(s)",
|
||||
@ -3142,7 +3151,6 @@
|
||||
"cloud_delinquency.banner.buttonText": "Update billing now",
|
||||
"cloud_delinquency.banner.end_user_notify_admin_button": "Notify admin",
|
||||
"cloud_delinquency.banner.end_user_notify_admin_title": "Your workspace has been downgraded. Notify your admin to fix billing issues",
|
||||
"cloud_delinquency.banner.title": "Update your billing information now to keep paid features.",
|
||||
"cloud_delinquency.cc_modal.card.reactivate": "Reactivate",
|
||||
"cloud_delinquency.cc_modal.card.totalOwed": "Total owed",
|
||||
"cloud_delinquency.cc_modal.card.viewBreakdown": "View breakdown",
|
||||
@ -3160,7 +3168,6 @@
|
||||
"cloud_delinquency.modal.workspace_downgraded_messages_surpassed": "Some of your workspace's message history are no longer accessible. Upgrade to a paid plan and get unlimited access to your message history.",
|
||||
"cloud_delinquency.modal.workspace_downgraded_multiples_limits_surpassed": "Your workspace has reached free plan limits. Upgrade to a paid plan.",
|
||||
"cloud_delinquency.modal.workspace_downgraded_storage_surpassed": "Some of your workspace's files are no longer accessible. Upgrade to a paid plan and get unlimited access to your files.",
|
||||
"cloud_delinquency.post_downgrade_banner.title": "Update your billing information now to re-activate paid features.",
|
||||
"cloud_signup.signup_consequences": "Your credit card will be charged today. <a>See how billing works.</a>",
|
||||
"cloud_subscribe.contact_support": "Compare plans",
|
||||
"cloud_upgrade.error_min_seats": "Minimum of 10 seats required",
|
||||
|
@ -27,3 +27,8 @@ export const fakeDate = (expected: Date): () => void => {
|
||||
global.Date = OGDate;
|
||||
};
|
||||
};
|
||||
|
||||
export const unixTimestampFromNow = (daysFromNow: number) => {
|
||||
const now = new Date();
|
||||
return Math.ceil(new Date(now.getTime() + (daysFromNow * 24 * 60 * 60 * 1000)).getTime() / 1000);
|
||||
};
|
||||
|
@ -36,3 +36,10 @@ export function isCloudFreePlan(product: Product | undefined, limits: Limits): b
|
||||
}
|
||||
|
||||
export const FREEMIUM_TO_ENTERPRISE_TRIAL_LENGTH_DAYS = 30;
|
||||
|
||||
export function daysToExpiration(expirationDate: number): number {
|
||||
const now = new Date();
|
||||
const expiration = new Date(expirationDate);
|
||||
const diff = expiration.getTime() - now.getTime();
|
||||
return Math.ceil(diff / (1000 * 3600 * 24));
|
||||
}
|
||||
|
@ -148,6 +148,7 @@ export const Preferences = {
|
||||
OVERAGE_USERS_BANNER: 'overage_users_banner',
|
||||
TO_CLOUD_YEARLY_PLAN_NUDGE: 'to_cloud_yearly_plan_nudge',
|
||||
TO_PAID_PLAN_NUDGE: 'to_paid_plan_nudge',
|
||||
CLOUD_ANNUAL_RENEWAL_BANNER: 'cloud_annual_renewal_banner',
|
||||
};
|
||||
|
||||
// For one off things that have a special, attention-grabbing UI until you interact with them
|
||||
@ -719,6 +720,8 @@ export const CloudBanners = {
|
||||
THREE_DAYS_LEFT_TRIAL_MODAL_DISMISSED: 'dismiss_3_days_left_trial_modal',
|
||||
NUDGE_TO_CLOUD_YEARLY_PLAN_SNOOZED: 'nudge_to_cloud_yearly_plan_snoozed',
|
||||
NUDGE_TO_PAID_PLAN_SNOOZED: 'nudge_to_paid_plan_snoozed',
|
||||
ANNUAL_RENEWAL_60_DAY: 'annual_renewal_60_day',
|
||||
ANNUAL_RENEWAL_30_DAY: 'annual_renewal_30_day',
|
||||
};
|
||||
|
||||
export const ConfigurationBanners = {
|
||||
|
@ -49,6 +49,8 @@ export type Subscription = {
|
||||
delinquent_since?: number;
|
||||
compliance_blocked?: string;
|
||||
billing_type?: string;
|
||||
cancel_at?: number;
|
||||
will_renew?: string;
|
||||
}
|
||||
|
||||
export type Product = {
|
||||
|
Loading…
Reference in New Issue
Block a user