import { useCallback, useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { api, ApiError } from '../api/client'; import { useAuth } from '../hooks/useAuth'; import { useDevices } from '../hooks/useDevices'; import { useToastStore } from '../store/useToast'; import { Shell } from '../components/Shell'; import { Reveal } from '../components/Reveal'; import { Skeleton } from '../components/Skeleton'; import { StatStrip } from '../components/StatStrip'; import { DeviceCard } from '../components/DeviceCard'; import { Modal } from '../components/Modal'; import { SubscriptionQrModal } from '../components/SubscriptionQrModal'; import { ClientLinksModal } from '../components/ClientLinksModal'; import { formatMoney } from '../lib/format'; import { redirectTo } from '../lib/redirect'; import type { Device, SubscriptionData, UserInfo } from '../types'; export function AccountPage() { const navigate = useNavigate(); const { user, isAuthenticated, clear } = useAuth(); const showToast = useToastStore((s) => s.show); const { data: devices, loading: devicesLoading, remove } = useDevices(); const [userInfo, setUserInfo] = useState(user); const [subscription, setSubscription] = useState(null); const [loadingMeta, setLoadingMeta] = useState(true); const [pending, setPending] = useState(null); const [isQrOpen, setIsQrOpen] = useState(false); const [isReferralQrOpen, setIsReferralQrOpen] = useState(false); const [isClientLinksOpen, setIsClientLinksOpen] = useState(false); const [isRenewalOpen, setIsRenewalOpen] = useState(false); const [isRenewing, setIsRenewing] = useState(false); useEffect(() => { if (!isAuthenticated) { navigate('/login', { replace: true, state: { returnTo: '/account' } }); } }, [isAuthenticated, navigate]); const loadMeta = useCallback(async () => { setLoadingMeta(true); try { const [me, sub] = await Promise.all([api.getMe(), api.getSubscription()]); setUserInfo(me); setSubscription(sub); } catch (err: unknown) { const message = err instanceof ApiError ? err.message : 'Не удалось загрузить аккаунт'; showToast(message); if (err instanceof ApiError && err.status === 401) clear(); } finally { setLoadingMeta(false); } }, [clear, showToast]); useEffect(() => { if (!isAuthenticated) return; void loadMeta(); }, [isAuthenticated, loadMeta]); const handleDelete = (device: Device) => setPending(device); const referralLink = userInfo?.referal_code ? `${window.location.origin}/?ref=${encodeURIComponent(userInfo.referal_code)}` : null; const copyLink = async (link: string | null, label: string) => { if (!link) return; try { if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(link); } else { const input = document.createElement('textarea'); input.value = link; input.style.position = 'fixed'; input.style.opacity = '0'; document.body.appendChild(input); input.select(); document.execCommand('copy'); input.remove(); } showToast(`${label} скопирована`); } catch { showToast('Не удалось скопировать ссылку'); } }; const confirmDelete = async () => { if (!pending) return; const ok = await remove(pending.hwid); setPending(null); if (ok) { showToast(`${pending.model ?? 'Устройство'} отключено`); } else { showToast('Не удалось удалить устройство'); } }; const signOut = () => { clear(); showToast('Сессия завершена'); navigate('/', { replace: true }); }; const openPlanWithCurrentConfig = () => { if (!subscription) return; const params = new URLSearchParams(); if (subscription.devices) params.set('devices', String(subscription.devices)); if (subscription.duration_days) params.set('duration', String(subscription.duration_days)); if (subscription.addon_ids.length > 0) params.set('addons', subscription.addon_ids.join(',')); navigate(`/plan?${params.toString()}`); }; const renewCurrentPlan = async () => { if (!subscription?.devices || !subscription.duration_days) { openPlanWithCurrentConfig(); return; } setIsRenewing(true); try { const result = await api.checkout({ devices: subscription.devices, addons: subscription.addon_ids, duration_days: subscription.duration_days, }); if (result.payment_link) { redirectTo(result.payment_link); return; } setIsRenewalOpen(false); showToast(`Заказ ${result.order_id} оформлен. К оплате: ${formatMoney(result.amount_to_pay)}`); } catch (err: unknown) { const message = err instanceof ApiError ? err.message : 'Не удалось оформить продление'; showToast(message); } finally { setIsRenewing(false); } }; return (
Ваш аккаунт

{userInfo?.username ?? 'Аккаунт'}

{!loadingMeta ? ( <>

{subscription?.has_subscription && subscription.expires_at ? `Подписка до ${new Date(subscription.expires_at).toLocaleDateString('ru-RU')}` : 'Активная подписка отсутствует'}

{subscription?.has_subscription ? ( ) : null} ) : null}
{loadingMeta ? (
{Array.from({ length: 5 }).map((_, i) => (
))}
) : ( )} {!loadingMeta && subscription?.subscription_link ? (
Ссылка подписки
{subscription.subscription_link}
) : null} {!loadingMeta && referralLink ? (
Реферальная ссылка
{referralLink}
) : null}

Подключённые устройства

{devices.length}активно
{devicesLoading ? (
{Array.from({ length: 2 }).map((_, i) => (
))}
) : devices.length === 0 ? (
Нет подключённых устройств
) : (
{devices.map((d) => ( ))}
)}
MaleniaVPN
setPending(null)} kicker="Подтверждение" title="Удалить это устройство?" body={ <> Отключение {pending?.model ?? 'устройства'} немедленно завершит его VPN-сессию. Вы сможете подключить его снова в любой момент. } confirmLabel="Удалить" onConfirm={confirmDelete} /> setIsRenewalOpen(false)} kicker="Продление" title="Продлить с текущими параметрами?" body="Мы повторим число устройств, дополнения и срок вашей текущей подписки." confirmLabel={isRenewing ? 'Оформление…' : 'Да, продлить'} cancelLabel="Нет, настроить" confirmClassName="btn-primary" pending={isRenewing} onCancel={openPlanWithCurrentConfig} onConfirm={() => void renewCurrentPlan()} /> setIsQrOpen(false)} /> setIsReferralQrOpen(false)} /> setIsClientLinksOpen(false)} />
); }