Files
remnawave-bedolaga-telegram…/miniapp/index.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>