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'; declare global { interface Window { paypal?: { Buttons: (options: Record) => { render: (selector: HTMLElement | string) => Promise; close?: () => void; }; }; } } const PRIMARY_CTA_STYLES = 'min-w-[200px] disabled:bg-muted disabled:text-muted-foreground disabled:hover:bg-muted disabled:hover:text-muted-foreground'; const PAYPAL_SDK_BASE = 'https://www.paypal.com/sdk/js'; 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'; } const PAYPAL_LOCALE_FALLBACKS: Record = { de: 'DE', en: 'US', }; function resolvePayPalLocale(rawLocale?: string | null): string | null { if (!rawLocale) { return null; } const trimmed = rawLocale.trim(); if (trimmed.length === 0) { return null; } const normalized = trimmed.replace('_', '-'); const parts = normalized.split('-').filter(Boolean); if (parts.length >= 2) { return `${parts[0].toLowerCase()}_${parts[1].toUpperCase()}`; } const language = parts[0].toLowerCase(); const region = PAYPAL_LOCALE_FALLBACKS[language]; return region ? `${language}_${region}` : null; } type PayPalSdkOptions = { clientId: string; currency: string; intent: string; locale?: string | null; }; let paypalLoaderPromise: Promise | null = null; let paypalLoaderKey: string | null = null; async function loadPayPalSdk(options: PayPalSdkOptions): Promise { if (typeof window === 'undefined') { return null; } if (window.paypal) { return window.paypal; } const params = new URLSearchParams({ 'client-id': options.clientId, currency: options.currency, intent: options.intent, components: 'buttons', }); const paypalLocale = resolvePayPalLocale(options.locale); if (paypalLocale) { params.set('locale', paypalLocale); } const src = `${PAYPAL_SDK_BASE}?${params.toString()}`; if (paypalLoaderPromise && paypalLoaderKey === src) { return paypalLoaderPromise; } paypalLoaderKey = src; paypalLoaderPromise = new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = src; script.async = true; script.onload = () => resolve(window.paypal ?? null); script.onerror = (error) => reject(error); document.head.appendChild(script); }).catch((error) => { console.error('Failed to load PayPal SDK', error); paypalLoaderPromise = null; return null; }); return paypalLoaderPromise; } export const PaymentStep: React.FC = () => { const { t, i18n } = useTranslation('marketing'); const { trackEvent } = useAnalytics(); const { selectedPackage, nextStep, authUser, isAuthenticated, goToStep, setPaymentCompleted, checkoutSessionId, setCheckoutSessionId, checkoutActionUrl, setCheckoutActionUrl, clearCheckoutActionUrl, paypalConfig, } = useCheckoutWizard(); const [status, setStatus] = useState('idle'); const [message, setMessage] = useState(''); 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 [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 paypalContainerRef = useRef(null); const paypalButtonsRef = useRef<{ close?: () => void } | null>(null); const paypalActionsRef = useRef<{ enable: () => void; disable: () => void } | null>(null); const checkoutSessionRef = useRef(checkoutSessionId); const acceptedTermsRef = useRef(acceptedTerms); const couponCodeRef = useRef(null); useEffect(() => { checkoutSessionRef.current = checkoutSessionId; }, [checkoutSessionId]); useEffect(() => { acceptedTermsRef.current = acceptedTerms; }, [acceptedTerms]); useEffect(() => { couponCodeRef.current = couponPreview?.coupon.code ?? null; }, [couponPreview]); 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 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(() => { setCouponPreview(null); setCouponNotice(null); setCouponError(null); }, [selectedPackage?.id]); useEffect(() => { if (selectedPackage) { clearCheckoutActionUrl(); } }, [clearCheckoutActionUrl, 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); clearCheckoutActionUrl(); 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.paypal_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.paypal_error'); setConsentError(fallbackMessage); toast.error(fallbackMessage); } finally { setFreeActivationBusy(false); } }; const handlePayPalCapture = useCallback(async (orderId: string) => { const sessionId = checkoutSessionRef.current; if (!sessionId) { throw new Error('Missing checkout session'); } await refreshCheckoutCsrfToken(); const response = await fetch('/paypal/capture-order', { method: 'POST', headers: buildCheckoutHeaders(), credentials: 'same-origin', body: JSON.stringify({ checkout_session_id: sessionId, order_id: orderId, }), }); const payload = await response.json().catch(() => ({})); if (!response.ok) { throw new Error(payload?.message || t('checkout.payment_step.paypal_error')); } return payload; }, [t]); useEffect(() => { if (!selectedPackage || isFree) { return; } const clientId = paypalConfig?.client_id ?? null; if (!clientId) { setStatus('error'); setMessage(t('checkout.payment_step.paypal_not_configured')); return; } let cancelled = false; const initButtons = async () => { const paypal = await loadPayPalSdk({ clientId, currency: paypalConfig?.currency ?? 'EUR', intent: paypalConfig?.intent ?? 'capture', locale: paypalConfig?.locale ?? checkoutLocale, }); if (cancelled || !paypal || !paypalContainerRef.current) { return; } if (paypalButtonsRef.current?.close) { paypalButtonsRef.current.close(); } paypalContainerRef.current.innerHTML = ''; paypalButtonsRef.current = paypal.Buttons({ onInit: (_data: unknown, actions: { enable: () => void; disable: () => void }) => { paypalActionsRef.current = actions; if (!acceptedTermsRef.current) { actions.disable(); } }, createOrder: async () => { if (!selectedPackage) { throw new Error('Missing package'); } if (!isAuthenticated || !authUser) { const authMessage = t('checkout.payment_step.auth_required'); setStatus('error'); setMessage(authMessage); toast.error(authMessage); goToStep('auth'); throw new Error(authMessage); } if (!acceptedTermsRef.current) { const consentMessage = t('checkout.legal.checkbox_terms_error'); setConsentError(consentMessage); throw new Error(consentMessage); } setConsentError(null); setStatus('processing'); setMessage(t('checkout.payment_step.paypal_preparing')); setPaymentCompleted(false); setCheckoutSessionId(null); setCheckoutActionUrl(null); await refreshCheckoutCsrfToken(); const response = await fetch('/paypal/create-order', { method: 'POST', headers: buildCheckoutHeaders(), credentials: 'same-origin', body: JSON.stringify({ package_id: selectedPackage.id, locale: checkoutLocale, coupon_code: couponCodeRef.current ?? undefined, accepted_terms: acceptedTermsRef.current, }), }); const payload = await response.json().catch(() => ({})); if (!response.ok) { const errorMessage = payload?.message || t('checkout.payment_step.paypal_error'); setStatus('error'); setMessage(errorMessage); throw new Error(errorMessage); } if (payload?.checkout_session_id) { setCheckoutSessionId(payload.checkout_session_id); checkoutSessionRef.current = payload.checkout_session_id; } const approveUrl = typeof payload?.approve_url === 'string' ? payload.approve_url : null; if (approveUrl) { setCheckoutActionUrl(approveUrl); } const orderId = payload?.order_id; if (!orderId) { throw new Error('PayPal order ID missing.'); } setStatus('ready'); setMessage(t('checkout.payment_step.paypal_ready')); return orderId; }, onApprove: async (data: { orderID?: string }) => { if (!data?.orderID) { throw new Error('Missing PayPal order ID.'); } setStatus('processing'); setMessage(t('checkout.payment_step.processing_confirmation')); try { const payload = await handlePayPalCapture(data.orderID); if (payload?.status === 'completed') { setPaymentCompleted(true); toast.success(t('checkout.payment_step.toast_success')); nextStep(); return; } if (payload?.status === 'requires_customer_action') { nextStep(); return; } setStatus('error'); setMessage(t('checkout.payment_step.paypal_error')); } catch (error) { console.error('Failed to capture PayPal order', error); setStatus('error'); setMessage(t('checkout.payment_step.paypal_error')); } }, onCancel: () => { setStatus('idle'); setMessage(t('checkout.payment_step.paypal_cancelled')); }, onError: (error: unknown) => { console.error('PayPal button error', error); setStatus('error'); setMessage(t('checkout.payment_step.paypal_error')); }, }) as { render: (selector: HTMLElement | string) => Promise; close?: () => void }; await paypalButtonsRef.current.render(paypalContainerRef.current); }; void initButtons(); return () => { cancelled = true; if (paypalButtonsRef.current?.close) { paypalButtonsRef.current.close(); } }; }, [authUser, checkoutLocale, goToStep, isAuthenticated, isFree, paypalConfig?.client_id, paypalConfig?.currency, paypalConfig?.intent, paypalConfig?.locale, selectedPackage, setCheckoutActionUrl, setCheckoutSessionId, setPaymentCompleted, t, handlePayPalCapture, nextStep]); useEffect(() => { if (paypalActionsRef.current) { if (acceptedTerms) { paypalActionsRef.current.enable(); } else { paypalActionsRef.current.disable(); } } }, [acceptedTerms]); 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 handleOpenPayPal = useCallback(() => { if (checkoutActionUrl && typeof window !== 'undefined') { window.open(checkoutActionUrl, '_blank', 'noopener'); } }, [checkoutActionUrl]); 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 PayPalBadge = () => (
PayPal {t('checkout.payment_step.paypal_partner')}
); return (

{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-[#003087]" />

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

{consentError && (
{consentError}
)}

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

{checkoutActionUrl && (

{t('checkout.payment_step.resume_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')}
)}
{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.paypal_disclaimer')}

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