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