// Copyright: (c) 2026 TWWIM UG. All rights reserved. import { useEffect, useState } from 'react'; import { useNavigate, useSearch } from '@tanstack/react-router'; import { authenticatedUserStore } from '@/infrastructure/storage/AuthenticatedUserStore'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { Tag, CheckCircle2, AlertCircle, ArrowLeft, CreditCard, ShieldCheck, Lock, Sparkles, Receipt, Package, } from 'lucide-react'; import { PageLayout } from '@/components/shared'; import { useTranslation } from '@/i18n/TranslationProvider'; import { useSubscription, useAvailablePlans } from '@/features/subscription/hooks/useSubscription'; import { useValidateCode } from './hooks/useValidateCode'; import { useApplyCheckoutCode } from './hooks/useApplyCheckoutCode'; import { PaymentPanel } from '@/features/subscription/components/PaymentPanel'; import { subscriptionApi } from '@/infrastructure/http/api/subscription'; import { queryKeys } from '@/lib/query-keys'; import type { ValidateCodeResponse } from '@archer/api-interface/schemas/customer-api/response'; import type { AvailablePlanResponse } from '@/infrastructure/http/api/subscription'; import { resolveCheckoutBreakdown, resolveCurrency } from './lib/checkoutBreakdown'; import { useAuthenticatedUser } from '@/features/auth/hooks/useAuthenticatedUser'; const codeSchema = z.object({ code: z.string().min(3).max(30).regex(/^[A-Z0-9_-]+$/i, 'Code must be alphanumeric'), }); type CodeForm = z.infer; export function CheckoutPage() { const { t, locale } = useTranslation(); const navigate = useNavigate(); const search = useSearch({ strict: false }) as { planId?: string; txId?: string; commandSize?: number; commandQty?: number }; const { data: subscription } = useSubscription(); const { data: plans } = useAvailablePlans(); const queryClient = useQueryClient(); const validateCode = useValidateCode(); const applyCode = useApplyCheckoutCode(); // Defense-in-depth for deep-links, Back/Forward, and legacy Stripe redirects. // The plan-selection modal already blocks unready users from reaching here, // but the page is URL-addressable — anyone landing directly must bounce // back to /dashboard/subscription where ReadinessGateModal will open. // Pure client read, matches the subscription-page gate exactly. useEffect(() => { const account = authenticatedUserStore.get()?.account; if (!account || !account.isProfileComplete || !account.emailVerified) { navigate({ to: '/dashboard/subscription', replace: true }); } // Run only on mount — the modal flow already prevents render-time arrival // in non-ready state; subsequent state flips to "not ready" mid-session // shouldn't kick users out of checkout (shouldn't happen anyway). // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const [codePreview, setCodePreview] = useState(null); const [codeApplied, setCodeApplied] = useState(false); const [showPayment, setShowPayment] = useState(false); const [commandClientSecret, setCommandClientSecret] = useState(null); const [commandTxId, setCommandTxId] = useState(null); // ── Mode detection ── const isCommandMode = !!search.commandSize; const commandSize = (search.commandSize === 100 || search.commandSize === 1000) ? search.commandSize : null; const commandQuantity = (() => { const q = Number(search.commandQty ?? 1); if (!Number.isInteger(q) || q < 1 || q > 100) return 1; return q; })(); const commandPackage = isCommandMode ? subscription?.commandPackages?.find((p: any) => p.size === commandSize) : null; const planId = search.planId ?? subscription?.pendingSubscription?.planId; const transactionId = search.txId; const plan = !isCommandMode ? plans?.find((p: AvailablePlanResponse) => p.id === planId) : null; const pendingTx = !isCommandMode ? subscription?.pendingTransaction : null; const { register, handleSubmit, formState: { errors } } = useForm({ resolver: zodResolver(codeSchema), }); // ── Price calculation ────────────────────────────────────────────── // resolvedCurrency = user's preferredCurrency, falling back to the // API-returned plan/sub currency (post-fix already preferredCurrency-aware). // planNetMajor reads the per-currency cents from plan.prices map; falls // back to scalar plan.price (EUR-primary, derived) only when the map lacks // the resolved currency. Command packs already arrive in preferredCurrency // via GetSubscriptionUseCase (post-fix). const authed = useAuthenticatedUser(); const preferredCurrency = authed?.preferredCurrency ?? undefined; const sourceCurrencyString = isCommandMode ? subscription?.subscription?.plan?.currency : plan?.currency; const resolvedCurrency = preferredCurrency ?? resolveCurrency(sourceCurrencyString); const planNetMajor = (() => { if (!plan) return 0; const cents = plan.prices?.[resolvedCurrency]; if (typeof cents === 'number') return Number((cents / 100).toFixed(2)); return Number(plan.price); })(); const originalAmount = isCommandMode ? (commandPackage ? Number((commandPackage.price * commandQuantity).toFixed(2)) : 0) : planNetMajor; const existingDiscount = !isCommandMode && pendingTx && pendingTx.discountAmount > 0; const activeDiscount = codePreview?.valid ? { amount: codePreview.discountAmount, total: codePreview.discountedAmount, label: codePreview.campaignName?.[locale] ?? codePreview.campaignName?.['en'] ?? '', pct: codePreview.discountUnit === 'percentage' ? codePreview.discountValue : undefined } : existingDiscount ? { amount: pendingTx!.discountAmount, total: Number(pendingTx!.amount), label: (pendingTx!.appliedCampaigns?.[0] as any)?.name?.[locale] ?? (pendingTx!.appliedCampaigns?.[0] as any)?.name?.en ?? '' } : null; const netAmount = activeDiscount ? activeDiscount.total : originalAmount; // Currency + VAT mirror the backend: preferredCurrency wins, falls back to // the API-returned plan.currency / sub.plan.currency. Lookup-table VAT // (EUR 19%, USD 0%) matches Price.applyTax() in @archer/domain \u2014 so gross // displayed here is byte-identical to what Stripe will actually charge. const breakdown = resolveCheckoutBreakdown(netAmount, resolvedCurrency); const vatAmount = breakdown.vat; const grossAmount = breakdown.gross; const currency = breakdown.currencySymbol; // ── Command purchase mutation ── const purchaseCommandsMutation = useMutation({ mutationFn: ({ size, quantity }: { size: 100 | 1000; quantity: number }) => subscriptionApi.purchaseCommands(size, quantity), onSuccess: (data) => { setCommandClientSecret(data.clientSecret); setCommandTxId(data.transactionId); setShowPayment(true); }, }); // ── Handlers ── const onValidate = (data: CodeForm) => { if (!planId) return; setCodePreview(null); setCodeApplied(false); validateCode.mutate({ code: data.code, planId }, { onSuccess: (res) => setCodePreview(res), }); }; const onApplyAndPay = () => { if (isCommandMode && commandSize) { purchaseCommandsMutation.mutate({ size: commandSize, quantity: commandQuantity }); return; } if (codePreview?.valid && !codeApplied) { const code = (document.getElementById('checkout-code') as HTMLInputElement)?.value; if (code) { applyCode.mutate(code, { onSuccess: () => { setCodeApplied(true); setShowPayment(true); }, onError: () => setShowPayment(true), }); return; } } setShowPayment(true); }; const onPaymentSuccess = () => { queryClient.invalidateQueries({ queryKey: queryKeys.subscription.all }); navigate({ to: '/dashboard/subscription' }); }; // ── Item info ── const itemTitle = isCommandMode ? t('checkout.commandPackage', { count: commandSize ?? 0 }) : (plan?.title?.[locale] ?? plan?.title?.en ?? plan?.slug ?? ''); const itemDescription = isCommandMode ? t('checkout.commandDescription') : (plan?.displayConfig?.description?.[locale] ?? plan?.displayConfig?.description?.en ?? ''); const billingLabel = isCommandMode ? t('checkout.oneTime') : (plan?.billingCycle === 'monthly' ? t('checkout.month') : plan?.billingCycle ?? ''); const ItemIcon = isCommandMode ? Package : Sparkles; // ── Validation ── if (!isCommandMode && !plan) { return (
{t('checkout.noPlan')}
); } if (isCommandMode && !commandPackage) { return (
{t('checkout.noPackage')}
); } const validateError = validateCode.error ? extractError(validateCode.error) : codePreview && !codePreview.valid ? codePreview.error : null; const isPaying = isCommandMode ? purchaseCommandsMutation.isPending : applyCode.isPending; // ── Payment overlay ── const paymentOverlay = showPayment ? ( isCommandMode && commandClientSecret ? setShowPayment(false)} onSuccess={onPaymentSuccess} /> : transactionId ? setShowPayment(false)} onSuccess={onPaymentSuccess} /> : plan ? setShowPayment(false)} onSuccess={onPaymentSuccess} /> : undefined ) : undefined; return ( } gradient={true} overlay={paymentOverlay} > {/* Back navigation */}
{/* Left column: Order details (3/5) */}
{/* Item card */}

