mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 17:19:37 +02:00
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:
@@ -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": "",
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user