import { useCallback, useRef, useState } from 'react'; import { useSearchParams, useNavigate } from 'react-router'; import { useQuery } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { motion } from 'framer-motion'; import { giftApi } from '../api/gift'; import { Spinner } from '@/components/ui/Spinner'; import { AnimatedCheckmark } from '@/components/ui/AnimatedCheckmark'; import { AnimatedCrossmark } from '@/components/ui/AnimatedCrossmark'; import { cn } from '@/lib/utils'; import { copyToClipboard } from '@/utils/clipboard'; import { CheckIcon, CopyIcon, InfoIcon, ExclamationIcon, ClockIcon } from '@/components/icons'; const MAX_POLL_MS = 10 * 60 * 1000; // 10 minutes const KNOWN_WARNINGS = new Set(['telegram_unresolvable']); // ============================================================ // Sub-components // ============================================================ function PendingState() { const { t } = useTranslation(); return (

{t('gift.processing', 'Processing your gift...')}

{t('gift.pendingDesc', 'Please wait while we process your payment')}

); } function CodeOnlySuccessState({ purchaseToken, tariffName, periodDays, }: { purchaseToken: string; tariffName: string | null; periodDays: number | null; }) { const { t } = useTranslation(); const navigate = useNavigate(); const [copied, setCopied] = useState(false); const shortCode = purchaseToken.slice(0, 12); const giftCode = `GIFT-${shortCode}`; const botUsername = import.meta.env.VITE_TELEGRAM_BOT_USERNAME as string | undefined; // Encode underscores as %5F so Telegram auto-link detection doesn't strip them const safeCode = shortCode.replace(/_/g, '%5F'); const botLink = botUsername ? `https://t.me/${botUsername}?start=GIFT%5F${safeCode}` : null; const cabinetLink = `${window.location.origin}/gift?tab=activate&code=${safeCode}`; const fullMessage = [ t('gift.shareText', 'I have a gift for you! Activate it here:'), '', botLink ? `${t('gift.shareModalActivateVia', 'Activate via bot:')} ${botLink}` : null, `${t('gift.shareModalActivateViaCabinet', 'Or via website:')} ${cabinetLink}`, ] .filter(Boolean) .join('\n'); const handleCopy = async () => { try { await copyToClipboard(fullMessage); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch { // fallback } }; return (

{t('gift.codeReadyTitle', 'Gift code is ready!')}

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

{tariffName} — {periodDays} {t('gift.days', 'days')}

)}
{/* Gift code display */}

{t('gift.codeLabel', 'Gift code')}

{giftCode}

{/* Share message preview */}

{t('gift.shareText', 'I have a gift for you! Activate it here:')}

{botLink && (

{t('gift.shareModalActivateVia', 'Activate via bot:')}

{botLink}

)}

{t('gift.shareModalActivateViaCabinet', 'Or via website:')}

{cabinetLink}

{/* Copy button */}
); } function DeliveredState({ recipientContact, tariffName, periodDays, giftMessage, warning, }: { recipientContact: string | null; tariffName: string | null; periodDays: number | null; giftMessage: string | null; warning: string | null; }) { const { t } = useTranslation(); const navigate = useNavigate(); return (

{t('gift.successTitle', 'Gift sent!')}

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

{tariffName} — {periodDays} {t('gift.days', 'days')}

)} {recipientContact && (

{t('gift.successDesc', { contact: recipientContact, defaultValue: `Sent to ${recipientContact}`, })}

)} {giftMessage && (

“{giftMessage}”

)}
{warning && (

{t(`gift.warning.${warning}`)}

)}
); } function PendingActivationState({ recipientContact, tariffName, periodDays, warning, }: { recipientContact: string | null; tariffName: string | null; periodDays: number | null; warning: string | null; }) { const { t } = useTranslation(); const navigate = useNavigate(); return ( {/* Info icon */}

{t('gift.pendingActivationTitle', 'Gift pending activation')}

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

{tariffName} — {periodDays} {t('gift.days', 'days')}

)} {recipientContact && (

{t('gift.successDesc', { contact: recipientContact, defaultValue: `Sent to ${recipientContact}`, })}

)}

{t( 'gift.pendingActivationDesc', 'The recipient currently has an active subscription. Your gift will be activated once their current subscription expires.', )}

{warning && (

{t(`gift.warning.${warning}`)}

)}
); } function FailedState() { const { t } = useTranslation(); const navigate = useNavigate(); return (

{t('gift.failedTitle', 'Something went wrong')}

{t('gift.failedDesc', 'Your gift could not be processed. Please try again.')}

); } function PollErrorState() { const { t } = useTranslation(); const navigate = useNavigate(); return (

{t('gift.pollErrorTitle', 'Could not check gift status')}

{t( 'gift.pollErrorDesc', 'Your purchase was successful. Check your dashboard for details.', )}

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

{t('gift.pollTimeout', 'Taking longer than expected')}

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

); } function NoTokenState() { const { t } = useTranslation(); const navigate = useNavigate(); return (

{t('gift.noToken', 'Invalid link')}

{t('gift.noTokenDesc', 'This gift link is invalid or has expired.')}

); } // ============================================================ // Main Component // ============================================================ export default function GiftResult() { const [searchParams] = useSearchParams(); const token = searchParams.get('token'); const mode = searchParams.get('mode'); const rawUrlWarning = searchParams.get('warning'); const urlWarning = rawUrlWarning && KNOWN_WARNINGS.has(rawUrlWarning) ? rawUrlWarning : null; const pollStart = useRef(Date.now()); const [pollTimedOut, setPollTimedOut] = useState(false); const isBalanceMode = mode === 'balance'; const { data: status, isError, refetch, } = useQuery({ queryKey: ['gift-status', token], queryFn: () => giftApi.getPurchaseStatus(token!), enabled: !!token && !pollTimedOut, refetchInterval: (query) => { // Balance mode: fetch once, no polling if (isBalanceMode) return false; const d = query.state.data; const s = d?.status; if (s === 'delivered' || s === 'failed' || s === 'pending_activation' || s === 'expired') return false; // Code-only gifts stay in 'paid' status — stop polling if (s === 'paid' && d?.is_code_only) return false; // Check poll timeout if (Date.now() - pollStart.current > MAX_POLL_MS) { setPollTimedOut(true); return false; } return 3000; }, retry: 2, }); const handleRetryPoll = useCallback(() => { pollStart.current = Date.now(); setPollTimedOut(false); refetch(); }, [refetch]); // No token if (!token) { return (
); } const isCodeOnlyPaid = status?.status === 'paid' && status?.is_code_only && status?.purchase_token != null; const isDelivered = status?.status === 'delivered'; const isPendingActivation = status?.status === 'pending_activation'; const isFailed = status?.status === 'failed' || status?.status === 'expired'; // Warning from status response (persisted on purchase) takes priority over URL param const statusWarning = status?.warning && KNOWN_WARNINGS.has(status.warning) ? status.warning : null; const warning = statusWarning ?? urlWarning; return (
{isError ? ( ) : isCodeOnlyPaid ? ( ) : isDelivered ? ( ) : isPendingActivation ? ( ) : isFailed ? ( ) : pollTimedOut ? ( ) : ( )}
); }