[MM-51412] - In-product banner to communicate Cloud Free deprecation (#22622)
* [MM-51412] - In-product banner to communicate Cloud Free deprecation * fix lint * make improvements * make improvements * put banners behind feature flag * cloud free overage case * add unit tests * feedback impl * fix test * rename and enable feature flag by default * fix alert banner mode * read end date from env var * disable ff by default * enter cloud free deprecation date * remove env var * fix text --------- Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
b00ca20abe
Коммит
155e896dcf
@@ -75,6 +75,8 @@ type FeatureFlags struct {
|
|||||||
|
|
||||||
OnboardingTourTips bool
|
OnboardingTourTips bool
|
||||||
|
|
||||||
|
DeprecateCloudFree bool
|
||||||
|
|
||||||
AppsSidebarCategory bool
|
AppsSidebarCategory bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,6 +103,7 @@ func (f *FeatureFlags) SetDefaults() {
|
|||||||
f.ReduceOnBoardingTaskList = false
|
f.ReduceOnBoardingTaskList = false
|
||||||
f.ThreadsEverywhere = false
|
f.ThreadsEverywhere = false
|
||||||
f.GlobalDrafts = true
|
f.GlobalDrafts = true
|
||||||
|
f.DeprecateCloudFree = false
|
||||||
f.WysiwygEditor = false
|
f.WysiwygEditor = false
|
||||||
f.OnboardingAutoShowLinkedBoard = false
|
f.OnboardingAutoShowLinkedBoard = false
|
||||||
f.OnboardingTourTips = true
|
f.OnboardingTourTips = true
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import {
|
|||||||
import LimitReachedBanner from './limit_reached_banner';
|
import LimitReachedBanner from './limit_reached_banner';
|
||||||
import CancelSubscription from './cancel_subscription';
|
import CancelSubscription from './cancel_subscription';
|
||||||
import {ToYearlyNudgeBanner} from './to_yearly_nudge_banner';
|
import {ToYearlyNudgeBanner} from './to_yearly_nudge_banner';
|
||||||
|
import {ToPaidNudgeBanner} from './to_paid_plan_nudge_banner';
|
||||||
|
|
||||||
import './billing_subscriptions.scss';
|
import './billing_subscriptions.scss';
|
||||||
|
|
||||||
@@ -136,6 +137,7 @@ const BillingSubscriptions = () => {
|
|||||||
/>
|
/>
|
||||||
{shouldShowPaymentFailedBanner() && paymentFailedBanner()}
|
{shouldShowPaymentFailedBanner() && paymentFailedBanner()}
|
||||||
{<ToYearlyNudgeBanner/>}
|
{<ToYearlyNudgeBanner/>}
|
||||||
|
{<ToPaidNudgeBanner/>}
|
||||||
{showCreditCardBanner &&
|
{showCreditCardBanner &&
|
||||||
isCardExpired &&
|
isCardExpired &&
|
||||||
creditCardExpiredBanner(setShowCreditCardBanner)}
|
creditCardExpiredBanner(setShowCreditCardBanner)}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
@import 'utils/mixins';
|
||||||
|
|
||||||
|
.ToPaidNudgeBanner {
|
||||||
|
&__actions {
|
||||||
|
padding-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__primary {
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
@include primary-button;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--button-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__secondary {
|
||||||
|
margin-left: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
@include tertiary-button;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--button-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {screen} from '@testing-library/react';
|
||||||
|
|
||||||
|
import {renderWithIntlAndStore} from 'tests/react_testing_utils';
|
||||||
|
import {CloudProducts} from 'utils/constants';
|
||||||
|
|
||||||
|
import {ToPaidNudgeBanner, ToPaidPlanBannerDismissable} from './to_paid_plan_nudge_banner';
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
views: {
|
||||||
|
announcementBar: {
|
||||||
|
announcementBarState: {
|
||||||
|
announcementBarCount: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
entities: {
|
||||||
|
general: {
|
||||||
|
config: {
|
||||||
|
CWSURL: '',
|
||||||
|
FeatureFlagDeprecateCloudFree: 'true',
|
||||||
|
},
|
||||||
|
license: {
|
||||||
|
IsLicensed: 'true',
|
||||||
|
Cloud: 'true',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
currentUserId: 'current_user_id',
|
||||||
|
profiles: {
|
||||||
|
current_user_id: {roles: 'system_user'},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
preferences: {
|
||||||
|
myPreferences: {},
|
||||||
|
},
|
||||||
|
cloud: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('ToPaidPlanBannerDismissable', () => {
|
||||||
|
test('should only show for admins on cloud free', () => {
|
||||||
|
const state = JSON.parse(JSON.stringify(initialState));
|
||||||
|
state.entities.users.profiles = {
|
||||||
|
current_user_id: {roles: 'system_admin'},
|
||||||
|
};
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
renderWithIntlAndStore(<ToPaidPlanBannerDismissable/>, state);
|
||||||
|
|
||||||
|
screen.getByTestId('cloud-free-deprecation-announcement-bar');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should NOT show for NON admins', () => {
|
||||||
|
const state = JSON.parse(JSON.stringify(initialState));
|
||||||
|
state.entities.users.profiles = {
|
||||||
|
current_user_id: {roles: 'system_user'},
|
||||||
|
};
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
renderWithIntlAndStore(<ToPaidPlanBannerDismissable/>, state);
|
||||||
|
|
||||||
|
expect(() => screen.getByTestId('cloud-free-deprecation-announcement-bar')).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should NOT show for admins on cloud pro', () => {
|
||||||
|
const state = JSON.parse(JSON.stringify(initialState));
|
||||||
|
state.entities.users.profiles = {
|
||||||
|
current_user_id: {roles: 'system_admin'},
|
||||||
|
};
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
renderWithIntlAndStore(<ToPaidPlanBannerDismissable/>, state);
|
||||||
|
|
||||||
|
expect(() => screen.getByTestId('cloud-free-deprecation-announcement-bar')).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should NOT show for admins on cloud enterprise', () => {
|
||||||
|
const state = JSON.parse(JSON.stringify(initialState));
|
||||||
|
state.entities.users.profiles = {
|
||||||
|
current_user_id: {roles: 'system_admin'},
|
||||||
|
};
|
||||||
|
state.entities.cloud = {
|
||||||
|
subscription: {
|
||||||
|
product_id: 'prod_enterprise',
|
||||||
|
is_free_trial: 'false',
|
||||||
|
trial_end_at: 1,
|
||||||
|
},
|
||||||
|
products: {
|
||||||
|
prod_enterprise: {
|
||||||
|
id: 'prod_enterprise',
|
||||||
|
sku: CloudProducts.ENTERPRISE,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
renderWithIntlAndStore(<ToPaidPlanBannerDismissable/>, state);
|
||||||
|
|
||||||
|
expect(() => screen.getByTestId('cloud-free-deprecation-announcement-bar')).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should NOT show for admins when banner was dismissed in preferences', () => {
|
||||||
|
const state = JSON.parse(JSON.stringify(initialState));
|
||||||
|
state.entities.users.profiles = {
|
||||||
|
current_user_id: {roles: 'system_admin'},
|
||||||
|
};
|
||||||
|
state.entities.preferences = {
|
||||||
|
myPreferences: {
|
||||||
|
'to_paid_plan_nudge--nudge_to_paid_plan_snoozed': {
|
||||||
|
category: 'to_paid_plan_nudge',
|
||||||
|
name: 'nudge_to_paid_plan_snoozed',
|
||||||
|
value: '{"range": 0, "show": false}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
renderWithIntlAndStore(<ToPaidPlanBannerDismissable/>, state);
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
renderWithIntlAndStore(<ToPaidNudgeBanner/>, state);
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
renderWithIntlAndStore(<ToPaidNudgeBanner/>, state);
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
renderWithIntlAndStore(<ToPaidNudgeBanner/>, state);
|
||||||
|
|
||||||
|
expect(() => screen.getByTestId('cloud-free-deprecation-alert-banner')).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React, {useEffect} from 'react';
|
||||||
|
import {useDispatch, useSelector} from 'react-redux';
|
||||||
|
import {useIntl, FormattedMessage} from 'react-intl';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
import AlertBanner from 'components/alert_banner';
|
||||||
|
import useOpenPricingModal from 'components/common/hooks/useOpenPricingModal';
|
||||||
|
import useOpenCloudPurchaseModal from 'components/common/hooks/useOpenCloudPurchaseModal';
|
||||||
|
import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink';
|
||||||
|
import AnnouncementBar from 'components/announcement_bar/default_announcement_bar';
|
||||||
|
|
||||||
|
import {getSubscriptionProduct as selectSubscriptionProduct} from 'mattermost-redux/selectors/entities/cloud';
|
||||||
|
import {getCurrentUser, isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users';
|
||||||
|
import {savePreferences} from 'mattermost-redux/actions/preferences';
|
||||||
|
import {deprecateCloudFree, get as getPreference} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
|
|
||||||
|
import {AnnouncementBarTypes, CloudBanners, CloudProducts, Preferences} from 'utils/constants';
|
||||||
|
import {t} from 'utils/i18n';
|
||||||
|
|
||||||
|
import {GlobalState} from '@mattermost/types/store';
|
||||||
|
|
||||||
|
import './to_paid_plan_nudge_banner.scss';
|
||||||
|
|
||||||
|
enum DismissShowRange {
|
||||||
|
GreaterThanEqual90 = '>=90',
|
||||||
|
BetweenNinetyAnd60 = '89-61',
|
||||||
|
SixtyTo31 = '60-31',
|
||||||
|
ThirtyTo11 = '30-11',
|
||||||
|
TenTo1 = '10-1',
|
||||||
|
Zero = '0'
|
||||||
|
}
|
||||||
|
|
||||||
|
const cloudFreeCloseMoment = '20230727';
|
||||||
|
|
||||||
|
interface ToPaidPlanDismissPreference {
|
||||||
|
|
||||||
|
// range represents the range for the days to the deprecation of cloud free e.g. in 30 to 10 days to deprecate cloud free
|
||||||
|
// Incase of dismissing the banner, range represents the time (days) period when this banner was dismissed.
|
||||||
|
// This is important because in case the banner was dismissed for a certain period, it helps us know that we should not show it again for that period.
|
||||||
|
range: DismissShowRange;
|
||||||
|
show: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ToPaidPlanBannerDismissable = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const openPricingModal = useOpenPricingModal();
|
||||||
|
|
||||||
|
const currentUser = useSelector(getCurrentUser);
|
||||||
|
const isAdmin = useSelector(isCurrentUserSystemAdmin);
|
||||||
|
const product = useSelector(selectSubscriptionProduct);
|
||||||
|
const cloudFreeDeprecated = useSelector(deprecateCloudFree);
|
||||||
|
const currentProductStarter = product?.sku === CloudProducts.STARTER;
|
||||||
|
|
||||||
|
const now = moment(Date.now());
|
||||||
|
const cloudFreeEndDate = moment(cloudFreeCloseMoment, 'YYYYMMDD');
|
||||||
|
const daysToCloudFreeEnd = cloudFreeEndDate.diff(now, 'days');
|
||||||
|
|
||||||
|
const snoozePreferenceVal = useSelector((state: GlobalState) => getPreference(state, Preferences.TO_PAID_PLAN_NUDGE, CloudBanners.NUDGE_TO_PAID_PLAN_SNOOZED, '{"range": 0, "show": true}'));
|
||||||
|
const snoozeInfo = JSON.parse(snoozePreferenceVal) as ToPaidPlanDismissPreference;
|
||||||
|
const show = snoozeInfo.show;
|
||||||
|
|
||||||
|
const snoozedForRange = (range: DismissShowRange) => {
|
||||||
|
return snoozeInfo.range === range;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!snoozeInfo.show) {
|
||||||
|
if (daysToCloudFreeEnd >= 90 && !snoozedForRange(DismissShowRange.GreaterThanEqual90)) {
|
||||||
|
showBanner(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (daysToCloudFreeEnd < 90 && daysToCloudFreeEnd > 60 && !snoozedForRange(DismissShowRange.BetweenNinetyAnd60)) {
|
||||||
|
showBanner(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (daysToCloudFreeEnd <= 60 && daysToCloudFreeEnd > 30 && !snoozedForRange(DismissShowRange.SixtyTo31)) {
|
||||||
|
showBanner(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (daysToCloudFreeEnd <= 30 && daysToCloudFreeEnd > 10 && !snoozedForRange(DismissShowRange.ThirtyTo11)) {
|
||||||
|
showBanner(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (daysToCloudFreeEnd <= 10) {
|
||||||
|
showBanner(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const showBanner = (show = false) => {
|
||||||
|
let dRange = DismissShowRange.Zero;
|
||||||
|
if (daysToCloudFreeEnd >= 90) {
|
||||||
|
dRange = DismissShowRange.GreaterThanEqual90;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (daysToCloudFreeEnd < 90 && daysToCloudFreeEnd > 60) {
|
||||||
|
dRange = DismissShowRange.BetweenNinetyAnd60;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (daysToCloudFreeEnd <= 60 && daysToCloudFreeEnd > 30) {
|
||||||
|
dRange = DismissShowRange.SixtyTo31;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (daysToCloudFreeEnd <= 30 && daysToCloudFreeEnd > 10) {
|
||||||
|
dRange = DismissShowRange.ThirtyTo11;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ideally this case should not happen because snooze button is not shown when TenTo1 days are remaining
|
||||||
|
if (daysToCloudFreeEnd <= 10 && daysToCloudFreeEnd > 0) {
|
||||||
|
dRange = DismissShowRange.TenTo1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const snoozeInfo: ToPaidPlanDismissPreference = {
|
||||||
|
range: dRange,
|
||||||
|
show,
|
||||||
|
};
|
||||||
|
|
||||||
|
dispatch(savePreferences(currentUser.id, [{
|
||||||
|
category: Preferences.TO_PAID_PLAN_NUDGE,
|
||||||
|
name: CloudBanners.NUDGE_TO_PAID_PLAN_SNOOZED,
|
||||||
|
user_id: currentUser.id,
|
||||||
|
value: JSON.stringify(snoozeInfo),
|
||||||
|
}]));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!cloudFreeDeprecated) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!show) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAdmin) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentProductStarter) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let message = {
|
||||||
|
id: 'cloud_billing.nudge_to_paid.announcement_bar',
|
||||||
|
defaultMessage: 'Cloud Free will be deprecated on {date}. To keep your workspace, upgrade to a paid plan',
|
||||||
|
values: {
|
||||||
|
date: moment(cloudFreeCloseMoment, 'YYYYMMDD').format('MMMM DD, YYYY'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (daysToCloudFreeEnd < 0) {
|
||||||
|
message = {
|
||||||
|
id: 'cloud_billing.nudge_to_paid.announcement_bar_deprecated',
|
||||||
|
defaultMessage: 'Cloud Free was deprecated. To keep your workspace, upgrade to a paid plan',
|
||||||
|
} as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const announcementType = (daysToCloudFreeEnd <= 10) ? AnnouncementBarTypes.CRITICAL : AnnouncementBarTypes.ANNOUNCEMENT;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnnouncementBar
|
||||||
|
id='cloud-free-deprecation-announcement-bar'
|
||||||
|
type={announcementType}
|
||||||
|
showCloseButton={daysToCloudFreeEnd > 10}
|
||||||
|
onButtonClick={openPricingModal}
|
||||||
|
modalButtonText={t('cloud_billing.nudge_to_paid.view_plans')}
|
||||||
|
modalButtonDefaultText='View plans'
|
||||||
|
message={<FormattedMessage {...message}/>}
|
||||||
|
showLinkAsButton={true}
|
||||||
|
handleClose={showBanner}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -21,6 +21,7 @@ import './alert_banner.scss';
|
|||||||
export type ModeType = 'danger' | 'warning' | 'info' | 'success';
|
export type ModeType = 'danger' | 'warning' | 'info' | 'success';
|
||||||
|
|
||||||
export type AlertBannerProps = {
|
export type AlertBannerProps = {
|
||||||
|
id?: string;
|
||||||
mode: ModeType;
|
mode: ModeType;
|
||||||
title?: React.ReactNode;
|
title?: React.ReactNode;
|
||||||
message?: React.ReactNode;
|
message?: React.ReactNode;
|
||||||
@@ -35,6 +36,7 @@ export type AlertBannerProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const AlertBanner = ({
|
const AlertBanner = ({
|
||||||
|
id,
|
||||||
mode,
|
mode,
|
||||||
title,
|
title,
|
||||||
message,
|
message,
|
||||||
@@ -70,6 +72,7 @@ const AlertBanner = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
data-testid={id}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'AlertBanner',
|
'AlertBanner',
|
||||||
mode,
|
mode,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import {ToYearlyNudgeBannerDismissable} from 'components/admin_console/billing/billing_subscriptions/to_yearly_nudge_banner';
|
import {ToYearlyNudgeBannerDismissable} from 'components/admin_console/billing/billing_subscriptions/to_yearly_nudge_banner';
|
||||||
|
import {ToPaidPlanBannerDismissable} from 'components/admin_console/billing/billing_subscriptions/to_paid_plan_nudge_banner';
|
||||||
|
|
||||||
import {ClientLicense, ClientConfig, WarnMetricStatus} from '@mattermost/types/config';
|
import {ClientLicense, ClientConfig, WarnMetricStatus} from '@mattermost/types/config';
|
||||||
import withGetCloudSubscription from '../common/hocs/cloud/with_get_cloud_subscription';
|
import withGetCloudSubscription from '../common/hocs/cloud/with_get_cloud_subscription';
|
||||||
@@ -72,6 +73,7 @@ class AnnouncementBarController extends React.PureComponent<Props> {
|
|||||||
let cloudDelinquencyAnnouncementBar = null;
|
let cloudDelinquencyAnnouncementBar = null;
|
||||||
let notifyAdminDowngradeDelinquencyBar = null;
|
let notifyAdminDowngradeDelinquencyBar = null;
|
||||||
let toYearlyNudgeBannerDismissable = null;
|
let toYearlyNudgeBannerDismissable = null;
|
||||||
|
let toPaidPlanNudgeBannerDismissable = null;
|
||||||
if (this.props.license?.Cloud === 'true') {
|
if (this.props.license?.Cloud === 'true') {
|
||||||
paymentAnnouncementBar = (
|
paymentAnnouncementBar = (
|
||||||
<PaymentAnnouncementBar/>
|
<PaymentAnnouncementBar/>
|
||||||
@@ -89,6 +91,7 @@ class AnnouncementBarController extends React.PureComponent<Props> {
|
|||||||
<NotifyAdminDowngradeDelinquencyBar/>
|
<NotifyAdminDowngradeDelinquencyBar/>
|
||||||
);
|
);
|
||||||
toYearlyNudgeBannerDismissable = (<ToYearlyNudgeBannerDismissable/>);
|
toYearlyNudgeBannerDismissable = (<ToYearlyNudgeBannerDismissable/>);
|
||||||
|
toPaidPlanNudgeBannerDismissable = (<ToPaidPlanBannerDismissable/>);
|
||||||
}
|
}
|
||||||
|
|
||||||
let autoStartTrialModal = null;
|
let autoStartTrialModal = null;
|
||||||
@@ -108,6 +111,7 @@ class AnnouncementBarController extends React.PureComponent<Props> {
|
|||||||
{cloudDelinquencyAnnouncementBar}
|
{cloudDelinquencyAnnouncementBar}
|
||||||
{notifyAdminDowngradeDelinquencyBar}
|
{notifyAdminDowngradeDelinquencyBar}
|
||||||
{toYearlyNudgeBannerDismissable}
|
{toYearlyNudgeBannerDismissable}
|
||||||
|
{toPaidPlanNudgeBannerDismissable}
|
||||||
{this.props.license?.Cloud !== 'true' && <OverageUsersBanner/>}
|
{this.props.license?.Cloud !== 'true' && <OverageUsersBanner/>}
|
||||||
{autoStartTrialModal}
|
{autoStartTrialModal}
|
||||||
<ShowThreeDaysLeftTrialModal/>
|
<ShowThreeDaysLeftTrialModal/>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import ToggleModalButton from 'components/toggle_modal_button';
|
|||||||
import {trackEvent} from 'actions/telemetry_actions.jsx';
|
import {trackEvent} from 'actions/telemetry_actions.jsx';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
id?: string;
|
||||||
showCloseButton: boolean;
|
showCloseButton: boolean;
|
||||||
color: string;
|
color: string;
|
||||||
textColor: string;
|
textColor: string;
|
||||||
@@ -172,6 +173,7 @@ export default class AnnouncementBar extends React.PureComponent<Props, State> {
|
|||||||
style={barStyle}
|
style={barStyle}
|
||||||
// eslint-disable-next-line react/no-unknown-property
|
// eslint-disable-next-line react/no-unknown-property
|
||||||
css={{gridArea: 'announcement'}}
|
css={{gridArea: 'announcement'}}
|
||||||
|
data-testid={this.props.id}
|
||||||
>
|
>
|
||||||
<OverlayTrigger
|
<OverlayTrigger
|
||||||
delayShow={Constants.OVERLAY_TIME_DELAY}
|
delayShow={Constants.OVERLAY_TIME_DELAY}
|
||||||
|
|||||||
@@ -3031,6 +3031,11 @@
|
|||||||
"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.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_archived.error.title": "Message Archived",
|
||||||
"cloud_billing_history_modal.title": "Invoice(s)",
|
"cloud_billing_history_modal.title": "Invoice(s)",
|
||||||
|
"cloud_billing.nudge_to_paid.contact_sales": "Contact sales",
|
||||||
|
"cloud_billing.nudge_to_paid.description": "Cloud Free will be deprecated in {days} days. Upgrade to a paid plan or contact sales.",
|
||||||
|
"cloud_billing.nudge_to_paid.learn_more": "Upgrade",
|
||||||
|
"cloud_billing.nudge_to_paid.title": "Upgrade to paid plan to keep your workspace",
|
||||||
|
"cloud_billing.nudge_to_paid.view_plans": "View plans",
|
||||||
"cloud_billing.nudge_to_yearly.contact_sales": "Contact sales",
|
"cloud_billing.nudge_to_yearly.contact_sales": "Contact sales",
|
||||||
"cloud_billing.nudge_to_yearly.description": "Simplify your billing by switching to an annual subscription.",
|
"cloud_billing.nudge_to_yearly.description": "Simplify your billing by switching to an annual subscription.",
|
||||||
"cloud_billing.nudge_to_yearly.learn_more": "Learn more",
|
"cloud_billing.nudge_to_yearly.learn_more": "Learn more",
|
||||||
|
|||||||
@@ -300,6 +300,10 @@ export function onboardingTourTipsEnabled(state: GlobalState): boolean {
|
|||||||
return getFeatureFlagValue(state, 'OnboardingTourTips') === 'true';
|
return getFeatureFlagValue(state, 'OnboardingTourTips') === 'true';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function deprecateCloudFree(state: GlobalState): boolean {
|
||||||
|
return getFeatureFlagValue(state, 'DeprecateCloudFree') === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
export function appsSidebarCategoryEnabled(state: GlobalState): boolean {
|
export function appsSidebarCategoryEnabled(state: GlobalState): boolean {
|
||||||
return getFeatureFlagValue(state, 'AppsSidebarCategory') === 'true';
|
return getFeatureFlagValue(state, 'AppsSidebarCategory') === 'true';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ export const Preferences = {
|
|||||||
NOTIFY_ADMIN_REVOKE_DOWNGRADED_WORKSPACE: 'admin_revoke_downgraded_instance',
|
NOTIFY_ADMIN_REVOKE_DOWNGRADED_WORKSPACE: 'admin_revoke_downgraded_instance',
|
||||||
OVERAGE_USERS_BANNER: 'overage_users_banner',
|
OVERAGE_USERS_BANNER: 'overage_users_banner',
|
||||||
CLOUD_YEARLY_NUDGE_BANNER: 'cloud_yearly_nudge_banner',
|
CLOUD_YEARLY_NUDGE_BANNER: 'cloud_yearly_nudge_banner',
|
||||||
|
TO_PAID_PLAN_NUDGE: 'to_paid_plan_nudge',
|
||||||
};
|
};
|
||||||
|
|
||||||
// For one off things that have a special, attention-grabbing UI until you interact with them
|
// For one off things that have a special, attention-grabbing UI until you interact with them
|
||||||
@@ -726,6 +727,7 @@ export const CloudBanners = {
|
|||||||
UPGRADE_FROM_TRIAL: 'upgrade_from_trial',
|
UPGRADE_FROM_TRIAL: 'upgrade_from_trial',
|
||||||
THREE_DAYS_LEFT_TRIAL_MODAL_DISMISSED: 'dismiss_3_days_left_trial_modal',
|
THREE_DAYS_LEFT_TRIAL_MODAL_DISMISSED: 'dismiss_3_days_left_trial_modal',
|
||||||
NUDGE_TO_YEARLY_BANNER_DISMISSED: 'nudge_to_yearly_banner_dismissed',
|
NUDGE_TO_YEARLY_BANNER_DISMISSED: 'nudge_to_yearly_banner_dismissed',
|
||||||
|
NUDGE_TO_PAID_PLAN_SNOOZED: 'nudge_to_paid_plan_snoozed',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ConfigurationBanners = {
|
export const ConfigurationBanners = {
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user