import { useState, useEffect } from 'react'; import { useParams, useNavigate, Link } from 'react-router'; import { useTranslation } from 'react-i18next'; import { useQuery, useMutation } from '@tanstack/react-query'; import { motion } from 'framer-motion'; import { authApi } from '../api/auth'; import { useAuthStore } from '../store/auth'; import { useToast } from '../components/Toast'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/data-display/Card'; import { Button } from '@/components/primitives/Button'; import { staggerContainer, staggerItem } from '@/components/motion/transitions'; import { cn } from '@/lib/utils'; import OAuthProviderIcon from '../components/OAuthProviderIcon'; import type { MergeAccountPreview } from '../types'; // -- Icons -- function WarningIcon({ className = 'h-5 w-5' }: { className?: string }) { return ( ); } function ClockIcon({ className = 'h-4 w-4' }: { className?: string }) { return ( ); } function CheckCircleIcon({ className = 'h-5 w-5' }: { className?: string }) { return ( ); } function TelegramIcon({ className = 'h-5 w-5' }: { className?: string }) { return ( ); } function EmailIcon({ className = 'h-5 w-5' }: { className?: string }) { return ( ); } // -- Helpers -- function ProviderBadgeIcon({ provider }: { provider: string }) { switch (provider) { case 'telegram': return ; case 'email': return ; default: return ; } } function formatCountdown(seconds: number): string { const clamped = Math.max(0, seconds); const min = Math.floor(clamped / 60); const sec = clamped % 60; return `${min.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}`; } function formatDate(dateStr: string | null): string { if (!dateStr) return '-'; try { return new Date(dateStr).toLocaleDateString(undefined, { day: 'numeric', month: 'long', year: 'numeric', }); } catch { return dateStr; } } function formatBalance(kopeks: number): string { return Math.floor(kopeks / 100).toLocaleString(); } // -- Radio Indicator -- function RadioIndicator({ selected }: { selected: boolean }) { return (
{selected &&
}
); } // -- Account Card -- interface AccountCardProps { account: MergeAccountPreview; label: string; isSelected: boolean; onSelect: () => void; showRadio: boolean; } function AccountCard({ account, label, isSelected, onSelect, showRadio }: AccountCardProps) { const { t } = useTranslation(); return ( {label} {/* Auth methods */}
{t('merge.authMethods')}:
{account.auth_methods.map((method) => ( {t(`profile.accounts.providers.${method}`)} ))}
{/* Subscription */} {account.subscription ? (
{t('merge.subscription')}:

{account.subscription.tariff_name ?? account.subscription.status}

{account.subscription.end_date && (

{t('merge.until', { date: formatDate(account.subscription.end_date) })}

)}

{t('merge.traffic')}: {account.subscription.traffic_limit_gb} GB, {t('merge.devices')} : {account.subscription.device_limit}

) : (
{t('merge.subscription')}:

{t('merge.noSubscription')}

)} {/* Balance */}
{t('merge.balance')}: {formatBalance(account.balance_kopeks)} ₽
{/* Radio selection */} {showRadio && account.subscription && ( )}
); } // -- Loading Skeleton -- function LoadingSkeleton() { return (
{Array.from({ length: 3 }).map((_, i) => (
))}
); } // -- Expired State -- function ExpiredState() { const { t } = useTranslation(); return (

{t('merge.expired')}

{t('profile.accounts.goToAccounts')}
); } // -- Error State -- function ErrorState() { const { t } = useTranslation(); return (

{t('merge.error')}

{t('profile.accounts.goToAccounts')}
); } // -- Main Component -- export default function MergeAccounts() { const { t } = useTranslation(); const { mergeToken } = useParams<{ mergeToken: string }>(); const navigate = useNavigate(); const { showToast } = useToast(); const [selectedUserId, setSelectedUserId] = useState(null); const [expiresIn, setExpiresIn] = useState(0); const [isExpired, setIsExpired] = useState(false); // Fetch merge preview (no auth required) const { data, isLoading, error } = useQuery({ queryKey: ['merge-preview', mergeToken], queryFn: () => { if (!mergeToken) return Promise.reject(new Error('Missing merge token')); return authApi.getMergePreview(mergeToken); }, enabled: !!mergeToken, retry: false, staleTime: Infinity, refetchOnWindowFocus: false, }); // Auto-select subscription when data loads useEffect(() => { if (!data) return; const primaryHasSub = !!data.primary.subscription; const secondaryHasSub = !!data.secondary.subscription; if (primaryHasSub && !secondaryHasSub) { setSelectedUserId(data.primary.id); } else if (!primaryHasSub && secondaryHasSub) { setSelectedUserId(data.secondary.id); } else if (!primaryHasSub && !secondaryHasSub) { // Neither has subscription — default to primary setSelectedUserId(data.primary.id); } // If both have subs — null until user picks }, [data]); // Countdown timer useEffect(() => { if (!data) return; setExpiresIn(data.expires_in_seconds); const interval = setInterval(() => { setExpiresIn((prev) => { if (prev <= 1) { clearInterval(interval); setIsExpired(true); return 0; } return prev - 1; }); }, 1000); return () => clearInterval(interval); }, [data]); // Execute merge const mergeMutation = useMutation({ mutationFn: () => { if (!mergeToken || !selectedUserId) { return Promise.reject(new Error('Missing merge token or user selection')); } return authApi.executeMerge(mergeToken, selectedUserId); }, onSuccess: async (response) => { if (!response.success) { showToast({ type: 'error', message: t('merge.error') }); return; } if (response.access_token && response.refresh_token) { const { setTokens, setUser, checkAdminStatus } = useAuthStore.getState(); setTokens(response.access_token, response.refresh_token); if (response.user) { setUser(response.user); } await checkAdminStatus(); } showToast({ type: 'success', message: t('merge.success') }); navigate('/', { replace: true }); }, onError: () => { showToast({ type: 'error', message: t('merge.error'), }); }, }); const handleMerge = () => { if (!selectedUserId || mergeMutation.isPending || isExpired) return; mergeMutation.mutate(); }; const handleCancel = () => { navigate('/profile/accounts', { replace: true }); }; // Derived state const bothHaveSubscriptions = data && !!data.primary.subscription && !!data.secondary.subscription; const canConfirm = selectedUserId !== null && !isExpired && !mergeMutation.isPending; const combinedBalance = data ? data.primary.balance_kopeks + data.secondary.balance_kopeks : 0; // Missing token param if (!mergeToken) { return ; } // Loading if (isLoading) { return ; } // Fetch error (404 = expired/invalid token) if (error || !data) { return ; } // Timer expired if (isExpired) { return ; } return ( {/* Header with warning */}

{t('merge.title')}

{t('merge.description')}

{/* Subscription choice prompt (when both have subs) */} {bothHaveSubscriptions && !selectedUserId && (

{t('merge.chooseSubscription')}

)} {/* Primary account card */} setSelectedUserId(data.primary.id)} showRadio={!!bothHaveSubscriptions} /> {/* Secondary account card */} setSelectedUserId(data.secondary.id)} showRadio={!!bothHaveSubscriptions} /> {/* After merge summary */} {t('merge.afterMerge')}
  • {t('merge.allAuthMethodsMerged')}
  • {t('merge.balanceSummed', { amount: formatBalance(combinedBalance) })}
  • {bothHaveSubscriptions && (
  • {t('merge.unselectedSubscriptionDeleted')}
  • )}
  • {t('merge.historyPreserved')}
{/* Confirm button */} {/* Cancel link */} {/* Countdown timer */} {t('merge.expiresIn', { minutes: formatCountdown(expiresIn) })}
); }