From 823f11426bf7ae9cd2d4313f6b38c2c152f93801 Mon Sep 17 00:00:00 2001 From: Olzhas Askar Date: Thu, 30 Apr 2026 16:25:14 +0200 Subject: [PATCH] Merge pull request #33109 from overleaf/oa-upgrade-path [web] Upgrade path GitOrigin-RevId: 532993e613bdc42cf92a7b10e629aa94596d854e --- .../Subscription/SubscriptionController.mjs | 2 + .../successful-subscription-react.pug | 1 + .../web/frontend/extracted-translations.json | 9 +- .../canceled-subscription/canceled.tsx | 12 +- .../preview-subscription-change/root.tsx | 469 ++++++++++-------- .../successful-subscription.tsx | 101 +++- .../subscription/util/plan-display-names.ts | 10 + services/web/frontend/js/utils/meta.ts | 1 + services/web/locales/en.json | 8 +- .../preview-subscription-change.test.tsx | 99 ++++ .../successful-subscription.test.tsx | 81 ++- .../SubscriptionController.test.mjs | 1 + 12 files changed, 572 insertions(+), 222 deletions(-) create mode 100644 services/web/frontend/js/features/subscription/util/plan-display-names.ts create mode 100644 services/web/test/frontend/features/subscription/components/preview-subscription-change/preview-subscription-change.test.tsx diff --git a/services/web/app/src/Features/Subscription/SubscriptionController.mjs b/services/web/app/src/Features/Subscription/SubscriptionController.mjs index 025a234c48..eca26e4ab6 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionController.mjs +++ b/services/web/app/src/Features/Subscription/SubscriptionController.mjs @@ -359,6 +359,7 @@ async function successfulSubscription(req, res) { ) const postCheckoutRedirect = req.session?.postCheckoutRedirect + const isUpgrade = req.query.upgrade === 'true' if (!personalSubscription) { res.redirect('/user/subscription/plans') @@ -376,6 +377,7 @@ async function successfulSubscription(req, res) { title: 'thank_you', personalSubscription, postCheckoutRedirect, + isUpgrade, user: { _id: user._id, features: userInDb.features, diff --git a/services/web/app/views/subscriptions/successful-subscription-react.pug b/services/web/app/views/subscriptions/successful-subscription-react.pug index 63103e3df2..025dd61e6f 100644 --- a/services/web/app/views/subscriptions/successful-subscription-react.pug +++ b/services/web/app/views/subscriptions/successful-subscription-react.pug @@ -9,6 +9,7 @@ block entrypointVar block append meta meta(name='ol-subscription' data-type='json' content=personalSubscription) meta(name='ol-postCheckoutRedirect' content=postCheckoutRedirect) + meta(name='ol-isUpgrade' data-type='boolean' content=isUpgrade) meta(name='ol-user' data-type='json' content=user) block content diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index a6f0b09fdb..b38b240b77 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -661,6 +661,7 @@ "filter_projects": "", "find": "", "find_and_check_citations": "", + "find_out_how_to_get_the_most_out_of_your_new_subscription": "", "find_out_more": "", "find_out_more_about_institution_login": "", "find_out_more_about_the_file_outline": "", @@ -727,6 +728,8 @@ "get_most_subscription_by_checking_ai_writefull": "", "get_most_subscription_by_checking_overleaf": "", "get_most_subscription_by_checking_overleaf_ai_writefull": "", + "get_ready_for_overleaf_at_its_best_pro": "", + "get_ready_for_overleaf_standard": "", "get_real_time_track_changes": "", "get_your_hands_on_the_ultimate_research_writing_ai_assistant": "", "git": "", @@ -1431,6 +1434,7 @@ "priority_support": "", "privacy_and_terms": "", "private": "", + "pro": "", "problem_talking_to_publishing_service": "", "problem_with_subscription_contact_us": "", "proceed_to_paypal": "", @@ -1538,6 +1542,7 @@ "refresh_page_after_linking_dropbox": "", "refresh_page_after_starting_free_trial": "", "refreshing": "", + "refund_with_description": "", "regards": "", "registering": "", "reject": "", @@ -1878,7 +1883,6 @@ "submit_title": "", "subscribe": "", "subscribe_to_find_the_symbols_you_need_faster": "", - "subscribe_to_plan": "", "subscription": "", "subscription_admins_cannot_be_deleted": "", "subscription_canceled": "", @@ -2203,6 +2207,7 @@ "upgrade_to_add_more_collaborators_and_more": "", "upgrade_to_get_feature": "", "upgrade_to_get_started": "", + "upgrade_to_plan": "", "upgrade_to_review": "", "upgrade_your_subscription": "", "upload": "", @@ -2306,6 +2311,7 @@ "website_status": "", "wed_love_you_to_stay": "", "welcome_to_overleaf_opening_workspace": "", + "welcome_to_plan": "", "welcome_to_sl": "", "well_be_here_when_youre_ready": "", "were_making_some_changes_to_project_sharing_this_means_you_will_be_visible": "", @@ -2452,6 +2458,7 @@ "youve_reached_the_fair_usage_limit_on_your_plan_you_can_start_chatting_again_in_time": "", "youve_unlinked_all_users": "", "youve_upgraded_your_plan": "", + "youve_upgraded_your_subscription": "", "zoom_in": "", "zoom_out": "", "zoom_to": "", diff --git a/services/web/frontend/js/features/subscription/components/canceled-subscription/canceled.tsx b/services/web/frontend/js/features/subscription/components/canceled-subscription/canceled.tsx index a369aba47d..b3fa9178db 100644 --- a/services/web/frontend/js/features/subscription/components/canceled-subscription/canceled.tsx +++ b/services/web/frontend/js/features/subscription/components/canceled-subscription/canceled.tsx @@ -12,18 +12,16 @@ function Canceled() { -
-

