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