Merge pull request #11981 from overleaf/jel-downgrade-when-cancel

[web] Migrate downgrade plan option when cancelling in React subscription dash

GitOrigin-RevId: 11a4b39deb58493f3f56e65e8760d71527a8e8fc
This commit is contained in:
Jessica Lawshe
2023-02-27 08:14:58 -06:00
committed by Copybot
parent bb9bafdf1a
commit 7469f01acd
4 changed files with 199 additions and 12 deletions

View File

@@ -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": "",

View File

@@ -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<unknown>) => Promise<unknown>
}) {
const { t } = useTranslation()
@@ -85,6 +94,34 @@ function NotCancelOption({
)
}
if (showDowngrade && planToDowngradeTo) {
return (
<>
<p>
<Trans
i18nKey="interested_in_cheaper_personal_plan"
values={{
price: planToDowngradeTo.displayPrice,
}}
components={[
// eslint-disable-next-line react/jsx-key
<strong />,
]}
/>
</p>
<p>
<DowngradePlanButton
isButtonDisabled={isButtonDisabled}
isLoadingSecondaryAction={isLoadingSecondaryAction}
isSuccessSecondaryAction={isSuccessSecondaryAction}
planToDowngradeTo={planToDowngradeTo}
runAsyncSecondaryAction={runAsyncSecondaryAction}
/>
</p>
</>
)
}
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 <LoadingSpinner />
}
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() {
<NotCancelOption
showExtendFreeTrial={showExtendFreeTrial}
showDowngrade={showDowngrade}
isButtonDisabled={isButtonDisabled}
isLoadingSecondaryAction={isLoadingSecondaryAction}
isSuccessSecondaryAction={isSuccessSecondaryAction}
planToDowngradeTo={planToDowngradeTo}
runAsyncSecondaryAction={runAsyncSecondaryAction}
/>

View File

@@ -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<unknown>) => Promise<unknown>
}) {
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 (
<>
<button
className="btn btn-primary"
onClick={handleDowngradePlan}
disabled={isButtonDisabled}
>
<ActionButtonText
inflight={isLoadingSecondaryAction || isSuccessSecondaryAction}
buttonText={buttonText}
/>
</button>
</>
)
}

View File

@@ -19,6 +19,7 @@ import fetchMock from 'fetch-mock'
import {
cancelSubscriptionUrl,
extendTrialUrl,
subscriptionUpdateUrl,
} from '../../../../../../../../frontend/js/features/subscription/data/subscription-url'
describe('<ActiveSubscription />', function () {
@@ -380,7 +381,7 @@ describe('<ActiveSubscription />', 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('<ActiveSubscription />', 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
})
})
})