{t('subscription_canceled')}

-
+

{t('subscription_canceled')}

- {t('to_modify_your_subscription_go_to')}  +
+ {t('to_modify_your_subscription_go_to')} - {t('manage_subscription')}. + {t('manage_subscription')} -

+
} />

diff --git a/services/web/frontend/js/features/subscription/components/preview-subscription-change/root.tsx b/services/web/frontend/js/features/subscription/components/preview-subscription-change/root.tsx index df57eec55c..dcc82e08a5 100644 --- a/services/web/frontend/js/features/subscription/components/preview-subscription-change/root.tsx +++ b/services/web/frontend/js/features/subscription/components/preview-subscription-change/root.tsx @@ -1,11 +1,7 @@ import { useCallback, useEffect } from 'react' import moment from 'moment' import { useTranslation, Trans } from 'react-i18next' -import { - SubscriptionChangePreview, - AddOnPurchase, - PremiumSubscriptionChange, -} from '../../../../../../types/subscription/subscription-change-preview' +import { SubscriptionChangePreview } from '../../../../../../types/subscription/subscription-change-preview' import getMeta from '@/utils/meta' import { formatCurrency } from '@/shared/utils/currency' import useAsync from '@/shared/hooks/use-async' @@ -23,6 +19,150 @@ import PaymentErrorNotification from '@/features/subscription/components/shared/ import handleStripePaymentAction from '../../util/handle-stripe-payment-action' import RedirectedPaymentErrorNotification from '../shared/redirected-payment-error-notification' import TrialDisabledNotification from './trial-disabled-notification' +import { getUpgradePlanDisplayName } from '../../util/plan-display-names' + +function FuturePayments({ preview }: { preview: SubscriptionChangePreview }) { + const { t } = useTranslation() + + return ( + <> + + {preview.nextInvoice.plan.name} + + {formatCurrency(preview.nextInvoice.plan.amount, preview.currency)} + + + + {preview.nextInvoice.addOns.map(addOn => ( + + + {addOn.name} + {addOn.quantity > 1 ? ` ×${addOn.quantity}` : ''} + + + {formatCurrency(addOn.amount, preview.currency)} + + + ))} + + {preview.nextInvoice.tax.rate > 0 && ( + + + {t('vat')} {preview.nextInvoice.tax.rate * 100}% + + + {formatCurrency(preview.nextInvoice.tax.amount, preview.currency)} + + + )} + + + + {preview.nextPlan.annual ? t('total_per_year') : t('total_per_month')} + + + {formatCurrency(preview.nextInvoice.total, preview.currency)} + + + + ) +} + +function NextPaymentDate({ preview }: { preview: SubscriptionChangePreview }) { + return ( +

