Merge pull request #1165 from Fr1ngg/revert-1164-revert-1153-revert-1152-7gq87l-bedolaga/update-miniapp/index.html-logic

Revert "Revert "Revert "Improve miniapp onboarding states for unregistered users"""
This commit is contained in:
Egor
2025-10-11 01:05:02 +03:00
committed by GitHub

View File

@@ -320,90 +320,6 @@
line-height: 1.6;
}
/* Empty States */
.empty-state {
margin: 24px 0;
}
.empty-state-card {
padding: 28px 24px;
border-radius: var(--radius-xl);
background: var(--bg-secondary);
box-shadow: var(--shadow-sm);
text-align: center;
display: flex;
flex-direction: column;
gap: 16px;
align-items: center;
}
.empty-state-icon {
font-size: 42px;
}
.empty-state-title {
font-size: 20px;
font-weight: 800;
color: var(--text-primary);
}
.empty-state-text {
font-size: 15px;
color: var(--text-secondary);
line-height: 1.6;
}
.empty-state-actions {
display: flex;
flex-direction: column;
gap: 12px;
width: 100%;
}
.empty-state-button {
width: 100%;
padding: 14px 18px;
border: none;
border-radius: var(--radius-lg);
font-size: 15px;
font-weight: 700;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
background: var(--primary);
color: #fff;
}
.empty-state-button:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-sm);
}
.empty-state-button.secondary {
background: transparent;
border: 1px solid var(--border-color);
color: var(--text-primary);
}
.empty-state-chip {
font-size: 12px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
padding: 4px 10px;
border-radius: 999px;
background: rgba(var(--primary-rgb), 0.12);
color: var(--primary);
}
:root[data-theme="dark"] .empty-state-card {
background: rgba(15, 23, 42, 0.75);
box-shadow: var(--shadow-md);
}
:root[data-theme="dark"] .empty-state-button.secondary {
border-color: rgba(148, 163, 184, 0.4);
}
.error-actions {
margin-top: 24px;
display: flex;
@@ -4307,48 +4223,13 @@
</div>
</div>
<!-- Registration Prompt -->
<div id="registrationState" class="empty-state hidden">
<div class="empty-state-card">
<div class="empty-state-icon">🤖</div>
<div class="empty-state-title" data-i18n="state.registration.title">Register in the bot</div>
<div class="empty-state-text" data-i18n="state.registration.text">
Start the bot in Telegram to create an account and get access to the mini app features.
</div>
<div class="empty-state-actions">
<button class="empty-state-button" id="openBotBtn" type="button" data-i18n="state.registration.button">
Open bot
</button>
</div>
</div>
</div>
<!-- Main Content -->
<div id="mainContent" class="hidden">
<!-- Promo Offers -->
<div id="promoOffersContainer" class="promo-offers hidden"></div>
<div id="noSubscriptionState" class="empty-state hidden">
<div class="empty-state-card">
<div class="empty-state-chip" data-i18n="state.no_subscription.badge">No active plan</div>
<div class="empty-state-icon">🛒</div>
<div class="empty-state-title" data-i18n="state.no_subscription.title">No subscription yet</div>
<div class="empty-state-text" data-i18n="state.no_subscription.text">
Choose a plan and activate your subscription to unlock all features.
</div>
<div class="empty-state-actions">
<button class="empty-state-button" id="noSubscriptionPurchaseBtn" type="button" data-i18n="state.no_subscription.action.purchase">
Choose a plan
</button>
<button class="empty-state-button secondary" id="noSubscriptionTopupBtn" type="button" data-i18n="state.no_subscription.action.topup">
Top up balance
</button>
</div>
</div>
</div>
<!-- User Card -->
<div class="card user-card animate-in" id="userCard">
<div class="card user-card animate-in">
<div class="user-header">
<div class="user-avatar" id="userAvatar">U</div>
<div class="user-info">
@@ -5091,14 +4972,6 @@
'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',
@@ -5454,14 +5327,6 @@
'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': 'Устройства',
@@ -5915,58 +5780,6 @@
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';
@@ -5975,7 +5788,6 @@
let preferredLanguage = 'en';
let languageLockedByUser = false;
let currentErrorState = null;
let subscriptionViewState = 'loading';
let paymentMethodsCache = null;
let paymentMethodsPromise = null;
let activePaymentMethod = null;
@@ -6022,10 +5834,6 @@
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;
@@ -6685,25 +6493,6 @@
}
}
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);
@@ -6902,7 +6691,6 @@
currentErrorState = null;
updateErrorTexts();
setSubscriptionViewState('active');
const errorState = document.getElementById('errorState');
if (errorState) {
@@ -6928,83 +6716,6 @@
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 || '';
@@ -7019,16 +6730,8 @@
document.getElementById('loadingState')?.classList.remove('hidden');
}
try {
const payload = await fetchSubscriptionPayload(initData);
return applySubscriptionData(payload);
} catch (error) {
const handled = await handleMissingSubscription(error);
if (handled) {
return userData;
}
throw error;
}
const payload = await fetchSubscriptionPayload(initData);
return applySubscriptionData(payload);
}
async function init() {
@@ -7047,10 +6750,7 @@
await refreshSubscriptionData();
} catch (error) {
console.error('Initialization error:', error);
const handled = await handleMissingSubscription(error);
if (!handled) {
showError(error);
}
showError(error);
}
}
@@ -12134,11 +11834,6 @@
return;
}
if (subscriptionViewState !== 'active') {
card.classList.add('hidden');
return;
}
const shouldShow = hasPaidSubscription();
card.classList.toggle('hidden', !shouldShow);
if (!shouldShow) {
@@ -12565,11 +12260,6 @@
return;
}
if (subscriptionViewState !== 'active') {
card.classList.add('hidden');
return;
}
const shouldShow = hasPaidSubscription();
card.classList.toggle('hidden', !shouldShow);
if (!shouldShow) {
@@ -13747,23 +13437,6 @@
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;
@@ -15585,7 +15258,6 @@
if (copyBtn) {
const hasUrl = Boolean(subscriptionUrl);
copyBtn.disabled = !hasUrl || !navigator.clipboard;
copyBtn.classList.toggle('hidden', subscriptionViewState !== 'active');
}
}
@@ -15602,7 +15274,6 @@
}
function showError(error) {
setSubscriptionViewState('loading');
document.getElementById('loadingState').classList.add('hidden');
document.getElementById('mainContent').classList.add('hidden');
currentErrorState = {
@@ -15673,44 +15344,6 @@
}
});
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();