From dad1460d717c00aea07a23bd884cfe6c344c63c9 Mon Sep 17 00:00:00 2001 From: Jessica Lawshe Date: Thu, 26 Jan 2023 11:01:54 -0600 Subject: [PATCH] Merge pull request #11414 from overleaf/jel-react-personal-subscription-dash-pt-2 [web] Continue migration of personal subscription dash commons to React GitOrigin-RevId: 997716f70c953b088f686d4b0f638705ad838520 --- .../views/subscriptions/dashboard-react.pug | 3 + .../web/frontend/extracted-translations.json | 7 + .../dashboard/personal-subscription.tsx | 20 ++- .../components/dashboard/states/active.tsx | 111 ------------ .../dashboard/states/active/active.tsx | 117 +++++++++++++ .../active/cancel-subscription-button.tsx | 56 ++++++ .../states/active/cancel-subscription.tsx | 15 ++ .../dashboard/states/active/change-plan.tsx | 18 ++ .../states/active/pending-plan-change.tsx | 49 ++++++ .../dashboard/states/active/trial-ending.tsx | 26 +++ .../subscription-dashboard-context.tsx | 19 +- services/web/locales/en.json | 1 + .../dashboard/personal-subscription.test.tsx | 38 +++- .../dashboard/states/active.test.tsx | 158 ++++++++++++++++- .../dashboard/subscription-dashboard.test.tsx | 7 +- .../subscription/fixtures/subscriptions.tsx | 165 ++++++++++++++++++ .../subscription/dashboard/subscription.ts | 5 +- services/web/types/window.ts | 1 + 18 files changed, 696 insertions(+), 120 deletions(-) delete mode 100644 services/web/frontend/js/features/subscription/components/dashboard/states/active.tsx create mode 100644 services/web/frontend/js/features/subscription/components/dashboard/states/active/active.tsx create mode 100644 services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-subscription-button.tsx create mode 100644 services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-subscription.tsx create mode 100644 services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan.tsx create mode 100644 services/web/frontend/js/features/subscription/components/dashboard/states/active/pending-plan-change.tsx create mode 100644 services/web/frontend/js/features/subscription/components/dashboard/states/active/trial-ending.tsx diff --git a/services/web/app/views/subscriptions/dashboard-react.pug b/services/web/app/views/subscriptions/dashboard-react.pug index 7ddf9e8d05..932b5df240 100644 --- a/services/web/app/views/subscriptions/dashboard-react.pug +++ b/services/web/app/views/subscriptions/dashboard-react.pug @@ -3,6 +3,9 @@ extends ../layout-marketing block entrypointVar - entrypoint = 'pages/user/subscription/dashboard' +block head-scripts + script(type="text/javascript", nonce=scriptNonce, src="https://js.recurly.com/v4/recurly.js") + block append meta meta(name="ol-managedInstitutions", data-type="json", content=managedInstitutions) meta(name="ol-planCodesChangingAtTermEnd", data-type="json", content=plans.planCodesChangingAtTermEnd) diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 50a95aa28d..25e4fbc628 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -22,6 +22,7 @@ "add_or_remove_project_from_tag": "", "add_role_and_department": "", "add_to_folder": "", + "additional_licenses": "", "all_projects": "", "also": "", "anyone_with_link_can_edit": "", @@ -57,6 +58,7 @@ "can_link_your_institution_acct_2": "", "can_now_relink_dropbox": "", "cancel": "", + "cancel_your_subscription": "", "cannot_invite_non_user": "", "cannot_invite_self": "", "cannot_verify_user_not_robot": "", @@ -475,6 +477,7 @@ "password": "", "password_managed_externally": "", "password_was_detected_on_a_public_list_of_known_compromised_passwords": "", + "payment_provider_unreachable_error": "", "pdf_compile_in_progress_error": "", "pdf_compile_rate_limit_hit": "", "pdf_compile_try_again": "", @@ -484,6 +487,7 @@ "pdf_rendering_error": "", "pdf_viewer": "", "pdf_viewer_error": "", + "pending_additional_licenses": "", "plan_tooltip": "", "please_change_primary_to_remove": "", "please_check_your_inbox": "", @@ -663,6 +667,7 @@ "submit_title": "", "subscription_admins_cannot_be_deleted": "", "subscription_canceled_and_terminate_on_x": "", + "subscription_will_remain_active_until_end_of_billing_period_x": "", "sure_you_want_to_delete": "", "switch_to_editor": "", "switch_to_pdf": "", @@ -763,6 +768,7 @@ "want_change_to_apply_before_plan_end": "", "we_cant_find_any_sections_or_subsections_in_this_file": "", "we_logged_you_in": "", + "wed_love_you_to_stay": "", "welcome_to_sl": "", "wide": "", "with_premium_subscription_you_also_get": "", @@ -779,6 +785,7 @@ "your_projects": "", "your_subscription": "", "your_subscription_has_expired": "", + "youre_on_free_trial_which_ends_on": "", "zotero_groups_loading_error": "", "zotero_groups_relink": "", "zotero_integration": "", diff --git a/services/web/frontend/js/features/subscription/components/dashboard/personal-subscription.tsx b/services/web/frontend/js/features/subscription/components/dashboard/personal-subscription.tsx index ca55fee085..fbcea64113 100644 --- a/services/web/frontend/js/features/subscription/components/dashboard/personal-subscription.tsx +++ b/services/web/frontend/js/features/subscription/components/dashboard/personal-subscription.tsx @@ -1,8 +1,10 @@ +import { useEffect } from 'react' import { useTranslation } from 'react-i18next' import { Subscription } from '../../../../../../types/subscription/dashboard/subscription' -import { ActiveSubsciption } from './states/active' +import { ActiveSubsciption } from './states/active/active' import { CanceledSubsciption } from './states/canceled' import { ExpiredSubsciption } from './states/expired' +import { useSubscriptionDashboardContext } from '../../context/subscription-dashboard-context' function PastDueSubscriptionAlert({ subscription, @@ -51,9 +53,18 @@ function PersonalSubscription({ }: { subscription?: Subscription }) { + const { t } = useTranslation() + const { recurlyLoadError, setRecurlyLoadError } = + useSubscriptionDashboardContext() const state = subscription?.recurly?.state - if (!subscription) return <> + useEffect(() => { + if (typeof window.recurly === 'undefined' || !window.recurly) { + setRecurlyLoadError(true) + } + }) + + if (!subscription) return null return ( <> @@ -61,6 +72,11 @@ function PersonalSubscription({ )} + {recurlyLoadError && ( +
+ {t('payment_provider_unreachable_error')} +
+ )} ) } diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active.tsx deleted file mode 100644 index ade818d5e3..0000000000 --- a/services/web/frontend/js/features/subscription/components/dashboard/states/active.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { useTranslation, Trans } from 'react-i18next' -import PremiumFeaturesLink from '../premium-features-link' -import { PriceExceptions } from '../../shared/price-exceptions' -import { useSubscriptionDashboardContext } from '../../../context/subscription-dashboard-context' -import { Subscription } from '../../../../../../../types/subscription/dashboard/subscription' - -function ChangePlan() { - const { t } = useTranslation() - const { showChangePersonalPlan } = useSubscriptionDashboardContext() - - if (!showChangePersonalPlan) return null - - return ( - <> -

{t('change_plan')}

-

- TODO: change subscription placeholder -

- - ) -} - -export function ActiveSubsciption({ - subscription, -}: { - subscription: Subscription -}) { - const { t } = useTranslation() - const { setShowChangePersonalPlan } = useSubscriptionDashboardContext() - - return ( - <> -

- , - ]} - /> - {subscription.pendingPlan && - subscription.pendingPlan.name !== subscription.plan.name && ( - <> - {' '} - , - ]} - /> - - )}{' '} - {/* TODO: pending_additional_licenses */} - {/* TODO: additionalLicenses */} - -