+ }} + shouldUnescape + tOptions={{ interpolation: { escapeValue: true } }} + /> +
+ ) +} + +function PreviewLayout({ children }: { children: React.ReactNode }) { + return ( +
+ + + + + {children} + + +
+ ) +} + +function DueTodayBlock({ + preview, + filteredLineItems, + singleItemName, + headingClassName, +}: { + preview: SubscriptionChangePreview + filteredLineItems: SubscriptionChangePreview['immediateCharge']['lineItems'] + singleItemName: string + headingClassName?: string +}) { + const { t } = useTranslation() + + return ( + <> +

{t('due_today')}:

+ {filteredLineItems.length > 1 ? ( + filteredLineItems.map((item, index) => ( + + + {item.subtotal < 0 + ? t('refund_with_description', { + description: item.description, + }) + : item.description} + + + {formatCurrency(item.subtotal, preview.currency)} + + + )) + ) : ( + + {singleItemName} + + + {formatCurrency( + preview.immediateCharge.subtotal, + preview.currency + )} + + + + )} + + {preview.immediateCharge.tax > 0 && ( + + + {t('vat')} {preview.nextInvoice.tax.rate * 100}% + + + {formatCurrency(preview.immediateCharge.tax, preview.currency)} + + + )} + + + {t('total_today')} + + + {formatCurrency(preview.immediateCharge.total, preview.currency)} + + + + + ) +} function PreviewSubscriptionChange() { const preview = getMeta( @@ -33,7 +173,7 @@ function PreviewSubscriptionChange() { const payNowTask = useAsync() const location = useLocation() - // Filter out items that cancel each other out (AI assist items with subtotals that sum to 0) + const isPremiumUpgrade = preview.change.type === 'premium-subscription' const filteredLineItems = preview.immediateCharge.lineItems.filter( (item, index, arr) => { if (!item.isAiAssist) return true @@ -80,223 +220,142 @@ function PreviewSubscriptionChange() { referrer: purchaseReferrer, }) } - location.replace('/user/subscription/thank-you') + location.replace( + isPremiumUpgrade + ? '/user/subscription/thank-you?upgrade=true' + : '/user/subscription/thank-you' + ) }) .catch(debugConsole.error) - }, [purchaseReferrer, location, payNowTask, preview]) + }, [purchaseReferrer, location, payNowTask, preview, isPremiumUpgrade]) + + if (preview.change.type === 'premium-subscription') { + const change = preview.change + const upgradePlanName = getUpgradePlanDisplayName(change.plan.code, t) + + return ( + +

{t('upgrade_to_plan', { planName: upgradePlanName })}

+ + {payNowTask.isError && ( + + )} + +

{t('payment_summary')}

+ + + +
+ +

{t('future_payments')}:

+ + + +
+ + {t('upgrade_now')} + +
+
+ ) + } const aiAddOnChange = preview.change.type === 'add-on-purchase' && preview.change.addOn.code === 'assistant' - // the driver of the change, which we can display as the immediate charge const changeName = - preview.change.type === 'add-on-purchase' - ? (preview.change as AddOnPurchase).addOn.name - : (preview.change as PremiumSubscriptionChange).plan.name + preview.change.type === 'add-on-purchase' ? preview.change.addOn.name : '' return ( -
- - - - - - {preview.change.type === 'add-on-purchase' ? ( -

- {t('add_add_on_to_your_plan', { - addOnName: preview.change.addOn.name, - })} -

- ) : preview.change.type === 'premium-subscription' ? ( -

- {t('subscribe_to_plan', { planName: preview.change.plan.name })} -

- ) : null} + + {preview.change.type === 'add-on-purchase' ? ( +

+ {t('add_add_on_to_your_plan', { + addOnName: preview.change.addOn.name, + })} +

+ ) : null} - {payNowTask.isError && ( - - )} + {payNowTask.isError && ( + + )} - {aiAddOnChange && ( -
-
+ )} - -

{t('due_today')}:

- {filteredLineItems.length > 1 ? ( - <> - {filteredLineItems.map((item, index) => ( - - - {item.subtotal < 0 - ? `Refund: ${item.description}` - : item.description} - - - - {formatCurrency(item.subtotal, preview.currency)} - - - - ))} - - ) : ( - <> - - {changeName} - - - {formatCurrency( - preview.immediateCharge.subtotal, - preview.currency - )} - - - - - )} + + + - {preview.immediateCharge.tax > 0 && ( - - - {t('vat')} {preview.nextInvoice.tax.rate * 100}% - - - {formatCurrency( - preview.immediateCharge.tax, - preview.currency - )} - - - )} +
+ }} + shouldUnescape + tOptions={{ interpolation: { escapeValue: true } }} + />{' '} + }} + shouldUnescape + tOptions={{ interpolation: { escapeValue: true } }} + /> +
+ {aiAddOnChange && ( +
*{t('fair_usage_policy_applies')}
+ )} - - {t('total_today')} - - - {formatCurrency( - preview.immediateCharge.total, - preview.currency - )} - - - -
+
+ + {t('pay_now')} + +
-
- }} - shouldUnescape - tOptions={{ interpolation: { escapeValue: true } }} - />{' '} - }} - shouldUnescape - tOptions={{ interpolation: { escapeValue: true } }} - /> -
- {aiAddOnChange && ( -
- *{t('fair_usage_policy_applies')} -
- )} + +

