From 7469f01acd75c997e07b1f583c640e17ab70e808 Mon Sep 17 00:00:00 2001 From: Jessica Lawshe Date: Mon, 27 Feb 2023 08:14:58 -0600 Subject: [PATCH] Merge pull request #11981 from overleaf/jel-downgrade-when-cancel [web] Migrate downgrade plan option when cancelling in React subscription dash GitOrigin-RevId: 11a4b39deb58493f3f56e65e8760d71527a8e8fc --- .../web/frontend/extracted-translations.json | 2 + .../cancel-plan/cancel-subscription.tsx | 61 ++++++++++-- .../cancel-plan/downgrade-plan-button.tsx | 50 ++++++++++ .../dashboard/states/active/active.test.tsx | 98 ++++++++++++++++++- 4 files changed, 199 insertions(+), 12 deletions(-) create mode 100644 services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-plan/downgrade-plan-button.tsx diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index fd7c050035..ec1ecd669f 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -387,6 +387,7 @@ "institution_and_role": "", "institutional_leavers_survey_notification": "", "integrations": "", + "interested_in_cheaper_personal_plan": "", "invalid_email": "", "invalid_file_name": "", "invalid_filename": "", @@ -916,6 +917,7 @@ "x_price_per_user": "", "x_price_per_year": "", "year": "", + "yes_move_me_to_personal_plan": "", "you_already_have_a_subscription": "", "you_are_a_manager_and_member_of_x_plan_as_member_of_group_subscription_y_administered_by_z": "", "you_are_a_manager_of_commons_at_institution_x": "", diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-plan/cancel-subscription.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-plan/cancel-subscription.tsx index 20e708ed1e..ef921f88a8 100644 --- a/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-plan/cancel-subscription.tsx +++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-plan/cancel-subscription.tsx @@ -1,5 +1,7 @@ import { Trans, useTranslation } from 'react-i18next' +import { Plan } from '../../../../../../../../../types/subscription/plan' import { postJSON } from '../../../../../../../infrastructure/fetch-json' +import LoadingSpinner from '../../../../../../../shared/components/loading-spinner' import useAsync from '../../../../../../../shared/hooks/use-async' import { useSubscriptionDashboardContext } from '../../../../../context/subscription-dashboard-context' import { @@ -10,8 +12,11 @@ import canExtendTrial from '../../../../../util/can-extend-trial' import showDowngradeOption from '../../../../../util/show-downgrade-option' import ActionButtonText from '../../../action-button-text' import GenericErrorAlert from '../../../generic-error-alert' +import DowngradePlanButton from './downgrade-plan-button' import ExtendTrialButton from './extend-trial-button' +const planCodeToDowngradeTo = 'paid-personal' + function ConfirmCancelSubscriptionButton({ buttonClass, buttonText, @@ -45,13 +50,17 @@ function NotCancelOption({ isButtonDisabled, isLoadingSecondaryAction, isSuccessSecondaryAction, + planToDowngradeTo, showExtendFreeTrial, + showDowngrade, runAsyncSecondaryAction, }: { isButtonDisabled: boolean isLoadingSecondaryAction: boolean isSuccessSecondaryAction: boolean + planToDowngradeTo?: Plan showExtendFreeTrial: boolean + showDowngrade: boolean runAsyncSecondaryAction: (promise: Promise) => Promise }) { const { t } = useTranslation() @@ -85,6 +94,34 @@ function NotCancelOption({ ) } + if (showDowngrade && planToDowngradeTo) { + return ( + <> +

+ , + ]} + /> +

+

+ +

