/* WEBAPP_DEV_MOCK_START */ window.__WEBAPP_DEV_MOCK__ = { config: { title: 'Моя подписка', primaryColor: '#00fe7a', logoUrl: '', apiBase: '/api', supportUrl: 'https://t.me/support', privacyPolicyUrl: 'https://example.com/privacy', userAgreementUrl: 'https://example.com/agreement', currency: 'RUB', language: 'ru', emailAuthEnabled: true, telegramLoginBotUsername: 'preview_bot' }, data: { ok: true, user: { id: 100200300, username: 'preview', email: '', email_verified: false, telegram_id: 100200300, telegram_linked: true, first_name: 'Preview', language_code: 'ru' }, subscription: { active: true, status: 'ACTIVE', remaining_text: '27 д. 5 ч.', end_date_text: '19.05.2026 22:40', days_left: 27, config_link: 'https://sub.example.com/sub/preview-token', connect_url: 'https://sub.example.com/connect/preview-token', traffic_used: '18.42 GB', traffic_limit: '500.00 GB', auto_renew_enabled: false, provider: null }, plans: [ {months: 1, price: 190, currency: 'RUB', title: '1 месяц'}, {months: 3, price: 550, currency: 'RUB', title: '3 месяца'}, {months: 6, price: 1100, currency: 'RUB', title: '6 месяцев'}, {months: 12, price: 2000, currency: 'RUB', title: '12 месяцев'} ], payment_methods: [ {id: 'cryptopay', name: 'CryptoPay'}, {id: 'platega', name: 'Platega'}, {id: 'freekassa', name: 'FreeKassa / СБП'} ], referral: { code: 'AB12CD34E', bot_link: 'https://t.me/preview_bot?start=ref_uAB12CD34E', webapp_link: 'https://app.example.com/?ref=uAB12CD34E', invited_count: 3, purchased_count: 1, bonus_details: [ {months: 1, title: '1 месяц', inviter_days: 3, friend_days: 3}, {months: 3, title: '3 месяца', inviter_days: 10, friend_days: 7} ] }, settings: { support_url: 'https://t.me/support', traffic_mode: false, email_auth_enabled: true } } }; /* WEBAPP_DEV_MOCK_END */ const MOCK = (() => { const mock = window.__WEBAPP_DEV_MOCK__; const host = window.location.hostname; const isLocal = window.location.protocol === 'file:' || host === 'localhost' || host === '127.0.0.1' || host === ''; return mock && isLocal ? mock : null; })(); const CFG = window.__WEBAPP_CONFIG__ || (MOCK && MOCK.config) || {}; const tg = window.Telegram && window.Telegram.WebApp ? window.Telegram.WebApp : null; const TELEGRAM_LOGIN_WIDGET_URL = './telegram-widget.js'; const state = { token: MOCK ? 'local-preview' : (localStorage.getItem('rw_webapp_token') || ''), data: null, selectedPlan: null, selectedMethod: null, referralParam: readReferralParam(), promoApplying: false, payment: null, paymentFlowOpen: false, promoModalOpen: false, referralModalOpen: false, paymentStep: 'plan', creatingPayment: false, authInProgress: false, authMode: (CFG.emailAuthEnabled === false ? 'telegram' : 'email'), emailLoginPending: false, emailLoginEmail: '', emailLoginCodeModalOpen: false, emailLoginVerifying: false, emailLoginResending: false, emailLinkPending: false, emailLinkEmail: '', promoStatusTimer: null, promoStatusType: '', promoStatusText: '', telegramLinkRendered: false, telegramLinkInProgress: false, toastTimer: null }; const I18N = { ru: { page_title: 'Моя подписка', loading: 'Загрузка...', refresh: 'Обновить', logout: 'Выйти', subscription: 'Подписка', subscription_subtitle: 'Ваша ссылка подключения и срок доступа', ends_at: 'Окончание', traffic: 'Трафик', connect: 'Подключиться', copy_link: 'Скопировать ссылку', extend_subscription: 'Купить подписку / Добавить дни', payment_title: 'Оплата подписки', payment_caption: 'Выберите срок, способ оплаты и создайте платеж.', close: 'Закрыть', close_payment: 'Закрыть оплату', payment_steps: 'Шаги оплаты', step_plan: 'Срок', step_method: 'Метод', step_result: 'Платеж', select_period: 'Выберите срок', select_method: 'Выберите способ оплаты', payment_status: 'Статус платежа', choose_payment_method: 'Выбрать способ оплаты', back: 'Назад', create_payment: 'Создать платеж', open_payment: 'Открыть оплату', check_payment: 'Проверить оплату', choose_other_method: 'Выбрать другой способ', support: 'Поддержка', account_title: 'Аккаунт', account_caption: 'Способы входа', email_label: 'Email', telegram_label: 'Telegram', linked: 'Привязан', not_linked: 'Не привязан', email_login_tab: 'Email', telegram_login_tab: 'Telegram', email_placeholder: 'mail@example.com', code_placeholder: '000000', send_code: 'Отправить код', resend_code: 'Отправить еще раз', confirm: 'Подтвердить', login: 'Войти', email_code_title: 'Подтвердите вход', email_code_caption: 'Введите 6-значный код из письма.', email_code_aria: 'Код подтверждения', email_auth_disabled: 'Вход по email пока не настроен.', email_required: 'Введите email', email_invalid: 'Введите корректный email', email_code_sending: 'Отправляю код...', email_code_sent: 'Код отправлен на почту', email_code_send_failed: 'Не удалось отправить код', email_code_invalid: 'Неверный код', email_code_expired: 'Код устарел', email_rate_limited: 'Повторная отправка доступна через {seconds} сек.', email_linked: 'Email привязан', telegram_linked: 'Telegram привязан', account_merge_conflict: 'Этот аккаунт уже связан с другими данными.', telegram_auth: 'Telegram auth', telegram_auth_verifying: 'Проверяю вход...', telegram_auth_failed: 'Не удалось подтвердить Telegram-вход. Попробуйте еще раз.', telegram_auth_unavailable: 'Telegram Login Widget недоступен. Проверьте username бота и доступ к telegram.org.', telegram_auth_access_denied: 'Доступ запрещен.', active: 'Активна', inactive: 'Не активна', no_active_subscription: 'Нет активной подписки', not_available: 'N/A', no_plans: 'Тарифы не настроены.', access_period: 'Срок доступа', choose_plan_first: 'Сначала выберите срок подписки.', no_methods: 'Нет доступных способов оплаты.', payment_not_created: 'Платеж еще не создан.', invoice_sent: 'Счет отправлен в чат с ботом. После оплаты вернитесь сюда и проверьте статус.', payment_created: 'Платеж создан. Откройте оплату, завершите ее у провайдера и затем проверьте статус.', updating: 'Обновляю данные', choose_plan_toast: 'Выберите срок подписки', choose_method_toast: 'Выберите способ оплаты', create_payment_toast: 'Создайте платеж', payment_confirmed: 'Оплата подтверждена', payment_pending: 'Платеж пока не подтвержден', link_copied: 'Ссылка скопирована', no_link: 'Ссылка пока недоступна', payment_error: 'Ошибка оплаты', privacy_policy: 'Политика конфиденциальности', user_agreement: 'Пользовательское соглашение', promo_title: 'Промокод', promo_caption: 'Введите код, чтобы начислить бонусные дни.', promo_placeholder: 'PROMO2026', apply_promo: 'Применить', promo_applied: 'Промокод применен', promo_applied_ok: 'Промокод применен, всё ок.', promo_empty: 'Введите промокод', promo_invalid: 'Промокод не найден или недействителен.', referral_title: 'Пригласить друга', referral_caption: 'Поделитесь кодом или ссылкой и получайте бонусы.', referral_code: 'Ваш код', invited_friends: 'Приглашено', purchased_friends: 'Оплатили', copy_code: 'Скопировать код', copy_webapp_link: 'Скопировать ссылку Web App', copy_bot_link: 'Скопировать ссылку бота', referral_copied: 'Ссылка скопирована', code_copied: 'Код скопирован', referral_site_label: 'Пригласить через сайт', referral_telegram_label: 'Пригласить через телеграм', referral_bonus_title: 'Бонусы', referral_bonus_pair: 'Вы: {inviter} дн. / друг: {friend} дн.', referral_no_bonuses: 'Бонусы пока не настроены.' }, en: { page_title: 'My subscription', loading: 'Loading...', refresh: 'Refresh', logout: 'Log out', subscription: 'Subscription', subscription_subtitle: 'Your connection link and access time', ends_at: 'Ends at', traffic: 'Traffic', connect: 'Connect', copy_link: 'Copy link', extend_subscription: 'Renew subscription/Add days', payment_title: 'Subscription payment', payment_caption: 'Choose a period, payment method, and create a payment.', close: 'Close', close_payment: 'Close payment', payment_steps: 'Payment steps', step_plan: 'Period', step_method: 'Method', step_result: 'Payment', select_period: 'Select period', select_method: 'Select payment method', payment_status: 'Payment status', choose_payment_method: 'Choose payment method', back: 'Back', create_payment: 'Create payment', open_payment: 'Open payment', check_payment: 'Check payment', choose_other_method: 'Choose another method', support: 'Support', account_title: 'Account', account_caption: 'Sign-in methods', email_label: 'Email', telegram_label: 'Telegram', linked: 'Linked', not_linked: 'Not linked', email_login_tab: 'Email', telegram_login_tab: 'Telegram', email_placeholder: 'mail@example.com', code_placeholder: '000000', send_code: 'Send code', resend_code: 'Send again', confirm: 'Confirm', login: 'Log in', email_code_title: 'Confirm login', email_code_caption: 'Enter the 6-digit code from the email.', email_code_aria: 'Verification code', email_auth_disabled: 'Email sign-in is not configured yet.', email_required: 'Enter your email', email_invalid: 'Enter a valid email address', email_code_sending: 'Sending code...', email_code_sent: 'Code sent to email', email_code_send_failed: 'Could not send code', email_code_invalid: 'Invalid code', email_code_expired: 'Code expired', email_rate_limited: 'Try again in {seconds} sec.', email_linked: 'Email linked', telegram_linked: 'Telegram linked', account_merge_conflict: 'This account is already linked to different data.', telegram_auth: 'Telegram auth', telegram_auth_verifying: 'Verifying login...', telegram_auth_failed: 'Could not verify Telegram login. Try again.', telegram_auth_unavailable: 'Telegram Login Widget is unavailable. Check the bot username and access to telegram.org.', telegram_auth_access_denied: 'Access denied.', active: 'Active', inactive: 'Inactive', no_active_subscription: 'No active subscription', not_available: 'N/A', no_plans: 'Plans are not configured.', access_period: 'Access period', choose_plan_first: 'Choose a subscription period first.', no_methods: 'No payment methods available.', payment_not_created: 'Payment has not been created yet.', invoice_sent: 'The invoice was sent to the bot chat. Pay it, then return here and check the status.', payment_created: 'Payment was created. Open it, complete payment with the provider, then check the status.', updating: 'Refreshing data', choose_plan_toast: 'Choose a subscription period', choose_method_toast: 'Choose a payment method', create_payment_toast: 'Create a payment', payment_confirmed: 'Payment confirmed', payment_pending: 'Payment is not confirmed yet', link_copied: 'Link copied', no_link: 'Link is not available yet', payment_error: 'Payment error', privacy_policy: 'Privacy policy', user_agreement: 'User agreement', promo_title: 'Promo code', promo_caption: 'Enter a code to add bonus days.', promo_placeholder: 'PROMO2026', apply_promo: 'Apply', promo_applied: 'Promo code applied', promo_applied_ok: 'Promo code applied, all good.', promo_empty: 'Enter promo code', promo_invalid: 'Promo code was not found or is not valid.', referral_title: 'Invite friend', referral_caption: 'Share your code or link and receive bonuses.', referral_code: 'Your code', invited_friends: 'Invited', purchased_friends: 'Purchased', copy_code: 'Copy code', copy_webapp_link: 'Copy Web App link', copy_bot_link: 'Copy bot link', referral_copied: 'Link copied', code_copied: 'Code copied', referral_site_label: 'Invite via site', referral_telegram_label: 'Invite via Telegram', referral_bonus_title: 'Bonuses', referral_bonus_pair: 'You: {inviter} d. / friend: {friend} d.', referral_no_bonuses: 'Bonuses are not configured yet.' } }; const accent = CFG.primaryColor || '#00fe7a'; document.documentElement.style.setProperty('--accent', accent); setBrand(); applyI18n(); applyLegalLinks(); if (CFG.supportUrl) { const support = document.getElementById('support-link'); support.href = CFG.supportUrl; support.classList.remove('hidden'); } if (tg) { try { tg.ready(); tg.expand(); const canSetColors = typeof tg.isVersionAtLeast === 'function' && tg.isVersionAtLeast('6.1'); if (canSetColors) { tg.setHeaderColor('#05070a'); tg.setBackgroundColor('#05070a'); } } catch (e) { } } bindEmailLoginInput(); bindEmailCodeInput(); bindPromoInput(); document.addEventListener('keydown', event => { if (event.key !== 'Escape') return; if (state.emailLoginCodeModalOpen) { closeEmailLoginCodeModal(); } else if (state.promoModalOpen) { closePromoModal(); } else if (state.referralModalOpen) { closeReferralModal(); } else if (state.paymentFlowOpen) { closePaymentFlow(); } }); boot(); async function boot() { showLoader(); if (MOCK) { await loadData(); return; } const widgetAuthData = readTelegramLoginWidgetAuthData(); if (widgetAuthData) { const authenticated = await finalizeTelegramAuth(widgetAuthData); if (authenticated) { return; } clearToken(); startExternalAuth({resetStatus: false}); return; } if (tg && tg.initData) { try { const authenticated = await finalizeTelegramAuth(tg.initData, 'init_data'); if (authenticated) { return; } } catch (e) { } } if (state.token) { try { await loadData(); return; } catch (e) { clearToken(); } } await startExternalAuth(); } function readTelegramLoginWidgetAuthData() { const query = new URLSearchParams(window.location.search); const keys = ['id', 'first_name', 'last_name', 'username', 'photo_url', 'auth_date', 'hash']; const authData = {}; let hasAuthValue = false; keys.forEach(key => { if (!query.has(key)) return; authData[key] = query.get(key) || ''; hasAuthValue = true; }); if (!hasAuthValue || !authData.id || !authData.auth_date || !authData.hash) { return null; } return authData; } function readReferralParam() { const query = new URLSearchParams(window.location.search); const fromQuery = query.get('ref') || query.get('start') || query.get('start_param') || ''; const fromTelegram = tg && tg.initDataUnsafe && tg.initDataUnsafe.start_param ? tg.initDataUnsafe.start_param : ''; const value = String(fromTelegram || fromQuery || '').trim(); if (value) { localStorage.setItem('rw_webapp_referral', value); return value; } return localStorage.getItem('rw_webapp_referral') || ''; } function clearTelegramLoginWidgetQuery() { const url = new URL(window.location.href); const keys = ['id', 'first_name', 'last_name', 'username', 'photo_url', 'auth_date', 'hash']; keys.forEach(key => url.searchParams.delete(key)); if (window.history && window.history.replaceState) { window.history.replaceState({}, document.title, url.pathname + url.search + url.hash); } } async function finalizeTelegramAuth(authData, source = 'auth_data') { if (state.authInProgress) return false; state.authInProgress = true; setAuthStatus(t('telegram_auth_verifying')); try { const payload = source === 'init_data' ? {init_data: authData} : {auth_data: authData}; if (state.referralParam) payload.referral_code = state.referralParam; const response = await fetch(CFG.apiBase + '/auth/token', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload) }); const data = await response.json(); if (response.ok && data.ok && data.token) { setToken(data.token); clearTelegramLoginWidgetQuery(); try { setAuthStatus(''); await loadData(); return true; } catch (e) { clearToken(); setAuthStatus(t('telegram_auth_failed'), true); return false; } } const errorKey = data.error === 'banned' || data.error === 'access_denied' ? 'telegram_auth_access_denied' : 'telegram_auth_failed'; setAuthStatus(t(errorKey), true); } catch (e) { setAuthStatus(t('telegram_auth_failed'), true); } finally { state.authInProgress = false; } clearTelegramLoginWidgetQuery(); return false; } async function handleTelegramLoginWidgetAuth(user) { if (!user) { setAuthStatus(t('telegram_auth_failed'), true); return; } await finalizeTelegramAuth(user, 'auth_data'); } function setAuthMode(mode) { state.authMode = mode === 'telegram' ? 'telegram' : 'email'; renderAuthMode(); } function renderAuthMode() { const emailTab = document.getElementById('email-auth-tab'); const telegramTab = document.getElementById('telegram-auth-tab'); const emailPane = document.getElementById('email-login-pane'); const telegramPane = document.getElementById('telegram-login-pane'); const emailEnabled = CFG.emailAuthEnabled !== false; if (!emailEnabled && state.authMode === 'email') { state.authMode = 'telegram'; } emailTab.classList.toggle('active', state.authMode === 'email'); telegramTab.classList.toggle('active', state.authMode === 'telegram'); emailPane.classList.toggle('hidden', state.authMode !== 'email'); telegramPane.classList.toggle('hidden', state.authMode !== 'telegram'); emailTab.disabled = !emailEnabled; if (state.authMode === 'telegram') { renderTelegramLoginWidget(); } else if (!emailEnabled) { setAuthStatus(t('email_auth_disabled'), true); } } function renderTelegramLoginWidget() { const container = document.getElementById('telegram-login-widget'); if (!container) return; container.innerHTML = ''; const botUsername = String(CFG.telegramLoginBotUsername || '').trim(); if (!botUsername) { setAuthStatus(t('telegram_auth_unavailable'), true); return; } if (typeof window.onTelegramAuth !== 'function') { window.onTelegramAuth = async function(user) { await handleTelegramLoginWidgetAuth(user); }; } const script = document.createElement('script'); script.async = true; script.src = TELEGRAM_LOGIN_WIDGET_URL; script.setAttribute('data-telegram-login', botUsername); script.setAttribute('data-size', 'large'); script.setAttribute('data-userpic', 'false'); script.setAttribute('data-request-access', 'write'); script.setAttribute('data-onauth', 'onTelegramAuth(user)'); script.onerror = () => setAuthStatus(t('telegram_auth_unavailable'), true); container.appendChild(script); } function bindEmailLoginInput() { const input = document.getElementById('email-login-input'); if (!input) return; input.addEventListener('keydown', event => { if (event.key !== 'Enter') return; event.preventDefault(); requestEmailLoginCode(); }); input.addEventListener('input', () => { if (input.getAttribute('aria-invalid') !== 'true') return; input.removeAttribute('aria-invalid'); setAuthStatus(''); }); } function bindEmailCodeInput() { const input = document.getElementById('email-login-code-input'); if (!input) return; input.addEventListener('input', () => { input.value = sanitizeCode(input.value); updateEmailLoginCodeSlots(); }); input.addEventListener('focus', updateEmailLoginCodeSlots); input.addEventListener('blur', updateEmailLoginCodeSlots); input.addEventListener('keydown', event => { if (event.key === 'Enter') { event.preventDefault(); if (sanitizeCode(input.value).length === 6 && !state.emailLoginVerifying) { verifyEmailLoginCode(); } } }); const field = input.closest('.otp-field'); if (field) { field.addEventListener('click', () => input.focus()); } } function sanitizeCode(value) { return String(value || '').replace(/\D/g, '').slice(0, 6); } function isValidEmail(value) { const email = normalizeEmail(value); return Boolean(email && email.length <= 254 && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)); } function readValidEmailLoginInput() { const input = document.getElementById('email-login-input'); const email = normalizeEmail(input && input.value); if (!email || !isValidEmail(email)) { if (input) { input.setAttribute('aria-invalid', 'true'); input.focus(); } setAuthStatus(t(email ? 'email_invalid' : 'email_required'), true); return ''; } if (input) { input.value = email; input.removeAttribute('aria-invalid'); } return email; } function openEmailLoginCodeModal(email, message, isError = false) { state.emailLoginEmail = email; state.emailLoginCodeModalOpen = true; const input = document.getElementById('email-login-code-input'); if (input) input.value = ''; renderEmailLoginCodeModal(); setEmailCodeStatus(message || t('email_code_sent'), isError); window.setTimeout(() => { const codeInput = document.getElementById('email-login-code-input'); if (state.emailLoginCodeModalOpen && codeInput) codeInput.focus(); }, 80); } function closeEmailLoginCodeModal() { state.emailLoginCodeModalOpen = false; const input = document.getElementById('email-login-code-input'); if (input) input.value = ''; setEmailCodeStatus(''); renderEmailLoginCodeModal(); } function renderEmailLoginCodeModal() { const modal = document.getElementById('email-code-modal'); if (!modal) return; if (!state.emailLoginCodeModalOpen) { modal.classList.remove('show'); syncModalLock(); window.setTimeout(() => { if (!state.emailLoginCodeModalOpen) modal.classList.add('hidden'); }, 180); return; } const address = document.getElementById('email-code-address'); if (address) address.textContent = state.emailLoginEmail || '...'; modal.classList.remove('hidden'); syncModalLock(); applyI18n(modal); updateEmailLoginCodeSlots(); window.requestAnimationFrame(() => modal.classList.add('show')); } function updateEmailLoginCodeSlots() { const input = document.getElementById('email-login-code-input'); const value = sanitizeCode(input && input.value); if (input && input.value !== value) input.value = value; for (let index = 0; index < 6; index += 1) { const slot = document.getElementById('email-code-slot-' + index); if (!slot) continue; const character = value[index] || ''; slot.textContent = character; slot.classList.toggle('filled', Boolean(character)); slot.classList.toggle( 'active', state.emailLoginCodeModalOpen && document.activeElement === input && index === Math.min(value.length, 5) ); } const verifyButton = document.getElementById('email-login-verify-btn'); if (verifyButton) { verifyButton.disabled = value.length !== 6 || state.emailLoginVerifying || state.emailLoginPending; } const resendButton = document.getElementById('email-login-resend-btn'); if (resendButton) { resendButton.disabled = state.emailLoginPending || state.emailLoginResending; } } async function requestEmailLoginCode() { if (state.emailLoginPending) return; if (CFG.emailAuthEnabled === false) { setAuthStatus(t('email_auth_disabled'), true); return; } const email = readValidEmailLoginInput(); if (!email) { return; } state.emailLoginPending = true; setButtonBusy('email-login-send-btn', true); openEmailLoginCodeModal(email, t('email_code_sending')); setAuthStatus(''); try { const data = await publicApi('/auth/email/request', { email, language: getLanguage() }); if (!data.ok) throw data; setEmailCodeStatus(t('email_code_sent')); } catch (e) { const message = emailErrorMessage(e, 'email_code_send_failed'); if (e && e.error === 'invalid_email') { closeEmailLoginCodeModal(); const input = document.getElementById('email-login-input'); if (input) input.setAttribute('aria-invalid', 'true'); setAuthStatus(message, true); } else { setEmailCodeStatus(message, true); } } finally { state.emailLoginPending = false; setButtonBusy('email-login-send-btn', false); updateEmailLoginCodeSlots(); } } async function resendEmailLoginCode() { if (state.emailLoginPending || state.emailLoginResending) return; const email = state.emailLoginEmail || normalizeEmail(document.getElementById('email-login-input').value); if (!email || !isValidEmail(email)) { setEmailCodeStatus(t(email ? 'email_invalid' : 'email_required'), true); return; } state.emailLoginResending = true; updateEmailLoginCodeSlots(); setEmailCodeStatus(t('email_code_sending')); try { const data = await publicApi('/auth/email/request', { email, language: getLanguage() }); if (!data.ok) throw data; state.emailLoginEmail = email; const input = document.getElementById('email-login-code-input'); if (input) input.value = ''; setEmailCodeStatus(t('email_code_sent')); } catch (e) { setEmailCodeStatus(emailErrorMessage(e, 'email_code_send_failed'), true); } finally { state.emailLoginResending = false; updateEmailLoginCodeSlots(); } } async function verifyEmailLoginCode() { const email = state.emailLoginEmail || normalizeEmail(document.getElementById('email-login-input').value); const input = document.getElementById('email-login-code-input'); const code = sanitizeCode(input && input.value); if (!email || code.length !== 6) { setEmailCodeStatus(t('email_code_invalid'), true); updateEmailLoginCodeSlots(); return; } state.emailLoginVerifying = true; updateEmailLoginCodeSlots(); setEmailCodeStatus(t('telegram_auth_verifying')); try { const data = await publicApi('/auth/email/verify', { email, code, referral_code: state.referralParam || '' }); if (!data.ok || !data.token) throw data; setToken(data.token); closeEmailLoginCodeModal(); await loadData(); } catch (e) { clearToken(); setEmailCodeStatus(emailErrorMessage(e, 'email_code_invalid'), true); } finally { state.emailLoginVerifying = false; updateEmailLoginCodeSlots(); } } function startExternalAuth(options = {}) { const resetStatus = options.resetStatus !== false; showLogin(); if (resetStatus) { setAuthStatus(''); } renderAuthMode(); } async function loadData() { const previousMonths = state.selectedPlan && state.selectedPlan.months; const data = await api('/me'); if (!data.ok) throw new Error(data.error || 'load failed'); state.data = data; setBrand(); applyI18n(); applyLegalLinks(); const plans = data.plans || []; state.selectedPlan = plans.find(plan => plan.months === previousMonths) || plans[0] || null; state.selectedMethod = null; state.payment = null; state.paymentStep = 'plan'; state.telegramLinkRendered = false; render(); showApp(); } async function reloadData() { showToast(t('updating')); await loadData(); } function render() { renderSubscription(state.data.subscription); renderAccount(state.data.user || {}); renderReferral(state.data.referral || {}); renderPaymentFlow(); } function renderSubscription(sub) { const badge = document.getElementById('status-badge'); badge.textContent = sub.active ? t('active') : t('inactive'); badge.classList.toggle('off', !sub.active); document.getElementById('remaining').textContent = sub.remaining_text || t('no_active_subscription'); document.getElementById('end-date').textContent = sub.end_date_text || t('not_available'); document.getElementById('traffic').textContent = (sub.traffic_used || t('not_available')) + ' / ' + (sub.traffic_limit || t('not_available')); document.getElementById('connect-actions').classList.toggle('hidden', !sub.connect_url && !sub.config_link); } function renderAccount(user) { const emailLinked = Boolean(user.email && user.email_verified); const telegramLinked = Boolean(user.telegram_linked); document.getElementById('account-email').textContent = emailLinked ? user.email : t('not_linked'); document.getElementById('account-telegram').textContent = telegramLinked ? (user.telegram_id ? String(user.telegram_id) : t('linked')) : t('not_linked'); const emailBox = document.getElementById('email-link-box'); const telegramBox = document.getElementById('telegram-link-box'); emailBox.classList.toggle('hidden', emailLinked || !state.data.settings.email_auth_enabled); telegramBox.classList.toggle('hidden', telegramLinked); if (!telegramLinked) { renderTelegramLinkWidget(); } else { state.telegramLinkRendered = false; } } function openPromoModal() { state.promoModalOpen = true; clearPromoStatus(); renderPromoModal(); window.setTimeout(() => { const input = document.getElementById('promo-code-input'); if (input) input.focus(); }, 80); } function closePromoModal() { state.promoModalOpen = false; clearPromoStatus(); renderPromoModal(); } function renderPromoModal() { const panel = document.querySelector('.promo-panel'); if (!panel) return; panel.classList.toggle('hidden', !state.promoModalOpen); panel.classList.toggle('modal-panel', state.promoModalOpen); renderToolsBackdrop(); syncModalLock(); renderPromoStatus(); if (state.promoModalOpen && !panel.querySelector('[data-modal-close="promo"]')) { const head = panel.querySelector('.panel-head'); if (head) { head.insertAdjacentHTML( 'beforeend', '' ); applyI18n(panel); } } } function bindPromoInput() { const input = document.getElementById('promo-code-input'); if (!input) return; input.addEventListener('input', () => { if (input.getAttribute('aria-invalid') === 'true') { input.removeAttribute('aria-invalid'); } if (state.promoStatusText) { clearPromoStatus(); } }); input.addEventListener('keydown', event => { if (event.key !== 'Enter') return; event.preventDefault(); applyPromoCode(); }); } function setPromoStatus(message, type = 'error') { state.promoStatusType = type === 'success' ? 'success' : 'error'; state.promoStatusText = normalizeStatusText(message || ''); renderPromoStatus(); } function clearPromoStatus() { state.promoStatusType = ''; state.promoStatusText = ''; renderPromoStatus(); } function renderPromoStatus() { const status = document.getElementById('promo-status'); if (!status) return; if (!state.promoStatusText) { status.textContent = ''; status.classList.add('hidden'); status.classList.remove('error', 'success'); return; } status.textContent = state.promoStatusText; status.classList.remove('hidden'); status.classList.toggle('error', state.promoStatusType === 'error'); status.classList.toggle('success', state.promoStatusType === 'success'); } function normalizeStatusText(value) { const text = String(value || ''); const wrapper = document.createElement('div'); wrapper.innerHTML = text; return (wrapper.textContent || wrapper.innerText || '').trim(); } function openReferralModal() { state.referralModalOpen = true; renderReferral(state.data && state.data.referral || {}); } function closeReferralModal() { state.referralModalOpen = false; renderReferral(state.data && state.data.referral || {}); } function closeToolModals() { state.promoModalOpen = false; state.referralModalOpen = false; clearPromoStatus(); renderPromoModal(); renderReferral(state.data && state.data.referral || {}); } function renderToolsBackdrop() { const backdrop = document.getElementById('tools-modal-backdrop'); if (!backdrop) return; backdrop.classList.toggle('hidden', !(state.promoModalOpen || state.referralModalOpen)); } function renderReferral(referral) { const panel = document.querySelector('.referral-panel'); if (!panel) return; panel.classList.toggle('hidden', !state.referralModalOpen); panel.classList.toggle('modal-panel', state.referralModalOpen); renderToolsBackdrop(); syncModalLock(); if (!state.referralModalOpen) return; const bonusRows = (referral.bonus_details || []).map(item => `