Merge pull request #1173 from Fr1ngg/revert-1172-3gh1f0-bedolaga/update-miniapp/index.html-logic

Revert "Handle miniapp flows for unregistered and inactive users"
This commit is contained in:
Egor
2025-10-11 01:56:37 +03:00
committed by GitHub
3 changed files with 119 additions and 489 deletions

View File

@@ -152,7 +152,6 @@ from ..schemas.miniapp import (
MiniAppSubscriptionRenewalPeriod,
MiniAppSubscriptionRenewalRequest,
MiniAppSubscriptionRenewalResponse,
MiniAppEmptyState,
)
@@ -1740,7 +1739,6 @@ def _status_label(status: str) -> str:
"trial": "Trial",
"expired": "Expired",
"disabled": "Disabled",
"none": "No subscription",
}
return mapping.get(status, status.title())
@@ -2087,44 +2085,35 @@ async def get_subscription_details(
user = await get_user_by_telegram_id(db, telegram_id)
purchase_url = (settings.MINIAPP_PURCHASE_URL or "").strip()
if not user:
detail: Dict[str, str] = {
"code": "user_not_found",
"message": "User is not registered in the bot",
}
if not user or not user.subscription:
detail: Union[str, Dict[str, str]] = "Subscription not found"
if purchase_url:
detail["purchase_url"] = purchase_url
detail = {
"message": "Subscription not found",
"purchase_url": purchase_url,
}
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=detail,
)
subscription: Optional[Subscription] = getattr(user, "subscription", None)
status_actual = subscription.actual_status if subscription else "none"
has_active_subscription = status_actual in {"active", "trial"}
traffic_used = _format_gb(subscription.traffic_used_gb) if subscription else 0.0
traffic_limit_value = subscription.traffic_limit_gb if subscription else None
subscription = user.subscription
traffic_used = _format_gb(subscription.traffic_used_gb)
traffic_limit = subscription.traffic_limit_gb or 0
lifetime_used = _bytes_to_gb(getattr(user, "lifetime_used_traffic_bytes", 0))
links_payload: Dict[str, Any] = {}
if subscription:
links_payload = await _load_subscription_links(subscription)
status_actual = subscription.actual_status
links_payload = await _load_subscription_links(subscription)
subscription_url = links_payload.get("subscription_url") if subscription else None
if not subscription_url and subscription:
subscription_url = subscription.subscription_url
subscription_crypto_link = links_payload.get("happ_crypto_link") if subscription else None
if not subscription_crypto_link and subscription:
subscription_crypto_link = subscription.subscription_crypto_link
subscription_url = links_payload.get("subscription_url") or subscription.subscription_url
subscription_crypto_link = (
links_payload.get("happ_crypto_link")
or subscription.subscription_crypto_link
)
happ_redirect_link = get_happ_cryptolink_redirect_link(subscription_crypto_link)
connected_squads: List[str] = []
if subscription and subscription.connected_squads:
connected_squads = list(subscription.connected_squads)
connected_squads: List[str] = list(subscription.connected_squads or [])
connected_servers = await _resolve_connected_servers(db, connected_squads)
devices_count, devices = await _load_devices_info(user)
links: List[str] = links_payload.get("links") or connected_squads
@@ -2323,21 +2312,6 @@ async def get_subscription_details(
updated_at=getattr(service_rules, "updated_at", None),
)
subscription_status = subscription.status if subscription else "none"
expires_at = subscription.end_date if subscription else None
device_limit = subscription.device_limit if subscription else None
traffic_limit_label = (
_format_limit_label(traffic_limit_value)
if subscription and traffic_limit_value is not None
else ""
)
traffic_limit_gb = traffic_limit_value if subscription else None
traffic_used_label = (
_format_gb_label(traffic_used)
if subscription
else "0 GB"
)
response_user = MiniAppSubscriptionUser(
telegram_id=user.telegram_id,
username=user.username,
@@ -2353,17 +2327,17 @@ async def get_subscription_details(
),
language=user.language,
status=user.status,
subscription_status=subscription_status,
subscription_status=subscription.status,
subscription_actual_status=status_actual,
status_label=_status_label(status_actual),
expires_at=expires_at,
device_limit=device_limit,
expires_at=subscription.end_date,
device_limit=subscription.device_limit,
traffic_used_gb=round(traffic_used, 2),
traffic_used_label=traffic_used_label,
traffic_limit_gb=traffic_limit_gb,
traffic_limit_label=traffic_limit_label,
traffic_used_label=_format_gb_label(traffic_used),
traffic_limit_gb=traffic_limit,
traffic_limit_label=_format_limit_label(traffic_limit),
lifetime_used_traffic_gb=lifetime_used,
has_active_subscription=has_active_subscription,
has_active_subscription=status_actual in {"active", "trial"},
promo_offer_discount_percent=active_discount_percent,
promo_offer_discount_expires_at=active_discount_expires_at,
promo_offer_discount_source=promo_offer_source,
@@ -2371,31 +2345,9 @@ async def get_subscription_details(
referral_info = await _build_referral_info(db, user)
trial_available = not getattr(user, "has_had_paid_subscription", False)
empty_state_payload: Optional[MiniAppEmptyState] = None
if not has_active_subscription:
empty_state_payload = MiniAppEmptyState(
code="no_subscription",
title="Subscription not found",
message="Purchase a plan or activate a trial in the bot.",
purchase_url=purchase_url or None,
trial_available=trial_available,
)
subscription_id_value = subscription.id if subscription else None
remnawave_short_uuid = subscription.remnawave_short_uuid if subscription else None
subscription_type_value = (
"trial"
if subscription and subscription.is_trial
else ("paid" if has_active_subscription else "none")
)
autopay_enabled_value = bool(subscription.autopay_enabled) if subscription else False
return MiniAppSubscriptionResponse(
success=has_active_subscription,
state="subscription" if has_active_subscription else "no_subscription",
subscription_id=subscription_id_value,
remnawave_short_uuid=remnawave_short_uuid,
subscription_id=subscription.id,
remnawave_short_uuid=subscription.remnawave_short_uuid,
user=response_user,
subscription_url=subscription_url,
subscription_crypto_link=subscription_crypto_link,
@@ -2406,9 +2358,9 @@ async def get_subscription_details(
connected_servers=connected_servers,
connected_devices_count=devices_count,
connected_devices=devices,
happ=links_payload.get("happ") if subscription else None,
happ_link=links_payload.get("happ_link") if subscription else None,
happ_crypto_link=links_payload.get("happ_crypto_link") if subscription else None,
happ=links_payload.get("happ"),
happ_link=links_payload.get("happ_link"),
happ_crypto_link=links_payload.get("happ_crypto_link"),
happ_cryptolink_redirect_link=happ_redirect_link,
balance_kopeks=user.balance_kopeks,
balance_rubles=round(user.balance_rubles, 2),
@@ -2428,13 +2380,12 @@ async def get_subscription_details(
total_spent_kopeks=total_spent_kopeks,
total_spent_rubles=round(total_spent_kopeks / 100, 2),
total_spent_label=settings.format_price(total_spent_kopeks),
subscription_type=subscription_type_value,
autopay_enabled=autopay_enabled_value,
subscription_type="trial" if subscription.is_trial else "paid",
autopay_enabled=bool(subscription.autopay_enabled),
branding=settings.get_miniapp_branding(),
faq=faq_payload,
legal_documents=legal_documents_payload,
referral=referral_info,
empty_state=empty_state_payload,
)

View File

@@ -381,18 +381,9 @@ class MiniAppPaymentStatusResponse(BaseModel):
results: List[MiniAppPaymentStatusResult] = Field(default_factory=list)
class MiniAppEmptyState(BaseModel):
code: str
title: Optional[str] = None
message: Optional[str] = None
purchase_url: Optional[str] = Field(default=None, alias="purchaseUrl")
trial_available: bool = Field(default=False, alias="trialAvailable")
class MiniAppSubscriptionResponse(BaseModel):
success: bool = True
state: str = "subscription"
subscription_id: Optional[int] = None
subscription_id: int
remnawave_short_uuid: Optional[str] = None
user: MiniAppSubscriptionUser
subscription_url: Optional[str] = None
@@ -424,7 +415,6 @@ class MiniAppSubscriptionResponse(BaseModel):
faq: Optional[MiniAppFaq] = None
legal_documents: Optional[MiniAppLegalDocuments] = None
referral: Optional[MiniAppReferralInfo] = None
empty_state: Optional[MiniAppEmptyState] = Field(default=None, alias="emptyState")
class MiniAppSubscriptionServerOption(BaseModel):

View File

@@ -320,50 +320,6 @@
line-height: 1.6;
}
.empty-state {
text-align: center;
padding: 32px 24px;
background: var(--bg-secondary);
border-radius: var(--radius-xl);
margin: 20px 0;
box-shadow: var(--shadow-sm);
display: flex;
flex-direction: column;
gap: 16px;
}
.empty-state .empty-icon {
font-size: 48px;
}
.empty-state .empty-title {
font-size: 20px;
font-weight: 700;
color: var(--text-primary);
}
.empty-state .empty-text {
font-size: 15px;
color: var(--text-secondary);
line-height: 1.6;
}
.empty-state .empty-note {
font-size: 13px;
color: var(--text-secondary);
}
.empty-state .empty-actions {
display: flex;
justify-content: center;
}
.empty-state .empty-hint {
font-size: 13px;
color: var(--primary);
font-weight: 600;
}
.error-actions {
margin-top: 24px;
display: flex;
@@ -1926,11 +1882,6 @@
color: #41464b;
}
.status-none {
background: linear-gradient(135deg, #f8d7da, #fce4e6);
color: #842029;
}
/* Stats Grid */
.stats-grid {
display: grid;
@@ -4272,42 +4223,13 @@
</div>
</div>
<div id="registrationState" class="empty-state hidden">
<div class="empty-icon">🤖</div>
<div class="empty-title" data-i18n="registration.title">Start in Telegram</div>
<div class="empty-text" data-i18n="registration.subtitle">Open the bot and complete registration to access the mini app.</div>
<div class="empty-actions">
<button
id="registrationActionBtn"
class="btn btn-primary"
type="button"
data-i18n="registration.action"
>Open bot</button>
</div>
</div>
<!-- Main Content -->
<div id="mainContent" class="hidden">
<div id="noSubscriptionState" class="empty-state hidden">
<div class="empty-icon">🛡️</div>
<div class="empty-title" id="noSubscriptionTitle" data-i18n="empty.no_subscription.title">No active subscription</div>
<div class="empty-text" id="noSubscriptionText" data-i18n="empty.no_subscription.subtitle">Purchase a plan or activate the trial in the bot to start using VPN.</div>
<div class="empty-note" id="noSubscriptionNote" data-i18n="empty.no_subscription.info">Your balance and other sections remain available below.</div>
<div class="empty-actions">
<button
id="noSubscriptionActionBtn"
class="btn btn-primary"
type="button"
data-i18n="empty.no_subscription.action.open_bot"
>Open bot</button>
</div>
<div class="empty-hint hidden" id="noSubscriptionTrialHint" data-i18n="empty.no_subscription.trial_hint">A trial is available — activate it in the bot to try the service for free.</div>
</div>
<!-- Promo Offers -->
<div id="promoOffersContainer" class="promo-offers hidden"></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">
@@ -5050,14 +4972,6 @@
'app.loading': 'Loading your subscription...',
'error.default.title': 'Subscription Not Found',
'error.default.message': 'Please contact support to activate your subscription.',
'empty.no_subscription.title': 'No active subscription',
'empty.no_subscription.subtitle': 'Purchase a plan or activate the trial in the bot to start using VPN.',
'empty.no_subscription.info': 'Your balance and other sections remain available below.',
'empty.no_subscription.action.open_bot': 'Open bot',
'empty.no_subscription.trial_hint': 'A trial is available — activate it in the bot to try the service for free.',
'registration.title': 'Start in Telegram',
'registration.subtitle': 'Open the bot and complete registration to access the mini app.',
'registration.action': 'Open bot',
'stats.days_left': 'Days left',
'stats.servers': 'Servers',
'stats.devices': 'Devices',
@@ -5367,10 +5281,8 @@
'status.expired': 'Expired',
'status.disabled': 'Disabled',
'status.unknown': 'Unknown',
'status.none': 'No subscription',
'subscription.type.trial': 'Trial',
'subscription.type.paid': 'Paid',
'subscription.type.none': 'No subscription',
'autopay.enabled': 'Enabled',
'autopay.disabled': 'Disabled',
'platform.ios': 'iOS',
@@ -5415,14 +5327,6 @@
'app.loading': 'Загружаем вашу подписку...',
'error.default.title': 'Подписка не найдена',
'error.default.message': 'Свяжитесь с поддержкой, чтобы активировать подписку.',
'empty.no_subscription.title': 'Подписка не активна',
'empty.no_subscription.subtitle': 'Оформите подписку или активируйте пробный период в боте, чтобы пользоваться VPN.',
'empty.no_subscription.info': 'Ваш баланс и другие разделы доступны ниже.',
'empty.no_subscription.action.open_bot': 'Открыть бота',
'empty.no_subscription.trial_hint': 'Доступен пробный период — активируйте его в боте, чтобы протестировать сервис бесплатно.',
'registration.title': 'Начните в Telegram',
'registration.subtitle': 'Откройте бота и зарегистрируйтесь, чтобы пользоваться мини-приложением.',
'registration.action': 'Открыть бота',
'stats.days_left': 'Осталось дней',
'stats.servers': 'Серверы',
'stats.devices': 'Устройства',
@@ -5732,10 +5636,8 @@
'status.expired': 'Истекла',
'status.disabled': 'Отключена',
'status.unknown': 'Неизвестно',
'status.none': 'Нет подписки',
'subscription.type.trial': 'Триал',
'subscription.type.paid': 'Платная',
'subscription.type.none': 'Нет подписки',
'autopay.enabled': 'Включен',
'autopay.disabled': 'Выключен',
'platform.ios': 'iOS',
@@ -5886,7 +5788,6 @@
let preferredLanguage = 'en';
let languageLockedByUser = false;
let currentErrorState = null;
let currentRegistrationState = null;
let paymentMethodsCache = null;
let paymentMethodsPromise = null;
let activePaymentMethod = null;
@@ -6559,8 +6460,6 @@
function getEffectivePurchaseUrl() {
const candidates = [
currentErrorState?.purchaseUrl,
currentRegistrationState?.purchaseUrl,
userData?.empty_state?.purchase_url,
subscriptionPurchaseUrl,
configPurchaseUrl,
];
@@ -6617,8 +6516,6 @@
languageSelect.setAttribute('aria-label', t('language.ariaLabel'));
}
updateErrorTexts();
renderRegistrationState();
renderNoSubscriptionState();
}
function updateConnectButtonLabel() {
@@ -6674,7 +6571,7 @@
setLanguage(event.target.value, { persist: true });
});
function createError(title, message, status, code) {
function createError(title, message, status) {
const error = new Error(message || title);
if (title) {
error.title = title;
@@ -6682,9 +6579,6 @@
if (status) {
error.status = status;
}
if (code) {
error.code = code;
}
return error;
}
@@ -6728,7 +6622,6 @@
: 'Subscription not found';
let title = response.status === 401 ? 'Authorization Error' : 'Subscription Not Found';
let purchaseUrl = null;
let code = null;
try {
const errorPayload = await response.json();
@@ -6739,9 +6632,6 @@
if (typeof errorPayload.detail.message === 'string') {
detail = errorPayload.detail.message;
}
if (typeof errorPayload.detail.code === 'string') {
code = errorPayload.detail.code;
}
purchaseUrl = errorPayload.detail.purchase_url
|| errorPayload.detail.purchaseUrl
|| purchaseUrl;
@@ -6754,10 +6644,6 @@
title = errorPayload.title;
}
if (typeof errorPayload?.code === 'string' && !code) {
code = errorPayload.code;
}
purchaseUrl = purchaseUrl
|| errorPayload?.purchase_url
|| errorPayload?.purchaseUrl
@@ -6766,7 +6652,7 @@
// ignore JSON parsing errors
}
const errorObject = createError(title, detail, response.status, code);
const errorObject = createError(title, detail, response.status);
const normalizedPurchaseUrl = normalizeUrl(purchaseUrl);
if (normalizedPurchaseUrl) {
errorObject.purchaseUrl = normalizedPurchaseUrl;
@@ -6784,29 +6670,6 @@
userData.subscriptionCryptoLink = userData.subscription_crypto_link || null;
userData.referral = userData.referral || null;
const registrationStateElement = document.getElementById('registrationState');
currentRegistrationState = null;
registrationStateElement?.classList.add('hidden');
renderRegistrationState();
const normalizedState = typeof userData.state === 'string'
? userData.state.toLowerCase()
: 'subscription';
userData.state = normalizedState;
const rawEmptyState = userData.empty_state || userData.emptyState;
if (rawEmptyState && typeof rawEmptyState === 'object') {
const emptyState = { ...rawEmptyState };
const purchaseUrl = emptyState.purchase_url || emptyState.purchaseUrl;
emptyState.purchase_url = normalizeUrl(purchaseUrl);
emptyState.trial_available = Boolean(
emptyState.trial_available ?? emptyState.trialAvailable
);
userData.empty_state = emptyState;
} else {
userData.empty_state = null;
}
resetSubscriptionRenewalState(null);
prepareSubscriptionRenewalFromUserData();
@@ -6921,237 +6784,100 @@
}
}
function renderNoSubscriptionState() {
const block = document.getElementById('noSubscriptionState');
if (!block) {
return;
}
const stateValue = String(userData?.state || '').toLowerCase();
const isNoSubscription = stateValue === 'no_subscription';
block.classList.toggle('hidden', !isNoSubscription);
if (!isNoSubscription) {
const actionBtn = document.getElementById('noSubscriptionActionBtn');
if (actionBtn) {
actionBtn.dataset.link = '';
}
return;
}
const emptyState = userData?.empty_state || {};
const titleElement = document.getElementById('noSubscriptionTitle');
if (titleElement) {
const key = 'empty.no_subscription.title';
const translation = t(key);
titleElement.textContent = translation === key
? (emptyState.title || 'No active subscription')
: translation;
}
const textElement = document.getElementById('noSubscriptionText');
if (textElement) {
const key = 'empty.no_subscription.subtitle';
const translation = t(key);
textElement.textContent = translation === key
? (emptyState.message || 'Purchase a plan or activate the trial in the bot to start using VPN.')
: translation;
}
const noteElement = document.getElementById('noSubscriptionNote');
if (noteElement) {
const key = 'empty.no_subscription.info';
const translation = t(key);
if (translation && translation !== key) {
noteElement.textContent = translation;
}
}
const purchaseCandidates = [
emptyState.purchase_url,
subscriptionPurchaseUrl,
configPurchaseUrl,
];
const purchaseLink = purchaseCandidates
.map(candidate => normalizeUrl(candidate))
.find(Boolean) || null;
const actionButton = document.getElementById('noSubscriptionActionBtn');
if (actionButton) {
const key = 'empty.no_subscription.action.open_bot';
const translation = t(key);
actionButton.textContent = translation === key ? 'Open bot' : translation;
actionButton.dataset.link = purchaseLink || '';
actionButton.disabled = !purchaseLink;
actionButton.classList.toggle('hidden', !purchaseLink);
}
const trialHint = document.getElementById('noSubscriptionTrialHint');
if (trialHint) {
const trialAvailable = Boolean(emptyState.trial_available);
trialHint.classList.toggle('hidden', !trialAvailable);
if (trialAvailable) {
const key = 'empty.no_subscription.trial_hint';
const translation = t(key);
trialHint.textContent = translation === key
? 'A trial is available — activate it in the bot to try the service for free.'
: translation;
}
}
}
function renderRegistrationState() {
const state = document.getElementById('registrationState');
if (!state) {
return;
}
const isActive = Boolean(currentRegistrationState);
state.classList.toggle('hidden', !isActive);
if (!isActive) {
const button = document.getElementById('registrationActionBtn');
if (button) {
button.dataset.link = '';
}
return;
}
const titleKey = 'registration.title';
const subtitleKey = 'registration.subtitle';
const actionKey = 'registration.action';
const titleElement = state.querySelector('[data-i18n="registration.title"]');
if (titleElement) {
const translation = t(titleKey);
titleElement.textContent = translation === titleKey
? 'Start in Telegram'
: translation;
}
const textElement = state.querySelector('[data-i18n="registration.subtitle"]');
if (textElement) {
const translation = t(subtitleKey);
textElement.textContent = translation === subtitleKey
? 'Open the bot and complete registration to access the mini app.'
: translation;
}
const button = document.getElementById('registrationActionBtn');
if (button) {
const translation = t(actionKey);
button.textContent = translation === actionKey ? 'Open bot' : translation;
const link = normalizeUrl(currentRegistrationState?.purchaseUrl)
|| getEffectivePurchaseUrl();
button.dataset.link = link || '';
button.disabled = !link;
button.classList.toggle('hidden', !link);
}
}
function renderUserData() {
if (!userData?.user) {
return;
}
const isNoSubscription = String(userData?.state || '').toLowerCase() === 'no_subscription';
const userCard = document.getElementById('userCard');
if (userCard) {
userCard.classList.toggle('hidden', isNoSubscription);
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 knownStatuses = ['active', 'trial', 'expired', 'disabled'];
const statusValueRaw = (user.subscription_actual_status || user.subscription_status || 'active').toLowerCase();
const statusClass = knownStatuses.includes(statusValueRaw) ? statusValueRaw : 'unknown';
const statusBadge = document.getElementById('statusBadge');
const statusKey = `status.${statusClass}`;
const statusLabel = t(statusKey);
statusBadge.textContent = statusLabel === statusKey
? (user.status_label || statusClass.charAt(0).toUpperCase() + statusClass.slice(1))
: statusLabel;
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 = formatDate(user.expires_at);
if (!isNoSubscription) {
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 knownStatuses = ['active', 'trial', 'expired', 'disabled'];
const statusValueRaw = (user.subscription_actual_status || user.subscription_status || 'active').toLowerCase();
const statusClass = knownStatuses.includes(statusValueRaw) ? statusValueRaw : 'unknown';
const statusBadge = document.getElementById('statusBadge');
const statusKey = `status.${statusClass}`;
const statusLabel = t(statusKey);
statusBadge.textContent = statusLabel === statusKey
? (user.status_label || statusClass.charAt(0).toUpperCase() + statusClass.slice(1))
: statusLabel;
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 = formatDate(user.expires_at);
const serversCount = Array.isArray(userData.connected_squads)
? userData.connected_squads.length
: Array.isArray(userData.connected_servers)
? userData.connected_servers.length
: Array.isArray(userData.links)
? userData.links.length
: 0;
document.getElementById('serversCount').textContent = serversCount;
const devicesCountRaw = Number(userData?.connected_devices_count);
const devicesCount = Number.isFinite(devicesCountRaw)
? devicesCountRaw
: Array.isArray(userData?.connected_devices)
? userData.connected_devices.length
const serversCount = Array.isArray(userData.connected_squads)
? userData.connected_squads.length
: Array.isArray(userData.connected_servers)
? userData.connected_servers.length
: Array.isArray(userData.links)
? userData.links.length
: 0;
const devicesCountElement = document.getElementById('devicesCount');
if (devicesCountElement) {
devicesCountElement.textContent = devicesCount;
}
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);
const deviceLimitElement = document.getElementById('deviceLimit');
if (deviceLimitElement) {
const limitValue = typeof user.device_limit === 'number'
? user.device_limit
: Number.parseInt(user.device_limit ?? '', 10);
deviceLimitElement.textContent = Number.isFinite(limitValue)
? String(limitValue)
: t('values.not_available');
}
const subscriptionTypeElement = document.getElementById('subscriptionType');
if (subscriptionTypeElement) {
const fallbackSubscriptionType = (user?.subscription_status || '').toLowerCase() === 'trial'
? 'trial'
: 'paid';
const subscriptionTypeRaw = String(
userData?.subscription_type
|| fallbackSubscriptionType
).toLowerCase();
const subscriptionTypeKey = `subscription.type.${subscriptionTypeRaw}`;
const subscriptionTypeLabel = t(subscriptionTypeKey);
subscriptionTypeElement.textContent = subscriptionTypeLabel === subscriptionTypeKey
? subscriptionTypeRaw
: subscriptionTypeLabel;
}
const autopayElement = document.getElementById('autopayStatus');
if (autopayElement) {
const autopayKey = userData?.autopay_enabled ? 'autopay.enabled' : 'autopay.disabled';
const autopayLabel = t(autopayKey);
autopayElement.textContent = autopayLabel === autopayKey
? (userData?.autopay_enabled ? 'On' : 'Off')
: autopayLabel;
}
const devicesCountRaw = Number(userData?.connected_devices_count);
const devicesCount = Number.isFinite(devicesCountRaw)
? devicesCountRaw
: Array.isArray(userData?.connected_devices)
? userData.connected_devices.length
: 0;
const devicesCountElement = document.getElementById('devicesCount');
if (devicesCountElement) {
devicesCountElement.textContent = devicesCount;
}
renderNoSubscriptionState();
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);
const deviceLimitElement = document.getElementById('deviceLimit');
if (deviceLimitElement) {
const limitValue = typeof user.device_limit === 'number'
? user.device_limit
: Number.parseInt(user.device_limit ?? '', 10);
deviceLimitElement.textContent = Number.isFinite(limitValue)
? String(limitValue)
: t('values.not_available');
}
const subscriptionTypeElement = document.getElementById('subscriptionType');
if (subscriptionTypeElement) {
const fallbackSubscriptionType = (user?.subscription_status || '').toLowerCase() === 'trial'
? 'trial'
: 'paid';
const subscriptionTypeRaw = String(
userData?.subscription_type
|| fallbackSubscriptionType
).toLowerCase();
const subscriptionTypeKey = `subscription.type.${subscriptionTypeRaw}`;
const subscriptionTypeLabel = t(subscriptionTypeKey);
subscriptionTypeElement.textContent = subscriptionTypeLabel === subscriptionTypeKey
? subscriptionTypeRaw
: subscriptionTypeLabel;
}
const autopayElement = document.getElementById('autopayStatus');
if (autopayElement) {
const autopayKey = userData?.autopay_enabled ? 'autopay.enabled' : 'autopay.disabled';
const autopayLabel = t(autopayKey);
autopayElement.textContent = autopayLabel === autopayKey
? (userData?.autopay_enabled ? 'On' : 'Off')
: autopayLabel;
}
renderSubscriptionPurchaseCard();
renderSubscriptionRenewalCard();
@@ -15550,30 +15276,13 @@
function showError(error) {
document.getElementById('loadingState').classList.add('hidden');
document.getElementById('mainContent').classList.add('hidden');
const errorState = document.getElementById('errorState');
errorState?.classList.add('hidden');
currentErrorState = null;
currentRegistrationState = null;
renderRegistrationState();
renderNoSubscriptionState();
if (error?.code === 'user_not_found') {
currentRegistrationState = {
purchaseUrl: normalizeUrl(error?.purchaseUrl) || null,
};
renderRegistrationState();
updateActionButtons();
return;
}
currentErrorState = {
title: error?.title,
message: error?.message,
purchaseUrl: normalizeUrl(error?.purchaseUrl) || null,
};
updateErrorTexts();
errorState?.classList.remove('hidden');
document.getElementById('errorState').classList.remove('hidden');
updateActionButtons();
}
@@ -15635,26 +15344,6 @@
}
});
const registrationActionBtn = document.getElementById('registrationActionBtn');
if (registrationActionBtn) {
registrationActionBtn.addEventListener('click', () => {
const link = registrationActionBtn.dataset.link;
if (link) {
openExternalLink(link);
}
});
}
const noSubscriptionActionBtn = document.getElementById('noSubscriptionActionBtn');
if (noSubscriptionActionBtn) {
noSubscriptionActionBtn.addEventListener('click', () => {
const link = noSubscriptionActionBtn.dataset.link;
if (link) {
openExternalLink(link);
}
});
}
document.getElementById('referralToggleBtn')?.addEventListener('click', () => {
referralListExpanded = !referralListExpanded;
updateReferralToggleState();