/* 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' }, data: { ok: true, user: { id: 100200300, username: 'preview', 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 / СБП'} ], settings: { support_url: 'https://t.me/support', traffic_mode: false } } }; /* 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, payment: null, paymentFlowOpen: false, paymentStep: 'plan', creatingPayment: false, authInProgress: 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_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: 'Поддержка', 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: 'Пользовательское соглашение' }, 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_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', 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' } }; 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) { } } document.addEventListener('keydown', event => { if (event.key === 'Escape' && 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 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}; 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 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 startExternalAuth(options = {}) { const resetStatus = options.resetStatus !== false; showLogin(); if (resetStatus) { setAuthStatus(''); } renderTelegramLoginWidget(); } 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'; render(); showApp(); } async function reloadData() { showToast(t('updating')); await loadData(); } function render() { renderSubscription(state.data.subscription); 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 togglePaymentFlow() { if (state.paymentFlowOpen) { closePaymentFlow(); return; } state.paymentFlowOpen = true; state.paymentStep = 'plan'; state.payment = null; state.selectedMethod = null; renderPaymentFlow(); } function closePaymentFlow() { state.paymentFlowOpen = false; state.paymentStep = 'plan'; state.payment = null; state.selectedMethod = null; renderPaymentFlow(); } function renderPaymentFlow() { const modal = document.getElementById('payment-modal'); if (!state.paymentFlowOpen) { modal.classList.remove('show'); document.body.classList.remove('modal-open'); window.setTimeout(() => { if (!state.paymentFlowOpen) modal.classList.add('hidden'); }, 180); return; } modal.classList.remove('hidden'); document.body.classList.add('modal-open'); window.requestAnimationFrame(() => modal.classList.add('show')); applyI18n(modal); renderStepState(); renderPlans(state.data.plans || []); renderMethods(); renderPaymentResult(); } function renderStepState() { const order = ['plan', 'method', 'result']; order.forEach((step, index) => { const node = document.getElementById('step-' + step); const currentIndex = order.indexOf(state.paymentStep); node.classList.toggle('active', state.paymentStep === step); node.classList.toggle('done', currentIndex > index); }); document.getElementById('plan-step').classList.toggle('hidden', state.paymentStep !== 'plan'); document.getElementById('method-step').classList.toggle('hidden', state.paymentStep !== 'method'); document.getElementById('result-step').classList.toggle('hidden', state.paymentStep !== 'result'); } function renderPlans(plans) { const wrap = document.getElementById('plans'); const nextBtn = document.getElementById('to-methods-btn'); if (!plans.length) { wrap.innerHTML = '