mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-03-04 21:04:00 +00:00
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:
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user