From c1b8bfd73cc2550277c6feec947415374ff347f0 Mon Sep 17 00:00:00 2001 From: roo hutton Date: Wed, 22 Jan 2025 13:08:34 +0000 Subject: [PATCH] Merge pull request #21905 from overleaf/rh-pause-sub Add support for pausing subscription GitOrigin-RevId: f939ea4e7f3c2b1fa16dcb8aff1b2460d091d4e2 --- .../Features/Subscription/FeaturesUpdater.js | 2 +- .../Features/Subscription/RecurlyClient.js | 14 ++ .../Subscription/RecurlyEventHandler.js | 46 ++++++ .../Subscription/SubscriptionController.js | 56 +++++++ .../Subscription/SubscriptionHandler.js | 46 ++++++ .../Subscription/SubscriptionRouter.mjs | 17 ++ .../SubscriptionViewModelBuilder.js | 2 + .../web/frontend/extracted-translations.json | 25 +++ .../current-plan-widget.tsx | 9 ++ .../current-plan-widget/paused-plan.tsx | 59 +++++++ .../components/dashboard/pause-duck.svg | 42 +++++ .../components/dashboard/pause-modal.tsx | 137 ++++++++++++++++ .../dashboard/personal-subscription.tsx | 3 + .../dashboard/states/active/active.tsx | 116 ++++++++++++-- .../active/cancel-subscription-button.tsx | 38 ++++- .../states/active/confirm-unpause-modal.tsx | 103 ++++++++++++ .../dashboard/states/active/flash-message.tsx | 61 ++++++++ .../dashboard/states/active/paused.tsx | 95 +++++++++++ .../subscription-dashboard-context.tsx | 18 +++ .../frontend/js/shared/components/location.js | 7 + .../frontend/js/shared/hooks/use-location.ts | 21 ++- services/web/locales/en.json | 25 +++ .../components/current-plan-widget.test.tsx | 26 +++ .../modal-content-new-project-form.test.tsx | 1 + .../components/notifications.test.tsx | 1 + .../components/project-list-root.test.tsx | 1 + ...e-and-download-project-pdf-button.test.tsx | 1 + .../download-project-button.test.tsx | 1 + .../emails/reconfirmation-info.test.tsx | 1 + .../components/leave/modal-form.test.tsx | 1 + .../group-subscription-memberships.test.tsx | 1 + .../components/dashboard/pause-modal.test.tsx | 143 +++++++++++++++++ .../dashboard/personal-subscription.test.tsx | 2 + .../dashboard/states/active/active.test.tsx | 2 + .../active/change-plan/change-plan.test.tsx | 2 + .../src/Subscription/RecurlyClientTests.js | 18 +++ .../SubscriptionControllerTests.js | 60 +++++++ .../Subscription/SubscriptionHandlerTests.js | 148 ++++++++++++++++++ .../types/project/dashboard/subscription.ts | 5 + .../types/subscription/dashboard/modal-ids.ts | 2 + .../subscription/dashboard/subscription.ts | 4 +- 41 files changed, 1340 insertions(+), 22 deletions(-) create mode 100644 services/web/frontend/js/features/project-list/components/current-plan-widget/paused-plan.tsx create mode 100644 services/web/frontend/js/features/subscription/components/dashboard/pause-duck.svg create mode 100644 services/web/frontend/js/features/subscription/components/dashboard/pause-modal.tsx create mode 100644 services/web/frontend/js/features/subscription/components/dashboard/states/active/confirm-unpause-modal.tsx create mode 100644 services/web/frontend/js/features/subscription/components/dashboard/states/active/flash-message.tsx create mode 100644 services/web/frontend/js/features/subscription/components/dashboard/states/active/paused.tsx create mode 100644 services/web/test/frontend/features/subscription/components/dashboard/pause-modal.test.tsx diff --git a/services/web/app/src/Features/Subscription/FeaturesUpdater.js b/services/web/app/src/Features/Subscription/FeaturesUpdater.js index 8b188183eb..bbc040c4dd 100644 --- a/services/web/app/src/Features/Subscription/FeaturesUpdater.js +++ b/services/web/app/src/Features/Subscription/FeaturesUpdater.js @@ -117,7 +117,7 @@ async function computeFeatures(userId) { async function _getIndividualFeatures(userId) { const subscription = await SubscriptionLocator.promises.getUsersSubscription(userId) - if (subscription == null) { + if (subscription == null || subscription?.recurlyStatus?.state === 'paused') { return {} } diff --git a/services/web/app/src/Features/Subscription/RecurlyClient.js b/services/web/app/src/Features/Subscription/RecurlyClient.js index b094eac3ab..593b0286ab 100644 --- a/services/web/app/src/Features/Subscription/RecurlyClient.js +++ b/services/web/app/src/Features/Subscription/RecurlyClient.js @@ -176,6 +176,16 @@ async function cancelSubscriptionByUuid(subscriptionUuid) { } } +async function pauseSubscriptionByUuid(subscriptionUuid, pauseCycles) { + return await client.pauseSubscription('uuid-' + subscriptionUuid, { + remainingPauseCycles: pauseCycles, + }) +} + +async function resumeSubscriptionByUuid(subscriptionUuid) { + return await client.resumeSubscription('uuid-' + subscriptionUuid) +} + /** * Get the payment method for the given user * @@ -459,6 +469,8 @@ module.exports = { getAddOn: callbackify(getAddOn), getPlan: callbackify(getPlan), subscriptionIsCanceledOrExpired, + pauseSubscriptionByUuid: callbackify(pauseSubscriptionByUuid), + resumeSubscriptionByUuid: callbackify(resumeSubscriptionByUuid), promises: { getSubscription, @@ -471,6 +483,8 @@ module.exports = { removeSubscriptionChangeByUuid, reactivateSubscriptionByUuid, cancelSubscriptionByUuid, + pauseSubscriptionByUuid, + resumeSubscriptionByUuid, getPaymentMethod, getAddOn, getPlan, diff --git a/services/web/app/src/Features/Subscription/RecurlyEventHandler.js b/services/web/app/src/Features/Subscription/RecurlyEventHandler.js index dd7c7e3ac0..c4fdf6a724 100644 --- a/services/web/app/src/Features/Subscription/RecurlyEventHandler.js +++ b/services/web/app/src/Features/Subscription/RecurlyEventHandler.js @@ -30,6 +30,13 @@ async function sendRecurlyAnalyticsEvent(event, eventData) { case 'reactivated_account_notification': await _sendSubscriptionReactivatedEvent(userId, eventData) break + case 'subscription_paused_notification': + await _sendSubscriptionPausedEvent(userId, eventData) + break + case 'subscription_resumed_notification': + // 'resumed' here means resumed from pause + await _sendSubscriptionResumedEvent(userId, eventData) + break case 'paid_charge_invoice_notification': if ( eventData.invoice.state === 'paid' && @@ -49,6 +56,45 @@ async function sendRecurlyAnalyticsEvent(event, eventData) { } } +async function _sendSubscriptionResumedEvent(userId, eventData) { + const { planCode, state, subscriptionId } = _getSubscriptionData(eventData) + + AnalyticsManager.recordEventForUserInBackground( + userId, + 'subscription-resumed', + { + plan_code: planCode, + subscriptionId, + } + ) + AnalyticsManager.setUserPropertyForUserInBackground( + userId, + 'subscription-state', + state + ) +} + +async function _sendSubscriptionPausedEvent(userId, eventData) { + const { planCode, state, subscriptionId } = _getSubscriptionData(eventData) + + const pauseLength = eventData.subscription.remaining_pause_cycles + + AnalyticsManager.recordEventForUserInBackground( + userId, + 'subscription-paused', + { + pause_length: pauseLength, + plan_code: planCode, + subscriptionId, + } + ) + AnalyticsManager.setUserPropertyForUserInBackground( + userId, + 'subscription-state', + state + ) +} + async function _sendSubscriptionStartedEvent(userId, eventData) { const { planCode, quantity, state, isTrial, hasAiAddOn, subscriptionId } = _getSubscriptionData(eventData) diff --git a/services/web/app/src/Features/Subscription/SubscriptionController.js b/services/web/app/src/Features/Subscription/SubscriptionController.js index 987acd7fe5..3c25f4f4fa 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionController.js +++ b/services/web/app/src/Features/Subscription/SubscriptionController.js @@ -51,6 +51,8 @@ async function userSubscriptionPage(req, res) { await SplitTestHandler.promises.getAssignment(req, res, 'ai-add-on') + await SplitTestHandler.promises.getAssignment(req, res, 'pause-subscription') + // Populates splitTestVariants with a value for the split test name and allows // Pug to read it await SplitTestHandler.promises.getAssignment( @@ -173,6 +175,56 @@ async function successfulSubscription(req, res) { } } +async function pauseSubscription(req, res, next) { + const user = SessionManager.getSessionUser(req.session) + const pauseCycles = req.params.pauseCycles + if (!('pauseCycles' in req.params)) { + return HttpErrorHandler.badRequest( + req, + res, + `Pausing subscription requires a 'pauseCycles' argument with number of billing cycles to pause for` + ) + } + if (pauseCycles < 0) { + return HttpErrorHandler.badRequest( + req, + res, + `'pauseCycles' should be a number of billing cycles to pause for, or 0 to cancel a pending pause` + ) + } + logger.debug( + { userId: user._id }, + `pausing subscription for ${pauseCycles} billing cycles` + ) + try { + await SubscriptionHandler.promises.pauseSubscription(user, pauseCycles) + return res.sendStatus(200) + } catch (err) { + if (err instanceof Error) { + OError.tag(err, 'something went wrong pausing subscription', { + user_id: user._id, + }) + } + return next(err) + } +} + +async function resumeSubscription(req, res, next) { + const user = SessionManager.getSessionUser(req.session) + logger.debug({ userId: user._id }, `resuming subscription`) + try { + await SubscriptionHandler.promises.resumeSubscription(user) + return res.sendStatus(200) + } catch (err) { + if (err instanceof Error) { + OError.tag(err, 'something went wrong resuming subscription', { + user_id: user._id, + }) + } + return next(err) + } +} + function cancelSubscription(req, res, next) { const user = SessionManager.getSessionUser(req.session) logger.debug({ userId: user._id }, 'canceling subscription') @@ -458,6 +510,8 @@ function recurlyCallback(req, res, next) { 'new_subscription_notification', 'updated_subscription_notification', 'expired_subscription_notification', + 'subscription_paused_notification', + 'subscription_resumed_notification', ].includes(event) ) { const recurlySubscription = eventData.subscription @@ -667,6 +721,8 @@ module.exports = { userSubscriptionPage: expressify(userSubscriptionPage), successfulSubscription: expressify(successfulSubscription), cancelSubscription, + pauseSubscription, + resumeSubscription, canceledSubscription: expressify(canceledSubscription), cancelV1Subscription, previewSubscription: expressify(previewSubscription), diff --git a/services/web/app/src/Features/Subscription/SubscriptionHandler.js b/services/web/app/src/Features/Subscription/SubscriptionHandler.js index fe565e1bbb..4988bb5983 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionHandler.js +++ b/services/web/app/src/Features/Subscription/SubscriptionHandler.js @@ -381,6 +381,48 @@ async function getSubscriptionForUser(userId) { } } +async function pauseSubscription(user, pauseCycles) { + // only allow pausing on monthly plans not in a trial + const { subscription } = + await LimitationsManager.promises.userHasSubscription(user) + if (!subscription || !subscription.recurlyStatus) { + throw new Error('No active subscription to pause') + } + + if ( + !subscription.planCode || + subscription.planCode.includes('ann') || + subscription.groupPlan + ) { + throw new Error('Can only pause monthly individual plans') + } + if ( + subscription.recurlyStatus.trialEndsAt && + subscription.recurlyStatus.trialEndsAt > new Date() + ) { + throw new Error('Cannot pause a subscription in a trial') + } + if (subscription.addOns?.length) { + throw new Error('Cannot pause a subscription with addons') + } + + await RecurlyClient.promises.pauseSubscriptionByUuid( + subscription.recurlySubscription_id, + pauseCycles + ) +} + +async function resumeSubscription(user) { + const { subscription } = + await LimitationsManager.promises.userHasSubscription(user) + if (!subscription || !subscription.recurlyStatus) { + throw new Error('No active subscription to resume') + } + await RecurlyClient.promises.resumeSubscriptionByUuid( + subscription.recurlySubscription_id + ) +} + module.exports = { validateNoSubscriptionInRecurly: callbackify(validateNoSubscriptionInRecurly), createSubscription: callbackify(createSubscription), @@ -395,6 +437,8 @@ module.exports = { previewAddonPurchase: callbackify(previewAddonPurchase), purchaseAddon: callbackify(purchaseAddon), removeAddon: callbackify(removeAddon), + pauseSubscription: callbackify(pauseSubscription), + resumeSubscription: callbackify(resumeSubscription), promises: { validateNoSubscriptionInRecurly, createSubscription, @@ -409,5 +453,7 @@ module.exports = { previewAddonPurchase, purchaseAddon, removeAddon, + pauseSubscription, + resumeSubscription, }, } diff --git a/services/web/app/src/Features/Subscription/SubscriptionRouter.mjs b/services/web/app/src/Features/Subscription/SubscriptionRouter.mjs index 31c55c27ba..998b5f4b28 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionRouter.mjs +++ b/services/web/app/src/Features/Subscription/SubscriptionRouter.mjs @@ -205,6 +205,23 @@ export default { RateLimiterMiddleware.rateLimit(subscriptionRateLimiter), SubscriptionController.cancelSubscription ) + webRouter.post( + '/user/subscription/pause/:pauseCycles', + AuthenticationController.requireLogin(), + validate({ + params: Joi.object({ + pauseCycles: Joi.number().integer().max(12), + }), + }), + RateLimiterMiddleware.rateLimit(subscriptionRateLimiter), + SubscriptionController.pauseSubscription + ) + webRouter.post( + '/user/subscription/resume', + AuthenticationController.requireLogin(), + RateLimiterMiddleware.rateLimit(subscriptionRateLimiter), + SubscriptionController.resumeSubscription + ) webRouter.post( '/user/subscription/reactivate', AuthenticationController.requireLogin(), diff --git a/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js b/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js index c4b5dcaf29..6274f4d3bd 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js +++ b/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js @@ -279,6 +279,8 @@ async function buildUsersSubscriptionViewModel(user, locale = 'en') { trial_ends_at: recurlySubscription.trial_ends_at, activeCoupons: recurlyCoupons, account: recurlySubscription.account, + pausedAt: recurlySubscription.paused_at, + remainingPauseCycles: recurlySubscription.remaining_pause_cycles, } if (recurlySubscription.pending_subscription) { const pendingPlan = PlansLocator.findLocalPlanInSettings( diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index df68da6ec1..d9140714aa 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -42,6 +42,7 @@ "accepted_invite": "", "accepting_invite_as": "", "access_denied": "", + "access_edit_your_projects": "", "access_levels_changed": "", "account_has_been_link_to_institution_account": "", "account_has_past_due_invoice_change_plan_warning": "", @@ -188,6 +189,7 @@ "cancel_my_account": "", "cancel_my_subscription": "", "cancel_personal_subscription_first": "", + "cancel_subscription": "", "cancel_your_subscription": "", "cannot_invite_non_user": "", "cannot_invite_self": "", @@ -234,6 +236,7 @@ "clear_search": "", "click_here_to_view_sl_in_lng": "", "click_to_give_feedback": "", + "click_to_unpause": "", "clicking_delete_will_remove_sso_config_and_clear_saml_data": "", "clone_with_git": "", "close": "", @@ -296,6 +299,7 @@ "continue": "", "continue_github_merge": "", "continue_to": "", + "continue_using_free_features": "", "copied": "", "copy": "", "copy_code": "", @@ -394,6 +398,7 @@ "doing_this_allow_log_in_through_institution_2": "", "doing_this_will_verify_affiliation_and_allow_log_in_2": "", "done": "", + "dont_forget_you_currently_have": "", "download": "", "download_all": "", "download_metadata": "", @@ -493,6 +498,7 @@ "failed_to_send_managed_user_invite_to_email": "", "failed_to_send_sso_link_invite_to_email": "", "fast": "", + "features_like_track_changes": "", "file_action_created": "", "file_action_deleted": "", "file_action_edited": "", @@ -759,6 +765,7 @@ "institution_templates": "", "institutional_leavers_survey_notification": "", "integrations": "", + "integrations_like_github": "", "interested_in_cheaper_personal_plan": "", "interface": "", "interface_settings": "", @@ -835,6 +842,7 @@ "let_us_know": "", "let_us_know_how_we_can_help": "", "let_us_know_what_you_think": "", + "lets_get_those_premium_features": "", "library": "", "license_for_educational_purposes_confirmation": "", "limited_offer": "", @@ -944,9 +952,12 @@ "missing_fields_for_entry": "", "money_back_guarantee": "", "month": "", + "month_plural": "", "more": "", "more_actions": "", + "more_collabs_per_project": "", "more_comments": "", + "more_compile_time": "", "more_info": "", "more_options": "", "more_options_for_border_settings_coming_soon": "", @@ -1085,6 +1096,8 @@ "paste_options": "", "paste_with_formatting": "", "paste_without_formatting": "", + "pause_subscription": "", + "pause_subscription_for": "", "pay_now": "", "payment_provider_unreachable_error": "", "payment_summary": "", @@ -1107,6 +1120,7 @@ "percent_is_the_percentage_of_the_line_width": "", "permanently_disables_the_preview": "", "personal_library": "", + "pick_up_where_you_left_off": "", "plan": "", "plan_tooltip": "", "please_ask_the_project_owner_to_upgrade_more_editors": "", @@ -1778,9 +1792,11 @@ "unlink_warning_reference": "", "unlinking": "", "unmerge_cells": "", + "unpause_subscription": "", "unpublish": "", "unpublishing": "", "unsubscribe": "", + "until_then_you_can_still": "", "untrash": "", "update": "", "update_account_info": "", @@ -1874,6 +1890,7 @@ "website_status": "", "wed_love_you_to_stay": "", "welcome_to_sl": "", + "well_be_here_when_youre_ready": "", "were_making_some_changes_to_project_sharing_this_means_you_will_be_visible": "", "were_performing_maintenance": "", "weve_recently_reduced_the_compile_timeout_limit_which_may_have_affected_this_project": "", @@ -1886,6 +1903,7 @@ "what_should_we_call_you": "", "when_you_tick_the_include_caption_box": "", "why_latex": "", + "why_not_pause_instead": "", "wide": "", "will_lose_edit_access_on_date": "", "with_premium_subscription_you_also_get": "", @@ -1930,6 +1948,7 @@ "you_can_request_a_maximum_of_limit_fixes_per_day": "", "you_can_select_or_invite": "", "you_can_select_or_invite_plural": "", + "you_can_still_use_your_premium_features": "", "you_cant_add_or_change_password_due_to_sso": "", "you_cant_join_this_group_subscription": "", "you_dont_have_any_repositories": "", @@ -1943,6 +1962,7 @@ "you_have_x_users_and_your_plan_supports_up_to_y": "", "you_have_x_users_on_your_subscription": "", "you_need_to_configure_your_sso_settings": "", + "you_unpaused_your_subscription": "", "you_will_be_able_to_reassign_subscription": "", "youll_get_best_results_in_visual_but_can_be_used_in_source": "", "youll_need_to_ask_the_github_repository_owner": "", @@ -1952,6 +1972,7 @@ "your_affiliation_is_confirmed": "", "your_browser_does_not_support_this_feature": "", "your_compile_timed_out": "", + "your_current_plan_gives_you": "", "your_current_plan_supports_up_to_x_users": "", "your_current_project_will_revert_to_the_version_from_time": "", "your_git_access_info": "", @@ -1970,6 +1991,7 @@ "your_plan_is_changing_at_term_end": "", "your_plan_is_limited_to_n_editors": "", "your_plan_is_limited_to_n_editors_plural": "", + "your_premium_plan_is_paused": "", "your_project_exceeded_compile_timeout_limit_on_free_plan": "", "your_project_exceeded_editor_limit": "", "your_project_near_compile_timeout_limit": "", @@ -1977,6 +1999,8 @@ "your_role": "", "your_subscription": "", "your_subscription_has_expired": "", + "your_subscription_will_pause_on": "", + "your_subscription_will_pause_on_short": "", "youre_a_member_of_overleaf_labs": "", "youre_about_to_disable_single_sign_on": "", "youre_about_to_enable_single_sign_on": "", @@ -1989,6 +2013,7 @@ "youve_added_more_users": "", "youve_added_x_more_users_to_your_subscription_invite_people": "", "youve_lost_edit_access": "", + "youve_paused_your_subscription": "", "youve_unlinked_all_users": "", "youve_upgraded_your_plan": "", "zoom_in": "", diff --git a/services/web/frontend/js/features/project-list/components/current-plan-widget/current-plan-widget.tsx b/services/web/frontend/js/features/project-list/components/current-plan-widget/current-plan-widget.tsx index aae2053b19..20bfe55479 100644 --- a/services/web/frontend/js/features/project-list/components/current-plan-widget/current-plan-widget.tsx +++ b/services/web/frontend/js/features/project-list/components/current-plan-widget/current-plan-widget.tsx @@ -2,6 +2,7 @@ import FreePlan from './free-plan' import IndividualPlan from './individual-plan' import GroupPlan from './group-plan' import CommonsPlan from './commons-plan' +import PausedPlan from './paused-plan' import getMeta from '../../../../utils/meta' function CurrentPlanWidget() { @@ -16,8 +17,12 @@ function CurrentPlanWidget() { const isIndividualPlan = type === 'individual' const isGroupPlan = type === 'group' const isCommonsPlan = type === 'commons' + const isPaused = + isIndividualPlan && + usersBestSubscription.subscription?.recurlyStatus?.state === 'paused' const featuresPageURL = '/learn/how-to/Overleaf_premium_features' + const subscriptionPageUrl = '/user/subscription' let currentPlan @@ -35,6 +40,10 @@ function CurrentPlanWidget() { ) } + if (isPaused) { + currentPlan = + } + if (isGroupPlan) { currentPlan = ( , + ]} + /> + ) + + return ( + <> + + {currentPlanLabel} + + + + {currentPlanLabel}  + } + bs5={ + + } + /> + + + + ) +} + +export default PausedPlan diff --git a/services/web/frontend/js/features/subscription/components/dashboard/pause-duck.svg b/services/web/frontend/js/features/subscription/components/dashboard/pause-duck.svg new file mode 100644 index 0000000000..6297f13378 --- /dev/null +++ b/services/web/frontend/js/features/subscription/components/dashboard/pause-duck.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/services/web/frontend/js/features/subscription/components/dashboard/pause-modal.tsx b/services/web/frontend/js/features/subscription/components/dashboard/pause-modal.tsx new file mode 100644 index 0000000000..9d690dcb69 --- /dev/null +++ b/services/web/frontend/js/features/subscription/components/dashboard/pause-modal.tsx @@ -0,0 +1,137 @@ +import { useTranslation } from 'react-i18next' +import { useSubscriptionDashboardContext } from '../../context/subscription-dashboard-context' +import { useCallback, useMemo, useState } from 'react' +import { postJSON } from '@/infrastructure/fetch-json' +import { useLocation } from '@/shared/hooks/use-location' +import OLModal, { + OLModalBody, + OLModalHeader, +} from '@/features/ui/components/ol/ol-modal' +import { Select } from '@/shared/components/select' +import OLFormGroup from '@/features/ui/components/ol/ol-form-group' +import Button from '@/features/ui/components/bootstrap-5/button' +import { Stack } from 'react-bootstrap-5' +import { debugConsole } from '@/utils/debugging' +import * as eventTracking from '../../../../infrastructure/event-tracking' +import PauseDuck from './pause-duck.svg' +import GenericErrorAlert from './generic-error-alert' +import { RecurlySubscription } from '../../../../../../types/subscription/dashboard/subscription' + +const pauseMonthDurationOptions = [1, 2, 3] + +export const PAUSE_SUB_MODAL_ID = 'pause-subscription' + +export default function PauseSubscriptionModal() { + const { t } = useTranslation() + const { + handleCloseModal, + modalIdShown, + setShowCancellation, + personalSubscription, + } = useSubscriptionDashboardContext() + const [inflight, setInflight] = useState(false) + const [pauseError, setPauseError] = useState(false) + const [selectedDuration, setSelectedDuration] = useState(1) + const location = useLocation() + + function handleCancelSubscriptionClick() { + const subscription = personalSubscription as RecurlySubscription + eventTracking.sendMB('subscription-page-cancel-button-click', { + plan_code: subscription?.planCode, + is_trial: + subscription?.recurly.trialEndsAtFormatted && + subscription?.recurly.trial_ends_at && + new Date(subscription.recurly.trial_ends_at).getTime() > Date.now(), + }) + setShowCancellation(true) + } + + const pauseSelectItems = useMemo( + () => + pauseMonthDurationOptions.map(month => ({ + key: month, + value: `${month} ${t('month', { count: month })}`, + })), + [t] + ) + + const handleConfirmPauseSubscriptionClick = useCallback(async () => { + if (!selectedDuration) { + return + } + setPauseError(false) + setInflight(true) + try { + await postJSON(`/user/subscription/pause/${selectedDuration}`) + const newUrl = new URL(location.toString()) + newUrl.searchParams.set('flash', 'paused') + window.history.replaceState(null, '', newUrl) + location.reload() + } catch (err) { + debugConsole.error('error pausing subscription', err) + setInflight(false) + setPauseError(true) + } + }, [location, selectedDuration]) + + if (modalIdShown !== PAUSE_SUB_MODAL_ID) { + return null + } + + return ( + + + + Need to duck out for a while? + {pauseError && } + +

{t('why_not_pause_instead')}

+

{t('your_current_plan_gives_you')}

+ {t('dont_forget_you_currently_have')} +
    + {personalSubscription?.plan?.features?.collaborators !== 1 && ( +
  • {t('more_collabs_per_project')}
  • + )} +
  • {t('more_compile_time')}
  • +
  • {t('features_like_track_changes')}
  • +
  • {t('integrations_like_github')}
  • +
+ +