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 toast from 'react-hot-toast'; type PaymentStatus = 'idle' | 'processing' | 'ready' | 'error'; declare global { interface Window { Paddle?: { Environment?: { set: (environment: string) => void; }; Initialize?: (options: { token: string }) => void; Checkout: { open: (options: Record) => void; }; }; } } const PADDLE_SCRIPT_URL = 'https://cdn.paddle.com/paddle/v2/paddle.js'; const PADDLE_SUPPORTED_LOCALES = ['en', 'de', 'fr', 'es', 'it', 'nl', 'pt', 'sv', 'da', 'fi', 'no']; const PRIMARY_CTA_STYLES = 'min-w-[200px] disabled:bg-muted disabled:text-muted-foreground disabled:hover:bg-muted disabled:hover:text-muted-foreground'; export function resolvePaddleLocale(rawLocale?: string | null): string { if (!rawLocale) { return 'en'; } const normalized = rawLocale.toLowerCase(); if (PADDLE_SUPPORTED_LOCALES.includes(normalized)) { return normalized; } const short = normalized.split('-')[0]; if (short && PADDLE_SUPPORTED_LOCALES.includes(short)) { return short; } return 'en'; } type PaddleEnvironment = 'sandbox' | 'production'; let paddleLoaderPromise: Promise | null = null; function configurePaddle(paddle: typeof window.Paddle | undefined | null, environment: PaddleEnvironment): typeof window.Paddle | null { if (!paddle) { return null; } try { paddle.Environment?.set?.(environment); } catch (error) { console.warn('[Paddle] Failed to set environment', error); } return paddle; } async function loadPaddle(environment: PaddleEnvironment): Promise { if (typeof window === 'undefined') { return null; } if (window.Paddle) { return configurePaddle(window.Paddle, environment); } if (!paddleLoaderPromise) { paddleLoaderPromise = new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = PADDLE_SCRIPT_URL; script.async = true; script.onload = () => resolve(window.Paddle ?? null); script.onerror = (error) => reject(error); document.head.appendChild(script); }).catch((error) => { console.error('Failed to load Paddle.js', error); paddleLoaderPromise = null; return null; }); } const paddle = await paddleLoaderPromise; return configurePaddle(paddle, environment); } const PaddleCta: 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 { selectedPackage, nextStep, paddleConfig, authUser, setPaymentCompleted } = useCheckoutWizard(); const [status, setStatus] = useState('idle'); const [message, setMessage] = useState(''); const [initialised, setInitialised] = useState(false); const [inlineActive, setInlineActive] = useState(false); 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 paddleRef = useRef(null); const checkoutContainerRef = useRef(null); const eventCallbackRef = useRef<(event: Record) => void>(); const hasAutoAppliedCoupon = useRef(false); const checkoutContainerClass = 'paddle-checkout-container'; const paddleLocale = useMemo(() => { const sourceLocale = i18n.language || (typeof document !== 'undefined' ? document.documentElement.lang : null); return resolvePaddleLocale(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; } 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, }) ); 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')); } finally { setCouponLoading(false); } }, [selectedPackage, t]); 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 () => { setPaymentCompleted(true); nextStep(); }; const startPaddleCheckout = async () => { if (!selectedPackage) { return; } if (!selectedPackage.paddle_price_id) { setStatus('error'); setMessage(t('checkout.payment_step.paddle_not_configured')); return; } setPaymentCompleted(false); setStatus('processing'); setMessage(t('checkout.payment_step.paddle_preparing')); setInlineActive(false); try { const inlineSupported = initialised && !!paddleConfig?.client_token; if (typeof window !== 'undefined') { console.info('[Checkout] Paddle inline status', { inlineSupported, initialised, hasClientToken: Boolean(paddleConfig?.client_token), environment: paddleConfig?.environment, paddlePriceId: selectedPackage.paddle_price_id, }); } if (inlineSupported) { const paddle = paddleRef.current; if (!paddle || !paddle.Checkout || typeof paddle.Checkout.open !== 'function') { throw new Error('Inline Paddle checkout is not available.'); } const inlinePayload: Record = { items: [ { priceId: selectedPackage.paddle_price_id, quantity: 1, }, ], settings: { displayMode: 'inline', frameTarget: checkoutContainerClass, frameInitialHeight: '550', frameStyle: 'width: 100%; min-width: 320px; background-color: transparent; border: none;', theme: 'light', locale: paddleLocale, }, customData: { package_id: String(selectedPackage.id), locale: paddleLocale, }, }; const customerEmail = authUser?.email ?? null; if (customerEmail) { inlinePayload.customer = { email: customerEmail }; } if (typeof window !== 'undefined') { console.info('[Checkout] Opening inline Paddle checkout', inlinePayload); } paddle.Checkout.open(inlinePayload); setInlineActive(true); setStatus('ready'); setMessage(t('checkout.payment_step.paddle_overlay_ready')); if (typeof window !== 'undefined' && checkoutContainerRef.current) { window.requestAnimationFrame(() => { const rect = checkoutContainerRef.current?.getBoundingClientRect(); if (!rect) { return; } const offset = 120; const target = Math.max(window.scrollY + rect.top - offset, 0); window.scrollTo({ top: target, behavior: 'smooth' }); }); } return; } const response = await fetch('/paddle/create-checkout', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '', }, body: JSON.stringify({ package_id: selectedPackage.id, locale: paddleLocale, coupon_code: couponPreview?.coupon.code ?? undefined, }), }); const rawBody = await response.text(); if (typeof window !== 'undefined') { console.info('[Checkout] Hosted 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 Paddle checkout payload as JSON', parseError); data = 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 Paddle checkout.'; throw new Error(message); } window.open(checkoutUrl, '_blank', 'noopener'); setInlineActive(false); setStatus('ready'); setMessage(t('checkout.payment_step.paddle_ready')); } catch (error) { console.error('Failed to start Paddle checkout', error); setStatus('error'); setMessage(t('checkout.payment_step.paddle_error')); setInlineActive(false); setPaymentCompleted(false); } }; useEffect(() => { let cancelled = false; const environment = paddleConfig?.environment === 'sandbox' ? 'sandbox' : 'production'; const clientToken = paddleConfig?.client_token ?? null; eventCallbackRef.current = (event) => { if (!event?.name) { return; } if (typeof window !== 'undefined') { console.debug('[Checkout] Paddle event', event); } if (event.name === 'checkout.completed') { setStatus('ready'); setMessage(t('checkout.payment_step.paddle_overlay_ready')); setInlineActive(false); setPaymentCompleted(true); toast.success(t('checkout.payment_step.toast_success')); } if (event.name === 'checkout.closed') { setStatus('idle'); setMessage(''); setInlineActive(false); setPaymentCompleted(false); } if (event.name === 'checkout.error') { setStatus('error'); setMessage(t('checkout.payment_step.paddle_error')); setInlineActive(false); setPaymentCompleted(false); } }; (async () => { const paddle = await loadPaddle(environment); if (cancelled || !paddle) { return; } try { let inlineReady = false; if (typeof paddle.Initialize === 'function' && clientToken) { if (typeof window !== 'undefined') { console.info('[Checkout] Initializing Paddle.js', { environment, hasToken: Boolean(clientToken) }); } paddle.Initialize({ token: clientToken, checkout: { settings: { displayMode: 'inline', frameTarget: checkoutContainerClass, frameInitialHeight: '550', frameStyle: 'width: 100%; min-width: 320px; background-color: transparent; border: none;', locale: paddleLocale, }, }, eventCallback: (event: Record) => eventCallbackRef.current?.(event), }); inlineReady = true; } paddleRef.current = paddle; setInitialised(inlineReady); } catch (error) { console.error('Failed to initialize Paddle', error); setInitialised(false); setStatus('error'); setMessage(t('checkout.payment_step.paddle_error')); setPaymentCompleted(false); } })(); return () => { cancelled = true; }; }, [paddleConfig?.environment, paddleConfig?.client_token, paddleLocale, setPaymentCompleted, t]); useEffect(() => { setPaymentCompleted(false); }, [selectedPackage?.id, setPaymentCompleted]); const handleCouponSubmit = useCallback((event: FormEvent) => { event.preventDefault(); if (!selectedPackage) { return; } applyCoupon(couponCode); }, [applyCoupon, couponCode, selectedPackage]); const handleRemoveCoupon = useCallback(() => { setCouponPreview(null); setCouponNotice(null); setCouponError(null); setCouponCode(''); if (typeof window !== 'undefined') { localStorage.removeItem('preferred_coupon_code'); } }, []); 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')}
); } const TrustPill = ({ icon: Icon, label }: { icon: React.ElementType; label: string }) => (
{label}
); const PaddleLogo = () => (
Paddle {t('checkout.payment_step.paddle_partner')}
); return (
{!inlineActive && (

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

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

{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}
)}
{!inlineActive && (

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

); };