mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-04-29 01:00:03 +00:00
949 lines
30 KiB
HTML
949 lines
30 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
<meta name="theme-color" content="#2481cc">
|
|
<title>VPN Subscription</title>
|
|
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
-webkit-tap-highlight-color: transparent;
|
|
}
|
|
|
|
:root {
|
|
--tg-theme-bg-color: #ffffff;
|
|
--tg-theme-text-color: #000000;
|
|
--tg-theme-hint-color: #999999;
|
|
--tg-theme-link-color: #2481cc;
|
|
--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);
|
|
--bg-primary: var(--tg-theme-bg-color);
|
|
--bg-secondary: var(--tg-theme-secondary-bg-color);
|
|
--border-color: rgba(0, 0, 0, 0.08);
|
|
--shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
--radius: 12px;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
padding: 16px;
|
|
padding-bottom: 32px;
|
|
min-height: 100vh;
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
.container {
|
|
max-width: 480px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
/* Header */
|
|
.header {
|
|
text-align: center;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.logo {
|
|
font-size: 28px;
|
|
font-weight: 700;
|
|
color: var(--primary);
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.subtitle {
|
|
font-size: 14px;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
/* Loading State */
|
|
.loading {
|
|
text-align: center;
|
|
padding: 60px 20px;
|
|
}
|
|
|
|
.spinner {
|
|
width: 40px;
|
|
height: 40px;
|
|
border: 3px solid var(--bg-secondary);
|
|
border-top-color: var(--primary);
|
|
border-radius: 50%;
|
|
animation: spin 0.8s linear infinite;
|
|
margin: 0 auto 16px;
|
|
}
|
|
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
/* Card */
|
|
.card {
|
|
background: var(--bg-secondary);
|
|
border-radius: var(--radius);
|
|
padding: 16px;
|
|
margin-bottom: 12px;
|
|
box-shadow: var(--shadow);
|
|
}
|
|
|
|
.card-title {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: var(--text-secondary);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
/* User Info */
|
|
.user-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.user-avatar {
|
|
width: 56px;
|
|
height: 56px;
|
|
border-radius: 50%;
|
|
background: linear-gradient(135deg, var(--primary), #1a6fb8);
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 24px;
|
|
font-weight: 700;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.user-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.user-name {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
margin-bottom: 4px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.status-badge {
|
|
display: inline-block;
|
|
padding: 4px 10px;
|
|
border-radius: 12px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.status-active {
|
|
background: #d4f4dd;
|
|
color: #0f5132;
|
|
}
|
|
|
|
.status-expired {
|
|
background: #f8d7da;
|
|
color: #842029;
|
|
}
|
|
|
|
.status-trial {
|
|
background: #fff3cd;
|
|
color: #664d03;
|
|
}
|
|
|
|
.status-disabled {
|
|
background: #e2e3e5;
|
|
color: #41464b;
|
|
}
|
|
|
|
.status-unknown {
|
|
background: #e7eaf3;
|
|
color: #495057;
|
|
}
|
|
|
|
/* Stats Grid */
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 12px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.stat-item {
|
|
text-align: center;
|
|
padding: 16px 12px;
|
|
background: white;
|
|
border-radius: 10px;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 24px;
|
|
font-weight: 700;
|
|
color: var(--primary);
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 12px;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
/* Info Items */
|
|
.info-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 12px 0;
|
|
border-bottom: 1px solid var(--border-color);
|
|
}
|
|
|
|
.info-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.info-label {
|
|
font-size: 14px;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.info-value {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
text-align: right;
|
|
}
|
|
|
|
/* Buttons */
|
|
.btn {
|
|
width: 100%;
|
|
padding: 14px 20px;
|
|
border: none;
|
|
border-radius: var(--radius);
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: var(--primary);
|
|
color: white;
|
|
}
|
|
|
|
.btn-primary:active {
|
|
transform: scale(0.98);
|
|
opacity: 0.9;
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: white;
|
|
color: var(--primary);
|
|
border: 1px solid var(--border-color);
|
|
}
|
|
|
|
.btn:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.btn-icon {
|
|
width: 20px;
|
|
height: 20px;
|
|
}
|
|
|
|
/* App Installation */
|
|
.app-section {
|
|
margin-top: 24px;
|
|
}
|
|
|
|
.platform-selector {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
gap: 8px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.platform-btn {
|
|
padding: 12px 8px;
|
|
background: white;
|
|
border: 2px solid var(--border-color);
|
|
border-radius: var(--radius);
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
text-align: center;
|
|
}
|
|
|
|
.platform-btn.active {
|
|
border-color: var(--primary);
|
|
background: rgba(36, 129, 204, 0.1);
|
|
color: var(--primary);
|
|
}
|
|
|
|
.app-card {
|
|
background: white;
|
|
border-radius: var(--radius);
|
|
padding: 16px;
|
|
margin-bottom: 12px;
|
|
border: 2px solid var(--border-color);
|
|
}
|
|
|
|
.app-card.featured {
|
|
border-color: var(--primary);
|
|
background: rgba(36, 129, 204, 0.02);
|
|
}
|
|
|
|
.app-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.app-icon {
|
|
width: 48px;
|
|
height: 48px;
|
|
border-radius: 12px;
|
|
background: var(--primary);
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 24px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.app-name {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.featured-badge {
|
|
display: inline-block;
|
|
background: var(--primary);
|
|
color: white;
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
padding: 3px 8px;
|
|
border-radius: 8px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.app-steps {
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.step {
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.step-title {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
margin-bottom: 6px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.step-number {
|
|
width: 20px;
|
|
height: 20px;
|
|
background: var(--primary);
|
|
color: white;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.step-description {
|
|
font-size: 13px;
|
|
color: var(--text-secondary);
|
|
line-height: 1.5;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.step-buttons {
|
|
display: flex;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.step-btn {
|
|
padding: 8px 14px;
|
|
background: var(--primary);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 8px;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
text-decoration: none;
|
|
display: inline-block;
|
|
}
|
|
|
|
/* Error State */
|
|
.error {
|
|
text-align: center;
|
|
padding: 40px 20px;
|
|
}
|
|
|
|
.error-icon {
|
|
font-size: 48px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.error-title {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.error-text {
|
|
font-size: 14px;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
/* Hidden */
|
|
.hidden {
|
|
display: none !important;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<!-- Header -->
|
|
<div class="header">
|
|
<div class="logo">RemnaWave VPN</div>
|
|
<div class="subtitle">Secure & Fast Connection</div>
|
|
</div>
|
|
|
|
<!-- Loading State -->
|
|
<div id="loadingState" class="loading">
|
|
<div class="spinner"></div>
|
|
<div>Loading your subscription...</div>
|
|
</div>
|
|
|
|
<!-- Error State -->
|
|
<div id="errorState" class="error hidden">
|
|
<div class="error-icon">⚠️</div>
|
|
<div class="error-title" id="errorTitle">Subscription Not Found</div>
|
|
<div class="error-text" id="errorText">Please contact support to activate your subscription</div>
|
|
</div>
|
|
|
|
<!-- Main Content -->
|
|
<div id="mainContent" class="hidden">
|
|
<!-- User Card -->
|
|
<div class="card">
|
|
<div class="user-header">
|
|
<div class="user-avatar" id="userAvatar">U</div>
|
|
<div class="user-info">
|
|
<div class="user-name" id="userName">Loading...</div>
|
|
<span class="status-badge status-active" id="statusBadge">Active</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stats-grid">
|
|
<div class="stat-item">
|
|
<div class="stat-value" id="daysLeft">-</div>
|
|
<div class="stat-label">Days Left</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-value" id="serversCount">-</div>
|
|
<div class="stat-label">Servers</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<span class="info-label">Expires</span>
|
|
<span class="info-value" id="expiresAt">-</span>
|
|
</div>
|
|
<div class="info-item">
|
|
<span class="info-label">Traffic Used</span>
|
|
<span class="info-value" id="trafficUsed">-</span>
|
|
</div>
|
|
<div class="info-item">
|
|
<span class="info-label">Traffic Limit</span>
|
|
<span class="info-value" id="trafficLimit">-</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Action Buttons -->
|
|
<button class="btn btn-primary" id="connectBtn">
|
|
<svg class="btn-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
|
</svg>
|
|
Connect to VPN
|
|
</button>
|
|
|
|
<button class="btn btn-secondary" id="copyBtn" style="margin-top: 8px;">
|
|
<svg class="btn-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
|
|
</svg>
|
|
Copy Subscription Link
|
|
</button>
|
|
|
|
<!-- App Installation Section -->
|
|
<div class="app-section">
|
|
<div class="card">
|
|
<div class="card-title">Installation Guide</div>
|
|
|
|
<div class="platform-selector">
|
|
<button class="platform-btn" data-platform="ios">iOS</button>
|
|
<button class="platform-btn active" data-platform="android">Android</button>
|
|
<button class="platform-btn" data-platform="pc">PC</button>
|
|
<button class="platform-btn" data-platform="tv">TV</button>
|
|
</div>
|
|
|
|
<div id="appsContainer">
|
|
<!-- Apps will be rendered here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const tg = window.Telegram?.WebApp || {
|
|
initData: '',
|
|
initDataUnsafe: {},
|
|
expand: () => {},
|
|
ready: () => {},
|
|
themeParams: {},
|
|
showPopup: null,
|
|
};
|
|
|
|
if (typeof tg.expand === 'function') {
|
|
tg.expand();
|
|
}
|
|
if (typeof tg.ready === 'function') {
|
|
tg.ready();
|
|
}
|
|
|
|
if (tg.themeParams) {
|
|
Object.keys(tg.themeParams).forEach(key => {
|
|
document.documentElement.style.setProperty(`--tg-theme-${key.replace(/_/g, '-')}`, tg.themeParams[key]);
|
|
});
|
|
}
|
|
|
|
let userData = null;
|
|
let appsConfig = {};
|
|
let currentPlatform = 'android';
|
|
let preferredLanguage = 'en';
|
|
|
|
function createError(title, message, status) {
|
|
const error = new Error(message || title);
|
|
if (title) {
|
|
error.title = title;
|
|
}
|
|
if (status) {
|
|
error.status = status;
|
|
}
|
|
return error;
|
|
}
|
|
|
|
async function init() {
|
|
try {
|
|
const telegramUser = tg.initDataUnsafe?.user;
|
|
if (telegramUser?.language_code) {
|
|
preferredLanguage = telegramUser.language_code.split('-')[0];
|
|
}
|
|
|
|
await loadAppsConfig();
|
|
|
|
const initData = tg.initData || '';
|
|
if (!initData) {
|
|
throw createError('Authorization Error', 'Missing Telegram init data');
|
|
}
|
|
|
|
const response = await fetch('/miniapp/subscription', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({ initData })
|
|
});
|
|
|
|
if (!response.ok) {
|
|
let detail = response.status === 401
|
|
? '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();
|
|
if (errorPayload?.detail) {
|
|
detail = errorPayload.detail;
|
|
}
|
|
} catch (parseError) {
|
|
// ignore
|
|
}
|
|
|
|
throw createError(title, detail, response.status);
|
|
}
|
|
|
|
userData = await response.json();
|
|
userData.subscriptionUrl = userData.subscription_url || null;
|
|
userData.subscriptionCryptoLink = userData.subscription_crypto_link || null;
|
|
|
|
if (userData?.user?.language) {
|
|
preferredLanguage = userData.user.language;
|
|
}
|
|
|
|
renderUserData();
|
|
detectPlatform();
|
|
setActivePlatformButton();
|
|
renderApps();
|
|
|
|
document.getElementById('loadingState').classList.add('hidden');
|
|
document.getElementById('mainContent').classList.remove('hidden');
|
|
} catch (error) {
|
|
console.error('Initialization error:', error);
|
|
showError(error);
|
|
}
|
|
}
|
|
|
|
async function loadAppsConfig() {
|
|
try {
|
|
const response = await fetch('/app-config.json', { cache: 'no-cache' });
|
|
if (!response.ok) {
|
|
throw new Error('Failed to load app config');
|
|
}
|
|
|
|
const data = await response.json();
|
|
appsConfig = data?.platforms || {};
|
|
} catch (error) {
|
|
console.warn('Unable to load apps configuration:', error);
|
|
appsConfig = {};
|
|
}
|
|
}
|
|
|
|
function renderUserData() {
|
|
if (!userData?.user) {
|
|
return;
|
|
}
|
|
|
|
const user = userData.user;
|
|
const rawName = user.display_name || user.username || '';
|
|
const fallbackName = rawName || [user.first_name, user.last_name].filter(Boolean).join(' ') || `User ${user.telegram_id || ''}`.trim();
|
|
const avatarChar = (fallbackName.replace(/^@/, '')[0] || 'U').toUpperCase();
|
|
|
|
document.getElementById('userAvatar').textContent = avatarChar;
|
|
document.getElementById('userName').textContent = fallbackName;
|
|
|
|
const statusValueRaw = (user.subscription_actual_status || user.subscription_status || 'active').toLowerCase();
|
|
const knownStatuses = ['active', 'expired', 'trial', 'disabled'];
|
|
const statusClass = knownStatuses.includes(statusValueRaw) ? statusValueRaw : 'unknown';
|
|
const statusBadge = document.getElementById('statusBadge');
|
|
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;
|
|
let daysLeft = '—';
|
|
if (expiresAt && !Number.isNaN(expiresAt.getTime())) {
|
|
const diffDays = Math.ceil((expiresAt - new Date()) / (1000 * 60 * 60 * 24));
|
|
daysLeft = diffDays > 0 ? diffDays : '0';
|
|
}
|
|
document.getElementById('daysLeft').textContent = daysLeft;
|
|
document.getElementById('expiresAt').textContent = expiresAt && !Number.isNaN(expiresAt.getTime())
|
|
? expiresAt.toLocaleDateString()
|
|
: '—';
|
|
|
|
const serversCount = userData.links?.length ?? userData.connected_squads?.length ?? 0;
|
|
document.getElementById('serversCount').textContent = serversCount;
|
|
|
|
document.getElementById('trafficUsed').textContent =
|
|
user.traffic_used_label || formatTraffic(user.traffic_used_gb);
|
|
document.getElementById('trafficLimit').textContent =
|
|
user.traffic_limit_label || formatTrafficLimit(user.traffic_limit_gb);
|
|
|
|
updateActionButtons();
|
|
}
|
|
|
|
function detectPlatform() {
|
|
const userAgent = navigator.userAgent.toLowerCase();
|
|
if (userAgent.includes('iphone') || userAgent.includes('ipad')) {
|
|
currentPlatform = 'ios';
|
|
} else if (userAgent.includes('android')) {
|
|
currentPlatform = 'android';
|
|
} else {
|
|
currentPlatform = 'pc';
|
|
}
|
|
}
|
|
|
|
function setActivePlatformButton() {
|
|
document.querySelectorAll('.platform-btn').forEach(btn => {
|
|
btn.classList.toggle('active', btn.dataset.platform === currentPlatform);
|
|
});
|
|
}
|
|
|
|
function getPlatformKey(platform) {
|
|
const mapping = {
|
|
ios: 'ios',
|
|
android: 'android',
|
|
pc: 'windows',
|
|
tv: 'androidTV',
|
|
mac: 'macos'
|
|
};
|
|
return mapping[platform] || platform;
|
|
}
|
|
|
|
function getAppsForCurrentPlatform() {
|
|
const platformKey = getPlatformKey(currentPlatform);
|
|
return appsConfig?.[platformKey] || [];
|
|
}
|
|
|
|
function renderApps() {
|
|
const container = document.getElementById('appsContainer');
|
|
if (!container) {
|
|
return;
|
|
}
|
|
|
|
const apps = getAppsForCurrentPlatform();
|
|
|
|
if (!apps.length) {
|
|
container.innerHTML = '<div class="step-description">No installation guide available for this platform yet.</div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = apps.map(app => {
|
|
const iconChar = (app.name?.[0] || 'A').toUpperCase();
|
|
const featuredBadge = app.isFeatured ? '<span class="featured-badge">Recommended</span>' : '';
|
|
return `
|
|
<div class="app-card ${app.isFeatured ? 'featured' : ''}">
|
|
<div class="app-header">
|
|
<div class="app-icon">${iconChar}</div>
|
|
<div>
|
|
<div class="app-name">${app.name || 'App'}</div>
|
|
${featuredBadge}
|
|
</div>
|
|
</div>
|
|
<div class="app-steps">
|
|
${renderAppSteps(app)}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
function renderAppSteps(app) {
|
|
let html = '';
|
|
let stepNum = 1;
|
|
|
|
if (app.installationStep) {
|
|
html += `
|
|
<div class="step">
|
|
<div class="step-title">
|
|
<span class="step-number">${stepNum++}</span>
|
|
Download & Install
|
|
</div>
|
|
${app.installationStep.description ? `<div class="step-description">${getLocalizedText(app.installationStep.description)}</div>` : ''}
|
|
${Array.isArray(app.installationStep.buttons) && app.installationStep.buttons.length ? `
|
|
<div class="step-buttons">
|
|
${app.installationStep.buttons.map(btn => `
|
|
<a href="${btn.buttonLink}" class="step-btn" target="_blank" rel="noopener">
|
|
${getLocalizedText(btn.buttonText)}
|
|
</a>
|
|
`).join('')}
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
if (app.addSubscriptionStep) {
|
|
html += `
|
|
<div class="step">
|
|
<div class="step-title">
|
|
<span class="step-number">${stepNum++}</span>
|
|
Add Subscription
|
|
</div>
|
|
<div class="step-description">${getLocalizedText(app.addSubscriptionStep.description)}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
if (app.connectAndUseStep) {
|
|
html += `
|
|
<div class="step">
|
|
<div class="step-title">
|
|
<span class="step-number">${stepNum++}</span>
|
|
Connect & Use
|
|
</div>
|
|
<div class="step-description">${getLocalizedText(app.connectAndUseStep.description)}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
return html;
|
|
}
|
|
|
|
function getLocalizedText(textObj) {
|
|
if (!textObj) {
|
|
return '';
|
|
}
|
|
if (typeof textObj === 'string') {
|
|
return textObj;
|
|
}
|
|
|
|
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());
|
|
|
|
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) {
|
|
const numeric = typeof value === 'number' ? value : Number.parseFloat(value ?? '0');
|
|
if (!Number.isFinite(numeric)) {
|
|
return '0 GB';
|
|
}
|
|
if (numeric >= 100) {
|
|
return `${numeric.toFixed(0)} GB`;
|
|
}
|
|
if (numeric >= 10) {
|
|
return `${numeric.toFixed(1)} GB`;
|
|
}
|
|
return `${numeric.toFixed(2)} GB`;
|
|
}
|
|
|
|
function formatTrafficLimit(limit) {
|
|
const numeric = typeof limit === 'number' ? limit : Number.parseFloat(limit ?? '0');
|
|
if (!Number.isFinite(numeric) || numeric <= 0) {
|
|
return 'Unlimited';
|
|
}
|
|
return `${numeric.toFixed(0)} GB`;
|
|
}
|
|
|
|
function getCurrentSubscriptionUrl() {
|
|
return userData?.subscription_url || userData?.subscriptionUrl || '';
|
|
}
|
|
|
|
function updateActionButtons() {
|
|
const connectBtn = document.getElementById('connectBtn');
|
|
const copyBtn = document.getElementById('copyBtn');
|
|
const hasUrl = Boolean(getCurrentSubscriptionUrl());
|
|
|
|
if (connectBtn) {
|
|
connectBtn.disabled = !hasUrl;
|
|
}
|
|
if (copyBtn) {
|
|
copyBtn.disabled = !hasUrl || !navigator.clipboard;
|
|
}
|
|
}
|
|
|
|
function showPopup(message, title = 'Info') {
|
|
if (typeof tg.showPopup === 'function') {
|
|
tg.showPopup({
|
|
title,
|
|
message,
|
|
buttons: [{ type: 'ok' }]
|
|
});
|
|
} else {
|
|
alert(message);
|
|
}
|
|
}
|
|
|
|
document.querySelectorAll('.platform-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
currentPlatform = btn.dataset.platform;
|
|
setActivePlatformButton();
|
|
renderApps();
|
|
});
|
|
});
|
|
|
|
document.getElementById('connectBtn')?.addEventListener('click', () => {
|
|
const subscriptionUrl = getCurrentSubscriptionUrl();
|
|
if (!subscriptionUrl) {
|
|
return;
|
|
}
|
|
|
|
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 () => {
|
|
const subscriptionUrl = getCurrentSubscriptionUrl();
|
|
if (!subscriptionUrl || !navigator.clipboard) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await navigator.clipboard.writeText(subscriptionUrl);
|
|
showPopup('Subscription link copied to clipboard', 'Copied');
|
|
} catch (error) {
|
|
console.warn('Clipboard copy failed:', error);
|
|
showPopup('Unable to copy the subscription link automatically. Please copy it manually.', 'Copy failed');
|
|
}
|
|
});
|
|
|
|
function showError(error) {
|
|
document.getElementById('loadingState').classList.add('hidden');
|
|
|
|
const titleElement = document.getElementById('errorTitle');
|
|
const textElement = document.getElementById('errorText');
|
|
|
|
if (titleElement) {
|
|
titleElement.textContent = error?.title || 'Subscription Not Found';
|
|
}
|
|
if (textElement) {
|
|
textElement.textContent = error?.message || 'Please contact support to activate your subscription';
|
|
}
|
|
|
|
document.getElementById('errorState').classList.remove('hidden');
|
|
updateActionButtons();
|
|
}
|
|
|
|
init();
|
|
</script>
|
|
</body>
|
|
</html>
|