[MM-51475] - Monthly Subscriptions: Notifications to move to annual (#22842)

* [MM-51475] - Monthly Subscriptions: Notifications to move to annual

* change wording

* change banners impl

* fix translation

* rm env var

* fix tests

* rm unused consts

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Allan Guwatudde
2023-04-13 18:37:37 +03:00
коммит произвёл GitHub
родитель 48cd0f884e
Коммит ea44b6bd38
4 изменённых файлов: 148 добавлений и 90 удалений

Просмотреть файл

@@ -2,11 +2,9 @@
// See LICENSE.txt for license information.
import React from 'react';
import {screen} from '@testing-library/react';
import {Provider} from 'react-redux';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import mockStore from 'tests/test_store';
import {renderWithIntlAndStore} from 'tests/react_testing_utils';
import {CloudProducts, RecurringIntervals} from 'utils/constants';
import {ToYearlyNudgeBanner, ToYearlyNudgeBannerDismissable} from './to_yearly_nudge_banner';
@@ -42,7 +40,7 @@ const initialState = {
},
};
describe('components/admin_console/billing/ToYearlyNudgeBannerDismissable', () => {
describe('ToYearlyNudgeBannerDismissable', () => {
test('should show for admins cloud professional monthly', () => {
const state = JSON.parse(JSON.stringify(initialState));
state.entities.users.profiles = {
@@ -63,14 +61,9 @@ describe('components/admin_console/billing/ToYearlyNudgeBannerDismissable', () =
},
};
const store = mockStore(state);
const wrapper = mountWithIntl(
<Provider store={store}>
<ToYearlyNudgeBannerDismissable/>
</Provider>,
);
renderWithIntlAndStore(<ToYearlyNudgeBannerDismissable/>, state);
expect(wrapper.find('AnnouncementBar').exists()).toBe(true);
screen.getByTestId('cloud-pro-monthly-deprecation-announcement-bar');
});
test('should NOT show for NON admins', () => {
@@ -93,14 +86,9 @@ describe('components/admin_console/billing/ToYearlyNudgeBannerDismissable', () =
},
};
const store = mockStore(state);
const wrapper = mountWithIntl(
<Provider store={store}>
<ToYearlyNudgeBannerDismissable/>
</Provider>,
);
renderWithIntlAndStore(<ToYearlyNudgeBannerDismissable/>, state);
expect(wrapper.find('AnnouncementBar').exists()).toBe(false);
expect(() => screen.getByTestId('cloud-pro-monthly-deprecation-announcement-bar')).toThrow();
});
test('should NOT show for admins on cloud free', () => {
@@ -123,14 +111,9 @@ describe('components/admin_console/billing/ToYearlyNudgeBannerDismissable', () =
},
};
const store = mockStore(state);
const wrapper = mountWithIntl(
<Provider store={store}>
<ToYearlyNudgeBannerDismissable/>
</Provider>,
);
renderWithIntlAndStore(<ToYearlyNudgeBannerDismissable/>, state);
expect(wrapper.find('AnnouncementBar').exists()).toBe(false);
expect(() => screen.getByTestId('cloud-pro-monthly-deprecation-announcement-bar')).toThrow();
});
test('should NOT show for admins on cloud enterprise', () => {
@@ -153,14 +136,9 @@ describe('components/admin_console/billing/ToYearlyNudgeBannerDismissable', () =
},
};
const store = mockStore(state);
const wrapper = mountWithIntl(
<Provider store={store}>
<ToYearlyNudgeBannerDismissable/>
</Provider>,
);
renderWithIntlAndStore(<ToYearlyNudgeBannerDismissable/>, state);
expect(wrapper.find('AnnouncementBar').exists()).toBe(false);
expect(() => screen.getByTestId('cloud-pro-monthly-deprecation-announcement-bar')).toThrow();
});
test('should NOT show for admins on cloud pro annual', () => {
@@ -182,15 +160,9 @@ describe('components/admin_console/billing/ToYearlyNudgeBannerDismissable', () =
},
},
};
renderWithIntlAndStore(<ToYearlyNudgeBannerDismissable/>, state);
const store = mockStore(state);
const wrapper = mountWithIntl(
<Provider store={store}>
<ToYearlyNudgeBannerDismissable/>
</Provider>,
);
expect(wrapper.find('AnnouncementBar').exists()).toBe(false);
expect(() => screen.getByTestId('cloud-pro-monthly-deprecation-announcement-bar')).toThrow();
});
test('should NOT show for admins when banner was dismissed in preferences', () => {
@@ -200,10 +172,10 @@ describe('components/admin_console/billing/ToYearlyNudgeBannerDismissable', () =
};
state.entities.preferences = {
myPreferences: {
'cloud_yearly_nudge_banner--nudge_to_yearly_banner_dismissed': {
category: 'cloud_yearly_nudge_banner',
name: 'nudge_to_yearly_banner_dismissed',
value: 'true',
'to_cloud_yearly_plan_nudge--nudge_to_cloud_yearly_plan_snoozed': {
category: 'to_cloud_yearly_plan_nudge',
name: 'nudge_to_cloud_yearly_plan_snoozed',
value: '{"range": 0, "show": false}',
},
},
};
@@ -221,19 +193,13 @@ describe('components/admin_console/billing/ToYearlyNudgeBannerDismissable', () =
},
},
};
renderWithIntlAndStore(<ToYearlyNudgeBannerDismissable/>, state);
const store = mockStore(state);
const wrapper = mountWithIntl(
<Provider store={store}>
<ToYearlyNudgeBannerDismissable/>
</Provider>,
);
expect(wrapper.find('AnnouncementBar').exists()).toBe(false);
expect(() => screen.getByTestId('cloud-pro-monthly-deprecation-announcement-bar')).toThrow();
});
});
describe('components/admin_console/billing/ToYearlyNudgeBanner', () => {
describe('ToYearlyNudgeBanner', () => {
test('should show for cloud professional monthly', () => {
const state = JSON.parse(JSON.stringify(initialState));
state.entities.cloud = {
@@ -251,14 +217,9 @@ describe('components/admin_console/billing/ToYearlyNudgeBanner', () => {
},
};
const store = mockStore(state);
const wrapper = mountWithIntl(
<Provider store={store}>
<ToYearlyNudgeBanner/>
</Provider>,
);
renderWithIntlAndStore(<ToYearlyNudgeBanner/>, state);
expect(wrapper.find('AlertBanner').exists()).toBe(true);
screen.getByTestId('cloud-pro-monthly-deprecation-alert-banner');
});
test('should NOT show for non cloud professional monthly', () => {
@@ -278,13 +239,9 @@ describe('components/admin_console/billing/ToYearlyNudgeBanner', () => {
},
};
const store = mockStore(state);
const wrapper = mountWithIntl(
<Provider store={store}>
<ToYearlyNudgeBanner/>
</Provider>,
);
renderWithIntlAndStore(<ToYearlyNudgeBanner/>, state);
expect(wrapper.find('AlertBanner').exists()).toBe(false);
expect(() => screen.getByTestId('cloud-pro-monthly-deprecation-alert-banner')).toThrow();
});
});

Просмотреть файл

@@ -1,9 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
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 useOpenCloudPurchaseModal from 'components/common/hooks/useOpenCloudPurchaseModal';
@@ -22,12 +23,35 @@ import {GlobalState} from '@mattermost/types/store';
import './to_yearly_nudge_banner.scss';
enum DismissShowRange {
GreaterThanEqual90 = '>=90',
BetweenNinetyAnd60 = '89-61',
SixtyTo31 = '60-31',
ThirtyTo11 = '30-11',
TenTo1 = '10-1',
Zero = '0'
}
const cloudProMonthlyCloseMoment = '20230727';
interface ToYearlyPlanDismissPreference {
// 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;
}
const ToYearlyNudgeBannerDismissable = () => {
const dispatch = useDispatch();
const openPurchaseModal = useOpenCloudPurchaseModal({});
const nudgeDismissed = useSelector((state: GlobalState) => getPreference(state, Preferences.CLOUD_YEARLY_NUDGE_BANNER, CloudBanners.NUDGE_TO_YEARLY_BANNER_DISMISSED)) === 'true';
const snoozePreferenceVal = useSelector((state: GlobalState) => getPreference(state, Preferences.TO_CLOUD_YEARLY_PLAN_NUDGE, CloudBanners.NUDGE_TO_CLOUD_YEARLY_PLAN_SNOOZED, '{"range": 0, "show": true}'));
const snoozeInfo = JSON.parse(snoozePreferenceVal) as ToYearlyPlanDismissPreference;
const show = snoozeInfo.show;
const currentUser = useSelector(getCurrentUser);
const isAdmin = useSelector(isCurrentUserSystemAdmin);
const product = useSelector(selectSubscriptionProduct);
@@ -35,16 +59,75 @@ const ToYearlyNudgeBannerDismissable = () => {
const currentProductIsMonthly = product?.recurring_interval === RecurringIntervals.MONTH;
const currentProductProMonthly = currentProductProfessional && currentProductIsMonthly;
const savedDismissedPref = () => {
const now = moment(Date.now());
const proMonthlyEndDate = moment(cloudProMonthlyCloseMoment, 'YYYYMMDD');
const daysToProMonthlyEnd = proMonthlyEndDate.diff(now, 'days');
const snoozedForRange = (range: DismissShowRange) => {
return snoozeInfo.range === range;
};
useEffect(() => {
if (!snoozeInfo.show) {
if (daysToProMonthlyEnd >= 90 && !snoozedForRange(DismissShowRange.GreaterThanEqual90)) {
showBanner(true);
}
if (daysToProMonthlyEnd < 90 && daysToProMonthlyEnd > 60 && !snoozedForRange(DismissShowRange.BetweenNinetyAnd60)) {
showBanner(true);
}
if (daysToProMonthlyEnd <= 60 && daysToProMonthlyEnd > 30 && !snoozedForRange(DismissShowRange.SixtyTo31)) {
showBanner(true);
}
if (daysToProMonthlyEnd <= 30 && daysToProMonthlyEnd > 10 && !snoozedForRange(DismissShowRange.ThirtyTo11)) {
showBanner(true);
}
if (daysToProMonthlyEnd <= 10) {
showBanner(true);
}
}
}, []);
const showBanner = (show = false) => {
let dRange = DismissShowRange.Zero;
if (daysToProMonthlyEnd >= 90) {
dRange = DismissShowRange.GreaterThanEqual90;
}
if (daysToProMonthlyEnd < 90 && daysToProMonthlyEnd > 60) {
dRange = DismissShowRange.BetweenNinetyAnd60;
}
if (daysToProMonthlyEnd <= 60 && daysToProMonthlyEnd > 30) {
dRange = DismissShowRange.SixtyTo31;
}
if (daysToProMonthlyEnd <= 30 && daysToProMonthlyEnd > 10) {
dRange = DismissShowRange.ThirtyTo11;
}
// ideally this case should not happen because snooze button is not shown when TenTo1 days are remaining
if (daysToProMonthlyEnd <= 10 && daysToProMonthlyEnd > 0) {
dRange = DismissShowRange.TenTo1;
}
const snoozeInfo: ToYearlyPlanDismissPreference = {
range: dRange,
show,
};
dispatch(savePreferences(currentUser.id, [{
category: Preferences.CLOUD_YEARLY_NUDGE_BANNER,
name: CloudBanners.NUDGE_TO_YEARLY_BANNER_DISMISSED,
category: Preferences.TO_CLOUD_YEARLY_PLAN_NUDGE,
name: CloudBanners.NUDGE_TO_CLOUD_YEARLY_PLAN_SNOOZED,
user_id: currentUser.id,
value: 'true',
value: JSON.stringify(snoozeInfo),
}]));
};
if (nudgeDismissed) {
if (!show) {
return null;
}
@@ -56,21 +139,30 @@ const ToYearlyNudgeBannerDismissable = () => {
return null;
}
const message = {
id: 'cloud_billing.nudge_to_yearly.announcement_bar',
defaultMessage: 'Simplify your billing and switch to an annual plan today',
};
const message = (
<FormattedMessage
id='cloud_billing.nudge_to_yearly.announcement_bar'
defaultMessage='Monthly billing will be discontinued in {days} days . Switch to annual billing'
values={{
days: daysToProMonthlyEnd,
}}
/>
);
const announcementType = (daysToProMonthlyEnd <= 10) ? AnnouncementBarTypes.CRITICAL : AnnouncementBarTypes.ANNOUNCEMENT;
return (
<AnnouncementBar
type={AnnouncementBarTypes.ANNOUNCEMENT}
showCloseButton={true}
id='cloud-pro-monthly-deprecation-announcement-bar'
type={announcementType}
showCloseButton={daysToProMonthlyEnd > 10}
onButtonClick={() => openPurchaseModal({trackingLocation: 'to_yearly_nudge_annoucement_bar'})}
modalButtonText={t('cloud_billing.nudge_to_yearly.learn_more')}
modalButtonDefaultText='Learn more'
message={<FormattedMessage {...message}/>}
message={message}
showLinkAsButton={true}
handleClose={savedDismissedPref}
handleClose={showBanner}
/>
);
};
@@ -90,17 +182,22 @@ const ToYearlyNudgeBanner = () => {
return null;
}
const now = moment(Date.now());
const proMonthlyEndDate = moment(cloudProMonthlyCloseMoment, 'YYYYMMDD');
const daysToProMonthlyEnd = proMonthlyEndDate.diff(now, 'days');
const title = (
<FormattedMessage
id='cloud_billing.nudge_to_yearly.title'
defaultMessage='Switch to an annual plan today'
defaultMessage='Action required: Switch to annual billing to keep your workspace.'
/>
);
const description = (
<FormattedMessage
id='cloud_billing.nudge_to_yearly.description'
defaultMessage='Simplify your billing by switching to an annual subscription.'
defaultMessage='Monthly billing will be discontinued on {date}. To keep your workspace, switch to annual billing.'
values={{date: moment(cloudProMonthlyCloseMoment, 'YYYYMMDD').format('MMMM DD, YYYY')}}
/>
);
@@ -122,9 +219,12 @@ const ToYearlyNudgeBanner = () => {
</button>
);
const bannerMode = (daysToProMonthlyEnd <= 10) ? 'danger' : 'info';
return (
<AlertBanner
mode='info'
id='cloud-pro-monthly-deprecation-alert-banner'
mode={bannerMode}
title={title}
message={description}
className='ToYearlyNudgeBanner'

Просмотреть файл

@@ -3039,10 +3039,11 @@
"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.announcement_bar": "Monthly billing will be discontinued in {days} days . Switch to annual billing",
"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": "Monthly billing will be discontinued on {date}. To keep your workspace, switch to annual billing.",
"cloud_billing.nudge_to_yearly.learn_more": "Learn more",
"cloud_billing.nudge_to_yearly.title": "Switch to an annual plan today",
"cloud_billing.nudge_to_yearly.title": "Action required: Switch to annual billing to keep your workspace.",
"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",

Просмотреть файл

@@ -157,7 +157,7 @@ export const Preferences = {
CONFIGURATION_BANNERS: 'configuration_banners',
NOTIFY_ADMIN_REVOKE_DOWNGRADED_WORKSPACE: 'admin_revoke_downgraded_instance',
OVERAGE_USERS_BANNER: 'overage_users_banner',
CLOUD_YEARLY_NUDGE_BANNER: 'cloud_yearly_nudge_banner',
TO_CLOUD_YEARLY_PLAN_NUDGE: 'to_cloud_yearly_plan_nudge',
TO_PAID_PLAN_NUDGE: 'to_paid_plan_nudge',
};
@@ -726,7 +726,7 @@ export const CloudBanners = {
TRIAL: 'trial',
UPGRADE_FROM_TRIAL: 'upgrade_from_trial',
THREE_DAYS_LEFT_TRIAL_MODAL_DISMISSED: 'dismiss_3_days_left_trial_modal',
NUDGE_TO_YEARLY_BANNER_DISMISSED: 'nudge_to_yearly_banner_dismissed',
NUDGE_TO_CLOUD_YEARLY_PLAN_SNOOZED: 'nudge_to_cloud_yearly_plan_snoozed',
NUDGE_TO_PAID_PLAN_SNOOZED: 'nudge_to_paid_plan_snoozed',
};