{isCommandMode ? t('checkout.package') : t('checkout.plan')}

{itemTitle}

{itemDescription}

{currency}{originalAmount} {billingLabel && /{billingLabel}}
{/* Plan features preview (plan mode only) */} {!isCommandMode && plan?.features && plan.features.length > 0 && (
{plan.features .slice() .sort((a, b) => a.sortOrder - b.sortOrder) .slice(0, 6) .map((f, i) => (
{f.description?.[locale] ?? f.description?.en ?? ''}
))}
)}
{/* Promo code card (plan mode only — command purchases use existing campaign discounts automatically) */} {!isCommandMode && (

{t('checkout.codeLabel')}

{errors.code &&

{errors.code.message}

}
{validateError && (

{t(`checkout.error.${validateError}`, undefined as any) || validateError}

)} {activeDiscount && (

{activeDiscount.label}

{activeDiscount.pct && (

{activeDiscount.pct}% {t('checkout.discount').toLowerCase()}

)}
-{currency}{activeDiscount.amount.toFixed(2)}
)}
)}
{/* Right column: Summary + Pay (2/5) */}

{t('checkout.orderSummary')}

{/* Line items */}
{itemTitle} {currency}{originalAmount.toFixed(2)}
{activeDiscount && (
{activeDiscount.label} -{currency}{activeDiscount.amount.toFixed(2)}
)}
{/* Net + VAT */}
{t('checkout.netAmount')} {currency}{netAmount.toFixed(2)}
{t('checkout.vat')} {currency}{vatAmount.toFixed(2)}
{/* Gross total */}
{t('checkout.total')}
{currency}{grossAmount.toFixed(2)} {billingLabel &&

/{billingLabel}

}
{/* Pay button */} {/* Trust signals */}
{t('checkout.securePayment')}
{t('checkout.stripeProcessed')}
); } function extractError(error: unknown): string { const err = error as { response?: { data?: { error?: { message?: string } } } }; return err?.response?.data?.error?.message ?? 'UNKNOWN_ERROR'; }