import { useMemo, useRef, useState } from 'react'; import { useParams, useNavigate } from 'react-router'; import { useQuery, useMutation } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { motion } from 'framer-motion'; import { landingApi, type GiftClaimResult } from '../api/landings'; import { getApiErrorMessage } from '../utils/api-error'; import { copyToClipboard } from '@/utils/clipboard'; import { Spinner } from '@/components/ui/Spinner'; import { AnimatedCheckmark } from '@/components/ui/AnimatedCheckmark'; import { cn } from '@/lib/utils'; import { CheckCircleIcon, CheckIcon, CopyIcon } from '@/components/icons'; const MAX_POLL_MS = 10 * 60 * 1000; // poll an unsettled payment for up to 10 min function isValidEmail(value: string): boolean { return /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(value.trim()); } function Shell({ children }: { children: React.ReactNode }) { return (
{children}
); } export default function GiftClaim() { const { token } = useParams<{ token: string }>(); const { t } = useTranslation(); const navigate = useNavigate(); const startedAt = useRef(Date.now()); const [email, setEmail] = useState(''); const [showEmail, setShowEmail] = useState(false); const [claimError, setClaimError] = useState(null); const [result, setResult] = useState(null); const [copied, setCopied] = useState(false); const { data: gift, isLoading, error, } = useQuery({ queryKey: ['gift-claim', token], queryFn: () => landingApi.getGiftClaim(token!), enabled: !!token, retry: 1, // Poll only while the payment is still settling (not yet claimable / terminal). refetchInterval: (query) => { const data = query.state.data; if (!data) return 3000; if (Date.now() - startedAt.current > MAX_POLL_MS) return false; const settled = data.is_claimable || data.status === 'delivered' || data.status === 'failed'; return settled ? false : 3000; }, }); const claimMutation = useMutation({ mutationFn: () => landingApi.claimGift(token!, email.trim()), onSuccess: (res) => { // One-click cabinet login for a fresh email account; otherwise show the link. if (res.auto_login_token) { navigate(`/auto-login?token=${encodeURIComponent(res.auto_login_token)}`); return; } setResult(res); }, onError: (err) => { setClaimError( getApiErrorMessage(err, t('landing.giftClaim.error', 'Could not activate the gift.')), ); }, }); const willReplace = gift?.status === 'pending_activation'; const handleCopyLink = async () => { const url = result?.subscription_url; if (!url) return; try { await copyToClipboard(url); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch { /* ignore */ } }; const periodLabel = useMemo(() => { const days = gift?.period_days; if (!days) return ''; return `${days} ${t('gift.days', 'days')}`; }, [gift?.period_days, t]); if (isLoading) { return (

{t('common.loading', 'Loading...')}

); } // 404 / unknown gift if (error || !gift || !gift.is_gift) { return (

{t('landing.giftClaim.notFoundTitle', 'Gift not found')}

{t( 'landing.giftClaim.notFoundDesc', 'This gift link is invalid or no longer available.', )}

); } // Already activated if (gift.status === 'delivered' && !result) { return (

{t('landing.giftClaim.alreadyTitle', 'Gift already activated')}

{t('landing.giftClaim.alreadyDesc', 'This gift has already been claimed.')}

); } // Payment failed/expired → tell the recipient instead of spinning forever if (gift.status === 'failed' || gift.status === 'expired') { return (

{t('landing.giftClaim.failedTitle', 'Gift unavailable')}

{t( 'landing.giftClaim.failedDesc', 'The payment for this gift did not go through, so it cannot be activated.', )}

); } // Successful web claim → show connection link if (result) { return (

{t('landing.giftClaim.successTitle', 'Gift activated!')}

{result.subscription_url && ( <>

{t('landing.giftClaim.connectDesc', 'Use this link to connect:')}

{result.subscription_url}

)}
); } // Payment still settling if (!gift.is_claimable) { return (

{t('landing.giftClaim.pendingTitle', 'Almost ready...')}

{t( 'landing.giftClaim.pendingDesc', 'The payment is still being confirmed. This page will update automatically.', )}

); } // Claimable — offer Telegram + web arms return (
🎁

{t('landing.giftClaim.title', 'You have a gift!')}

{gift.tariff_name && (

{gift.tariff_name} {periodLabel ? ` — ${periodLabel}` : ''}

)}
{gift.gift_message && (

“{gift.gift_message}”

)} {willReplace && (

{t( 'landing.giftClaim.replaceWarning', 'You already have a subscription — activating this gift will replace it.', )}

)} {/* Telegram arm */} {gift.bot_claim_link && ( {t('landing.giftClaim.activateTelegram', 'Activate in Telegram')} )} {/* Web/email arm */} {!showEmail ? ( ) : (
{ setEmail(e.target.value); setClaimError(null); }} placeholder="email@example.com" className="w-full rounded-xl border border-dark-700/50 bg-dark-800/50 px-4 py-3 text-sm text-dark-50 placeholder-dark-500 outline-none focus:border-accent-500/50 focus:ring-1 focus:ring-accent-500/25" /> {claimError &&

{claimError}

}
)}
); }