import React, { FormEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Button } from '@/components/ui/button'; import { LoaderCircle, CheckCircle2, XCircle, ShieldCheck, Receipt, Headphones } from 'lucide-react'; import { useCheckoutWizard } from '../WizardContext'; import { Input } from '@/components/ui/input'; import { Separator } from '@/components/ui/separator'; import { previewCoupon as requestCouponPreview } from '@/lib/coupons'; import type { CouponPreviewResponse } from '@/types/coupon'; import { cn } from '@/lib/utils'; import { useRateLimitHelper } from '@/hooks/useRateLimitHelper'; import toast from 'react-hot-toast'; import { Checkbox } from '@/components/ui/checkbox'; import { Label } from '@/components/ui/label'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { ScrollArea } from '@/components/ui/scroll-area'; import { useAnalytics } from '@/hooks/useAnalytics'; type PaymentStatus = 'idle' | 'processing' | 'ready' | 'error'; type LemonSqueezyEvent = { event?: string; data?: Record; }; declare global { interface Window { createLemonSqueezy?: () => void; LemonSqueezy?: { Setup: (options: { eventHandler?: (event: LemonSqueezyEvent) => void }) => void; Refresh?: () => void; Url: { Open: (url: string) => void; Close?: () => void; }; }; } } const LEMON_SCRIPT_URL = 'https://app.lemonsqueezy.com/js/lemon.js'; const PRIMARY_CTA_STYLES = 'min-w-[200px] disabled:bg-muted disabled:text-muted-foreground disabled:hover:bg-muted disabled:hover:text-muted-foreground'; const getCookieValue = (name: string): string | null => { if (typeof document === 'undefined') { return null; } const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`)); return match ? decodeURIComponent(match[1]) : null; }; export function resolveCheckoutCsrfToken(): string { if (typeof document === 'undefined') { return ''; } const cookieToken = getCookieValue('XSRF-TOKEN'); if (cookieToken) { return cookieToken; } const metaToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'); if (metaToken && metaToken.length > 0) { return metaToken; } return ''; } async function refreshCheckoutCsrfToken(): Promise { if (typeof window === 'undefined') { return; } try { await fetch('/sanctum/csrf-cookie', { method: 'GET', credentials: 'same-origin', }); } catch (error) { console.warn('[Checkout] Failed to refresh CSRF cookie', error); } } function buildCheckoutHeaders(): HeadersInit { const headers: HeadersInit = { 'Content-Type': 'application/json', Accept: 'application/json', }; const cookieToken = getCookieValue('XSRF-TOKEN'); if (cookieToken) { headers['X-XSRF-TOKEN'] = cookieToken; return headers; } const metaToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'); if (metaToken && metaToken.length > 0) { headers['X-CSRF-TOKEN'] = metaToken; } return headers; } export function resolveCheckoutLocale(rawLocale?: string | null): string { if (!rawLocale) { return 'en'; } const normalized = rawLocale.toLowerCase(); const short = normalized.split('-')[0]; return short || 'en'; } let lemonLoaderPromise: Promise | null = null; async function loadLemonSqueezy(): Promise { if (typeof window === 'undefined') { return null; } if (window.LemonSqueezy) { return window.LemonSqueezy; } if (!lemonLoaderPromise) { lemonLoaderPromise = new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = LEMON_SCRIPT_URL; script.defer = true; script.onload = () => { window.createLemonSqueezy?.(); resolve(window.LemonSqueezy ?? null); }; script.onerror = (error) => reject(error); document.head.appendChild(script); }).catch((error) => { console.error('Failed to load Lemon.js', error); lemonLoaderPromise = null; return null; }); } return lemonLoaderPromise; } const LemonSqueezyCta: React.FC<{ onCheckout: () => Promise; disabled: boolean; isProcessing: boolean; className?: string }> = ({ onCheckout, disabled, isProcessing, className }) => { const { t } = useTranslation('marketing'); return ( ); }; export const PaymentStep: React.FC = () => { const { t, i18n } = useTranslation('marketing'); const { trackEvent } = useAnalytics(); const { selectedPackage, nextStep, authUser, isAuthenticated, goToStep, setPaymentCompleted, checkoutSessionId, setCheckoutSessionId, } = useCheckoutWizard(); const [status, setStatus] = useState('idle'); const [message, setMessage] = useState(''); const [inlineActive, setInlineActive] = useState(false); const [acceptedTerms, setAcceptedTerms] = useState(false); const [consentError, setConsentError] = useState(null); const [couponCode, setCouponCode] = useState(() => { if (typeof window === 'undefined') { return ''; } const params = new URLSearchParams(window.location.search); const fromQuery = params.get('coupon'); if (fromQuery) { return fromQuery; } return localStorage.getItem('preferred_coupon_code') ?? ''; }); const [couponPreview, setCouponPreview] = useState(null); const [couponError, setCouponError] = useState(null); const [couponNotice, setCouponNotice] = useState(null); const [couponLoading, setCouponLoading] = useState(false); const lemonRef = useRef(null); const eventHandlerRef = useRef<(event: LemonSqueezyEvent) => void>(); const lastCheckoutIdRef = useRef(null); const hasAutoAppliedCoupon = useRef(false); const [showWithdrawalModal, setShowWithdrawalModal] = useState(false); const [withdrawalHtml, setWithdrawalHtml] = useState(null); const [withdrawalTitle, setWithdrawalTitle] = useState(null); const [withdrawalLoading, setWithdrawalLoading] = useState(false); const [withdrawalError, setWithdrawalError] = useState(null); const RateLimitHelper = useRateLimitHelper('coupon'); const [voucherExpiry, setVoucherExpiry] = useState(null); const [isGiftVoucher, setIsGiftVoucher] = useState(false); const [freeActivationBusy, setFreeActivationBusy] = useState(false); const [pendingConfirmation, setPendingConfirmation] = useState<{ orderId: string | null; checkoutId: string | null; } | null>(null); const checkoutLocale = useMemo(() => { const sourceLocale = i18n.language || (typeof document !== 'undefined' ? document.documentElement.lang : null); return resolveCheckoutLocale(sourceLocale ?? undefined); }, [i18n.language]); const isFree = useMemo(() => (selectedPackage ? Number(selectedPackage.price) <= 0 : false), [selectedPackage]); const confirmCheckoutSession = useCallback(async (payload: { orderId: string | null; checkoutId: string | null }) => { if (!checkoutSessionId) { return; } if (!payload.orderId && !payload.checkoutId) { return; } try { await refreshCheckoutCsrfToken(); await fetch(`/checkout/session/${checkoutSessionId}/confirm`, { method: 'POST', headers: buildCheckoutHeaders(), credentials: 'same-origin', body: JSON.stringify({ order_id: payload.orderId, checkout_id: payload.checkoutId, }), }); } catch (error) { console.warn('Failed to confirm Lemon Squeezy session', error); } }, [checkoutSessionId]); const applyCoupon = useCallback(async (code: string) => { if (!selectedPackage) { return; } const trimmed = code.trim(); if (!trimmed) { setCouponError(t('coupon.errors.required')); setCouponPreview(null); setCouponNotice(null); return; } if (RateLimitHelper.isLimited(trimmed)) { setCouponError(t('coupon.errors.too_many_attempts')); trackEvent({ category: 'marketing_coupon', action: 'rate_limited', }); return; } setCouponLoading(true); setCouponError(null); setCouponNotice(null); try { const preview = await requestCouponPreview(selectedPackage.id, trimmed); setCouponPreview(preview); setCouponNotice( t('coupon.applied', { code: preview.coupon.code, amount: preview.pricing.formatted.discount, }) ); trackEvent({ category: 'marketing_coupon', action: 'applied', name: preview.coupon.code, }); setVoucherExpiry(preview.coupon.expires_at ?? null); setIsGiftVoucher(preview.coupon.code?.toUpperCase().startsWith('GIFT-') ?? false); if (typeof window !== 'undefined') { localStorage.setItem('preferred_coupon_code', preview.coupon.code); } } catch (error) { setCouponPreview(null); setCouponNotice(null); setCouponError(error instanceof Error ? error.message : t('coupon.errors.generic')); trackEvent({ category: 'marketing_coupon', action: 'apply_failed', }); RateLimitHelper.bump(trimmed); } finally { setCouponLoading(false); } }, [RateLimitHelper, selectedPackage, t, trackEvent]); useEffect(() => { if (hasAutoAppliedCoupon.current) { return; } if (couponCode && selectedPackage) { hasAutoAppliedCoupon.current = true; applyCoupon(couponCode); } }, [applyCoupon, couponCode, selectedPackage]); useEffect(() => { setCouponPreview(null); setCouponNotice(null); setCouponError(null); hasAutoAppliedCoupon.current = false; }, [selectedPackage?.id]); useEffect(() => { if (typeof window === 'undefined') { return; } const params = new URLSearchParams(window.location.search); const queryCoupon = params.get('coupon'); if (queryCoupon) { const normalized = queryCoupon.toUpperCase(); setCouponCode((current) => current || normalized); localStorage.setItem('preferred_coupon_code', normalized); } }, []); useEffect(() => { if (typeof window !== 'undefined' && couponCode) { localStorage.setItem('preferred_coupon_code', couponCode); } }, [couponCode]); const handleFreeActivation = async () => { if (!isAuthenticated || !authUser) { const message = t('checkout.payment_step.auth_required'); toast.error(message); goToStep('auth'); return; } if (!selectedPackage) { return; } if (!acceptedTerms) { setConsentError(t('checkout.legal.checkbox_terms_error')); return; } setConsentError(null); setFreeActivationBusy(true); try { await refreshCheckoutCsrfToken(); const response = await fetch('/checkout/free-activate', { method: 'POST', headers: buildCheckoutHeaders(), credentials: 'same-origin', body: JSON.stringify({ package_id: selectedPackage.id, accepted_terms: acceptedTerms, locale: checkoutLocale, }), }); const payload = await response.json().catch(() => ({})); if (!response.ok) { const errorMessage = payload?.errors?.accepted_waiver?.[0] || payload?.message || t('checkout.payment_step.lemonsqueezy_error'); setConsentError(errorMessage); toast.error(errorMessage); return; } setCheckoutSessionId(payload?.checkout_session_id ?? null); setPaymentCompleted(true); nextStep(); } catch (error) { console.error('Failed to activate free package', error); const fallbackMessage = t('checkout.payment_step.lemonsqueezy_error'); setConsentError(fallbackMessage); toast.error(fallbackMessage); } finally { setFreeActivationBusy(false); } }; const startLemonSqueezyCheckout = async () => { if (!selectedPackage) { return; } if (!isAuthenticated || !authUser) { const message = t('checkout.payment_step.auth_required'); setStatus('error'); setMessage(message); toast.error(message); goToStep('auth'); return; } if (!acceptedTerms) { setConsentError(t('checkout.legal.checkbox_terms_error')); return; } if (!selectedPackage.lemonsqueezy_variant_id) { setStatus('error'); setMessage(t('checkout.payment_step.lemonsqueezy_not_configured')); return; } setPaymentCompleted(false); setStatus('processing'); setMessage(t('checkout.payment_step.lemonsqueezy_preparing')); setInlineActive(false); setCheckoutSessionId(null); try { await refreshCheckoutCsrfToken(); const response = await fetch('/lemonsqueezy/create-checkout', { method: 'POST', headers: buildCheckoutHeaders(), credentials: 'same-origin', body: JSON.stringify({ package_id: selectedPackage.id, locale: checkoutLocale, coupon_code: couponPreview?.coupon.code ?? undefined, accepted_terms: acceptedTerms, }), }); const rawBody = await response.text(); if ( response.status === 401 || response.status === 419 || (response.redirected && response.url.includes('/login')) ) { const message = t('checkout.payment_step.auth_required'); setStatus('error'); setMessage(message); toast.error(message); goToStep('auth'); return; } if (typeof window !== 'undefined') { console.info('[Checkout] Lemon Squeezy checkout response', { status: response.status, rawBody }); } let data: { checkout_url?: string; message?: string } | null = null; try { data = rawBody && rawBody.trim().startsWith('{') ? JSON.parse(rawBody) : null; } catch (parseError) { console.warn('Failed to parse Lemon Squeezy checkout payload as JSON', parseError); data = null; } if (data && typeof (data as { checkout_session_id?: string }).checkout_session_id === 'string') { setCheckoutSessionId((data as { checkout_session_id?: string }).checkout_session_id ?? null); } let checkoutUrl: string | null = typeof data?.checkout_url === 'string' ? data.checkout_url : null; if (!checkoutUrl) { const trimmed = rawBody.trim(); if (/^https?:\/\//i.test(trimmed)) { checkoutUrl = trimmed; } else if (trimmed.startsWith('<')) { const match = trimmed.match(/https?:\/\/["'a-zA-Z0-9._~:/?#@!$&'()*+,;=%-]+/); if (match) { checkoutUrl = match[0]; } } } if (!response.ok || !checkoutUrl) { const message = data?.message || rawBody || 'Unable to create Lemon Squeezy checkout.'; throw new Error(message); } if (data && typeof (data as { id?: string }).id === 'string') { lastCheckoutIdRef.current = (data as { id?: string }).id ?? null; } const lemon = await loadLemonSqueezy(); if (lemon?.Url?.Open) { lemon.Url.Open(checkoutUrl); setInlineActive(true); setStatus('ready'); setMessage(t('checkout.payment_step.lemonsqueezy_overlay_ready')); return; } window.open(checkoutUrl, '_blank', 'noopener'); setInlineActive(false); setStatus('ready'); setMessage(t('checkout.payment_step.lemonsqueezy_ready')); } catch (error) { console.error('Failed to start Lemon Squeezy checkout', error); setStatus('error'); setMessage(t('checkout.payment_step.lemonsqueezy_error')); setInlineActive(false); setPaymentCompleted(false); } }; useEffect(() => { let cancelled = false; (async () => { const lemon = await loadLemonSqueezy(); if (cancelled || !lemon) { return; } try { eventHandlerRef.current = (event) => { if (!event?.event) { return; } if (typeof window !== 'undefined') { console.debug('[Checkout] Lemon Squeezy event', event); } if (event.event === 'Checkout.Success') { const data = event.data as { id?: string; identifier?: string; attributes?: { checkout_id?: string } } | undefined; const orderId = typeof data?.id === 'string' ? data.id : (typeof data?.identifier === 'string' ? data.identifier : null); const checkoutId = typeof data?.attributes?.checkout_id === 'string' ? data?.attributes?.checkout_id : lastCheckoutIdRef.current; setStatus('processing'); setMessage(t('checkout.payment_step.processing_confirmation')); setInlineActive(false); setPaymentCompleted(false); setPendingConfirmation({ orderId, checkoutId }); toast.success(t('checkout.payment_step.toast_success')); setPaymentCompleted(true); nextStep(); } }; lemon.Setup({ eventHandler: (event) => eventHandlerRef.current?.(event), }); lemonRef.current = lemon; } catch (error) { console.error('Failed to initialize Lemon.js', error); setStatus('error'); setMessage(t('checkout.payment_step.lemonsqueezy_error')); setPaymentCompleted(false); } })(); return () => { cancelled = true; }; }, [nextStep, setPaymentCompleted, t]); useEffect(() => { setPaymentCompleted(false); setCheckoutSessionId(null); setStatus('idle'); setMessage(''); setInlineActive(false); setPendingConfirmation(null); }, [selectedPackage?.id, setPaymentCompleted]); useEffect(() => { if (!pendingConfirmation || !checkoutSessionId) { return; } void confirmCheckoutSession(pendingConfirmation); setPendingConfirmation(null); }, [checkoutSessionId, confirmCheckoutSession, pendingConfirmation]); const handleCouponSubmit = useCallback((event: FormEvent) => { event.preventDefault(); if (!selectedPackage) { return; } applyCoupon(couponCode); }, [applyCoupon, couponCode, selectedPackage]); const handleRemoveCoupon = useCallback(() => { if (couponPreview?.coupon.code) { trackEvent({ category: 'marketing_coupon', action: 'removed', name: couponPreview.coupon.code, }); } setCouponPreview(null); setCouponNotice(null); setCouponError(null); setCouponCode(''); if (typeof window !== 'undefined') { localStorage.removeItem('preferred_coupon_code'); } }, [couponPreview, trackEvent]); const openWithdrawalModal = useCallback(async () => { setShowWithdrawalModal(true); if (withdrawalHtml || withdrawalLoading) { return; } setWithdrawalLoading(true); setWithdrawalError(null); try { const response = await fetch(`/api/v1/legal/widerrufsbelehrung?lang=${checkoutLocale}`); if (!response.ok) { throw new Error(`Failed to load withdrawal page (${response.status})`); } const data = await response.json(); setWithdrawalHtml(data.body_html || ''); setWithdrawalTitle(data.title || t('checkout.legal.link_cancellation')); } catch (error) { setWithdrawalError(t('checkout.legal.modal_error')); } finally { setWithdrawalLoading(false); } }, [checkoutLocale, t, withdrawalHtml, withdrawalLoading]); if (!selectedPackage) { return ( {t('checkout.payment_step.no_package_title')} {t('checkout.payment_step.no_package_description')} ); } if (isFree) { return (
{t('checkout.payment_step.free_package_title')} {t('checkout.payment_step.free_package_desc')}
{ setAcceptedTerms(Boolean(checked)); if (consentError) { setConsentError(null); } }} />

{t('checkout.legal.legal_links_intro')}{' '}

{consentError && (
{consentError}
)}
); } const TrustPill = ({ icon: Icon, label }: { icon: React.ElementType; label: string }) => (
{label}
); const LemonSqueezyLogo = () => (
Lemon Squeezy {t('checkout.payment_step.lemonsqueezy_partner')}
); return (
{!inlineActive && (

{t('checkout.payment_step.guided_title')}

{t('checkout.payment_step.guided_body')}

{ setAcceptedTerms(Boolean(checked)); if (consentError) { setConsentError(null); } }} className="border-white/60 data-[state=checked]:bg-white data-[state=checked]:text-[#001835]" />

{t('checkout.legal.legal_links_intro')}{' '}

{consentError && (
{consentError}
)}

{t('checkout.payment_step.guided_cta_hint')}

)}
setCouponCode(event.target.value.toUpperCase())} placeholder={t('coupon.placeholder')} className="flex-1" />
{couponPreview && ( )}
{couponError && (
{couponError}
)} {couponNotice && (
{couponNotice}
)} {couponPreview && (

{t('coupon.summary_title')}

{t('coupon.fields.subtotal')} {couponPreview.pricing.formatted.subtotal}
{t('coupon.fields.discount')} {couponPreview.pricing.formatted.discount}
{t('coupon.fields.tax')} {couponPreview.pricing.formatted.tax}
{t('coupon.fields.total')} {couponPreview.pricing.formatted.total}
{voucherExpiry && (
{t('coupon.fields.expires')} {new Date(voucherExpiry).toLocaleDateString(i18n.language)}
)}
)} {isGiftVoucher && (
{t('coupon.legal_note')}{' '} {t('coupon.legal_link')}
)}
{!inlineActive && (

{t('checkout.payment_step.lemonsqueezy_intro')}

)} {status !== 'idle' && ( {status === 'processing' ? t('checkout.payment_step.status_processing_title') : status === 'ready' ? t('checkout.payment_step.status_ready_title') : status === 'error' ? t('checkout.payment_step.status_error_title') : t('checkout.payment_step.status_info_title')} {message} {status === 'processing' && } )}

{t('checkout.payment_step.lemonsqueezy_disclaimer')}

{withdrawalTitle || t('checkout.legal.link_cancellation')} {t('checkout.legal.modal_description')}
{withdrawalLoading && (
{t('checkout.legal.modal_loading')}
)} {withdrawalError && (
{withdrawalError}
)} {!withdrawalLoading && !withdrawalError && withdrawalHtml && (
)}
); };