+
+
-
+
+ No active plan
+ π
+ No subscription yet
+
+ Choose a plan and activate your subscription to unlock all features.
+
+
+
+
+
+
+
U
@@ -4972,6 +5091,14 @@
'app.loading': 'Loading your subscription...',
'error.default.title': 'Subscription Not Found',
'error.default.message': 'Please contact support to activate your subscription.',
+ 'state.registration.title': 'Register in the bot',
+ 'state.registration.text': 'Start the bot in Telegram to create an account and access the mini app.',
+ 'state.registration.button': 'Open bot',
+ 'state.no_subscription.badge': 'No active plan',
+ 'state.no_subscription.title': 'No subscription yet',
+ 'state.no_subscription.text': 'Choose a plan and activate your subscription to unlock all features.',
+ 'state.no_subscription.action.purchase': 'Choose a plan',
+ 'state.no_subscription.action.topup': 'Top up balance',
'stats.days_left': 'Days left',
'stats.servers': 'Servers',
'stats.devices': 'Devices',
@@ -5327,6 +5454,14 @@
'app.loading': 'ΠΠ°Π³ΡΡΠΆΠ°Π΅ΠΌ Π²Π°ΡΡ ΠΏΠΎΠ΄ΠΏΠΈΡΠΊΡ...',
'error.default.title': 'ΠΠΎΠ΄ΠΏΠΈΡΠΊΠ° Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½Π°',
'error.default.message': 'Π‘Π²ΡΠΆΠΈΡΠ΅ΡΡ Ρ ΠΏΠΎΠ΄Π΄Π΅ΡΠΆΠΊΠΎΠΉ, ΡΡΠΎΠ±Ρ Π°ΠΊΡΠΈΠ²ΠΈΡΠΎΠ²Π°ΡΡ ΠΏΠΎΠ΄ΠΏΠΈΡΠΊΡ.',
+ 'state.registration.title': 'ΠΠ°ΡΠ΅Π³ΠΈΡΡΡΠΈΡΡΠΉΡΠ΅ΡΡ Π² Π±ΠΎΡΠ΅',
+ 'state.registration.text': 'ΠΠ°ΠΏΡΡΡΠΈΡΠ΅ Π±ΠΎΡΠ° Π² Telegram, ΡΡΠΎΠ±Ρ ΡΠΎΠ·Π΄Π°ΡΡ Π°ΠΊΠΊΠ°ΡΠ½Ρ ΠΈ ΠΏΠΎΠ»ΡΡΠΈΡΡ Π΄ΠΎΡΡΡΠΏ ΠΊ ΠΌΠΈΠ½ΠΈ-ΠΏΡΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΡ.',
+ 'state.registration.button': 'ΠΡΠΊΡΡΡΡ Π±ΠΎΡΠ°',
+ 'state.no_subscription.badge': 'ΠΠ΅Ρ ΠΏΠΎΠ΄ΠΏΠΈΡΠΊΠΈ',
+ 'state.no_subscription.title': 'ΠΠΎΠ΄ΠΏΠΈΡΠΊΠ° Π½Π΅ Π°ΠΊΡΠΈΠ²Π½Π°',
+ 'state.no_subscription.text': 'ΠΡΠ±Π΅ΡΠΈΡΠ΅ ΠΏΠΎΠ΄Ρ
ΠΎΠ΄ΡΡΠΈΠΉ ΡΠ°ΡΠΈΡ ΠΈ ΠΏΠΎΠ΄ΠΊΠ»ΡΡΠΈΡΠ΅ ΠΏΠΎΠ΄ΠΏΠΈΡΠΊΡ, ΡΡΠΎΠ±Ρ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΡΡΡ ΡΠ΅ΡΠ²ΠΈΡΠΎΠΌ.',
+ 'state.no_subscription.action.purchase': 'ΠΡΠ±ΡΠ°ΡΡ ΡΠ°ΡΠΈΡ',
+ 'state.no_subscription.action.topup': 'ΠΠΎΠΏΠΎΠ»Π½ΠΈΡΡ Π±Π°Π»Π°Π½Ρ',
'stats.days_left': 'ΠΡΡΠ°Π»ΠΎΡΡ Π΄Π½Π΅ΠΉ',
'stats.servers': 'Π‘Π΅ΡΠ²Π΅ΡΡ',
'stats.devices': 'Π£ΡΡΡΠΎΠΉΡΡΠ²Π°',
@@ -5780,6 +5915,58 @@
applyKey('app.subtitle', rawServiceDescription);
}
+ function buildFallbackUserData(purchaseData) {
+ const currency = (purchaseData?.currency || 'RUB').toString().toUpperCase();
+ const balanceKopeks = coercePositiveInt(
+ purchaseData?.balanceKopeks
+ ?? purchaseData?.balance_kopeks
+ ?? null,
+ 0
+ ) || 0;
+
+ const telegramUser = tg?.initDataUnsafe?.user || {};
+ const username = telegramUser.username ? `@${telegramUser.username}` : null;
+ const nameCandidates = [
+ [telegramUser.first_name, telegramUser.last_name].filter(Boolean).join(' ').trim() || null,
+ telegramUser.first_name || null,
+ telegramUser.last_name || null,
+ username,
+ ].filter(Boolean);
+ const fallbackName = nameCandidates[0] || 'User';
+
+ return {
+ user: {
+ display_name: fallbackName,
+ username,
+ first_name: telegramUser.first_name || null,
+ last_name: telegramUser.last_name || null,
+ telegram_id: telegramUser.id || null,
+ subscription_status: 'disabled',
+ subscription_actual_status: 'disabled',
+ has_active_subscription: false,
+ expires_at: null,
+ traffic_used_label: t('values.not_available'),
+ traffic_limit_label: t('values.not_available'),
+ device_limit: null,
+ },
+ balance_kopeks: balanceKopeks,
+ balance_currency: currency,
+ balance_rubles: balanceKopeks / 100,
+ subscription_type: 'trial',
+ autopay_enabled: false,
+ connected_servers: [],
+ connected_devices: [],
+ connected_devices_count: 0,
+ transactions: [],
+ promo_offers: [],
+ referral: null,
+ faq: null,
+ legal_documents: null,
+ subscription_url: null,
+ subscriptionPurchaseUrl: null,
+ };
+ }
+
let userData = null;
let appsConfig = {};
let currentPlatform = 'android';
@@ -5788,6 +5975,7 @@
let preferredLanguage = 'en';
let languageLockedByUser = false;
let currentErrorState = null;
+ let subscriptionViewState = 'loading';
let paymentMethodsCache = null;
let paymentMethodsPromise = null;
let activePaymentMethod = null;
@@ -5834,6 +6022,10 @@
periodId: null,
};
+ const urlParams = new URLSearchParams(window.location.search || '');
+ const botUsernameRaw = urlParams.get('tgWebAppBotName') || urlParams.get('bot') || '';
+ const botUsername = botUsernameRaw ? botUsernameRaw.trim().replace(/^@/, '') : null;
+
const PAYMENT_STATUS_INITIAL_DELAY_MS = 2000;
const PAYMENT_STATUS_POLL_INTERVAL_MS = 5000;
const PAYMENT_STATUS_TIMEOUT_MS = 180000;
@@ -6493,6 +6685,25 @@
}
}
+ function setSubscriptionViewState(state) {
+ subscriptionViewState = state;
+
+ const registrationBlock = document.getElementById('registrationState');
+ if (registrationBlock) {
+ registrationBlock.classList.toggle('hidden', state !== 'unregistered');
+ }
+
+ const noSubscriptionBlock = document.getElementById('noSubscriptionState');
+ if (noSubscriptionBlock) {
+ noSubscriptionBlock.classList.toggle('hidden', state !== 'no-subscription');
+ }
+
+ const userCard = document.getElementById('userCard');
+ if (userCard) {
+ userCard.classList.toggle('hidden', state !== 'active');
+ }
+ }
+
function applyTranslations() {
document.title = t('app.title');
document.documentElement.setAttribute('lang', preferredLanguage);
@@ -6691,6 +6902,7 @@
currentErrorState = null;
updateErrorTexts();
+ setSubscriptionViewState('active');
const errorState = document.getElementById('errorState');
if (errorState) {
@@ -6716,6 +6928,83 @@
return userData;
}
+ async function handleMissingSubscription(error) {
+ if (!error || error.status !== 404) {
+ return false;
+ }
+
+ const initData = tg.initData || '';
+ if (!initData) {
+ return false;
+ }
+
+ try {
+ const response = await fetch('/miniapp/subscription/purchase/options', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ initData }),
+ });
+
+ const body = await parseJsonSafe(response);
+ if (!response.ok || (body && body.success === false)) {
+ const message = extractPurchaseErrorMessage(body, response.status);
+ const fallbackError = createError('Subscription purchase error', message, response.status);
+ const code = extractErrorCode(body);
+ if (code) {
+ fallbackError.code = code;
+ }
+ fallbackError.payload = body;
+ throw fallbackError;
+ }
+
+ const normalized = normalizeSubscriptionPurchasePayload(body);
+ if (!normalized) {
+ throw createError('Subscription purchase error', t('subscription_purchase.error.default'));
+ }
+ subscriptionPurchaseData = normalized;
+ subscriptionPurchaseError = null;
+ subscriptionPurchaseLoading = false;
+ subscriptionPurchasePromise = null;
+ subscriptionPurchaseFeatureEnabled = true;
+ subscriptionPurchasePreview = null;
+ subscriptionPurchasePreviewError = null;
+ subscriptionPurchasePreviewLoading = false;
+ subscriptionPurchasePreviewPromise = null;
+ resetSubscriptionPurchaseSelections(normalized);
+ renderSubscriptionPurchaseCard();
+ requestSubscriptionPurchasePreviewUpdate({ immediate: true });
+
+ userData = buildFallbackUserData(normalized);
+ currentErrorState = null;
+ updateErrorTexts();
+
+ setSubscriptionViewState('no-subscription');
+ document.getElementById('errorState')?.classList.add('hidden');
+ document.getElementById('loadingState')?.classList.add('hidden');
+ document.getElementById('mainContent')?.classList.remove('hidden');
+
+ renderUserData();
+ updateActionButtons();
+
+ return true;
+ } catch (fallbackError) {
+ const code = fallbackError?.code || extractErrorCode(fallbackError?.payload);
+ if (fallbackError?.status === 404 || code === 'user_not_found') {
+ currentErrorState = null;
+ updateErrorTexts();
+ setSubscriptionViewState('unregistered');
+ document.getElementById('loadingState')?.classList.add('hidden');
+ document.getElementById('errorState')?.classList.add('hidden');
+ document.getElementById('mainContent')?.classList.add('hidden');
+ updateActionButtons();
+ return true;
+ }
+
+ console.warn('Failed to prepare fallback subscription state:', fallbackError);
+ return false;
+ }
+ }
+
async function refreshSubscriptionData(options = {}) {
const { silent = false } = options;
const initData = tg.initData || '';
@@ -6730,8 +7019,16 @@
document.getElementById('loadingState')?.classList.remove('hidden');
}
- const payload = await fetchSubscriptionPayload(initData);
- return applySubscriptionData(payload);
+ try {
+ const payload = await fetchSubscriptionPayload(initData);
+ return applySubscriptionData(payload);
+ } catch (error) {
+ const handled = await handleMissingSubscription(error);
+ if (handled) {
+ return userData;
+ }
+ throw error;
+ }
}
async function init() {
@@ -6750,7 +7047,10 @@
await refreshSubscriptionData();
} catch (error) {
console.error('Initialization error:', error);
- showError(error);
+ const handled = await handleMissingSubscription(error);
+ if (!handled) {
+ showError(error);
+ }
}
}
@@ -11834,6 +12134,11 @@
return;
}
+ if (subscriptionViewState !== 'active') {
+ card.classList.add('hidden');
+ return;
+ }
+
const shouldShow = hasPaidSubscription();
card.classList.toggle('hidden', !shouldShow);
if (!shouldShow) {
@@ -12260,6 +12565,11 @@
return;
}
+ if (subscriptionViewState !== 'active') {
+ card.classList.add('hidden');
+ return;
+ }
+
const shouldShow = hasPaidSubscription();
card.classList.toggle('hidden', !shouldShow);
if (!shouldShow) {
@@ -13437,6 +13747,23 @@
return t('subscription_purchase.error.default');
}
+ function extractErrorCode(payload) {
+ if (!payload || typeof payload !== 'object') {
+ return null;
+ }
+
+ if (typeof payload.code === 'string') {
+ return payload.code;
+ }
+
+ const detail = payload.detail;
+ if (detail && typeof detail === 'object' && typeof detail.code === 'string') {
+ return detail.code;
+ }
+
+ return null;
+ }
+
function ensureSubscriptionPurchaseData(options = {}) {
const { force = false } = options;
@@ -15258,6 +15585,7 @@
if (copyBtn) {
const hasUrl = Boolean(subscriptionUrl);
copyBtn.disabled = !hasUrl || !navigator.clipboard;
+ copyBtn.classList.toggle('hidden', subscriptionViewState !== 'active');
}
}
@@ -15274,6 +15602,7 @@
}
function showError(error) {
+ setSubscriptionViewState('loading');
document.getElementById('loadingState').classList.add('hidden');
document.getElementById('mainContent').classList.add('hidden');
currentErrorState = {
@@ -15344,6 +15673,44 @@
}
});
+ const openBotBtn = document.getElementById('openBotBtn');
+ if (openBotBtn) {
+ if (!botUsername) {
+ openBotBtn.disabled = true;
+ openBotBtn.classList.add('secondary');
+ } else {
+ openBotBtn.addEventListener('click', () => {
+ const link = `https://t.me/${botUsername}`;
+ if (typeof tg.openTelegramLink === 'function') {
+ try {
+ tg.openTelegramLink(link);
+ return;
+ } catch (openError) {
+ console.warn('tg.openTelegramLink failed:', openError);
+ }
+ }
+ openExternalLink(link, { openInMiniApp: true });
+ });
+ }
+ }
+
+ document.getElementById('noSubscriptionPurchaseBtn')?.addEventListener('click', event => {
+ if (shouldShowPurchaseConfigurator()) {
+ event.preventDefault();
+ openSubscriptionPurchaseModal();
+ return;
+ }
+ const link = getEffectivePurchaseUrl() || configPurchaseUrl;
+ if (!link) {
+ return;
+ }
+ openExternalLink(link, { openInMiniApp: true });
+ });
+
+ document.getElementById('noSubscriptionTopupBtn')?.addEventListener('click', () => {
+ openTopupModal();
+ });
+
document.getElementById('referralToggleBtn')?.addEventListener('click', () => {
referralListExpanded = !referralListExpanded;
updateReferralToggleState();