import { useEffect, useState } from 'react'; import { X, CheckCircle, Loader2 } from 'lucide-react'; import { Elements } from '@stripe/react-stripe-js'; import { StripePaymentForm } from './StripePaymentForm'; import { stripePromise } from '@/infrastructure/stripe'; import { useSubscribe } from '../hooks/useSubscribe'; import { useConfirmSubscription } from '../hooks/useConfirmSubscription'; import { useCompletePayment } from '../hooks/useCompletePayment'; import type { AvailablePlanResponse } from '@/infrastructure/http/api/subscription'; import { resolveCheckoutBreakdown, resolveCurrency } from '@/features/checkout/lib/checkoutBreakdown'; import { useAuthenticatedUser } from '@/features/auth/hooks/useAuthenticatedUser'; interface PaymentPanelProps { plan?: AvailablePlanResponse; transactionId?: string; /** Pre-created clientSecret — skips API call when provided (e.g. command purchase) */ directClientSecret?: string; /** Override amount display (gross). Used with directClientSecret. */ displayAmount?: number; onClose: () => void; onSuccess: () => void; } export function PaymentPanel({ plan, transactionId, directClientSecret, displayAmount, onClose, onSuccess }: PaymentPanelProps) { const subscribeMutation = useSubscribe(); const confirmMutation = useConfirmSubscription(); const completePaymentMutation = useCompletePayment(); const [isComplete, setIsComplete] = useState(false); const [error, setError] = useState(null); const isTransactionMode = !!transactionId || !!directClientSecret; // Initialize payment on mount (skip if directClientSecret provided) useEffect(() => { if (directClientSecret) return; // PI already created if (transactionId) { completePaymentMutation.mutate(transactionId!, { onError: (err) => setError((err as Error).message || 'Failed to initialize payment'), }); } else if (plan) { subscribeMutation.mutate(plan.id, { onError: (err) => setError((err as Error).message || 'Failed to initialize payment'), }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [transactionId, plan?.id, directClientSecret]); // Auto-close after success useEffect(() => { if (!isComplete) return; const timer = setTimeout(() => { onSuccess(); onClose(); }, 2000); return () => clearTimeout(timer); }, [isComplete, onSuccess, onClose]); const clientSecret = directClientSecret ?? (transactionId ? completePaymentMutation.data?.clientSecret : null) ?? subscribeMutation.data?.clientSecret; const subscriptionId = subscribeMutation.data?.subscriptionId; const handleStripeSuccess = (_piId: string) => { if (isTransactionMode) { // One-time payment — PI succeeded, done setIsComplete(true); } else if (subscriptionId) { // Subscription — confirm with backend confirmMutation.mutate(subscriptionId, { onSuccess: () => setIsComplete(true), onError: (err) => setError((err as Error).message || 'Subscription confirmation failed'), }); } }; const handleStripeError = (msg: string) => { setError(msg); }; // Local net→gross (VAT 19%) mirrors @archer/domain netToGross — keeps summary // consistent from mount until backend resolves with the authoritative gross amount. // Currency-aware net\u2192gross: pick net from plan.prices keyed by preferred // currency (fallback scalar plan.price), apply currency-specific VAT // via shared lookup (EUR 19%, USD 0% \u2014 same as backend Price.applyTax). const authed = useAuthenticatedUser(); const planCurrency = resolveCurrency(authed?.preferredCurrency ?? plan?.currency); const planNetMajor = (() => { if (!plan) return undefined; const cents = plan.prices?.[planCurrency]; if (typeof cents === 'number') return Number((cents / 100).toFixed(2)); return plan.price ?? undefined; })(); const planGross = planNetMajor != null ? resolveCheckoutBreakdown(planNetMajor, planCurrency).gross : undefined; const amount = directClientSecret ? displayAmount : transactionId ? completePaymentMutation.data?.amount : subscribeMutation.data?.amount ?? planGross; const currency = directClientSecret ? planCurrency : transactionId ? completePaymentMutation.data?.currency ?? planCurrency : subscribeMutation.data?.currency ?? planCurrency; const planName = plan?.title?.en || plan?.slug || 'Payment'; const priceSymbol = resolveCheckoutBreakdown(0, currency).currencySymbol; const priceLabel = amount == null ? '' : amount === 0 ? 'Free' : `${priceSymbol}${amount.toFixed(2)}`; const isLoading = directClientSecret ? false : transactionId ? completePaymentMutation.isPending : subscribeMutation.isPending; const isProcessing = confirmMutation.isPending; return ( <> {/* Backdrop */}