diff --git a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/to_yearly_nudge_banner.test.tsx b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/to_yearly_nudge_banner.test.tsx index af797c5f92..e9061333a5 100644 --- a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/to_yearly_nudge_banner.test.tsx +++ b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/to_yearly_nudge_banner.test.tsx @@ -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( - - - , - ); + renderWithIntlAndStore(, 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( - - - , - ); + renderWithIntlAndStore(, 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( - - - , - ); + renderWithIntlAndStore(, 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( - - - , - ); + renderWithIntlAndStore(, 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(, state); - const store = mockStore(state); - const wrapper = mountWithIntl( - - - , - ); - - 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(, state); - const store = mockStore(state); - const wrapper = mountWithIntl( - - - , - ); - - 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( - - - , - ); + renderWithIntlAndStore(, 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( - - - , - ); + renderWithIntlAndStore(, state); - expect(wrapper.find('AlertBanner').exists()).toBe(false); + expect(() => screen.getByTestId('cloud-pro-monthly-deprecation-alert-banner')).toThrow(); }); }); + diff --git a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/to_yearly_nudge_banner.tsx b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/to_yearly_nudge_banner.tsx index 3cc1e8aa75..f2eac86e69 100644 --- a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/to_yearly_nudge_banner.tsx +++ b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/to_yearly_nudge_banner.tsx @@ -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 = ( + + ); + + const announcementType = (daysToProMonthlyEnd <= 10) ? AnnouncementBarTypes.CRITICAL : AnnouncementBarTypes.ANNOUNCEMENT; return ( 10} onButtonClick={() => openPurchaseModal({trackingLocation: 'to_yearly_nudge_annoucement_bar'})} modalButtonText={t('cloud_billing.nudge_to_yearly.learn_more')} modalButtonDefaultText='Learn more' - 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 = ( ); const description = ( ); @@ -122,9 +219,12 @@ const ToYearlyNudgeBanner = () => { ); + const bannerMode = (daysToProMonthlyEnd <= 10) ? 'danger' : 'info'; + return (