mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 09:09:36 +02:00
Merge pull request #33109 from overleaf/oa-upgrade-path
[web] Upgrade path GitOrigin-RevId: 532993e613bdc42cf92a7b10e629aa94596d854e
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": "",
|
||||
|
||||
@@ -12,18 +12,16 @@ function Canceled() {
|
||||
<OLRow>
|
||||
<OLCol lg={{ span: 8, offset: 2 }}>
|
||||
<OLPageContentCard>
|
||||
<div className="page-header">
|
||||
<h2>{t('subscription_canceled')}</h2>
|
||||
</div>
|
||||
<h2>{t('subscription_canceled')}</h2>
|
||||
<OLNotification
|
||||
type="info"
|
||||
content={
|
||||
<p>
|
||||
{t('to_modify_your_subscription_go_to')}
|
||||
<div className="d-flex justify-content-between align-items-center gap-3">
|
||||
<span>{t('to_modify_your_subscription_go_to')}</span>
|
||||
<a href="/user/subscription" rel="noopener noreferrer">
|
||||
{t('manage_subscription')}.
|
||||
{t('manage_subscription')}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<p>
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<OLRow className="mt-1">
|
||||
<OLCol xs={9}>{preview.nextInvoice.plan.name}</OLCol>
|
||||
<OLCol xs={3} className="text-end">
|
||||
{formatCurrency(preview.nextInvoice.plan.amount, preview.currency)}
|
||||
</OLCol>
|
||||
</OLRow>
|
||||
|
||||
{preview.nextInvoice.addOns.map(addOn => (
|
||||
<OLRow className="mt-1" key={addOn.code}>
|
||||
<OLCol xs={9}>
|
||||
{addOn.name}
|
||||
{addOn.quantity > 1 ? ` ×${addOn.quantity}` : ''}
|
||||
</OLCol>
|
||||
<OLCol xs={3} className="text-end">
|
||||
{formatCurrency(addOn.amount, preview.currency)}
|
||||
</OLCol>
|
||||
</OLRow>
|
||||
))}
|
||||
|
||||
{preview.nextInvoice.tax.rate > 0 && (
|
||||
<OLRow className="mt-1">
|
||||
<OLCol xs={9}>
|
||||
{t('vat')} {preview.nextInvoice.tax.rate * 100}%
|
||||
</OLCol>
|
||||
<OLCol xs={3} className="text-end">
|
||||
{formatCurrency(preview.nextInvoice.tax.amount, preview.currency)}
|
||||
</OLCol>
|
||||
</OLRow>
|
||||
)}
|
||||
|
||||
<OLRow className="mt-1">
|
||||
<OLCol xs={9}>
|
||||
{preview.nextPlan.annual ? t('total_per_year') : t('total_per_month')}
|
||||
</OLCol>
|
||||
<OLCol xs={3} className="text-end">
|
||||
{formatCurrency(preview.nextInvoice.total, preview.currency)}
|
||||
</OLCol>
|
||||
</OLRow>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function NextPaymentDate({ preview }: { preview: SubscriptionChangePreview }) {
|
||||
return (
|
||||
<div className="mt-5">
|
||||
<Trans
|
||||
i18nKey="the_next_payment_will_be_collected_on"
|
||||
values={{ date: moment(preview.nextInvoice.date).format('LL') }}
|
||||
components={{ strong: <strong /> }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PreviewLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="container">
|
||||
<OLRow>
|
||||
<OLCol md={{ offset: 2, span: 8 }}>
|
||||
<RedirectedPaymentErrorNotification />
|
||||
<TrialDisabledNotification />
|
||||
<OLCard className="p-3">{children}</OLCard>
|
||||
</OLCol>
|
||||
</OLRow>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function DueTodayBlock({
|
||||
preview,
|
||||
filteredLineItems,
|
||||
singleItemName,
|
||||
headingClassName,
|
||||
}: {
|
||||
preview: SubscriptionChangePreview
|
||||
filteredLineItems: SubscriptionChangePreview['immediateCharge']['lineItems']
|
||||
singleItemName: string
|
||||
headingClassName?: string
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<h4 className={headingClassName}>{t('due_today')}:</h4>
|
||||
{filteredLineItems.length > 1 ? (
|
||||
filteredLineItems.map((item, index) => (
|
||||
<OLRow className="mt-1" key={index}>
|
||||
<OLCol xs={9}>
|
||||
{item.subtotal < 0
|
||||
? t('refund_with_description', {
|
||||
description: item.description,
|
||||
})
|
||||
: item.description}
|
||||
</OLCol>
|
||||
<OLCol xs={3} className="text-end">
|
||||
<strong>{formatCurrency(item.subtotal, preview.currency)}</strong>
|
||||
</OLCol>
|
||||
</OLRow>
|
||||
))
|
||||
) : (
|
||||
<OLRow className="mt-1">
|
||||
<OLCol xs={9}>{singleItemName}</OLCol>
|
||||
<OLCol xs={3} className="text-end">
|
||||
<strong>
|
||||
{formatCurrency(
|
||||
preview.immediateCharge.subtotal,
|
||||
preview.currency
|
||||
)}
|
||||
</strong>
|
||||
</OLCol>
|
||||
</OLRow>
|
||||
)}
|
||||
|
||||
{preview.immediateCharge.tax > 0 && (
|
||||
<OLRow className="mt-1">
|
||||
<OLCol xs={9}>
|
||||
{t('vat')} {preview.nextInvoice.tax.rate * 100}%
|
||||
</OLCol>
|
||||
<OLCol xs={3} className="text-end">
|
||||
{formatCurrency(preview.immediateCharge.tax, preview.currency)}
|
||||
</OLCol>
|
||||
</OLRow>
|
||||
)}
|
||||
|
||||
<OLRow className="mt-1">
|
||||
<OLCol xs={9}>{t('total_today')}</OLCol>
|
||||
<OLCol xs={3} className="text-end">
|
||||
<strong>
|
||||
{formatCurrency(preview.immediateCharge.total, preview.currency)}
|
||||
</strong>
|
||||
</OLCol>
|
||||
</OLRow>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<PreviewLayout>
|
||||
<h2>{t('upgrade_to_plan', { planName: upgradePlanName })}</h2>
|
||||
|
||||
{payNowTask.isError && (
|
||||
<PaymentErrorNotification error={payNowTask.error as FetchError} />
|
||||
)}
|
||||
|
||||
<h3 className="mt-5">{t('payment_summary')}</h3>
|
||||
|
||||
<DueTodayBlock
|
||||
preview={preview}
|
||||
filteredLineItems={filteredLineItems}
|
||||
singleItemName={change.plan.name}
|
||||
headingClassName="mt-4"
|
||||
/>
|
||||
|
||||
<hr />
|
||||
|
||||
<h4 className="mt-4">{t('future_payments')}:</h4>
|
||||
<FuturePayments preview={preview} />
|
||||
<NextPaymentDate preview={preview} />
|
||||
|
||||
<div className="mt-5">
|
||||
<OLButton
|
||||
variant="primary"
|
||||
size="lg"
|
||||
onClick={handlePayNowClick}
|
||||
disabled={payNowTask.isLoading || payNowTask.isSuccess}
|
||||
>
|
||||
{t('upgrade_now')}
|
||||
</OLButton>
|
||||
</div>
|
||||
</PreviewLayout>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="container">
|
||||
<OLRow>
|
||||
<OLCol md={{ offset: 2, span: 8 }}>
|
||||
<RedirectedPaymentErrorNotification />
|
||||
<TrialDisabledNotification />
|
||||
<OLCard className="p-3">
|
||||
{preview.change.type === 'add-on-purchase' ? (
|
||||
<h1>
|
||||
{t('add_add_on_to_your_plan', {
|
||||
addOnName: preview.change.addOn.name,
|
||||
})}
|
||||
</h1>
|
||||
) : preview.change.type === 'premium-subscription' ? (
|
||||
<h1>
|
||||
{t('subscribe_to_plan', { planName: preview.change.plan.name })}
|
||||
</h1>
|
||||
) : null}
|
||||
<PreviewLayout>
|
||||
{preview.change.type === 'add-on-purchase' ? (
|
||||
<h1>
|
||||
{t('add_add_on_to_your_plan', {
|
||||
addOnName: preview.change.addOn.name,
|
||||
})}
|
||||
</h1>
|
||||
) : null}
|
||||
|
||||
{payNowTask.isError && (
|
||||
<PaymentErrorNotification
|
||||
error={payNowTask.error as FetchError}
|
||||
/>
|
||||
)}
|
||||
{payNowTask.isError && (
|
||||
<PaymentErrorNotification error={payNowTask.error as FetchError} />
|
||||
)}
|
||||
|
||||
{aiAddOnChange && (
|
||||
<div>
|
||||
<Trans
|
||||
i18nKey="add_ai_assist_to_your_plan"
|
||||
components={{
|
||||
sparkle: (
|
||||
<img
|
||||
alt="sparkle"
|
||||
className="ai-error-assistant-sparkle"
|
||||
src={sparkleText}
|
||||
aria-hidden="true"
|
||||
key="sparkle"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
{aiAddOnChange && (
|
||||
<div>
|
||||
<Trans
|
||||
i18nKey="add_ai_assist_to_your_plan"
|
||||
components={{
|
||||
sparkle: (
|
||||
<img
|
||||
alt="sparkle"
|
||||
className="ai-error-assistant-sparkle"
|
||||
src={sparkleText}
|
||||
aria-hidden="true"
|
||||
key="sparkle"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<OLCard className="payment-summary-card mt-5">
|
||||
<h3>{t('due_today')}:</h3>
|
||||
{filteredLineItems.length > 1 ? (
|
||||
<>
|
||||
{filteredLineItems.map((item, index) => (
|
||||
<OLRow key={index}>
|
||||
<OLCol xs={9}>
|
||||
{item.subtotal < 0
|
||||
? `Refund: ${item.description}`
|
||||
: item.description}
|
||||
</OLCol>
|
||||
<OLCol xs={3} className="text-end">
|
||||
<strong>
|
||||
{formatCurrency(item.subtotal, preview.currency)}
|
||||
</strong>
|
||||
</OLCol>
|
||||
</OLRow>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<OLRow>
|
||||
<OLCol xs={9}>{changeName}</OLCol>
|
||||
<OLCol xs={3} className="text-end">
|
||||
<strong>
|
||||
{formatCurrency(
|
||||
preview.immediateCharge.subtotal,
|
||||
preview.currency
|
||||
)}
|
||||
</strong>
|
||||
</OLCol>
|
||||
</OLRow>
|
||||
</>
|
||||
)}
|
||||
<OLCard className="payment-summary-card mt-5">
|
||||
<DueTodayBlock
|
||||
preview={preview}
|
||||
filteredLineItems={filteredLineItems}
|
||||
singleItemName={changeName}
|
||||
/>
|
||||
</OLCard>
|
||||
|
||||
{preview.immediateCharge.tax > 0 && (
|
||||
<OLRow className="mt-1">
|
||||
<OLCol xs={9}>
|
||||
{t('vat')} {preview.nextInvoice.tax.rate * 100}%
|
||||
</OLCol>
|
||||
<OLCol xs={3} className="text-end">
|
||||
{formatCurrency(
|
||||
preview.immediateCharge.tax,
|
||||
preview.currency
|
||||
)}
|
||||
</OLCol>
|
||||
</OLRow>
|
||||
)}
|
||||
<div className="mt-5">
|
||||
<Trans
|
||||
i18nKey="this_total_reflects_the_amount_due_until"
|
||||
values={{ date: moment(preview.nextInvoice.date).format('LL') }}
|
||||
components={{ strong: <strong /> }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>{' '}
|
||||
<Trans
|
||||
i18nKey="we_will_use_your_existing_payment_method"
|
||||
values={{ paymentMethod: preview.paymentMethod }}
|
||||
components={{ strong: <strong /> }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</div>
|
||||
{aiAddOnChange && (
|
||||
<div className="plan-terms mt-3">*{t('fair_usage_policy_applies')}</div>
|
||||
)}
|
||||
|
||||
<OLRow className="mt-1">
|
||||
<OLCol xs={9}>{t('total_today')}</OLCol>
|
||||
<OLCol xs={3} className="text-end">
|
||||
<strong>
|
||||
{formatCurrency(
|
||||
preview.immediateCharge.total,
|
||||
preview.currency
|
||||
)}
|
||||
</strong>
|
||||
</OLCol>
|
||||
</OLRow>
|
||||
</OLCard>
|
||||
<div className="mt-5">
|
||||
<OLButton
|
||||
variant="primary"
|
||||
size="lg"
|
||||
onClick={handlePayNowClick}
|
||||
disabled={payNowTask.isLoading || payNowTask.isSuccess}
|
||||
>
|
||||
{t('pay_now')}
|
||||
</OLButton>
|
||||
</div>
|
||||
|
||||
<div className="mt-5">
|
||||
<Trans
|
||||
i18nKey="this_total_reflects_the_amount_due_until"
|
||||
values={{ date: moment(preview.nextInvoice.date).format('LL') }}
|
||||
components={{ strong: <strong /> }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>{' '}
|
||||
<Trans
|
||||
i18nKey="we_will_use_your_existing_payment_method"
|
||||
values={{ paymentMethod: preview.paymentMethod }}
|
||||
components={{ strong: <strong /> }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</div>
|
||||
{aiAddOnChange && (
|
||||
<div className="plan-terms mt-3">
|
||||
*{t('fair_usage_policy_applies')}
|
||||
</div>
|
||||
)}
|
||||
<OLCard className="payment-summary-card mt-5">
|
||||
<h3>{t('future_payments')}:</h3>
|
||||
<FuturePayments preview={preview} />
|
||||
</OLCard>
|
||||
|
||||
<div className="mt-5">
|
||||
<OLButton
|
||||
variant="primary"
|
||||
size="lg"
|
||||
onClick={handlePayNowClick}
|
||||
disabled={payNowTask.isLoading || payNowTask.isSuccess}
|
||||
>
|
||||
{t('pay_now')}
|
||||
</OLButton>
|
||||
</div>
|
||||
|
||||
<OLCard className="payment-summary-card mt-5">
|
||||
<h3>{t('future_payments')}:</h3>
|
||||
<OLRow className="mt-1">
|
||||
<OLCol xs={9}>{preview.nextInvoice.plan.name}</OLCol>
|
||||
<OLCol xs={3} className="text-end">
|
||||
{formatCurrency(
|
||||
preview.nextInvoice.plan.amount,
|
||||
preview.currency
|
||||
)}
|
||||
</OLCol>
|
||||
</OLRow>
|
||||
|
||||
{preview.nextInvoice.addOns.map(addOn => (
|
||||
<OLRow className="mt-1" key={addOn.code}>
|
||||
<OLCol xs={9}>
|
||||
{addOn.name}
|
||||
{addOn.quantity > 1 ? ` ×${addOn.quantity}` : ''}
|
||||
</OLCol>
|
||||
<OLCol xs={3} className="text-end">
|
||||
{formatCurrency(addOn.amount, preview.currency)}
|
||||
</OLCol>
|
||||
</OLRow>
|
||||
))}
|
||||
|
||||
{preview.nextInvoice.tax.rate > 0 && (
|
||||
<OLRow className="mt-1">
|
||||
<OLCol xs={9}>
|
||||
{t('vat')} {preview.nextInvoice.tax.rate * 100}%
|
||||
</OLCol>
|
||||
<OLCol xs={3} className="text-end">
|
||||
{formatCurrency(
|
||||
preview.nextInvoice.tax.amount,
|
||||
preview.currency
|
||||
)}
|
||||
</OLCol>
|
||||
</OLRow>
|
||||
)}
|
||||
|
||||
<OLRow className="mt-1">
|
||||
<OLCol xs={9}>
|
||||
{preview.nextPlan.annual
|
||||
? t('total_per_year')
|
||||
: t('total_per_month')}
|
||||
</OLCol>
|
||||
<OLCol xs={3} className="text-end">
|
||||
{formatCurrency(preview.nextInvoice.total, preview.currency)}
|
||||
</OLCol>
|
||||
</OLRow>
|
||||
</OLCard>
|
||||
|
||||
<div className="mt-5">
|
||||
<Trans
|
||||
i18nKey="the_next_payment_will_be_collected_on"
|
||||
values={{ date: moment(preview.nextInvoice.date).format('LL') }}
|
||||
components={{ strong: <strong /> }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
</div>
|
||||
</OLCard>
|
||||
</OLCol>
|
||||
</OLRow>
|
||||
</div>
|
||||
<NextPaymentDate preview={preview} />
|
||||
</PreviewLayout>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<UpgradeSuccess
|
||||
subscription={subscription}
|
||||
postCheckoutRedirect={postCheckoutRedirect}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const onAiStandalonePlan = isStandaloneAiPlanCode(subscription.planCode)
|
||||
|
||||
return (
|
||||
@@ -32,9 +43,7 @@ function SuccessfulSubscription() {
|
||||
<OLRow>
|
||||
<OLCol lg={{ span: 8, offset: 2 }}>
|
||||
<OLPageContentCard>
|
||||
<div className="page-header">
|
||||
<h2>{t('thanks_for_subscribing')}</h2>
|
||||
</div>
|
||||
<h2>{t('thanks_for_subscribing')}</h2>
|
||||
<OLNotification
|
||||
type="success"
|
||||
content={
|
||||
@@ -57,12 +66,12 @@ function SuccessfulSubscription() {
|
||||
<PriceExceptions subscription={subscription} />
|
||||
</>
|
||||
)}
|
||||
<p>
|
||||
{t('to_modify_your_subscription_go_to')}
|
||||
<div className="d-flex justify-content-between align-items-center gap-3">
|
||||
<span>{t('to_modify_your_subscription_go_to')}</span>
|
||||
<a href="/user/subscription" rel="noopener noreferrer">
|
||||
{t('manage_subscription')}.
|
||||
{t('manage_subscription')}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
@@ -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 (
|
||||
<div className="container">
|
||||
<OLRow>
|
||||
<OLCol lg={{ span: 8, offset: 2 }}>
|
||||
<OLPageContentCard>
|
||||
<h2>{t('welcome_to_plan', { planName: planDisplayName })}</h2>
|
||||
<OLNotification
|
||||
type="success"
|
||||
content={
|
||||
<div className="d-flex justify-content-between align-items-center gap-3">
|
||||
<span>{t('youve_upgraded_your_subscription')}</span>
|
||||
<a href="/user/subscription" rel="noopener noreferrer">
|
||||
{t('manage_subscription')}
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey="next_payment_of_x_collectected_on_y"
|
||||
values={{
|
||||
paymentAmmount: subscription.payment.displayPrice,
|
||||
collectionDate: subscription.payment.nextPaymentDueAt,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[<strong />, <strong />]} // eslint-disable-line react/jsx-key
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<i>* {t('subject_to_additional_vat')}</i>
|
||||
</p>
|
||||
{benefitsText && <p>{benefitsText}</p>}
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey="find_out_how_to_get_the_most_out_of_your_new_subscription"
|
||||
components={[
|
||||
// eslint-disable-next-line react/jsx-key, jsx-a11y/anchor-has-content
|
||||
<a
|
||||
href="https://docs.overleaf.com/integrations-and-add-ons/ai-features"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<a
|
||||
className="btn btn-primary"
|
||||
href={postCheckoutRedirect || '/project'}
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t('back_to_your_projects')}
|
||||
</a>
|
||||
</p>
|
||||
</OLPageContentCard>
|
||||
</OLCol>
|
||||
</OLRow>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ThankYouSection({
|
||||
subscription,
|
||||
onAiStandalonePlan,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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</0>.",
|
||||
"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</0>.",
|
||||
"get_most_subscription_by_checking_overleaf_ai_writefull": "Get the most out of your subscription by checking out <0>Overleaf’s features</0>, <1>Overleaf’s AI features</1> and <2>Writefull’s features</2>.",
|
||||
"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</0>. 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",
|
||||
|
||||
@@ -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('<PreviewSubscriptionChange/> 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(<PreviewSubscriptionChange />)
|
||||
|
||||
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(<PreviewSubscriptionChange />)
|
||||
|
||||
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(<PreviewSubscriptionChange />)
|
||||
|
||||
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(<PreviewSubscriptionChange />)
|
||||
|
||||
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'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -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(
|
||||
<UserProvider>
|
||||
<SuccessfulSubscription />
|
||||
</UserProvider>,
|
||||
{
|
||||
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(
|
||||
<UserProvider>
|
||||
<SuccessfulSubscription />
|
||||
</UserProvider>,
|
||||
{
|
||||
metaTags: [
|
||||
{
|
||||
name: 'ol-ExposedSettings',
|
||||
value: {
|
||||
adminEmail: 'foo@example.com',
|
||||
} as ExposedSettings,
|
||||
},
|
||||
{ name: 'ol-subscription', value: annualActiveSubscription },
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
screen.getByRole('heading', { name: /thanks for subscribing/i })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user