- {subscription.pendingPlan && ( -

{t('want_change_to_apply_before_plan_end')}

- )} - {/* TODO: groupPlan */} - {/* TODO: trialEndsAtFormatted */} -

- , - // eslint-disable-next-line react/jsx-key - , - ]} - /> -

- - - - {t('update_your_billing_details')} - {' '} - - {t('view_your_invoices')} - {' '} - {/* TODO: cancel button */} - - - ) -} diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/active.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/active.tsx new file mode 100644 index 0000000000..91b86b5fa8 --- /dev/null +++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/active.tsx @@ -0,0 +1,117 @@ +import { useTranslation, Trans } from 'react-i18next' +import PremiumFeaturesLink from '../../premium-features-link' +import { PriceExceptions } from '../../../shared/price-exceptions' +import { useSubscriptionDashboardContext } from '../../../../context/subscription-dashboard-context' +import { Subscription } from '../../../../../../../../types/subscription/dashboard/subscription' +import { CancelSubscriptionButton } from './cancel-subscription-button' +import { CancelSubscription } from './cancel-subscription' +import { PendingPlanChange } from './pending-plan-change' +import { TrialEnding } from './trial-ending' +import { ChangePlan } from './change-plan' + +export function ActiveSubsciption({ + subscription, +}: { + subscription: Subscription +}) { + const { t } = useTranslation() + const { recurlyLoadError, setShowChangePersonalPlan, showCancellation } = + useSubscriptionDashboardContext() + + if (showCancellation) return + + return ( + <> +

+ , + ]} + /> + {subscription.pendingPlan && ( + + )} + {!subscription.pendingPlan && + subscription.recurly.additionalLicenses > 0 && ( + <> + {' '} + , + // eslint-disable-next-line react/jsx-key + , + ]} + /> + + )}{' '} + {!recurlyLoadError && + !subscription.groupPlan && + subscription.recurly.account.has_past_due_invoice._ !== 'true' && ( + + )} +

+ {subscription.pendingPlan && ( +

{t('want_change_to_apply_before_plan_end')}

+ )} + {/* TODO: groupPlan */} + +

+ , + // eslint-disable-next-line react/jsx-key + , + ]} + /> +

+ + +

+ + {t('update_your_billing_details')} + {' '} + + {t('view_your_invoices')} + +

+ + {!recurlyLoadError && ( + + )} + + + + ) +} diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-subscription-button.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-subscription-button.tsx new file mode 100644 index 0000000000..bb961f2d31 --- /dev/null +++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-subscription-button.tsx @@ -0,0 +1,56 @@ +import { useTranslation, Trans } from 'react-i18next' +import * as eventTracking from '../../../../../../infrastructure/event-tracking' +import { Subscription } from '../../../../../../../../types/subscription/dashboard/subscription' +import { useSubscriptionDashboardContext } from '../../../../context/subscription-dashboard-context' + +export function CancelSubscriptionButton({ + subscription, +}: { + subscription: Subscription +}) { + const { t } = useTranslation() + const { recurlyLoadError, setShowCancellation } = + useSubscriptionDashboardContext() + + const stillInATrial = + subscription.recurly.trialEndsAtFormatted && + subscription.recurly.trial_ends_at && + new Date(subscription.recurly.trial_ends_at).getTime() > Date.now() + + function handleCancelSubscriptionClick() { + eventTracking.sendMB('subscription-page-cancel-button-click') + setShowCancellation(true) + } + + if (recurlyLoadError) return null + + return ( + <> +
+

+ +

+ {!stillInATrial && ( +

+ + , + ]} + /> + +

+ )} + + ) +} diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-subscription.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-subscription.tsx new file mode 100644 index 0000000000..eb4ed9d4a9 --- /dev/null +++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-subscription.tsx @@ -0,0 +1,15 @@ +import { useTranslation } from 'react-i18next' + +export function CancelSubscription() { + const { t } = useTranslation() + return ( +
+

+ {t('wed_love_you_to_stay')} +

+ {/* todo: showExtendFreeTrial */} + {/* todo: showDowngrade */} + {/* todo: showBasicCancel */} +
+ ) +} diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan.tsx new file mode 100644 index 0000000000..f56813a985 --- /dev/null +++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan.tsx @@ -0,0 +1,18 @@ +import { useTranslation } from 'react-i18next' +import { useSubscriptionDashboardContext } from '../../../../context/subscription-dashboard-context' + +export function ChangePlan() { + const { t } = useTranslation() + const { showChangePersonalPlan } = useSubscriptionDashboardContext() + + if (!showChangePersonalPlan) return null + + return ( + <> +

{t('change_plan')}

+

+ TODO: change subscription placeholder +

+ + ) +} diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/pending-plan-change.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/pending-plan-change.tsx new file mode 100644 index 0000000000..00e2a401d0 --- /dev/null +++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/pending-plan-change.tsx @@ -0,0 +1,49 @@ +import { Trans } from 'react-i18next' +import { Subscription } from '../../../../../../../../types/subscription/dashboard/subscription' + +export function PendingPlanChange({ + subscription, +}: { + subscription: Subscription +}) { + if (!subscription.pendingPlan) return null + + return ( + <> + {subscription.pendingPlan.name !== subscription.plan.name && ( + , + ]} + /> + )} + + {((subscription.recurly.pendingAdditionalLicenses && + subscription.recurly.pendingAdditionalLicenses > 0) || + subscription.recurly.additionalLicenses > 0) && ( + <> + {' '} + , + // eslint-disable-next-line react/jsx-key + , + ]} + /> + + )} + + ) +} diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/trial-ending.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/trial-ending.tsx new file mode 100644 index 0000000000..ce3615c9c4 --- /dev/null +++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/trial-ending.tsx @@ -0,0 +1,26 @@ +import { Trans } from 'react-i18next' +import { Subscription } from '../../../../../../../../types/subscription/dashboard/subscription' + +export function TrialEnding({ subscription }: { subscription: Subscription }) { + if ( + !subscription.recurly.trialEndsAtFormatted || + !subscription.recurly.trial_ends_at + ) + return null + + const endDate = new Date(subscription.recurly.trial_ends_at) + if (endDate.getTime() < Date.now()) return null + + return ( +

+ , + ]} + /> +

