import { useState, useCallback, useRef, useEffect } from 'react'; import { useParams } from 'react-router'; import { useQuery } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { motion } from 'framer-motion'; import { QRCodeSVG } from 'qrcode.react'; import { landingApi } from '../api/landings'; import { copyToClipboard } from '../utils/clipboard'; import { cn } from '../lib/utils'; const MAX_POLL_MS = 10 * 60 * 1000; // 10 minutes // ============================================================ // Sub-components // ============================================================ function Spinner({ className }: { className?: string }) { return (
); } function PendingState() { const { t } = useTranslation(); return (

{t('landing.awaitingPayment', 'Awaiting payment')}

{t('landing.awaitingPaymentDesc')}

); } function SuccessState({ subscriptionUrl, cryptoLink, contactValue, tariffName, periodDays, isGift, }: { subscriptionUrl: string | null; cryptoLink: string | null; contactValue: string | null; tariffName: string | null; periodDays: number | null; isGift: boolean; }) { const { t } = useTranslation(); const [copied, setCopied] = useState(false); const copyTimeoutRef = useRef>(undefined); useEffect(() => { return () => { if (copyTimeoutRef.current) clearTimeout(copyTimeoutRef.current); }; }, []); const handleCopy = useCallback(async () => { const url = subscriptionUrl ?? cryptoLink; if (!url) return; try { await copyToClipboard(url); setCopied(true); if (copyTimeoutRef.current) clearTimeout(copyTimeoutRef.current); copyTimeoutRef.current = setTimeout(() => setCopied(false), 2000); } catch { // Clipboard write failed silently } }, [subscriptionUrl, cryptoLink]); const displayUrl = subscriptionUrl ?? cryptoLink; return ( {/* Animated checkmark */} {/* Title */}

{t('landing.purchaseSuccess')}

{tariffName && periodDays && (

{tariffName} — {periodDays} {t('landing.daysAccess')}

)} {contactValue && (

{isGift ? t('landing.giftSentTo', { contact: contactValue }) : t('landing.keySentTo', { contact: contactValue })}

)}
{/* QR Code */} {displayUrl && (
{/* Copy button */}
)}
); } function FailedState() { const { t } = useTranslation(); return (

{t('landing.purchaseFailed')}

{t('landing.purchaseFailedDesc')}

); } function PollTimedOutState({ onRetry }: { onRetry: () => void }) { const { t } = useTranslation(); return (

{t('landing.pollTimedOut', 'Taking longer than expected')}

{t( 'landing.pollTimedOutDesc', 'Payment processing is taking longer than usual. You can try checking again.', )}

); } // ============================================================ // Main Component // ============================================================ export default function PurchaseSuccess() { const { token } = useParams<{ token: string }>(); const pollStart = useRef(Date.now()); const [pollTimedOut, setPollTimedOut] = useState(false); // Referrer-Policy: prevent leaking payment token via referer header useEffect(() => { const meta = document.createElement('meta'); meta.name = 'referrer'; meta.content = 'no-referrer'; document.head.appendChild(meta); return () => { document.head.removeChild(meta); }; }, []); const { data: status, isError, refetch, } = useQuery({ queryKey: ['purchase-status', token], queryFn: () => landingApi.getPurchaseStatus(token!), enabled: !!token && !pollTimedOut, refetchInterval: (query) => { const currentStatus = query.state.data?.status; if (currentStatus === 'pending' || currentStatus === 'paid') { if (Date.now() - pollStart.current > MAX_POLL_MS) { setPollTimedOut(true); return false; } return 3_000; } return false; }, retry: 2, }); const handleRetryPoll = useCallback(() => { pollStart.current = Date.now(); setPollTimedOut(false); refetch(); }, [refetch]); const isSuccess = status?.status === 'delivered'; const isFailed = status?.status === 'failed' || status?.status === 'expired'; return (
{isError ? ( ) : isSuccess ? ( ) : isFailed ? ( ) : pollTimedOut ? ( ) : ( )}
); }