mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 17:19:37 +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 postCheckoutRedirect = req.session?.postCheckoutRedirect
|
||||||
|
const isUpgrade = req.query.upgrade === 'true'
|
||||||
|
|
||||||
if (!personalSubscription) {
|
if (!personalSubscription) {
|
||||||
res.redirect('/user/subscription/plans')
|
res.redirect('/user/subscription/plans')
|
||||||
@@ -376,6 +377,7 @@ async function successfulSubscription(req, res) {
|
|||||||
title: 'thank_you',
|
title: 'thank_you',
|
||||||
personalSubscription,
|
personalSubscription,
|
||||||
postCheckoutRedirect,
|
postCheckoutRedirect,
|
||||||
|
isUpgrade,
|
||||||
user: {
|
user: {
|
||||||
_id: user._id,
|
_id: user._id,
|
||||||
features: userInDb.features,
|
features: userInDb.features,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ block entrypointVar
|
|||||||
block append meta
|
block append meta
|
||||||
meta(name='ol-subscription' data-type='json' content=personalSubscription)
|
meta(name='ol-subscription' data-type='json' content=personalSubscription)
|
||||||
meta(name='ol-postCheckoutRedirect' content=postCheckoutRedirect)
|
meta(name='ol-postCheckoutRedirect' content=postCheckoutRedirect)
|
||||||
|
meta(name='ol-isUpgrade' data-type='boolean' content=isUpgrade)
|
||||||
meta(name='ol-user' data-type='json' content=user)
|
meta(name='ol-user' data-type='json' content=user)
|
||||||
|
|
||||||
block content
|
block content
|
||||||
|
|||||||
@@ -661,6 +661,7 @@
|
|||||||
"filter_projects": "",
|
"filter_projects": "",
|
||||||
"find": "",
|
"find": "",
|
||||||
"find_and_check_citations": "",
|
"find_and_check_citations": "",
|
||||||
|
"find_out_how_to_get_the_most_out_of_your_new_subscription": "",
|
||||||
"find_out_more": "",
|
"find_out_more": "",
|
||||||
"find_out_more_about_institution_login": "",
|
"find_out_more_about_institution_login": "",
|
||||||
"find_out_more_about_the_file_outline": "",
|
"find_out_more_about_the_file_outline": "",
|
||||||
@@ -727,6 +728,8 @@
|
|||||||
"get_most_subscription_by_checking_ai_writefull": "",
|
"get_most_subscription_by_checking_ai_writefull": "",
|
||||||
"get_most_subscription_by_checking_overleaf": "",
|
"get_most_subscription_by_checking_overleaf": "",
|
||||||
"get_most_subscription_by_checking_overleaf_ai_writefull": "",
|
"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_real_time_track_changes": "",
|
||||||
"get_your_hands_on_the_ultimate_research_writing_ai_assistant": "",
|
"get_your_hands_on_the_ultimate_research_writing_ai_assistant": "",
|
||||||
"git": "",
|
"git": "",
|
||||||
@@ -1431,6 +1434,7 @@
|
|||||||
"priority_support": "",
|
"priority_support": "",
|
||||||
"privacy_and_terms": "",
|
"privacy_and_terms": "",
|
||||||
"private": "",
|
"private": "",
|
||||||
|
"pro": "",
|
||||||
"problem_talking_to_publishing_service": "",
|
"problem_talking_to_publishing_service": "",
|
||||||
"problem_with_subscription_contact_us": "",
|
"problem_with_subscription_contact_us": "",
|
||||||
"proceed_to_paypal": "",
|
"proceed_to_paypal": "",
|
||||||
@@ -1538,6 +1542,7 @@
|
|||||||
"refresh_page_after_linking_dropbox": "",
|
"refresh_page_after_linking_dropbox": "",
|
||||||
"refresh_page_after_starting_free_trial": "",
|
"refresh_page_after_starting_free_trial": "",
|
||||||
"refreshing": "",
|
"refreshing": "",
|
||||||
|
"refund_with_description": "",
|
||||||
"regards": "",
|
"regards": "",
|
||||||
"registering": "",
|
"registering": "",
|
||||||
"reject": "",
|
"reject": "",
|
||||||
@@ -1878,7 +1883,6 @@
|
|||||||
"submit_title": "",
|
"submit_title": "",
|
||||||
"subscribe": "",
|
"subscribe": "",
|
||||||
"subscribe_to_find_the_symbols_you_need_faster": "",
|
"subscribe_to_find_the_symbols_you_need_faster": "",
|
||||||
"subscribe_to_plan": "",
|
|
||||||
"subscription": "",
|
"subscription": "",
|
||||||
"subscription_admins_cannot_be_deleted": "",
|
"subscription_admins_cannot_be_deleted": "",
|
||||||
"subscription_canceled": "",
|
"subscription_canceled": "",
|
||||||
@@ -2203,6 +2207,7 @@
|
|||||||
"upgrade_to_add_more_collaborators_and_more": "",
|
"upgrade_to_add_more_collaborators_and_more": "",
|
||||||
"upgrade_to_get_feature": "",
|
"upgrade_to_get_feature": "",
|
||||||
"upgrade_to_get_started": "",
|
"upgrade_to_get_started": "",
|
||||||
|
"upgrade_to_plan": "",
|
||||||
"upgrade_to_review": "",
|
"upgrade_to_review": "",
|
||||||
"upgrade_your_subscription": "",
|
"upgrade_your_subscription": "",
|
||||||
"upload": "",
|
"upload": "",
|
||||||
@@ -2306,6 +2311,7 @@
|
|||||||
"website_status": "",
|
"website_status": "",
|
||||||
"wed_love_you_to_stay": "",
|
"wed_love_you_to_stay": "",
|
||||||
"welcome_to_overleaf_opening_workspace": "",
|
"welcome_to_overleaf_opening_workspace": "",
|
||||||
|
"welcome_to_plan": "",
|
||||||
"welcome_to_sl": "",
|
"welcome_to_sl": "",
|
||||||
"well_be_here_when_youre_ready": "",
|
"well_be_here_when_youre_ready": "",
|
||||||
"were_making_some_changes_to_project_sharing_this_means_you_will_be_visible": "",
|
"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_reached_the_fair_usage_limit_on_your_plan_you_can_start_chatting_again_in_time": "",
|
||||||
"youve_unlinked_all_users": "",
|
"youve_unlinked_all_users": "",
|
||||||
"youve_upgraded_your_plan": "",
|
"youve_upgraded_your_plan": "",
|
||||||
|
"youve_upgraded_your_subscription": "",
|
||||||
"zoom_in": "",
|
"zoom_in": "",
|
||||||
"zoom_out": "",
|
"zoom_out": "",
|
||||||
"zoom_to": "",
|
"zoom_to": "",
|
||||||
|
|||||||
@@ -12,18 +12,16 @@ function Canceled() {
|
|||||||
<OLRow>
|
<OLRow>
|
||||||
<OLCol lg={{ span: 8, offset: 2 }}>
|
<OLCol lg={{ span: 8, offset: 2 }}>
|
||||||
<OLPageContentCard>
|
<OLPageContentCard>
|
||||||
<div className="page-header">
|
<h2>{t('subscription_canceled')}</h2>
|
||||||
<h2>{t('subscription_canceled')}</h2>
|
|
||||||
</div>
|
|
||||||
<OLNotification
|
<OLNotification
|
||||||
type="info"
|
type="info"
|
||||||
content={
|
content={
|
||||||
<p>
|
<div className="d-flex justify-content-between align-items-center gap-3">
|
||||||
{t('to_modify_your_subscription_go_to')}
|
<span>{t('to_modify_your_subscription_go_to')}</span>
|
||||||
<a href="/user/subscription" rel="noopener noreferrer">
|
<a href="/user/subscription" rel="noopener noreferrer">
|
||||||
{t('manage_subscription')}.
|
{t('manage_subscription')}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import { useCallback, useEffect } from 'react'
|
import { useCallback, useEffect } from 'react'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import { useTranslation, Trans } from 'react-i18next'
|
import { useTranslation, Trans } from 'react-i18next'
|
||||||
import {
|
import { SubscriptionChangePreview } from '../../../../../../types/subscription/subscription-change-preview'
|
||||||
SubscriptionChangePreview,
|
|
||||||
AddOnPurchase,
|
|
||||||
PremiumSubscriptionChange,
|
|
||||||
} from '../../../../../../types/subscription/subscription-change-preview'
|
|
||||||
import getMeta from '@/utils/meta'
|
import getMeta from '@/utils/meta'
|
||||||
import { formatCurrency } from '@/shared/utils/currency'
|
import { formatCurrency } from '@/shared/utils/currency'
|
||||||
import useAsync from '@/shared/hooks/use-async'
|
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 handleStripePaymentAction from '../../util/handle-stripe-payment-action'
|
||||||
import RedirectedPaymentErrorNotification from '../shared/redirected-payment-error-notification'
|
import RedirectedPaymentErrorNotification from '../shared/redirected-payment-error-notification'
|
||||||
import TrialDisabledNotification from './trial-disabled-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() {
|
function PreviewSubscriptionChange() {
|
||||||
const preview = getMeta(
|
const preview = getMeta(
|
||||||
@@ -33,7 +173,7 @@ function PreviewSubscriptionChange() {
|
|||||||
const payNowTask = useAsync()
|
const payNowTask = useAsync()
|
||||||
const location = useLocation()
|
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(
|
const filteredLineItems = preview.immediateCharge.lineItems.filter(
|
||||||
(item, index, arr) => {
|
(item, index, arr) => {
|
||||||
if (!item.isAiAssist) return true
|
if (!item.isAiAssist) return true
|
||||||
@@ -80,223 +220,142 @@ function PreviewSubscriptionChange() {
|
|||||||
referrer: purchaseReferrer,
|
referrer: purchaseReferrer,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
location.replace('/user/subscription/thank-you')
|
location.replace(
|
||||||
|
isPremiumUpgrade
|
||||||
|
? '/user/subscription/thank-you?upgrade=true'
|
||||||
|
: '/user/subscription/thank-you'
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.catch(debugConsole.error)
|
.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 =
|
const aiAddOnChange =
|
||||||
preview.change.type === 'add-on-purchase' &&
|
preview.change.type === 'add-on-purchase' &&
|
||||||
preview.change.addOn.code === 'assistant'
|
preview.change.addOn.code === 'assistant'
|
||||||
|
|
||||||
// the driver of the change, which we can display as the immediate charge
|
|
||||||
const changeName =
|
const changeName =
|
||||||
preview.change.type === 'add-on-purchase'
|
preview.change.type === 'add-on-purchase' ? preview.change.addOn.name : ''
|
||||||
? (preview.change as AddOnPurchase).addOn.name
|
|
||||||
: (preview.change as PremiumSubscriptionChange).plan.name
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container">
|
<PreviewLayout>
|
||||||
<OLRow>
|
{preview.change.type === 'add-on-purchase' ? (
|
||||||
<OLCol md={{ offset: 2, span: 8 }}>
|
<h1>
|
||||||
<RedirectedPaymentErrorNotification />
|
{t('add_add_on_to_your_plan', {
|
||||||
<TrialDisabledNotification />
|
addOnName: preview.change.addOn.name,
|
||||||
<OLCard className="p-3">
|
})}
|
||||||
{preview.change.type === 'add-on-purchase' ? (
|
</h1>
|
||||||
<h1>
|
) : null}
|
||||||
{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}
|
|
||||||
|
|
||||||
{payNowTask.isError && (
|
{payNowTask.isError && (
|
||||||
<PaymentErrorNotification
|
<PaymentErrorNotification error={payNowTask.error as FetchError} />
|
||||||
error={payNowTask.error as FetchError}
|
)}
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{aiAddOnChange && (
|
{aiAddOnChange && (
|
||||||
<div>
|
<div>
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="add_ai_assist_to_your_plan"
|
i18nKey="add_ai_assist_to_your_plan"
|
||||||
components={{
|
components={{
|
||||||
sparkle: (
|
sparkle: (
|
||||||
<img
|
<img
|
||||||
alt="sparkle"
|
alt="sparkle"
|
||||||
className="ai-error-assistant-sparkle"
|
className="ai-error-assistant-sparkle"
|
||||||
src={sparkleText}
|
src={sparkleText}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
key="sparkle"
|
key="sparkle"
|
||||||
/>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
),
|
||||||
)}
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<OLCard className="payment-summary-card mt-5">
|
<OLCard className="payment-summary-card mt-5">
|
||||||
<h3>{t('due_today')}:</h3>
|
<DueTodayBlock
|
||||||
{filteredLineItems.length > 1 ? (
|
preview={preview}
|
||||||
<>
|
filteredLineItems={filteredLineItems}
|
||||||
{filteredLineItems.map((item, index) => (
|
singleItemName={changeName}
|
||||||
<OLRow key={index}>
|
/>
|
||||||
<OLCol xs={9}>
|
</OLCard>
|
||||||
{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>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{preview.immediateCharge.tax > 0 && (
|
<div className="mt-5">
|
||||||
<OLRow className="mt-1">
|
<Trans
|
||||||
<OLCol xs={9}>
|
i18nKey="this_total_reflects_the_amount_due_until"
|
||||||
{t('vat')} {preview.nextInvoice.tax.rate * 100}%
|
values={{ date: moment(preview.nextInvoice.date).format('LL') }}
|
||||||
</OLCol>
|
components={{ strong: <strong /> }}
|
||||||
<OLCol xs={3} className="text-end">
|
shouldUnescape
|
||||||
{formatCurrency(
|
tOptions={{ interpolation: { escapeValue: true } }}
|
||||||
preview.immediateCharge.tax,
|
/>{' '}
|
||||||
preview.currency
|
<Trans
|
||||||
)}
|
i18nKey="we_will_use_your_existing_payment_method"
|
||||||
</OLCol>
|
values={{ paymentMethod: preview.paymentMethod }}
|
||||||
</OLRow>
|
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">
|
<div className="mt-5">
|
||||||
<OLCol xs={9}>{t('total_today')}</OLCol>
|
<OLButton
|
||||||
<OLCol xs={3} className="text-end">
|
variant="primary"
|
||||||
<strong>
|
size="lg"
|
||||||
{formatCurrency(
|
onClick={handlePayNowClick}
|
||||||
preview.immediateCharge.total,
|
disabled={payNowTask.isLoading || payNowTask.isSuccess}
|
||||||
preview.currency
|
>
|
||||||
)}
|
{t('pay_now')}
|
||||||
</strong>
|
</OLButton>
|
||||||
</OLCol>
|
</div>
|
||||||
</OLRow>
|
|
||||||
</OLCard>
|
|
||||||
|
|
||||||
<div className="mt-5">
|
<OLCard className="payment-summary-card mt-5">
|
||||||
<Trans
|
<h3>{t('future_payments')}:</h3>
|
||||||
i18nKey="this_total_reflects_the_amount_due_until"
|
<FuturePayments preview={preview} />
|
||||||
values={{ date: moment(preview.nextInvoice.date).format('LL') }}
|
</OLCard>
|
||||||
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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="mt-5">
|
<NextPaymentDate preview={preview} />
|
||||||
<OLButton
|
</PreviewLayout>
|
||||||
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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,17 +14,28 @@ import {
|
|||||||
} from '../../data/add-on-codes'
|
} from '../../data/add-on-codes'
|
||||||
import { PaidSubscription } from '../../../../../../types/subscription/dashboard/subscription'
|
import { PaidSubscription } from '../../../../../../types/subscription/dashboard/subscription'
|
||||||
import { useBroadcastUser } from '@/shared/hooks/user-channel/use-broadcast-user'
|
import { useBroadcastUser } from '@/shared/hooks/user-channel/use-broadcast-user'
|
||||||
|
import { getUpgradePlanDisplayName } from '../../util/plan-display-names'
|
||||||
|
|
||||||
function SuccessfulSubscription() {
|
function SuccessfulSubscription() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { personalSubscription: subscription } =
|
const { personalSubscription: subscription } =
|
||||||
useSubscriptionDashboardContext()
|
useSubscriptionDashboardContext()
|
||||||
const postCheckoutRedirect = getMeta('ol-postCheckoutRedirect')
|
const postCheckoutRedirect = getMeta('ol-postCheckoutRedirect')
|
||||||
|
const isUpgrade = getMeta('ol-isUpgrade')
|
||||||
const { appName, adminEmail } = getMeta('ol-ExposedSettings')
|
const { appName, adminEmail } = getMeta('ol-ExposedSettings')
|
||||||
useBroadcastUser()
|
useBroadcastUser()
|
||||||
|
|
||||||
if (!subscription || !('payment' in subscription)) return null
|
if (!subscription || !('payment' in subscription)) return null
|
||||||
|
|
||||||
|
if (isUpgrade) {
|
||||||
|
return (
|
||||||
|
<UpgradeSuccess
|
||||||
|
subscription={subscription}
|
||||||
|
postCheckoutRedirect={postCheckoutRedirect}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const onAiStandalonePlan = isStandaloneAiPlanCode(subscription.planCode)
|
const onAiStandalonePlan = isStandaloneAiPlanCode(subscription.planCode)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -32,9 +43,7 @@ function SuccessfulSubscription() {
|
|||||||
<OLRow>
|
<OLRow>
|
||||||
<OLCol lg={{ span: 8, offset: 2 }}>
|
<OLCol lg={{ span: 8, offset: 2 }}>
|
||||||
<OLPageContentCard>
|
<OLPageContentCard>
|
||||||
<div className="page-header">
|
<h2>{t('thanks_for_subscribing')}</h2>
|
||||||
<h2>{t('thanks_for_subscribing')}</h2>
|
|
||||||
</div>
|
|
||||||
<OLNotification
|
<OLNotification
|
||||||
type="success"
|
type="success"
|
||||||
content={
|
content={
|
||||||
@@ -57,12 +66,12 @@ function SuccessfulSubscription() {
|
|||||||
<PriceExceptions subscription={subscription} />
|
<PriceExceptions subscription={subscription} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<p>
|
<div className="d-flex justify-content-between align-items-center gap-3">
|
||||||
{t('to_modify_your_subscription_go_to')}
|
<span>{t('to_modify_your_subscription_go_to')}</span>
|
||||||
<a href="/user/subscription" rel="noopener noreferrer">
|
<a href="/user/subscription" rel="noopener noreferrer">
|
||||||
{t('manage_subscription')}.
|
{t('manage_subscription')}
|
||||||
</a>
|
</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({
|
function ThankYouSection({
|
||||||
subscription,
|
subscription,
|
||||||
onAiStandalonePlan,
|
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-isRegisteredViaGoogle': boolean
|
||||||
'ol-isRestrictedTokenMember': boolean
|
'ol-isRestrictedTokenMember': boolean
|
||||||
'ol-isSaas': boolean
|
'ol-isSaas': boolean
|
||||||
|
'ol-isUpgrade': boolean
|
||||||
'ol-isUserGroupManager': boolean
|
'ol-isUserGroupManager': boolean
|
||||||
'ol-itm_campaign': string
|
'ol-itm_campaign': string
|
||||||
'ol-itm_content': string
|
'ol-itm_content': string
|
||||||
|
|||||||
@@ -879,6 +879,7 @@
|
|||||||
"filters": "Filters",
|
"filters": "Filters",
|
||||||
"find": "Find",
|
"find": "Find",
|
||||||
"find_and_check_citations": "Find and check citations",
|
"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": "Find out More",
|
||||||
"find_out_more_about_institution_login": "Find out more about institutional login",
|
"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",
|
"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": "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_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_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_real_time_track_changes": "Get real-time track changes",
|
||||||
"get_standard": "Get Standard",
|
"get_standard": "Get Standard",
|
||||||
"get_student": "Get Student",
|
"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_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.",
|
"refresh_page_after_starting_free_trial": "Please refresh this page after starting your free trial.",
|
||||||
"refreshing": "Refreshing",
|
"refreshing": "Refreshing",
|
||||||
|
"refund_with_description": "Refund: __description__",
|
||||||
"regards": "Regards",
|
"regards": "Regards",
|
||||||
"register": "Register",
|
"register": "Register",
|
||||||
"register_error": "Registration error",
|
"register_error": "Registration error",
|
||||||
@@ -2459,7 +2463,6 @@
|
|||||||
"submit_title": "Submit",
|
"submit_title": "Submit",
|
||||||
"subscribe": "Subscribe",
|
"subscribe": "Subscribe",
|
||||||
"subscribe_to_find_the_symbols_you_need_faster": "Subscribe to find the symbols you need faster",
|
"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": "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_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",
|
"subscription_canceled": "Subscription canceled",
|
||||||
@@ -2839,6 +2842,7 @@
|
|||||||
"upgrade_to_get_feature": "Upgrade to get __feature__, plus:",
|
"upgrade_to_get_feature": "Upgrade to get __feature__, plus:",
|
||||||
"upgrade_to_get_more_from_overleaf": "Upgrade to get more from Overleaf",
|
"upgrade_to_get_more_from_overleaf": "Upgrade to get more from Overleaf",
|
||||||
"upgrade_to_get_started": "Upgrade to get started",
|
"upgrade_to_get_started": "Upgrade to get started",
|
||||||
|
"upgrade_to_plan": "Upgrade to __planName__",
|
||||||
"upgrade_to_review": "Upgrade to review",
|
"upgrade_to_review": "Upgrade to review",
|
||||||
"upgrade_your_plan": "Upgrade your plan",
|
"upgrade_your_plan": "Upgrade your plan",
|
||||||
"upgrade_your_subscription": "Upgrade your subscription",
|
"upgrade_your_subscription": "Upgrade your subscription",
|
||||||
@@ -2958,6 +2962,7 @@
|
|||||||
"website_status": "Website status",
|
"website_status": "Website status",
|
||||||
"wed_love_you_to_stay": "We’d love you to stay",
|
"wed_love_you_to_stay": "We’d love you to stay",
|
||||||
"welcome_to_overleaf_opening_workspace": "Welcome to Overleaf. Opening your workspace.",
|
"welcome_to_overleaf_opening_workspace": "Welcome to Overleaf. Opening your workspace.",
|
||||||
|
"welcome_to_plan": "Welcome to __planName__!",
|
||||||
"welcome_to_sl": "Welcome to __appName__",
|
"welcome_to_sl": "Welcome to __appName__",
|
||||||
"well_be_here_when_youre_ready": "We’ll be here when you’re ready to dive back in! 🦆",
|
"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.",
|
"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_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_unlinked_all_users": "You’ve unlinked all users",
|
||||||
"youve_upgraded_your_plan": "You’ve upgraded your plan!",
|
"youve_upgraded_your_plan": "You’ve upgraded your plan!",
|
||||||
|
"youve_upgraded_your_subscription": "You’ve upgraded your subscription",
|
||||||
"zh-CN": "Chinese",
|
"zh-CN": "Chinese",
|
||||||
"zip_contents_too_large": "Zip contents too large",
|
"zip_contents_too_large": "Zip contents too large",
|
||||||
"zoom_in": "Zoom in",
|
"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 { screen, within } from '@testing-library/react'
|
||||||
import SuccessfulSubscription from '../../../../../../frontend/js/features/subscription/components/successful-subscription/successful-subscription'
|
import SuccessfulSubscription from '../../../../../../frontend/js/features/subscription/components/successful-subscription/successful-subscription'
|
||||||
import { renderWithSubscriptionDashContext } from '../../helpers/render-with-subscription-dash-context'
|
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 { ExposedSettings } from '../../../../../../types/exposed-settings'
|
||||||
import { UserProvider } from '@/shared/context/user-context'
|
import { UserProvider } from '@/shared/context/user-context'
|
||||||
|
|
||||||
@@ -84,4 +87,80 @@ describe('successful subscription page', function () {
|
|||||||
})
|
})
|
||||||
expect(backToYourProjectsLink.getAttribute('href')).to.equal('/project')
|
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',
|
title: 'thank_you',
|
||||||
personalSubscription: 'foo',
|
personalSubscription: 'foo',
|
||||||
postCheckoutRedirect: undefined,
|
postCheckoutRedirect: undefined,
|
||||||
|
isUpgrade: false,
|
||||||
user: {
|
user: {
|
||||||
_id: ctx.user._id,
|
_id: ctx.user._id,
|
||||||
features: ctx.user.features,
|
features: ctx.user.features,
|
||||||
|
|||||||
Reference in New Issue
Block a user