+ + ) + } + function handleKeepPlan() { setShowCancellation(false) } @@ -103,8 +140,7 @@ function NotCancelOption({ export function CancelSubscription() { const { t } = useTranslation() - const { personalSubscription } = useSubscriptionDashboardContext() - + const { personalSubscription, plans } = useSubscriptionDashboardContext() const { isLoading: isLoadingCancel, isError: isErrorCancel, @@ -117,7 +153,6 @@ export function CancelSubscription() { isSuccess: isSuccessSecondaryAction, runAsync: runAsyncSecondaryAction, } = useAsync() - const isButtonDisabled = isLoadingCancel || isLoadingSecondaryAction || @@ -126,6 +161,18 @@ export function CancelSubscription() { if (!personalSubscription || !('recurly' in personalSubscription)) return null + const showDowngrade = showDowngradeOption( + personalSubscription.plan.planCode, + personalSubscription.plan.groupPlan, + personalSubscription.recurly.trial_ends_at + ) + const planToDowngradeTo = plans.find( + plan => plan.planCode === planCodeToDowngradeTo + ) + if (showDowngrade && !planToDowngradeTo) { + return + } + async function handleCancelSubscription() { try { await runAsyncCancel(postJSON(cancelSubscriptionUrl)) @@ -141,12 +188,6 @@ export function CancelSubscription() { personalSubscription.recurly.trial_ends_at ) - const showDowngrade = showDowngradeOption( - personalSubscription.plan.planCode, - personalSubscription.plan.groupPlan, - personalSubscription.recurly.trial_ends_at - ) - let confirmCancelButtonText = t('cancel_my_account') let confirmCancelButtonClass = 'btn-primary' if (showExtendFreeTrial || showDowngrade) { @@ -164,9 +205,11 @@ export function CancelSubscription() { diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-plan/downgrade-plan-button.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-plan/downgrade-plan-button.tsx new file mode 100644 index 0000000000..62b5442467 --- /dev/null +++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-plan/downgrade-plan-button.tsx @@ -0,0 +1,50 @@ +import { useTranslation } from 'react-i18next' +import { Plan } from '../../../../../../../../../types/subscription/plan' +import { postJSON } from '../../../../../../../infrastructure/fetch-json' +import { subscriptionUpdateUrl } from '../../../../../data/subscription-url' +import ActionButtonText from '../../../action-button-text' + +export default function DowngradePlanButton({ + isButtonDisabled, + isLoadingSecondaryAction, + isSuccessSecondaryAction, + planToDowngradeTo, + runAsyncSecondaryAction, +}: { + isButtonDisabled: boolean + isLoadingSecondaryAction: boolean + isSuccessSecondaryAction: boolean + planToDowngradeTo: Plan + runAsyncSecondaryAction: (promise: Promise) => Promise +}) { + const { t } = useTranslation() + const buttonText = t('yes_move_me_to_personal_plan') + + async function handleDowngradePlan() { + try { + await runAsyncSecondaryAction( + postJSON(`${subscriptionUpdateUrl}?downgradeToPaidPersonal`, { + body: { plan_code: planToDowngradeTo.planCode }, + }) + ) + window.location.reload() + } catch (e) { + console.error(e) + } + } + + return ( + <> + + + ) +} diff --git a/services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx index 29be349be3..f429130eaf 100644 --- a/services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx +++ b/services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx @@ -19,6 +19,7 @@ import fetchMock from 'fetch-mock' import { cancelSubscriptionUrl, extendTrialUrl, + subscriptionUpdateUrl, } from '../../../../../../../../frontend/js/features/subscription/data/subscription-url' describe('', function () { @@ -380,7 +381,7 @@ describe('', function () { ).to.be.null }) - it('reloads page after the succesful request to extend trial', async function () { + it('reloads page after the successful request to extend trial', async function () { const endPointResponse = { status: 200, } @@ -399,11 +400,102 @@ describe('', function () { }) describe('downgrade plan', function () { - it('shows alternate cancel subscription button text', function () { + const cancelButtonText = 'No thanks, I still want to cancel' + const downgradeButtonText = 'Yes, move me to the Personal plan' + it('shows alternate cancel subscription button text', async function () { renderActiveSubscription(monthlyActiveCollaborator) showConfirmCancelUI() + await screen.findByRole('button', { + name: cancelButtonText, + }) screen.getByRole('button', { - name: 'No thanks, I still want to cancel', + name: downgradeButtonText, + }) + screen.getByText('Would you be interested in the cheaper', { + exact: false, + }) + screen.getByText('Personal plan?', { + exact: false, + }) + }) + + it('disables both buttons and updates text for when trial button clicked', async function () { + renderActiveSubscription(monthlyActiveCollaborator) + showConfirmCancelUI() + const downgradeButton = await screen.findByRole('button', { + name: downgradeButtonText, + }) + fireEvent.click(downgradeButton) + + const buttons = screen.getAllByRole('button') + expect(buttons.length).to.equal(2) + expect(buttons[0].getAttribute('disabled')).to.equal('') + expect(buttons[1].getAttribute('disabled')).to.equal('') + screen.getByRole('button', { + name: cancelButtonText, + }) + screen.getByRole('button', { + name: 'Processing…', + }) + }) + + it('disables both buttons and updates text for when cancel button clicked', async function () { + renderActiveSubscription(monthlyActiveCollaborator) + showConfirmCancelUI() + const cancelButtton = await screen.findByRole('button', { + name: cancelButtonText, + }) + fireEvent.click(cancelButtton) + + const buttons = screen.getAllByRole('button') + expect(buttons.length).to.equal(2) + expect(buttons[0].getAttribute('disabled')).to.equal('') + expect(buttons[1].getAttribute('disabled')).to.equal('') + screen.getByRole('button', { + name: 'Processing…', + }) + screen.getByRole('button', { + name: downgradeButtonText, + }) + }) + + it('does not show option to downgrade when not a collaborator plan', function () { + const trialPlan = cloneDeep(monthlyActiveCollaborator) + trialPlan.plan.planCode = 'anotherplan' + renderActiveSubscription(trialPlan) + showConfirmCancelUI() + expect( + screen.queryByRole('button', { + name: downgradeButtonText, + }) + ).to.be.null + }) + + it('does not show option to extend trial when on a collaborator trial', function () { + const trialPlan = cloneDeep(trialCollaboratorSubscription) + renderActiveSubscription(trialPlan) + showConfirmCancelUI() + expect( + screen.queryByRole('button', { + name: downgradeButtonText, + }) + ).to.be.null + }) + + it('reloads page after the successful request to downgrade plan', async function () { + const endPointResponse = { + status: 200, + } + fetchMock.post(subscriptionUpdateUrl, endPointResponse) + renderActiveSubscription(monthlyActiveCollaborator) + showConfirmCancelUI() + const downgradeButton = await screen.findByRole('button', { + name: downgradeButtonText, + }) + fireEvent.click(downgradeButton) + // page is reloaded on success + await waitFor(() => { + expect(reloadStub).to.have.been.called }) }) })