import { useState, useCallback, useRef, useEffect } from 'react'; import { useParams, useNavigate, useSearchParams } from 'react-router'; import { useQuery, useQueryClient } 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 { authApi } from '../api/auth'; import { useAuthStore } from '../store/auth'; import { copyToClipboard } from '../utils/clipboard'; import { Spinner } from '@/components/ui/Spinner'; import { AnimatedCheckmark } from '@/components/ui/AnimatedCheckmark'; import { AnimatedCrossmark } from '@/components/ui/AnimatedCrossmark'; import { cn } from '../lib/utils'; const MAX_POLL_MS = 10 * 60 * 1000; // 10 minutes function PendingState() { const { t } = useTranslation(); return (

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

{t('landing.awaitingPaymentDesc')}

); } function CopyableField({ label, value }: { label: string; value: string }) { const { t } = useTranslation(); const [copied, setCopied] = useState(false); const timeoutRef = useRef>(undefined); useEffect(() => { return () => { if (timeoutRef.current) clearTimeout(timeoutRef.current); }; }, []); const handleCopy = useCallback(async () => { try { await copyToClipboard(value); setCopied(true); if (timeoutRef.current) clearTimeout(timeoutRef.current); timeoutRef.current = setTimeout(() => setCopied(false), 2000); } catch { // Clipboard write failed silently } }, [value]); return (

{label}

{value}

); } function CabinetCredentialsState({ cabinetEmail, cabinetPassword, autoLoginToken, tariffName, periodDays, }: { cabinetEmail: string; cabinetPassword: string | null; autoLoginToken: string | null; tariffName: string | null; periodDays: number | null; }) { const { t } = useTranslation(); const navigate = useNavigate(); const { setTokens, setUser, checkAdminStatus } = useAuthStore(); const [isLoggingIn, setIsLoggingIn] = useState(false); const [loginError, setLoginError] = useState(false); const handleGoToCabinet = useCallback(async () => { if (!autoLoginToken) { navigate('/login'); return; } setIsLoggingIn(true); setLoginError(false); try { const response = await authApi.autoLogin(autoLoginToken); setTokens(response.access_token, response.refresh_token); setUser(response.user); await checkAdminStatus(); navigate('/'); } catch { setLoginError(true); setIsLoggingIn(false); } }, [autoLoginToken, navigate, setTokens, setUser, checkAdminStatus]); return ( {/* Title */}

{t('landing.cabinetReady')}

{tariffName && periodDays !== null && (

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

)}
{/* Credentials */}
{cabinetPassword && ( )} {cabinetPassword &&

{t('landing.saveCredentials')}

} {!cabinetPassword && (

{t('landing.credentialsSentToEmail')}

)}
{/* Go to Cabinet button */} {loginError &&

{t('landing.autoLoginFailed')}

}
); } function SuccessState({ subscriptionUrl, cryptoLink, contactValue, recipientContactValue, tariffName, periodDays, isGift, giftMessage, recipientInBot, botLink, contactType, }: { subscriptionUrl: string | null; cryptoLink: string | null; contactValue: string | null; recipientContactValue: string | null; tariffName: string | null; periodDays: number | null; isGift: boolean; giftMessage: string | null; recipientInBot: boolean | null; botLink: string | null; contactType: string | null; }) { 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; const displayContact = isGift ? recipientContactValue : contactValue; return ( {/* Title */}

{isGift ? t('landing.giftSentSuccess') : t('landing.purchaseSuccess')}

{tariffName && periodDays !== null && (

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

)} {isGift && contactType === 'telegram' && recipientInBot === true && (

{t('landing.giftTelegramSent')}

)} {isGift && contactType === 'telegram' && recipientInBot !== true && (

{t('landing.giftTelegramNotInBot')}

)} {!(isGift && contactType === 'telegram') && displayContact && (

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

)} {isGift && giftMessage && (

{t('landing.giftMessage')}: {giftMessage}

)}
{/* Bot link for telegram gifts where recipient is not in bot */} {isGift && contactType === 'telegram' && recipientInBot !== true && botLink && ( {t('landing.openBot')} )} {/* QR Code */} {displayUrl && (
{/* Copy button */}
)}
); } function PendingActivationState({ tariffName, periodDays, giftMessage, isGift, isActivating, onActivate, autoLoginToken, }: { tariffName: string | null; periodDays: number | null; giftMessage: string | null; isGift: boolean; isActivating: boolean; onActivate: () => void; autoLoginToken: string | null; }) { const { t } = useTranslation(); const navigate = useNavigate(); const { setTokens, setUser, checkAdminStatus } = useAuthStore(); const [isLoggingIn, setIsLoggingIn] = useState(false); const handleGoToCabinet = useCallback(async () => { if (!autoLoginToken) { navigate('/login'); return; } setIsLoggingIn(true); try { const response = await authApi.autoLogin(autoLoginToken); setTokens(response.access_token, response.refresh_token); setUser(response.user); await checkAdminStatus(); navigate('/'); } catch { setIsLoggingIn(false); navigate('/login'); } }, [autoLoginToken, navigate, setTokens, setUser, checkAdminStatus]); return ( {/* Warning icon */}

{t('landing.pendingActivation')}

{tariffName && periodDays !== null && (

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

)}

{t('landing.pendingActivationDesc')}

{isGift && giftMessage && (

{t('landing.giftMessage')}: {giftMessage}

)}
{autoLoginToken && ( )}
); } function GiftPendingActivationState({ tariffName, periodDays, recipientContactValue, giftMessage, recipientInBot, botLink, contactType, }: { tariffName: string | null; periodDays: number | null; recipientContactValue: string | null; giftMessage: string | null; recipientInBot: boolean | null; botLink: string | null; contactType: string | null; }) { const { t } = useTranslation(); return (

{t('landing.giftSentSuccess')}

{tariffName && periodDays !== null && (

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

)} {contactType === 'telegram' && recipientInBot === true && (

{t('landing.giftTelegramPendingSent')}

)} {contactType === 'telegram' && recipientInBot !== true && (

{t('landing.giftTelegramPendingNotInBot')}

)} {contactType !== 'telegram' && (

{t('landing.giftPendingActivationDesc')}

)} {contactType !== 'telegram' && recipientContactValue && (

{t('landing.giftSentTo', { contact: recipientContactValue })}

)} {giftMessage && (

{t('landing.giftMessage')}: {giftMessage}

)}
{/* Bot link for telegram gifts where recipient is not in bot */} {contactType === 'telegram' && recipientInBot !== true && botLink && ( {t('landing.openBot')} )}
); } 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.', )}

); } export default function PurchaseSuccess() { const { t } = useTranslation(); const { token } = useParams<{ token: string }>(); const [searchParams] = useSearchParams(); const isActivateHint = searchParams.get('activate') === '1'; const pollStart = useRef(Date.now()); const [pollTimedOut, setPollTimedOut] = useState(false); const [isActivating, setIsActivating] = useState(false); const [activationError, setActivationError] = useState(false); const activatingRef = useRef(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 queryClient = useQueryClient(); const { data: purchaseStatus, 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 handleActivate = useCallback(async () => { if (!token || activatingRef.current) return; activatingRef.current = true; setIsActivating(true); setActivationError(false); try { const result = await landingApi.activatePurchase(token); queryClient.setQueryData(['purchase-status', token], result); } catch { setActivationError(true); } finally { activatingRef.current = false; setIsActivating(false); } }, [token, queryClient]); const isSuccess = purchaseStatus?.status === 'delivered'; // Fire analytics goal on successful delivery (once per purchase). // Idempotency keyed by token so a page refresh doesn't double-count. useEffect(() => { if (!isSuccess || !token) return; const FIRED_KEY = `ym_buy_success_${token}`; try { if (localStorage.getItem(FIRED_KEY)) return; } catch { /* ignore */ } try { const counterId = localStorage.getItem('ym_counter_id'); const w = window as unknown as Record; if (counterId && typeof w.ym === 'function') { (w.ym as (...args: unknown[]) => void)(Number(counterId), 'reachGoal', 'buy_success'); try { localStorage.setItem(FIRED_KEY, '1'); } catch { /* ignore */ } } } catch { /* analytics error */ } }, [isSuccess, token]); const isPendingActivation = purchaseStatus?.status === 'pending_activation'; const isFailed = purchaseStatus?.status === 'failed' || purchaseStatus?.status === 'expired'; // Gift pending activation → buyer sees "gift sent" message, not the activate button. // Recipient arrives via email link with ?activate=1 and sees the activate button instead. const isGiftPendingActivation = isPendingActivation && purchaseStatus?.is_gift && !isActivateHint; // Email self-purchase delivered → show cabinet credentials const isEmailSelfPurchase = isSuccess && purchaseStatus.contact_type === 'email' && !purchaseStatus.is_gift && purchaseStatus.cabinet_email; return (
{isError ? ( ) : isEmailSelfPurchase ? ( ) : isSuccess ? ( ) : isGiftPendingActivation ? ( ) : isPendingActivation ? (
{activationError && (

{t('landing.activationFailed')}

)}
) : isFailed ? ( ) : pollTimedOut ? ( ) : ( )}
); }