diff --git a/app/webapi/routes/miniapp.py b/app/webapi/routes/miniapp.py index 136c6864..5c4d596c 100644 --- a/app/webapi/routes/miniapp.py +++ b/app/webapi/routes/miniapp.py @@ -3,13 +3,12 @@ from __future__ import annotations import logging from typing import Any, Dict, List, Optional -from sqlalchemy import select from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from app.config import settings from app.database.crud.user import get_user_by_telegram_id -from app.database.models import Subscription, Transaction +from app.database.models import Subscription from app.services.subscription_service import SubscriptionService from app.utils.telegram_webapp import ( TelegramWebAppAuthError, @@ -196,32 +195,8 @@ async def get_subscription_details( traffic_limit_label=_format_limit_label(traffic_limit), lifetime_used_traffic_gb=lifetime_used, has_active_subscription=status_actual in {"active", "trial"}, - balance_kopeks=user.balance_kopeks, - balance_rubles=round((user.balance_kopeks or 0) / 100, 2), ) - transactions_result = await db.execute( - select(Transaction) - .where(Transaction.user_id == user.id) - .order_by(Transaction.created_at.desc()) - .limit(20) - ) - transactions = transactions_result.scalars().all() - - serialized_transactions = [ - { - "id": transaction.id, - "type": transaction.type, - "amount_kopeks": transaction.amount_kopeks, - "amount_rubles": round(transaction.amount_kopeks / 100, 2), - "description": transaction.description, - "is_completed": transaction.is_completed, - "created_at": transaction.created_at, - "payment_method": transaction.payment_method, - } - for transaction in transactions - ] - return MiniAppSubscriptionResponse( subscription_id=subscription.id, remnawave_short_uuid=subscription.remnawave_short_uuid, @@ -234,9 +209,5 @@ async def get_subscription_details( happ=links_payload.get("happ"), happ_link=links_payload.get("happ_link"), happ_crypto_link=links_payload.get("happ_crypto_link"), - happ_cryptolink_redirect_template=settings.get_happ_cryptolink_redirect_template(), - transactions=serialized_transactions, - balance_kopeks=user.balance_kopeks, - balance_rubles=round((user.balance_kopeks or 0) / 100, 2), ) diff --git a/app/webapi/schemas/miniapp.py b/app/webapi/schemas/miniapp.py index 911202fb..9c5a4308 100644 --- a/app/webapi/schemas/miniapp.py +++ b/app/webapi/schemas/miniapp.py @@ -29,19 +29,6 @@ class MiniAppSubscriptionUser(BaseModel): traffic_limit_label: str lifetime_used_traffic_gb: float = 0.0 has_active_subscription: bool = False - balance_kopeks: int = 0 - balance_rubles: float = 0.0 - - -class MiniAppTransaction(BaseModel): - id: int - type: str - amount_kopeks: int - amount_rubles: float - description: Optional[str] = None - is_completed: bool = True - created_at: datetime - payment_method: Optional[str] = None class MiniAppSubscriptionResponse(BaseModel): @@ -57,10 +44,4 @@ class MiniAppSubscriptionResponse(BaseModel): happ: Optional[Dict[str, Any]] = None happ_link: Optional[str] = None happ_crypto_link: Optional[str] = None - happ_cryptolink_redirect_template: Optional[str] = None - transactions: List[MiniAppTransaction] = Field(default_factory=list) - balance_kopeks: int = 0 - balance_rubles: float = 0.0 - - diff --git a/miniapp/index.html b/miniapp/index.html index 4a4dadaa..ba983c31 100644 --- a/miniapp/index.html +++ b/miniapp/index.html @@ -22,7 +22,7 @@ --tg-theme-button-color: #2481cc; --tg-theme-button-text-color: #ffffff; --tg-theme-secondary-bg-color: #f0f0f0; - + --primary: var(--tg-theme-button-color); --text-primary: var(--tg-theme-text-color); --text-secondary: var(--tg-theme-hint-color); @@ -44,25 +44,16 @@ } .container { - max-width: 520px; + max-width: 480px; margin: 0 auto; } /* Header */ .header { - display: flex; - flex-direction: column; - gap: 12px; + text-align: center; margin-bottom: 24px; } - .header-top { - display: flex; - justify-content: space-between; - align-items: flex-start; - gap: 12px; - } - .logo { font-size: 28px; font-weight: 700; @@ -75,36 +66,6 @@ color: var(--text-secondary); } - .language-selector { - display: inline-flex; - background: var(--bg-secondary); - border-radius: 999px; - padding: 4px; - gap: 4px; - } - - .lang-btn { - border: none; - background: transparent; - color: var(--text-secondary); - font-size: 13px; - font-weight: 600; - padding: 6px 12px; - border-radius: 999px; - cursor: pointer; - transition: all 0.2s ease; - } - - .lang-btn.active { - background: var(--primary); - color: #ffffff; - box-shadow: 0 2px 6px rgba(36, 129, 204, 0.35); - } - - .lang-btn:active { - transform: scale(0.96); - } - /* Loading State */ .loading { text-align: center; @@ -264,101 +225,6 @@ text-align: right; } - /* Balance */ - .balance-amount { - font-size: 28px; - font-weight: 700; - color: var(--primary); - } - - .balance-hint { - margin-top: 6px; - font-size: 13px; - color: var(--text-secondary); - } - - /* History */ - .history-list { - display: flex; - flex-direction: column; - gap: 10px; - } - - .history-item { - display: flex; - justify-content: space-between; - align-items: center; - background: white; - border-radius: 10px; - padding: 12px; - } - - .history-item__info { - display: flex; - flex-direction: column; - gap: 4px; - font-size: 13px; - } - - .history-item__title { - font-weight: 600; - color: var(--text-primary); - } - - .history-item__date { - color: var(--text-secondary); - font-size: 12px; - } - - .history-item__amount { - font-weight: 700; - font-size: 14px; - } - - .history-item__amount.positive { - color: #0f5132; - } - - .history-item__amount.negative { - color: #842029; - } - - .empty-placeholder { - font-size: 13px; - color: var(--text-secondary); - background: white; - border-radius: 10px; - padding: 16px; - text-align: center; - } - - /* Servers */ - .server-list { - display: flex; - flex-direction: column; - gap: 8px; - } - - .server-item { - background: white; - border-radius: 10px; - padding: 12px; - font-size: 13px; - display: flex; - justify-content: space-between; - align-items: center; - color: var(--text-primary); - } - - .server-item__name { - font-weight: 600; - } - - .server-item__status { - color: var(--text-secondary); - font-size: 12px; - } - /* Buttons */ .btn { width: 100%; @@ -569,29 +435,21 @@
-
-
- -
Secure & Fast Connection
-
-
- - -
-
+ +
Secure & Fast Connection
-
Loading your subscription...
+
Loading your subscription...
@@ -609,67 +467,48 @@
-
-
Days Left
+
Days Left
-
-
Servers
+
Servers
- Expires + Expires -
- Traffic Used + Traffic Used -
- Traffic Limit + Traffic Limit -
- -
-
Balance
-
-
Available funds for subscription payments
-
- - -
-
Connected Servers
-
-
- - -
-
Latest Transactions
-
-
-
-
Installation Guide
- +
Installation Guide
+
@@ -708,224 +547,10 @@ }); } - const fallbackLanguage = 'en'; - const supportedLanguages = ['en', 'ru']; - - const translations = { - en: { - header: { - title: 'RemnaWave VPN', - subtitle: 'Secure & Fast Connection', - }, - loading: { - message: 'Loading your subscription...' - }, - errors: { - defaultTitle: 'Subscription Not Found', - defaultText: 'Please contact support to activate your subscription', - authTitle: 'Authorization Error', - authText: 'Authorization failed. Please open the mini app from Telegram.' - }, - stats: { - daysLeft: 'Days Left', - servers: 'Servers', - }, - info: { - expires: 'Expires', - trafficUsed: 'Traffic Used', - trafficLimit: 'Traffic Limit', - }, - balance: { - title: 'Balance', - hint: 'Available funds for subscription payments' - }, - servers: { - title: 'Connected Servers', - empty: 'No connected servers yet' - }, - history: { - title: 'Latest Transactions', - empty: 'There are no transactions yet' - }, - buttons: { - connect: 'Connect to VPN', - copy: 'Copy Subscription Link' - }, - apps: { - title: 'Installation Guide', - download: 'Download & Install', - open: 'Open the App', - import: 'Import Subscription', - manual: 'Configure Manually', - copy: 'Copy Link', - instructions: 'Follow the steps below to finish the setup', - empty: 'No installation guide available for this platform yet.', - recommended: 'Recommended' - }, - status: { - active: 'Active', - expired: 'Expired', - trial: 'Trial', - disabled: 'Disabled', - unknown: 'Unknown' - }, - popup: { - copiedTitle: 'Copied', - copiedText: 'Subscription link copied to clipboard', - copyFailedTitle: 'Copy failed', - copyFailedText: 'Unable to copy the subscription link automatically. Please copy it manually.' - }, - traffic: { - unlimited: 'Unlimited' - } - }, - ru: { - header: { - title: 'RemnaWave VPN', - subtitle: 'Безопасное и быстрое подключение', - }, - loading: { - message: 'Загружаем данные подписки...' - }, - errors: { - defaultTitle: 'Подписка не найдена', - defaultText: 'Свяжитесь с поддержкой для активации подписки', - authTitle: 'Ошибка авторизации', - authText: 'Не удалось авторизоваться. Откройте мини-приложение из Telegram.' - }, - stats: { - daysLeft: 'Дней осталось', - servers: 'Серверы' - }, - info: { - expires: 'Истекает', - trafficUsed: 'Израсходовано трафика', - trafficLimit: 'Лимит трафика' - }, - balance: { - title: 'Баланс', - hint: 'Доступные средства для оплаты подписки' - }, - servers: { - title: 'Подключенные серверы', - empty: 'Подключенных серверов пока нет' - }, - history: { - title: 'История операций', - empty: 'Операций пока нет' - }, - buttons: { - connect: 'Подключиться', - copy: 'Скопировать ссылку подписки' - }, - apps: { - title: 'Инструкция по установке', - download: 'Скачайте и установите', - open: 'Откройте приложение', - import: 'Импортируйте подписку', - manual: 'Настройте вручную', - copy: 'Скопировать ссылку', - instructions: 'Следуйте шагам, чтобы завершить настройку', - empty: 'Инструкция для этой платформы пока недоступна.', - recommended: 'Рекомендуем' - }, - status: { - active: 'Активна', - expired: 'Истекла', - trial: 'Пробная', - disabled: 'Выключена', - unknown: 'Неизвестно' - }, - popup: { - copiedTitle: 'Скопировано', - copiedText: 'Ссылка на подписку скопирована', - copyFailedTitle: 'Не удалось скопировать', - copyFailedText: 'Не удалось скопировать ссылку автоматически. Скопируйте её вручную.' - }, - traffic: { - unlimited: 'Безлимит' - } - } - }; - let userData = null; let appsConfig = {}; let currentPlatform = 'android'; - let preferredLanguage = fallbackLanguage; - let redirectTemplate = null; - let redirectTarget = null; - - function getLocale() { - const mapping = { - ru: 'ru-RU', - en: 'en-US' - }; - return mapping[preferredLanguage] || mapping.en; - } - - function translate(key, fallback = '') { - const langPack = translations[preferredLanguage] || translations[fallbackLanguage]; - if (!key || typeof key !== 'string') { - return fallback; - } - - const parts = key.split('.'); - let value = langPack; - - for (const part of parts) { - if (value && typeof value === 'object' && part in value) { - value = value[part]; - } else { - value = null; - break; - } - } - - if (typeof value === 'string') { - return value; - } - - return fallback; - } - - function translateStatus(status) { - if (!status) { - return translate('status.unknown', 'Unknown'); - } - const normalized = status.toLowerCase(); - return translate(`status.${normalized}`, status.charAt(0).toUpperCase() + status.slice(1)); - } - - function applyTranslations() { - document.querySelectorAll('[data-i18n]').forEach(element => { - const key = element.dataset.i18n; - const translated = translate(key, element.textContent); - if (translated) { - element.textContent = translated; - } - }); - } - - function setLanguage(language, options = {}) { - const { skipRender = false } = options; - if (!supportedLanguages.includes(language)) { - language = fallbackLanguage; - } - preferredLanguage = language; - - document.querySelectorAll('.lang-btn').forEach(btn => { - btn.classList.toggle('active', btn.dataset.lang === preferredLanguage); - }); - - applyTranslations(); - - if (!skipRender) { - renderUserData(); - renderTransactionHistory(); - renderConnectedServers(); - renderApps(); - } - } + let preferredLanguage = 'en'; function createError(title, message, status) { const error = new Error(message || title); @@ -944,17 +569,12 @@ if (telegramUser?.language_code) { preferredLanguage = telegramUser.language_code.split('-')[0]; } - setLanguage(preferredLanguage, { skipRender: true }); await loadAppsConfig(); const initData = tg.initData || ''; if (!initData) { - throw createError( - translate('errors.authTitle', 'Authorization Error'), - translate('errors.authText', 'Missing Telegram init data'), - 401 - ); + throw createError('Authorization Error', 'Missing Telegram init data'); } const response = await fetch('/miniapp/subscription', { @@ -966,12 +586,10 @@ }); if (!response.ok) { - let title = response.status === 401 - ? translate('errors.authTitle', 'Authorization Error') - : translate('errors.defaultTitle', 'Subscription Not Found'); let detail = response.status === 401 - ? translate('errors.authText', 'Authorization failed. Please open the mini app from Telegram.') - : translate('errors.defaultText', 'Subscription not found'); + ? 'Authorization failed. Please open the mini app from Telegram.' + : 'Subscription not found'; + let title = response.status === 401 ? 'Authorization Error' : 'Subscription Not Found'; try { const errorPayload = await response.json(); @@ -988,14 +606,15 @@ userData = await response.json(); userData.subscriptionUrl = userData.subscription_url || null; userData.subscriptionCryptoLink = userData.subscription_crypto_link || null; - redirectTemplate = userData.happ_cryptolink_redirect_template || null; - redirectTarget = userData.happ_crypto_link || userData.subscriptionCryptoLink || null; if (userData?.user?.language) { preferredLanguage = userData.user.language; } - setLanguage(preferredLanguage); + renderUserData(); + detectPlatform(); + setActivePlatformButton(); + renderApps(); document.getElementById('loadingState').classList.add('hidden'); document.getElementById('mainContent').classList.remove('hidden'); @@ -1037,7 +656,7 @@ const knownStatuses = ['active', 'expired', 'trial', 'disabled']; const statusClass = knownStatuses.includes(statusValueRaw) ? statusValueRaw : 'unknown'; const statusBadge = document.getElementById('statusBadge'); - statusBadge.textContent = user.status_label || translateStatus(statusClass); + statusBadge.textContent = user.status_label || statusClass.charAt(0).toUpperCase() + statusClass.slice(1); statusBadge.className = `status-badge status-${statusClass}`; const expiresAt = user.expires_at ? new Date(user.expires_at) : null; @@ -1048,11 +667,7 @@ } document.getElementById('daysLeft').textContent = daysLeft; document.getElementById('expiresAt').textContent = expiresAt && !Number.isNaN(expiresAt.getTime()) - ? expiresAt.toLocaleDateString(getLocale(), { - year: 'numeric', - month: 'long', - day: 'numeric' - }) + ? expiresAt.toLocaleDateString() : '—'; const serversCount = userData.links?.length ?? userData.connected_squads?.length ?? 0; @@ -1063,7 +678,6 @@ document.getElementById('trafficLimit').textContent = user.traffic_limit_label || formatTrafficLimit(user.traffic_limit_gb); - renderBalance(); updateActionButtons(); } @@ -1109,13 +723,13 @@ const apps = getAppsForCurrentPlatform(); if (!apps.length) { - container.innerHTML = `
${translate('apps.empty', 'No installation guide available for this platform yet.')}
`; + container.innerHTML = '
No installation guide available for this platform yet.
'; return; } container.innerHTML = apps.map(app => { const iconChar = (app.name?.[0] || 'A').toUpperCase(); - const featuredBadge = app.isFeatured ? `${translate('apps.recommended', 'Recommended')}` : ''; + const featuredBadge = app.isFeatured ? 'Recommended' : ''; return `
@@ -1142,49 +756,42 @@
${stepNum++} - ${translate('apps.download', 'Download & Install')} + Download & Install
-
${app.installationStep.description || translate('apps.instructions', 'Follow the steps below to finish the setup')}
- ${renderStepButtons(app.installationStep.buttons)} + ${app.installationStep.description ? `
${getLocalizedText(app.installationStep.description)}
` : ''} + ${Array.isArray(app.installationStep.buttons) && app.installationStep.buttons.length ? ` +
+ ${app.installationStep.buttons.map(btn => ` + + ${getLocalizedText(btn.buttonText)} + + `).join('')} +
+ ` : ''}
`; } - if (app.openStep) { + if (app.addSubscriptionStep) { html += `
${stepNum++} - ${translate('apps.open', 'Open the App')} + Add Subscription
-
${app.openStep.description || ''}
- ${renderStepButtons(app.openStep.buttons)} +
${getLocalizedText(app.addSubscriptionStep.description)}
`; } - if (app.importStep) { + if (app.connectAndUseStep) { html += `
${stepNum++} - ${translate('apps.import', 'Import Subscription')} + Connect & Use
-
${app.importStep.description || ''}
- ${renderStepButtons(app.importStep.buttons)} -
- `; - } - - if (app.manualStep) { - html += ` -
-
- ${stepNum++} - ${translate('apps.manual', 'Configure Manually')} -
-
${app.manualStep.description || ''}
- ${renderStepButtons(app.manualStep.buttons)} +
${getLocalizedText(app.connectAndUseStep.description)}
`; } @@ -1192,18 +799,38 @@ return html; } - function renderStepButtons(buttons) { - if (!Array.isArray(buttons) || !buttons.length) { + function getLocalizedText(textObj) { + if (!textObj) { return ''; } + if (typeof textObj === 'string') { + return textObj; + } - const html = buttons.map(btn => { - const text = btn.text || translate('apps.copy', 'Copy Link'); - const url = btn.url || '#'; - return `${text}`; - }).join(''); + const telegramLang = tg.initDataUnsafe?.user?.language_code; + const preferenceOrder = [ + preferredLanguage, + preferredLanguage?.split('-')[0], + userData?.user?.language, + telegramLang, + telegramLang?.split('-')[0], + 'en', + 'ru' + ].filter(Boolean).map(lang => lang.toLowerCase()); - return `
${html}
`; + const seen = new Set(); + for (const lang of preferenceOrder) { + if (seen.has(lang)) { + continue; + } + seen.add(lang); + if (textObj[lang]) { + return textObj[lang]; + } + } + + const fallback = Object.values(textObj).find(value => typeof value === 'string' && value.trim().length); + return fallback || ''; } function formatTraffic(value) { @@ -1223,212 +850,25 @@ function formatTrafficLimit(limit) { const numeric = typeof limit === 'number' ? limit : Number.parseFloat(limit ?? '0'); if (!Number.isFinite(numeric) || numeric <= 0) { - return translate('traffic.unlimited', 'Unlimited'); + return 'Unlimited'; } return `${numeric.toFixed(0)} GB`; } - function formatCurrency(amount) { - if (typeof amount !== 'number' || Number.isNaN(amount)) { - return '—'; - } - try { - return new Intl.NumberFormat(getLocale(), { - style: 'currency', - currency: preferredLanguage === 'ru' ? 'RUB' : 'USD', - currencyDisplay: 'symbol', - minimumFractionDigits: 2, - maximumFractionDigits: 2, - }).format(amount); - } catch (error) { - const currencySymbol = preferredLanguage === 'ru' ? '₽' : '$'; - return `${amount.toFixed(2)} ${currencySymbol}`; - } - } - - function renderBalance() { - const balanceElement = document.getElementById('balanceAmount'); - if (!balanceElement) { - return; - } - - const balanceRubles = userData?.user?.balance_rubles ?? userData?.balance_rubles; - const balanceKopeks = userData?.user?.balance_kopeks ?? userData?.balance_kopeks; - let amountRub = balanceRubles; - - if (typeof amountRub !== 'number' && typeof balanceKopeks === 'number') { - amountRub = balanceKopeks / 100; - } - - if (typeof amountRub !== 'number' || Number.isNaN(amountRub)) { - balanceElement.textContent = '—'; - return; - } - - balanceElement.textContent = formatCurrency(amountRub); - } - - function getTransactions() { - if (Array.isArray(userData?.transactions)) { - return userData.transactions; - } - if (Array.isArray(userData?.operations)) { - return userData.operations; - } - if (Array.isArray(userData?.history)) { - return userData.history; - } - return []; - } - - function formatTransactionAmount(transaction) { - const amountKopeks = transaction.amount_kopeks ?? Math.round((transaction.amount_rubles ?? 0) * 100); - const amountRubles = transaction.amount_rubles ?? (typeof amountKopeks === 'number' ? amountKopeks / 100 : 0); - return amountRubles; - } - - function renderTransactionHistory() { - const historyList = document.getElementById('historyList'); - if (!historyList) { - return; - } - - const transactions = getTransactions(); - if (!transactions.length) { - historyList.innerHTML = `
${translate('history.empty', 'There are no transactions yet')}
`; - return; - } - - const locale = getLocale(); - historyList.innerHTML = transactions.slice(0, 10).map(transaction => { - const createdAt = transaction.created_at ? new Date(transaction.created_at) : null; - const formattedDate = createdAt && !Number.isNaN(createdAt.getTime()) - ? createdAt.toLocaleString(locale, { - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit' - }) - : ''; - - const amountRubles = formatTransactionAmount(transaction); - const amountFormatted = formatCurrency(amountRubles); - const isPositive = amountRubles >= 0; - - const description = transaction.description || transaction.title || ''; - const typeLabel = transaction.type ? transaction.type.replace(/_/g, ' ') : ''; - const finalTitle = description || typeLabel || translate('history.title', 'Latest Transactions'); - - return ` -
-
- ${finalTitle} - ${formattedDate} -
- ${amountFormatted} -
- `; - }).join(''); - } - - function getConnectedServers() { - if (Array.isArray(userData?.links) && userData.links.length) { - return userData.links; - } - if (Array.isArray(userData?.connected_squads) && userData.connected_squads.length) { - return userData.connected_squads; - } - if (Array.isArray(userData?.user?.connected_squads) && userData.user.connected_squads.length) { - return userData.user.connected_squads; - } - return []; - } - - function renderConnectedServers() { - const serverList = document.getElementById('serverList'); - if (!serverList) { - return; - } - - const servers = getConnectedServers(); - if (!servers.length) { - serverList.innerHTML = `
${translate('servers.empty', 'No connected servers yet')}
`; - return; - } - - serverList.innerHTML = servers.map(server => { - if (typeof server === 'string') { - return ` -
- ${server} - ${translate('status.active', 'Active')} -
- `; - } - - const name = server.name || server.id || 'Server'; - const status = server.status ? translateStatus(server.status) : translate('status.active', 'Active'); - return ` -
- ${name} - ${status} -
- `; - }).join(''); - } - function getCurrentSubscriptionUrl() { - return userData?.subscriptionCryptoLink || userData?.subscriptionUrl || ''; - } - - function buildRedirectUrl(template, link) { - if (!template || !link) { - return ''; - } - - const encoded = encodeURIComponent(link); - const replacements = { - '{subscription_link}': encoded, - '{link}': encoded, - '{subscription_link_raw}': link, - '{link_raw}': link, - }; - - let result = template; - let replaced = false; - - Object.entries(replacements).forEach(([placeholder, value]) => { - if (result.includes(placeholder)) { - result = result.split(placeholder).join(value); - replaced = true; - } - }); - - if (!replaced) { - if (/[?=&]$/.test(result)) { - return `${result}${encoded}`; - } - return `${result}${encoded}`; - } - - return result; + return userData?.subscription_url || userData?.subscriptionUrl || ''; } function updateActionButtons() { const connectBtn = document.getElementById('connectBtn'); const copyBtn = document.getElementById('copyBtn'); - const subscriptionUrl = getCurrentSubscriptionUrl(); - const redirectLink = buildRedirectUrl(redirectTemplate, redirectTarget); + const hasUrl = Boolean(getCurrentSubscriptionUrl()); if (connectBtn) { - const shouldShow = Boolean(redirectTemplate && redirectTarget && redirectLink); - connectBtn.classList.toggle('hidden', !shouldShow); - connectBtn.disabled = !shouldShow; - connectBtn.dataset.targetUrl = shouldShow ? redirectLink : ''; + connectBtn.disabled = !hasUrl; } if (copyBtn) { - copyBtn.disabled = !subscriptionUrl || !navigator.clipboard; + copyBtn.disabled = !hasUrl || !navigator.clipboard; } } @@ -1452,19 +892,22 @@ }); }); - document.querySelectorAll('.lang-btn').forEach(btn => { - btn.addEventListener('click', () => { - setLanguage(btn.dataset.lang); - }); - }); - document.getElementById('connectBtn')?.addEventListener('click', () => { - const connectBtn = document.getElementById('connectBtn'); - const targetUrl = connectBtn?.dataset.targetUrl; - if (!targetUrl) { + const subscriptionUrl = getCurrentSubscriptionUrl(); + if (!subscriptionUrl) { return; } - window.location.href = targetUrl; + + const apps = getAppsForCurrentPlatform(); + const featuredApp = apps.find(app => app.isFeatured) || apps[0]; + + if (featuredApp?.urlScheme) { + window.location.href = `${featuredApp.urlScheme}${subscriptionUrl}`; + } else if (userData?.happ_link && featuredApp?.id === 'happ') { + window.location.href = userData.happ_link; + } else { + window.location.href = subscriptionUrl; + } }); document.getElementById('copyBtn')?.addEventListener('click', async () => { @@ -1475,16 +918,10 @@ try { await navigator.clipboard.writeText(subscriptionUrl); - showPopup( - translate('popup.copiedText', 'Subscription link copied to clipboard'), - translate('popup.copiedTitle', 'Copied') - ); + showPopup('Subscription link copied to clipboard', 'Copied'); } catch (error) { console.warn('Clipboard copy failed:', error); - showPopup( - translate('popup.copyFailedText', 'Unable to copy the subscription link automatically. Please copy it manually.'), - translate('popup.copyFailedTitle', 'Copy failed') - ); + showPopup('Unable to copy the subscription link automatically. Please copy it manually.', 'Copy failed'); } }); @@ -1495,19 +932,16 @@ const textElement = document.getElementById('errorText'); if (titleElement) { - titleElement.textContent = error?.title || translate('errors.defaultTitle', 'Subscription Not Found'); + titleElement.textContent = error?.title || 'Subscription Not Found'; } if (textElement) { - textElement.textContent = error?.message || translate('errors.defaultText', 'Please contact support to activate your subscription'); + textElement.textContent = error?.message || 'Please contact support to activate your subscription'; } document.getElementById('errorState').classList.remove('hidden'); updateActionButtons(); } - detectPlatform(); - setActivePlatformButton(); - renderApps(); init();