|
|
@@ -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}
|
|
|
-
|
|
|
- {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"
|
|
|
- />
|
|
|
- ),
|
|
|
- }}
|
|
|
+ <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} />
|
|
|
+ )}
|
|
|
+
|
|
|
+ {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>
|
|
|
- )}
|
|
|
-
|
|
|
- <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>
|
|
|
- </>
|
|
|
- )}
|
|
|
+ ),
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
|
|
|
- {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>
|
|
|
- )}
|
|
|
+ <OLCard className="payment-summary-card mt-5">
|
|
|
+ <DueTodayBlock
|
|
|
+ preview={preview}
|
|
|
+ filteredLineItems={filteredLineItems}
|
|
|
+ singleItemName={changeName}
|
|
|
+ />
|
|
|
+ </OLCard>
|
|
|
|
|
|
- <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">
|
|
|
- <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>
|
|
|
- )}
|
|
|
-
|
|
|
- <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>
|
|
|
- )}
|
|
|
+ <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}>
|
|
|
- {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>
|
|
|
+ <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>
|
|
|
+ <FuturePayments preview={preview} />
|
|
|
+ </OLCard>
|
|
|
+
|
|
|
+ <NextPaymentDate preview={preview} />
|
|
|
+ </PreviewLayout>
|
|
|
)
|
|
|
}
|
|
|
|