// ============================================ // MariusVPN — all screens // Exposes: LoginScreen, DashboardScreen, SubscriptionScreen, PaymentsScreen, // DevicesScreen, TrafficScreen, ReferralScreen, HelpScreen // ============================================ const { useState, useEffect, useRef, useCallback, useMemo } = React; const { api, MOCK, Icon, FMT, PLATFORM_LABEL, PLATFORM_ICON, Logo, Card, Button, IconButton, Badge, Toggle, ProgressBar, Skeleton, Spinner, Field, Input, Select, CopyField, Modal, Sheet, useToast, ScreenHeader, KV, } = window; // ============================================ // LOGIN // ============================================ const TG_BOT_USERNAME = 'marius_vpnbot'; function LoginScreen({ onLogin }) { const [code, setCode] = useState(''); const [loading, setLoading] = useState(false); const [showLink, setShowLink] = useState(false); const widgetRef = useRef(null); const toast = useToast(); const submit = async (e) => { e.preventDefault(); let t = code.trim(); if (!t) { toast.error('Вставьте ссылку или код из бота'); return; } // The bot sends a login LINK (…/cabinet/?token=XXXX). Accept either the // whole link or just the token. const m = t.match(/[?&]token=([^&\s]+)/); if (m) t = decodeURIComponent(m[1]); setLoading(true); try { await api('/login', { method: 'POST', body: { token: t } }); toast.success('Вход выполнен'); onLogin?.(); } catch (err) { toast.error('Ссылка/код недействительны. Получите новые в боте.'); } finally { setLoading(false); } }; // Fallback: ask the bot to DM a one-time browser login link. const getLink = () => { if (MOCK) { onLogin?.(); return; } window.open(`https://t.me/${TG_BOT_USERNAME}?start=weblogin`, '_blank'); }; // Telegram Login Widget — official "Log in with Telegram" button (browser OAuth). useEffect(() => { if (MOCK) return; window.onTelegramAuth = async (user) => { try { const r = await fetch('/api/tg/cabinet-widget-auth', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(user), }); if (!r.ok) throw new Error('widget auth failed'); toast.success('Вход выполнен'); onLogin?.(); } catch (e) { toast.error('Не удалось войти. Возможно, этот Telegram ещё не регистрировался в боте.'); } }; const el = widgetRef.current; if (el && !el.querySelector('script')) { const s = document.createElement('script'); s.async = true; s.src = 'https://telegram.org/js/telegram-widget.js?22'; s.setAttribute('data-telegram-login', TG_BOT_USERNAME); s.setAttribute('data-size', 'large'); s.setAttribute('data-radius', '12'); s.setAttribute('data-onauth', 'onTelegramAuth(user)'); s.setAttribute('data-request-access', 'write'); el.appendChild(s); } }, []); // Try to auto-auth via Telegram WebApp initData if opened inside Telegram. useEffect(() => { const tgInitData = window.Telegram?.WebApp?.initData; if (!tgInitData) return; (async () => { try { const r = await fetch('/api/tg/cabinet-auth', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ init_data: tgInitData }), }); if (r.ok) { onLogin?.(); } } catch (e) {} })(); }, []); return (

Личный кабинет

Войдите через Telegram — в один клик, на любом устройстве.

{/* Primary: official Telegram Login Widget button */}
или
{/* Secondary: bot DMs a one-time browser link */} {/* Tertiary: paste a link manually */} {showLink && (
setCode(e.target.value)} className="!text-sm font-mono" />
)}