{t('future_payments')}:

+ +
-
- - {t('pay_now')} - -
- - -

{t('future_payments')}:

- - {preview.nextInvoice.plan.name} - - {formatCurrency( - preview.nextInvoice.plan.amount, - preview.currency - )} - - - - {preview.nextInvoice.addOns.map(addOn => ( - - - {addOn.name} - {addOn.quantity > 1 ? ` ×${addOn.quantity}` : ''} - - - {formatCurrency(addOn.amount, preview.currency)} - - - ))} - - {preview.nextInvoice.tax.rate > 0 && ( - - - {t('vat')} {preview.nextInvoice.tax.rate * 100}% - - - {formatCurrency( - preview.nextInvoice.tax.amount, - preview.currency - )} - - - )} - - - - {preview.nextPlan.annual - ? t('total_per_year') - : t('total_per_month')} - - - {formatCurrency(preview.nextInvoice.total, preview.currency)} - - -
- -
- }} - shouldUnescape - tOptions={{ interpolation: { escapeValue: true } }} - /> -
-
-
-
-
+ + ) } diff --git a/services/web/frontend/js/features/subscription/components/successful-subscription/successful-subscription.tsx b/services/web/frontend/js/features/subscription/components/successful-subscription/successful-subscription.tsx index 3934605b48..68dac21626 100644 --- a/services/web/frontend/js/features/subscription/components/successful-subscription/successful-subscription.tsx +++ b/services/web/frontend/js/features/subscription/components/successful-subscription/successful-subscription.tsx @@ -14,17 +14,28 @@ import { } from '../../data/add-on-codes' import { PaidSubscription } from '../../../../../../types/subscription/dashboard/subscription' import { useBroadcastUser } from '@/shared/hooks/user-channel/use-broadcast-user' +import { getUpgradePlanDisplayName } from '../../util/plan-display-names' function SuccessfulSubscription() { const { t } = useTranslation() const { personalSubscription: subscription } = useSubscriptionDashboardContext() const postCheckoutRedirect = getMeta('ol-postCheckoutRedirect') + const isUpgrade = getMeta('ol-isUpgrade') const { appName, adminEmail } = getMeta('ol-ExposedSettings') useBroadcastUser() if (!subscription || !('payment' in subscription)) return null + if (isUpgrade) { + return ( + + ) + } + const onAiStandalonePlan = isStandaloneAiPlanCode(subscription.planCode) return ( @@ -32,9 +43,7 @@ function SuccessfulSubscription() { -
-

{t('thanks_for_subscribing')}

-
+

{t('thanks_for_subscribing')}

)} -

- {t('to_modify_your_subscription_go_to')}  +

+ {t('to_modify_your_subscription_go_to')} - {t('manage_subscription')}. + {t('manage_subscription')} -

+
} /> @@ -124,6 +133,84 @@ function SuccessfulSubscription() { ) } +function UpgradeSuccess({ + subscription, + postCheckoutRedirect, +}: { + subscription: PaidSubscription + postCheckoutRedirect: string | undefined +}) { + const { t } = useTranslation() + const planDisplayName = getUpgradePlanDisplayName(subscription.planCode, t) + let benefitsText = '' + if (subscription.planCode.startsWith('professional')) { + benefitsText = t('get_ready_for_overleaf_at_its_best_pro') + } else if (subscription.planCode.startsWith('collaborator')) { + benefitsText = t('get_ready_for_overleaf_standard') + } + + return ( +
+ + + +

{t('welcome_to_plan', { planName: planDisplayName })}

+ + {t('youve_upgraded_your_subscription')} + + {t('manage_subscription')} + +
+ } + /> +

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

+

+ * {t('subject_to_additional_vat')} +

+ {benefitsText &&

{benefitsText}

} +

+ , + ]} + /> +