+ ) +} diff --git a/services/web/frontend/js/features/subscription/context/subscription-dashboard-context.tsx b/services/web/frontend/js/features/subscription/context/subscription-dashboard-context.tsx index 031846f674..04fdcc7629 100644 --- a/services/web/frontend/js/features/subscription/context/subscription-dashboard-context.tsx +++ b/services/web/frontend/js/features/subscription/context/subscription-dashboard-context.tsx @@ -1,6 +1,10 @@ import { createContext, ReactNode, useContext, useMemo, useState } from 'react' type SubscriptionDashboardContextValue = { + recurlyLoadError: boolean + setRecurlyLoadError: React.Dispatch> + showCancellation: boolean + setShowCancellation: React.Dispatch> showChangePersonalPlan: boolean setShowChangePersonalPlan: React.Dispatch> } @@ -14,14 +18,27 @@ export function SubscriptionDashboardProvider({ }: { children: ReactNode }) { + const [recurlyLoadError, setRecurlyLoadError] = useState(false) + const [showCancellation, setShowCancellation] = useState(false) const [showChangePersonalPlan, setShowChangePersonalPlan] = useState(false) const value = useMemo( () => ({ + recurlyLoadError, + setRecurlyLoadError, + showCancellation, + setShowCancellation, showChangePersonalPlan, setShowChangePersonalPlan, }), - [showChangePersonalPlan, setShowChangePersonalPlan] + [ + recurlyLoadError, + setRecurlyLoadError, + showCancellation, + setShowCancellation, + showChangePersonalPlan, + setShowChangePersonalPlan, + ] ) return ( diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 046471831c..a7610b9a0f 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -1657,6 +1657,7 @@ "your_sessions": "Your Sessions", "your_subscription": "Your Subscription", "your_subscription_has_expired": "Your subscription has expired.", + "youre_on_free_trial_which_ends_on": "You’re on a free trial which ends on <0>__date__.", "zh-CN": "Chinese", "zip_contents_too_large": "Zip contents too large", "zotero": "Zotero", diff --git a/services/web/test/frontend/features/subscription/components/dashboard/personal-subscription.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/personal-subscription.test.tsx index b958261624..eab3074c2e 100644 --- a/services/web/test/frontend/features/subscription/components/dashboard/personal-subscription.test.tsx +++ b/services/web/test/frontend/features/subscription/components/dashboard/personal-subscription.test.tsx @@ -11,10 +11,12 @@ import { describe('', function () { beforeEach(function () { window.metaAttributesCache = new Map() + window.recurly = {} }) afterEach(function () { window.metaAttributesCache = new Map() + delete window.recurly }) describe('no subscription', function () { @@ -74,7 +76,10 @@ describe('', function () { }) it('renders error message when an unknown subscription state', function () { - const withStateDeleted = Object.assign({}, annualActiveSubscription) + const withStateDeleted = Object.assign( + {}, + JSON.parse(JSON.stringify(annualActiveSubscription)) + ) withStateDeleted.recurly.state = undefined render( @@ -105,4 +110,35 @@ describe('', function () { expect(invoiceLinks.length).to.equal(2) }) }) + + describe('Recurly JS', function () { + const recurlyFailedToLoadText = + 'Sorry, there was an error talking to our payment provider. Please try again in a few moments. If you are using any ad or script blocking extensions in your browser, you may need to temporarily disable them.' + + it('shows an alert and hides "Change plan" option when Recurly did not load', function () { + delete window.recurly + render( + + + + ) + + screen.getByRole('alert') + screen.getByText(recurlyFailedToLoadText) + + expect(screen.queryByText('Change plan')).to.be.null + }) + + it('should not show an alert and should show "Change plan" option when Recurly did load', function () { + render( + + + + ) + + expect(screen.queryByRole('alert')).to.be.null + + screen.getByText('Change plan') + }) + }) }) diff --git a/services/web/test/frontend/features/subscription/components/dashboard/states/active.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/states/active.test.tsx index 3cf878e03c..8e09882e43 100644 --- a/services/web/test/frontend/features/subscription/components/dashboard/states/active.test.tsx +++ b/services/web/test/frontend/features/subscription/components/dashboard/states/active.test.tsx @@ -1,19 +1,29 @@ +import { expect } from 'chai' import { fireEvent, render, screen } from '@testing-library/react' -import { ActiveSubsciption } from '../../../../../../../frontend/js/features/subscription/components/dashboard/states/active' +import * as eventTracking from '../../../../../../../frontend/js/infrastructure/event-tracking' +import { ActiveSubsciption } from '../../../../../../../frontend/js/features/subscription/components/dashboard/states/active/active' import { SubscriptionDashboardProvider } from '../../../../../../../frontend/js/features/subscription/context/subscription-dashboard-context' import { Subscription } from '../../../../../../../types/subscription/dashboard/subscription' import { annualActiveSubscription, + groupActiveSubscription, + groupActiveSubscriptionWithPendingLicenseChange, pendingSubscriptionChange, + trialSubscription, } from '../../../fixtures/subscriptions' +import sinon from 'sinon' describe('', function () { + let sendMBSpy: sinon.SinonSpy + beforeEach(function () { window.metaAttributesCache = new Map() + sendMBSpy = sinon.spy(eventTracking, 'sendMB') }) afterEach(function () { window.metaAttributesCache = new Map() + sendMBSpy.restore() }) function expectedInActiveSubscription(subscription: Subscription) { @@ -25,13 +35,14 @@ describe('', function () { // sentence broken up by bolding screen.getByText('The next payment of', { exact: false }) - screen.getByText(annualActiveSubscription.recurly.displayPrice, { + screen.getByText(subscription.recurly.displayPrice, { exact: false, }) screen.getByText('will be collected on', { exact: false }) - screen.getByText(annualActiveSubscription.recurly.nextPaymentDueAt, { + const dates = screen.getAllByText(subscription.recurly.nextPaymentDueAt, { exact: false, }) + expect(dates.length).to.equal(2) // sentence broken up by link screen.getByText( @@ -89,4 +100,145 @@ describe('', function () { exact: false, }) }) + + it('does not show "Change plan" option for group plans', function () { + render( + + + + ) + + const changePlan = screen.queryByRole('button', { name: 'Change plan' }) + expect(changePlan).to.be.null + }) + + it('does not show "Change plan" option when past due', function () { + // account is likely in expired state, but be sure to not show option if state is still active + const activePastDueSubscription = Object.assign( + {}, + annualActiveSubscription + ) + + activePastDueSubscription.recurly.account.has_past_due_invoice._ = 'true' + + render( + + + + ) + + const changePlan = screen.queryByRole('button', { name: 'Change plan' }) + expect(changePlan).to.be.null + }) + + it('shows the pending license change message when plan change is pending', function () { + render( + + + + ) + + screen.getByText('Your subscription is changing to include', { + exact: false, + }) + + if ( + !groupActiveSubscriptionWithPendingLicenseChange.recurly + .pendingAdditionalLicenses + ) { + throw Error('not expected test data') + } + screen.getByText( + groupActiveSubscriptionWithPendingLicenseChange.recurly + .pendingAdditionalLicenses + ) + + screen.getByText('additional license(s) for a total of', { exact: false }) + + if ( + !groupActiveSubscriptionWithPendingLicenseChange.recurly + .pendingTotalLicenses + ) { + throw Error('not expected test data') + } + screen.getByText( + groupActiveSubscriptionWithPendingLicenseChange.recurly + .pendingTotalLicenses + ) + }) + + it('shows the pending license change message when plan change is not pending', function () { + const subscription = Object.assign({}, groupActiveSubscription) + subscription.recurly.additionalLicenses = 4 + subscription.recurly.totalLicenses = + subscription.recurly.totalLicenses + + subscription.recurly.additionalLicenses + + render( + + + + ) + + screen.getByText('Your subscription includes', { + exact: false, + }) + + if (!subscription.recurly.additionalLicenses) { + throw Error('not expected test data') + } + screen.getByText(subscription.recurly.additionalLicenses) + + screen.getByText('additional license(s) for a total of', { exact: false }) + + screen.getByText(subscription.recurly.totalLicenses) + }) + + it('shows when trial ends and first payment collected', function () { + render( + + + + ) + screen.getByText('You’re on a free trial which ends on', { exact: false }) + if (!trialSubscription.recurly.trialEndsAtFormatted) { + throw new Error('not expected test data') + } + const endDate = screen.getAllByText( + trialSubscription.recurly.trialEndsAtFormatted + ) + expect(endDate.length).to.equal(2) + }) + + it('shows cancel UI and sends event', function () { + render( + + + + ) + // before button clicked + screen.getByText( + 'Your subscription will remain active until the end of your billing period', + { exact: false } + ) + const dates = screen.getAllByText( + annualActiveSubscription.recurly.nextPaymentDueAt, + { + exact: false, + } + ) + expect(dates.length).to.equal(2) + + const button = screen.getByRole('button', { + name: 'Cancel Your Subscription', + }) + fireEvent.click(button) + expect(sendMBSpy).to.be.calledOnceWith( + 'subscription-page-cancel-button-click' + ) + + screen.getByText('We’d love you to stay') + }) }) diff --git a/services/web/test/frontend/features/subscription/components/dashboard/subscription-dashboard.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/subscription-dashboard.test.tsx index bca237f99a..6f329e2822 100644 --- a/services/web/test/frontend/features/subscription/components/dashboard/subscription-dashboard.test.tsx +++ b/services/web/test/frontend/features/subscription/components/dashboard/subscription-dashboard.test.tsx @@ -1,6 +1,7 @@ import { expect } from 'chai' import { render, screen } from '@testing-library/react' import SubscriptionDashboard from '../../../../../../frontend/js/features/subscription/components/dashboard/subscription-dashboard' +import { SubscriptionDashboardProvider } from '../../../../../../frontend/js/features/subscription/context/subscription-dashboard-context' describe('', function () { beforeEach(function () { @@ -13,7 +14,11 @@ describe('', function () { describe('Free Plan', function () { it('does not render the "Get the most out of your" subscription text', function () { - render() + render( + + + + ) const text = screen.queryByText('Get the most out of your', { exact: false, }) diff --git a/services/web/test/frontend/features/subscription/fixtures/subscriptions.tsx b/services/web/test/frontend/features/subscription/fixtures/subscriptions.tsx index 2f7fa5e2e7..353e4af699 100644 --- a/services/web/test/frontend/features/subscription/fixtures/subscriptions.tsx +++ b/services/web/test/frontend/features/subscription/fixtures/subscriptions.tsx @@ -3,6 +3,11 @@ const dateformat = require('dateformat') const today = new Date() const oneYearFromToday = new Date().setFullYear(today.getFullYear() + 1) const nextPaymentDueAt = dateformat(oneYearFromToday, 'dS mmmm yyyy') +const sevenDaysFromToday = new Date().setDate(today.getDate() + 7) +const sevenDaysFromTodayFormatted = dateformat( + sevenDaysFromToday, + 'dS mmmm yyyy' +) export const annualActiveSubscription: Subscription = { manager_ids: ['abc123'], @@ -171,3 +176,163 @@ export const pendingSubscriptionChange: Subscription = { featureDescription: [], }, } + +export const groupActiveSubscription: Subscription = { + manager_ids: ['abc123'], + member_ids: ['abc123'], + invited_emails: [], + groupPlan: true, + membersLimit: 10, + _id: 'def456', + admin_id: 'abc123', + teamInvites: [], + planCode: 'group_collaborator_10_enterprise', + recurlySubscription_id: 'ghi789', + plan: { + planCode: 'group_collaborator_10_enterprise', + name: 'Overleaf Standard (Collaborator) - Group Account (10 licenses) - Enterprise', + hideFromUsers: true, + price_in_cents: 129000, + annual: true, + features: {}, + groupPlan: true, + membersLimit: 10, + membersLimitAddOn: 'additional-license', + }, + recurly: { + tax: 0, + taxRate: 0, + billingDetailsLink: '/user/subscription/recurly/billing-details', + accountManagementLink: '/user/subscription/recurly/account-management', + additionalLicenses: 0, + totalLicenses: 10, + nextPaymentDueAt, + currency: 'USD', + state: 'active', + trialEndsAtFormatted: null, + trial_ends_at: null, + activeCoupons: [], + account: { + has_canceled_subscription: { _: 'false', $: { type: 'boolean' } }, + has_past_due_invoice: { _: 'false', $: { type: 'boolean' } }, + }, + displayPrice: '$1290.00', + }, +} + +export const groupActiveSubscriptionWithPendingLicenseChange: Subscription = { + manager_ids: ['abc123'], + member_ids: ['abc123'], + invited_emails: [], + groupPlan: true, + membersLimit: 10, + _id: 'def456', + admin_id: 'abc123', + teamInvites: [], + planCode: 'group_collaborator_10_enterprise', + recurlySubscription_id: 'ghi789', + plan: { + planCode: 'group_collaborator_10_enterprise', + name: 'Overleaf Standard (Collaborator) - Group Account (10 licenses) - Enterprise', + hideFromUsers: true, + price_in_cents: 129000, + annual: true, + features: {}, + groupPlan: true, + membersLimit: 10, + membersLimitAddOn: 'additional-license', + }, + recurly: { + tax: 0, + taxRate: 0, + billingDetailsLink: '/user/subscription/recurly/billing-details', + accountManagementLink: '/user/subscription/recurly/account-management', + additionalLicenses: 11, + totalLicenses: 21, + nextPaymentDueAt, + currency: 'USD', + state: 'active', + trialEndsAtFormatted: null, + trial_ends_at: null, + activeCoupons: [], + account: { + has_canceled_subscription: { + _: 'false', + $: { + type: 'boolean', + }, + }, + has_past_due_invoice: { + _: 'false', + $: { + type: 'boolean', + }, + }, + }, + displayPrice: '$2967.00', + currentPlanDisplayPrice: '$2709.00', + pendingAdditionalLicenses: 13, + pendingTotalLicenses: 23, + }, + pendingPlan: { + planCode: 'group_collaborator_10_enterprise', + name: 'Overleaf Standard (Collaborator) - Group Account (10 licenses) - Enterprise', + hideFromUsers: true, + price_in_cents: 129000, + annual: true, + features: {}, + groupPlan: true, + membersLimit: 10, + membersLimitAddOn: 'additional-license', + }, +} + +export const trialSubscription: Subscription = { + manager_ids: ['abc123'], + member_ids: [], + invited_emails: [], + groupPlan: false, + membersLimit: 0, + _id: 'def456', + admin_id: 'abc123', + teamInvites: [], + planCode: 'paid-personal_free_trial_7_days', + recurlySubscription_id: 'ghi789', + plan: { + planCode: 'paid-personal_free_trial_7_days', + name: 'Personal', + price_in_cents: 1500, + features: {}, + featureDescription: [], + hideFromUsers: true, + }, + recurly: { + tax: 0, + taxRate: 0, + billingDetailsLink: '/user/subscription/recurly/billing-details', + accountManagementLink: '/user/subscription/recurly/account-management', + additionalLicenses: 0, + totalLicenses: 0, + nextPaymentDueAt: sevenDaysFromTodayFormatted, + currency: 'USD', + state: 'active', + trialEndsAtFormatted: sevenDaysFromTodayFormatted, + trial_ends_at: new Date(sevenDaysFromToday).toString(), + activeCoupons: [], + account: { + has_canceled_subscription: { + _: 'false', + $: { + type: 'boolean', + }, + }, + has_past_due_invoice: { + _: 'false', + $: { + type: 'boolean', + }, + }, + }, + displayPrice: '$14.00', + }, +} diff --git a/services/web/types/subscription/dashboard/subscription.ts b/services/web/types/subscription/dashboard/subscription.ts index f0b8dc731e..8d2e5d4810 100644 --- a/services/web/types/subscription/dashboard/subscription.ts +++ b/services/web/types/subscription/dashboard/subscription.ts @@ -7,7 +7,10 @@ type Plan = { annual?: boolean features: object hideFromUsers?: boolean - featureDescription: object[] + featureDescription?: object[] + groupPlan?: boolean + membersLimit?: number + membersLimitAddOn?: string } type SubscriptionState = 'active' | 'canceled' | 'expired' diff --git a/services/web/types/window.ts b/services/web/types/window.ts index 9f02651ea4..f6f4dd255c 100644 --- a/services/web/types/window.ts +++ b/services/web/types/window.ts @@ -31,5 +31,6 @@ declare global { _reportAcePerf: () => void MathJax: Record overallThemes: OverallThemeMeta[] + recurly?: object } }