Входя, вы соглашаетесь с условиями использования и политикой конфиденциальности.

); } // ============================================ // DASHBOARD // ============================================ function DashboardScreen({ onRoute, refreshKey }) { const [me, setMe] = useState(null); const [traffic, setTraffic] = useState(null); const toast = useToast(); useEffect(() => { let alive = true; Promise.all([api('/me'), api('/traffic')]).then(([m, t]) => { if (!alive) return; setMe(m); setTraffic(t); }); return () => { alive = false; }; }, [refreshKey]); if (!me || !traffic) return ; const s = me.subscription; const lim = me.limits; const bypass = traffic.bypass; const bypassPct = (bypass.used_gb / bypass.quota_gb) * 100; return (
{/* Subscription status (hero) */}
Подписка
{s.plan_name} {s.status === 'active' && Активна} {s.status === 'expired' && Истекла} {s.status === 'trial' && Триал}
{s.days_left}
{FMT.daysWord(s.days_left)} до окончания
истекает {FMT.date(s.expires_at)}
оплачено {s.period_label} {Math.round((s.days_left / s.total_days) * 100)}%
{/* Traffic card */}
Трафик
{FMT.gb(traffic.total.down_gb)}
скачано
↓ {FMT.gb(traffic.total.down_gb)} · ↑ {FMT.gb(traffic.total.up_gb)}
onRoute('traffic')} />
Сервер обхода
{FMT.gb(bypass.used_gb)} / {FMT.gb(bypass.quota_gb)}
базовый лимит {FMT.gb(bypass.base_cap_gb)} · докуплено {FMT.gb(bypass.extra_gb)}
{/* Devices card */}
Устройства
{lim.devices_used}
из {lim.devices_max} активных
доп. устройство — {FMT.money(lim.extra_device_price, lim.extra_device_currency)}/мес
onRoute('devices')} />
{traffic.devices.slice(0, 3).map(d => ( {d.name} ))}
{/* Quick links */}
{[ { icon: 'card', title: 'Оплата', sub: 'Пополнение и история', route: 'payments' }, { icon: 'gift', title: 'Рефералы', sub: '+30 дней за друга', route: 'referral', badge: 'gold' }, { icon: 'globe', title: 'Локации', sub: '12 стран · 87 серверов', route: null }, { icon: 'help', title: 'Помощь', sub: 'Инструкции и поддержка', route: 'help' }, ].map((q, i) => ( ))}
); } function DashboardSkeleton() { return (
); } // ============================================ // SUBSCRIPTION // ============================================ function SubscriptionScreen({ onRoute, onRefresh }) { const [me, setMe] = useState(null); const [plans, setPlans] = useState(null); const [selected, setSelected] = useState(null); const [busy, setBusy] = useState(false); const toast = useToast(); useEffect(() => { Promise.all([api('/me'), api('/plans')]).then(([m, p]) => { setMe(m); setPlans(p); setSelected(m.subscription.plan_id); }); }, []); if (!me || !plans) return ; const s = me.subscription; const setAutoRenew = async (v) => { setMe({ ...me, subscription: { ...s, auto_renew: v } }); try { await api('/subscription/auto-renew', { method: 'POST', body: { enabled: v } }); toast.success(v ? 'Автопродление включено' : 'Автопродление отключено'); } catch { toast.error('Не удалось изменить настройку'); } }; const renew = async () => { if (!selected) return; const plan = plans.find(p => p.id === selected); setBusy(true); try { const res = await api('/subscription/renew', { method: 'POST', body: { plan_id: selected, period_months: plan.period_months } }); if (res.payment_url) { toast.info('Открываю страницу оплаты…'); window.location.href = res.payment_url; return; } else if (res.balance_ok) { toast.success('Подписка продлена с баланса'); onRefresh?.(); } } catch { toast.error('Не удалось оформить продление'); } setBusy(false); }; return (
{/* Current */}
Текущий тариф
{s.plan_name} · {s.period_label}
{FMT.money(s.price_per_month, s.currency)}/мес · истекает {FMT.date(s.expires_at)}
{s.status === 'expired' ? Истекла : {s.status === 'trial' ? 'Пробный' : 'Активна'}}
Автопродление
Подписка будет продлеваться автоматически за {FMT.money(s.price_per_month, s.currency)}/мес
{/* Plans */}
Сменить тариф или продлить
{plans.map(p => { const isCurrent = p.id === s.plan_id; const isSelected = p.id === selected; return ( ); })}
Безопасная оплата · можно отменить в любой момент
); } // ============================================ // PAYMENTS — top-up + history // ============================================ function PaymentsScreen() { const [methods, setMethods] = useState(null); const [history, setHistory] = useState(null); const [amount, setAmount] = useState(500); const [method, setMethod] = useState(null); const [busy, setBusy] = useState(false); const toast = useToast(); useEffect(() => { Promise.all([api('/payments/methods'), api('/payments/history')]).then(([m, h]) => { setMethods(m); setHistory(h); const first = m.find(x => x.enabled); if (first) setMethod(first.id); }); }, []); if (!methods || !history) return ; const topup = async () => { if (!amount || amount < 50) { toast.error('Минимальная сумма — 50 ₽'); return; } if (!method) { toast.error('Выберите способ оплаты'); return; } setBusy(true); try { const res = await api('/payments/topup', { method: 'POST', body: { amount: +amount, method } }); if (res.payment_url) { toast.info('Открываю страницу оплаты…'); window.location.href = res.payment_url; return; } } catch { toast.error('Ошибка оплаты'); } setBusy(false); }; const statusBadge = (s) => { if (s === 'paid') return оплачено; if (s === 'pending') return ожидание; if (s === 'cancelled') return отменён; return {s}; }; return (
{/* Top-up form */}
Пополнение баланса
setAmount(e.target.value)} />
{[500, 1000, 2000, 5000].map(v => ( ))}
{methods.filter(m => m.enabled).map(m => ( ))} {methods.filter(m => !m.enabled).map(m => (
{m.name}
{m.description}
))}
{/* History */}
История платежей
{history.map(h => (
{h.description}
{FMT.dateShort(h.date)} · {h.method}
{FMT.money(h.amount, h.currency)}
{statusBadge(h.status)}
))}
); } // ============================================ // DEVICES // ============================================ function DevicesScreen({ onRefresh }) { const [me, setMe] = useState(null); const [devices, setDevices] = useState(null); const [showAdd, setShowAdd] = useState(false); const [showConfig, setShowConfig] = useState(null); // device id const [renaming, setRenaming] = useState(null); // device obj const [deleting, setDeleting] = useState(null); // device obj const [syncing, setSyncing] = useState(false); const [repairing, setRepairing] = useState(false); const toast = useToast(); const repairVpn = async () => { setRepairing(true); try { const r = await api('/repair', { method: 'POST' }); if (r && r.ok) { toast.success('Готово! Ключи обновлены на ' + r.nodes + ' серверах. Обновите подписку в приложении, выберите сервер «' + r.recommended + '» и переподключитесь.'); onRefresh && onRefresh(); } else { toast.error((r && r.message) || 'Не удалось починить автоматически'); } } catch (e) { toast.error('Ошибка при починке, попробуйте ещё раз'); } finally { setRepairing(false); } }; const syncDevices = async () => { setSyncing(true); try { await api('/sync', { method: 'POST' }); toast.success('VPN синхронизирован на всех серверах. Обновите конфиг в приложении.'); onRefresh?.(); } catch { toast.error('Ошибка синхронизации'); } finally { setSyncing(false); } }; const reload = async () => { const [m, d] = await Promise.all([api('/me'), api('/devices')]); setMe(m); setDevices(d); }; useEffect(() => { reload(); }, []); if (!me || !devices) return ; const lim = me.limits; const atLimit = lim.devices_used >= lim.devices_max; const addDevice = async (name, platform) => { const res = await api('/devices', { method: 'POST', body: { name, platform } }); await reload(); onRefresh?.(); toast.success('Устройство добавлено'); setShowAdd(false); setShowConfig(res.device.id); }; const renameDevice = async (id, name) => { await api(`/devices/${id}`, { method: 'PATCH', body: { name } }); await reload(); toast.success('Переименовано'); setRenaming(null); }; const removeDevice = async (id) => { await api(`/devices/${id}`, { method: 'DELETE' }); await reload(); onRefresh?.(); toast.success('Устройство удалено'); setDeleting(null); }; return (
} /> {atLimit && (
Достигнут лимит устройств
Удалите неиспользуемое устройство или подключите дополнительное за {FMT.money(lim.extra_device_price, lim.extra_device_currency)}/мес.
)}
{devices.map(d => (
{d.name} {d.online ? online : offline}
{PLATFORM_LABEL[d.platform]} · HWID {d.hwid} · {d.last_activity}
setShowConfig(d.id)} /> setRenaming(d)} /> setDeleting(d)} />
))} {devices.length === 0 && (
Нет подключённых устройств
Добавьте устройство, чтобы получить конфигурацию.
)}
setShowAdd(false)} onSubmit={addDevice} /> d.id === showConfig)} onClose={() => setShowConfig(null)} /> setRenaming(null)} onSubmit={(name) => renameDevice(renaming.id, name)} /> removeDevice(deleting.id)} onClose={() => setDeleting(null)} />
); } function AddDeviceModal({ open, onClose, onSubmit }) { const [name, setName] = useState(''); const [platform, setPlatform] = useState('ios'); const [loading, setLoading] = useState(false); useEffect(() => { if (open) { setName(''); setPlatform('ios'); } }, [open]); const submit = async (e) => { e.preventDefault(); if (!name.trim()) return; setLoading(true); await onSubmit(name.trim(), platform); setLoading(false); }; return (
setName(e.target.value)} autoFocus />
{['ios','android','windows','macos'].map(p => ( ))}
); } // QR generator (offline, uses qrcode-generator if available, otherwise falls back to qrserver) function QrCode({ data }) { const ref = useRef(null); useEffect(() => { if (!ref.current) return; if (typeof window.qrcode === 'function') { try { const qr = window.qrcode(0, 'M'); qr.addData(data); qr.make(); // createSvgTag returns inline SVG; expand it to fill container const svg = qr.createSvgTag({ cellSize: 4, margin: 4, scalable: true }); ref.current.innerHTML = svg.replace('`; } }, [data]); return
; } function ConfigSheet({ open, deviceId, device, onClose }) { const [cfg, setCfg] = useState(null); useEffect(() => { if (!open || !deviceId) { setCfg(null); return; } api(`/devices/${deviceId}/config`).then(setCfg); }, [open, deviceId]); return ( {!cfg ? : (
Ссылка-подписка
VLESS конфиг
QR-код
Отсканируйте код в клиенте Happ, v2RayTun, Hiddify — подписка импортируется автоматически.
)}
); } function RenameModal({ open, device, onClose, onSubmit }) { const [name, setName] = useState(''); useEffect(() => { if (open && device) setName(device.name); }, [open, device]); const submit = (e) => { e.preventDefault(); onSubmit(name.trim()); }; return (
setName(e.target.value)} autoFocus />
); } function ConfirmModal({ open, title, message, confirmLabel = 'Подтвердить', onConfirm, onClose }) { return (

{message}

); } // ============================================ // TRAFFIC // ============================================ function TrafficScreen({ onRefresh }) { const [traffic, setTraffic] = useState(null); const [showBuy, setShowBuy] = useState(false); const toast = useToast(); const reload = () => api('/traffic').then(setTraffic); useEffect(() => { reload(); }, []); if (!traffic) return ; const b = traffic.bypass; const pct = (b.used_gb / b.quota_gb) * 100; const buy = async (gb) => { await api('/traffic/buy-gb', { method: 'POST', body: { gb } }); await reload(); onRefresh?.(); toast.success(`Докуплено ${gb} ГБ`); setShowBuy(false); }; return (
{/* Total */}
Общий трафик
скачано
{FMT.gb(traffic.total.down_gb)}
отправлено
{FMT.gb(traffic.total.up_gb)}
{/* Bypass */}
Сервер обхода
{FMT.gb(b.used_gb)}
из {FMT.gb(b.quota_gb)}
базовый лимит
{FMT.gb(b.base_cap_gb)}
докуплено
{FMT.gb(b.extra_gb)}
{/* Per-device breakdown */}
По устройствам
{traffic.devices.map(d => { const total = d.up_gb + d.down_gb; const max = Math.max(...traffic.devices.map(x => x.up_gb + x.down_gb)); return (
{d.name}
↓{FMT.gb(d.down_gb)} · ↑{FMT.gb(d.up_gb)}
); })}
setShowBuy(false)} bypass={b} onBuy={buy} />
); } function BuyGbModal({ open, onClose, bypass, onBuy }) { const packs = [ { gb: 10, price: 10 * bypass.price_per_gb }, { gb: 25, price: 25 * bypass.price_per_gb - 50 }, { gb: 50, price: 50 * bypass.price_per_gb - 150 }, { gb: 100, price: 100 * bypass.price_per_gb - 400 }, ]; const [pick, setPick] = useState(25); const [loading, setLoading] = useState(false); return (

Пакет добавится к лимиту сервера обхода и сохранится до конца расчётного периода.

{packs.map(p => ( ))}
); } // ============================================ // REFERRAL // ============================================ function ReferralScreen() { const [ref, setRef] = useState(null); const toast = useToast(); useEffect(() => { api('/referral').then(setRef); }, []); if (!ref) return ; const share = (platform) => { const text = `Быстрый и удобный VPN. Регистрируйся по моей ссылке и получи первый месяц в подарок: ${ref.link}`; if (platform === 'tg') window.open(`https://t.me/share/url?url=${encodeURIComponent(ref.link)}&text=${encodeURIComponent(text)}`); if (platform === 'wa') window.open(`https://wa.me/?text=${encodeURIComponent(text)}`); }; return (
Подарите другу VPN
и получите +30 дней

{ref.rule_text}

Приглашено
{ref.invited_count}
Оплатили
{ref.paid_count}
Бонус
+{ref.balance}
{ref.balance_unit}
Ваша реферальная ссылка
код: {ref.code}
Поделиться
); } // ============================================ // HELP // ============================================ function HelpScreen() { const [help, setHelp] = useState(null); const [tab, setTab] = useState('ios'); const [openFaq, setOpenFaq] = useState(null); useEffect(() => { api('/help').then(setHelp); }, []); if (!help) return ; const platform = help.platforms.find(p => p.id === tab) || help.platforms[0]; return (
Написать в поддержку } />
{help.platforms.map(p => ( ))}
    {platform.steps.map(step => (
  1. {String(step.n).padStart(2, '0')}
    {step.text}
  2. ))}
Частые вопросы
{help.faq.map((f, i) => { const isOpen = openFaq === i; return (
{isOpen && (
{f.a}
)}
); })}
); } // expose Object.assign(window, { LoginScreen, DashboardScreen, SubscriptionScreen, PaymentsScreen, DevicesScreen, TrafficScreen, ReferralScreen, HelpScreen, });