+

+ + {t('back_to_your_projects')} + +

+
+
+
+ + ) +} + function ThankYouSection({ subscription, onAiStandalonePlan, diff --git a/services/web/frontend/js/features/subscription/util/plan-display-names.ts b/services/web/frontend/js/features/subscription/util/plan-display-names.ts new file mode 100644 index 0000000000..087b188981 --- /dev/null +++ b/services/web/frontend/js/features/subscription/util/plan-display-names.ts @@ -0,0 +1,10 @@ +import { TFunction } from 'i18next' + +export function getUpgradePlanDisplayName( + planCode: string, + t: TFunction +): string { + if (planCode.startsWith('professional')) return t('pro') + if (planCode.startsWith('collaborator')) return t('standard') + return planCode +} diff --git a/services/web/frontend/js/utils/meta.ts b/services/web/frontend/js/utils/meta.ts index 4118345620..bfe7c249f2 100644 --- a/services/web/frontend/js/utils/meta.ts +++ b/services/web/frontend/js/utils/meta.ts @@ -189,6 +189,7 @@ export interface Meta { 'ol-isRegisteredViaGoogle': boolean 'ol-isRestrictedTokenMember': boolean 'ol-isSaas': boolean + 'ol-isUpgrade': boolean 'ol-isUserGroupManager': boolean 'ol-itm_campaign': string 'ol-itm_content': string diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 108c5c0e3f..cadc6b392f 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -879,6 +879,7 @@ "filters": "Filters", "find": "Find", "find_and_check_citations": "Find and check citations", + "find_out_how_to_get_the_most_out_of_your_new_subscription": "Find out how to get the most out of your new subscription by checking out <0>Overleaf’s AI features.", "find_out_more": "Find out More", "find_out_more_about_institution_login": "Find out more about institutional login", "find_out_more_about_the_file_outline": "Find out more about the file outline", @@ -969,6 +970,8 @@ "get_most_subscription_by_checking_overleaf": "Get the most out of your subscription by checking out <0>Overleaf’s features.", "get_most_subscription_by_checking_overleaf_ai_writefull": "Get the most out of your subscription by checking out <0>Overleaf’s features, <1>Overleaf’s AI features and <2>Writefull’s features.", "get_pro": "Get Pro", + "get_ready_for_overleaf_at_its_best_pro": "Get ready for Overleaf at its best, with full access to every AI tool and unlimited collaborators per project.", + "get_ready_for_overleaf_standard": "Get ready for enhanced collaboration, with increased AI access and up to 10 collaborators per project.", "get_real_time_track_changes": "Get real-time track changes", "get_standard": "Get Standard", "get_student": "Get Student", @@ -2038,6 +2041,7 @@ "refresh_page_after_linking_dropbox": "Please refresh this page after linking your account to Dropbox.", "refresh_page_after_starting_free_trial": "Please refresh this page after starting your free trial.", "refreshing": "Refreshing", + "refund_with_description": "Refund: __description__", "regards": "Regards", "register": "Register", "register_error": "Registration error", @@ -2459,7 +2463,6 @@ "submit_title": "Submit", "subscribe": "Subscribe", "subscribe_to_find_the_symbols_you_need_faster": "Subscribe to find the symbols you need faster", - "subscribe_to_plan": "Subscribe to __planName__", "subscription": "Subscription", "subscription_admins_cannot_be_deleted": "You cannot delete your account while on a subscription. Please cancel your subscription and try again. If you keep seeing this message please contact us.", "subscription_canceled": "Subscription canceled", @@ -2839,6 +2842,7 @@ "upgrade_to_get_feature": "Upgrade to get __feature__, plus:", "upgrade_to_get_more_from_overleaf": "Upgrade to get more from Overleaf", "upgrade_to_get_started": "Upgrade to get started", + "upgrade_to_plan": "Upgrade to __planName__", "upgrade_to_review": "Upgrade to review", "upgrade_your_plan": "Upgrade your plan", "upgrade_your_subscription": "Upgrade your subscription", @@ -2958,6 +2962,7 @@ "website_status": "Website status", "wed_love_you_to_stay": "We’d love you to stay", "welcome_to_overleaf_opening_workspace": "Welcome to Overleaf. Opening your workspace.", + "welcome_to_plan": "Welcome to __planName__!", "welcome_to_sl": "Welcome to __appName__", "well_be_here_when_youre_ready": "We’ll be here when you’re ready to dive back in! 🦆", "were_making_some_changes_to_project_sharing_this_means_you_will_be_visible": "We’re making some <0>changes to project sharing. This means, as someone with edit access, your name and email address will be visible to the project owner and other editors.", @@ -3124,6 +3129,7 @@ "youve_reached_the_fair_usage_limit_on_your_plan_you_can_start_chatting_again_in_time": "You’ve reached the fair usage limit on your plan. You can start chatting again in __time__.", "youve_unlinked_all_users": "You’ve unlinked all users", "youve_upgraded_your_plan": "You’ve upgraded your plan!", + "youve_upgraded_your_subscription": "You’ve upgraded your subscription", "zh-CN": "Chinese", "zip_contents_too_large": "Zip contents too large", "zoom_in": "Zoom in", diff --git a/services/web/test/frontend/features/subscription/components/preview-subscription-change/preview-subscription-change.test.tsx b/services/web/test/frontend/features/subscription/components/preview-subscription-change/preview-subscription-change.test.tsx new file mode 100644 index 0000000000..d7d3281a69 --- /dev/null +++ b/services/web/test/frontend/features/subscription/components/preview-subscription-change/preview-subscription-change.test.tsx @@ -0,0 +1,99 @@ +import sinon from 'sinon' +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import fetchMock from 'fetch-mock' +import { cloneDeep } from 'lodash' +import PreviewSubscriptionChange from '../../../../../../frontend/js/features/subscription/components/preview-subscription-change/root' +import { SubscriptionChangePreview } from '../../../../../../types/subscription/subscription-change-preview' +import { location } from '@/shared/components/location' + +const premiumPreview: SubscriptionChangePreview = { + change: { + type: 'premium-subscription', + plan: { code: 'professional-annual', name: 'Overleaf Professional' }, + }, + currency: 'USD', + paymentMethod: 'Visa **** 1111', + netTerms: 0, + nextPlan: { annual: true }, + immediateCharge: { + subtotal: 100, + tax: 20, + total: 120, + discount: 0, + lineItems: [], + }, + nextInvoice: { + date: '2026-05-01T00:00:00.000Z', + plan: { name: 'Overleaf Professional', amount: 100 }, + addOns: [], + subtotal: 100, + tax: { rate: 0.2, amount: 20 }, + total: 120, + }, +} + +describe(' upgrade variant', function () { + beforeEach(function () { + this.locationWrapperSandbox = sinon.createSandbox() + this.locationWrapperStub = this.locationWrapperSandbox.stub(location) + }) + + afterEach(function () { + fetchMock.removeRoutes().clearHistory() + this.locationWrapperSandbox.restore() + }) + + it('renders the "Upgrade to Pro" heading for a professional plan', function () { + window.metaAttributesCache.set( + 'ol-subscriptionChangePreview', + premiumPreview + ) + render() + + screen.getByRole('heading', { name: /upgrade to pro/i }) + }) + + it('renders the "Upgrade to Standard" heading for a collaborator plan', function () { + const preview = cloneDeep(premiumPreview) + preview.change = { + type: 'premium-subscription', + plan: { code: 'collaborator-annual', name: 'Overleaf Standard' }, + } + window.metaAttributesCache.set('ol-subscriptionChangePreview', preview) + render() + + screen.getByRole('heading', { name: /upgrade to standard/i }) + }) + + it('renders the payment summary, due today and future payments sections', function () { + window.metaAttributesCache.set( + 'ol-subscriptionChangePreview', + premiumPreview + ) + render() + + screen.getByRole('heading', { name: /payment summary/i }) + screen.getByRole('heading', { name: /due today/i }) + screen.getByText(/total today/i) + screen.getByRole('heading', { name: /future payments/i }) + }) + + it('redirects to the thank-you page with upgrade=true after a successful payment', async function () { + fetchMock.post('/user/subscription/update', 200) + window.metaAttributesCache.set( + 'ol-subscriptionChangePreview', + premiumPreview + ) + render() + + fireEvent.click(screen.getByRole('button', { name: /upgrade now/i })) + + await waitFor(() => { + sinon.assert.calledOnce(this.locationWrapperStub.replace) + }) + sinon.assert.calledWithMatch( + this.locationWrapperStub.replace, + '/user/subscription/thank-you?upgrade=true' + ) + }) +}) diff --git a/services/web/test/frontend/features/subscription/components/successful-subscription/successful-subscription.test.tsx b/services/web/test/frontend/features/subscription/components/successful-subscription/successful-subscription.test.tsx index 9259eaf259..b7cb4445b4 100644 --- a/services/web/test/frontend/features/subscription/components/successful-subscription/successful-subscription.test.tsx +++ b/services/web/test/frontend/features/subscription/components/successful-subscription/successful-subscription.test.tsx @@ -2,7 +2,10 @@ import { expect } from 'chai' import { screen, within } from '@testing-library/react' import SuccessfulSubscription from '../../../../../../frontend/js/features/subscription/components/successful-subscription/successful-subscription' import { renderWithSubscriptionDashContext } from '../../helpers/render-with-subscription-dash-context' -import { annualActiveSubscription } from '../../fixtures/subscriptions' +import { + annualActiveSubscription, + annualActiveSubscriptionPro, +} from '../../fixtures/subscriptions' import { ExposedSettings } from '../../../../../../types/exposed-settings' import { UserProvider } from '@/shared/context/user-context' @@ -84,4 +87,80 @@ describe('successful subscription page', function () { }) expect(backToYourProjectsLink.getAttribute('href')).to.equal('/project') }) + + describe('upgrade variant', function () { + it('renders the upgrade success page when isUpgrade is true', function () { + renderWithSubscriptionDashContext( + + + , + { + metaTags: [ + { + name: 'ol-ExposedSettings', + value: { + adminEmail: 'foo@example.com', + } as ExposedSettings, + }, + { name: 'ol-subscription', value: annualActiveSubscriptionPro }, + { name: 'ol-isUpgrade', value: true }, + ], + } + ) + + screen.getByRole('heading', { name: /welcome to pro/i }) + const alert = screen.getByRole('alert') + within(alert).getByText(/you.ve upgraded your subscription/i) + const manageLink = within(alert).getByRole('link', { + name: /manage subscription/i, + }) + expect(manageLink.getAttribute('href')).to.equal('/user/subscription') + + expect( + screen + .getByText(/the next payment of/i) + .textContent?.replace(/\xA0/g, ' ') + ).to.equal( + `The next payment of ${annualActiveSubscriptionPro.payment.displayPrice} will be collected on ${annualActiveSubscriptionPro.payment.nextPaymentDueAt}.` + ) + screen.getByText(/prices may be subject to additional vat/i) + + screen.getByText(/full access to every AI tool/i, { exact: false }) + + const aiFeaturesLink = screen.getByRole('link', { + name: /Overleaf.s AI features/i, + }) + expect(aiFeaturesLink.getAttribute('href')).to.equal( + 'https://docs.overleaf.com/integrations-and-add-ons/ai-features' + ) + expect(aiFeaturesLink.getAttribute('target')).to.equal('_blank') + expect(aiFeaturesLink.getAttribute('rel')).to.equal('noopener noreferrer') + + const backLink = screen.getByRole('link', { + name: /back to your projects/i, + }) + expect(backLink.getAttribute('href')).to.equal('/project') + }) + + it('renders the standard success page when isUpgrade is not set', function () { + renderWithSubscriptionDashContext( + + + , + { + metaTags: [ + { + name: 'ol-ExposedSettings', + value: { + adminEmail: 'foo@example.com', + } as ExposedSettings, + }, + { name: 'ol-subscription', value: annualActiveSubscription }, + ], + } + ) + + screen.getByRole('heading', { name: /thanks for subscribing/i }) + }) + }) }) diff --git a/services/web/test/unit/src/Subscription/SubscriptionController.test.mjs b/services/web/test/unit/src/Subscription/SubscriptionController.test.mjs index 50bc8695da..9a46ce0100 100644 --- a/services/web/test/unit/src/Subscription/SubscriptionController.test.mjs +++ b/services/web/test/unit/src/Subscription/SubscriptionController.test.mjs @@ -405,6 +405,7 @@ describe('SubscriptionController', function () { title: 'thank_you', personalSubscription: 'foo', postCheckoutRedirect: undefined, + isUpgrade: false, user: { _id: ctx.user._id, features: ctx.user.features,