first commit

This commit is contained in:
2025-04-24 13:11:28 +08:00
commit ff9c54d5e4
5960 changed files with 834111 additions and 0 deletions

View File

@@ -0,0 +1,250 @@
import { useCallback } from 'react'
import moment from 'moment'
import { useTranslation, Trans } from 'react-i18next'
import {
SubscriptionChangePreview,
AddOnPurchase,
PremiumSubscriptionChange,
} from '../../../../../../types/subscription/subscription-change-preview'
import getMeta from '@/utils/meta'
import { formatCurrency } from '@/shared/utils/currency'
import useAsync from '@/shared/hooks/use-async'
import { useLocation } from '@/shared/hooks/use-location'
import { debugConsole } from '@/utils/debugging'
import { postJSON } from '@/infrastructure/fetch-json'
import Notification from '@/shared/components/notification'
import OLCard from '@/features/ui/components/ol/ol-card'
import OLRow from '@/features/ui/components/ol/ol-row'
import OLCol from '@/features/ui/components/ol/ol-col'
import OLButton from '@/features/ui/components/ol/ol-button'
import { subscriptionUpdateUrl } from '@/features/subscription/data/subscription-url'
import * as eventTracking from '@/infrastructure/event-tracking'
import sparkleText from '@/shared/svgs/ai-sparkle-text.svg'
function PreviewSubscriptionChange() {
const preview = getMeta(
'ol-subscriptionChangePreview'
) as SubscriptionChangePreview
const { t } = useTranslation()
const payNowTask = useAsync()
const location = useLocation()
const handlePayNowClick = useCallback(() => {
eventTracking.sendMB('assistant-add-on-purchase')
payNowTask
.runAsync(payNow(preview))
.then(() => {
location.replace('/user/subscription/thank-you')
})
.catch(debugConsole.error)
}, [location, payNowTask, preview])
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
return (
<div className="container">
<OLRow>
<OLCol md={{ offset: 2, span: 8 }}>
<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 && (
<Notification
type="error"
aria-live="polite"
content={
<>
{t('generic_something_went_wrong')}. {t('try_again')}.{' '}
{t('generic_if_problem_continues_contact_us')}.
</>
}
/>
)}
{aiAddOnChange && (
<div>
<Trans
i18nKey="add_error_assist_to_your_projects"
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>
<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 && (
<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>
</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>
<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>
)
}
async function payNow(preview: SubscriptionChangePreview) {
if (preview.change.type === 'add-on-purchase') {
await postJSON(`/user/subscription/addon/${preview.change.addOn.code}/add`)
} else if (preview.change.type === 'premium-subscription') {
await postJSON(subscriptionUpdateUrl, {
body: { plan_code: preview.change.plan.code },
})
} else {
throw new Error(
`Unknown subscription change preview type: ${preview.change}`
)
}
}
export default PreviewSubscriptionChange