mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-02-22 20:31:47 +00:00
Merge pull request #1174 from Fr1ngg/cvpz16-bedolaga/update-miniapp/index.html-logic
Improve miniapp onboarding for unsubscribed users
This commit is contained in:
@@ -2055,6 +2055,280 @@ async def _build_referral_info(
|
||||
)
|
||||
|
||||
|
||||
async def _build_subscription_missing_response(
|
||||
db: AsyncSession,
|
||||
user: User,
|
||||
purchase_url: str,
|
||||
) -> MiniAppSubscriptionResponse:
|
||||
transactions_query = (
|
||||
select(Transaction)
|
||||
.where(Transaction.user_id == user.id)
|
||||
.order_by(Transaction.created_at.desc())
|
||||
.limit(10)
|
||||
)
|
||||
transactions_result = await db.execute(transactions_query)
|
||||
transactions = list(transactions_result.scalars().all())
|
||||
|
||||
balance_currency = getattr(user, "balance_currency", None)
|
||||
if isinstance(balance_currency, str):
|
||||
balance_currency = balance_currency.upper()
|
||||
|
||||
promo_group = getattr(user, "promo_group", None)
|
||||
total_spent_kopeks = await get_user_total_spent_kopeks(db, user.id)
|
||||
auto_assign_groups = await get_auto_assign_promo_groups(db)
|
||||
|
||||
auto_promo_levels: List[MiniAppAutoPromoGroupLevel] = []
|
||||
for group in auto_assign_groups:
|
||||
threshold = group.auto_assign_total_spent_kopeks or 0
|
||||
if threshold <= 0:
|
||||
continue
|
||||
|
||||
auto_promo_levels.append(
|
||||
MiniAppAutoPromoGroupLevel(
|
||||
id=group.id,
|
||||
name=group.name,
|
||||
threshold_kopeks=threshold,
|
||||
threshold_rubles=round(threshold / 100, 2),
|
||||
threshold_label=settings.format_price(threshold),
|
||||
is_reached=total_spent_kopeks >= threshold,
|
||||
is_current=bool(promo_group and promo_group.id == group.id),
|
||||
**_extract_promo_discounts(group),
|
||||
)
|
||||
)
|
||||
|
||||
active_discount_percent = 0
|
||||
try:
|
||||
active_discount_percent = int(getattr(user, "promo_offer_discount_percent", 0) or 0)
|
||||
except (TypeError, ValueError):
|
||||
active_discount_percent = 0
|
||||
|
||||
active_discount_expires_at = getattr(user, "promo_offer_discount_expires_at", None)
|
||||
now = datetime.utcnow()
|
||||
if active_discount_expires_at and active_discount_expires_at <= now:
|
||||
active_discount_expires_at = None
|
||||
active_discount_percent = 0
|
||||
|
||||
available_promo_offers = await list_active_discount_offers_for_user(db, user.id)
|
||||
|
||||
promo_offer_source = getattr(user, "promo_offer_discount_source", None)
|
||||
active_offer_contexts: List[ActiveOfferContext] = []
|
||||
if promo_offer_source or active_discount_percent > 0:
|
||||
active_discount_offer = await get_latest_claimed_offer_for_user(
|
||||
db,
|
||||
user.id,
|
||||
promo_offer_source,
|
||||
)
|
||||
if active_discount_offer and active_discount_percent > 0:
|
||||
active_offer_contexts.append(
|
||||
(
|
||||
active_discount_offer,
|
||||
active_discount_percent,
|
||||
active_discount_expires_at,
|
||||
)
|
||||
)
|
||||
|
||||
promo_offers = await _build_promo_offer_models(
|
||||
db,
|
||||
available_promo_offers,
|
||||
active_offer_contexts,
|
||||
user=user,
|
||||
)
|
||||
|
||||
content_language_preference = user.language or settings.DEFAULT_LANGUAGE or "ru"
|
||||
|
||||
requested_faq_language = FaqService.normalize_language(content_language_preference)
|
||||
faq_pages = await FaqService.get_pages(
|
||||
db,
|
||||
requested_faq_language,
|
||||
include_inactive=False,
|
||||
fallback=True,
|
||||
)
|
||||
|
||||
faq_payload: Optional[MiniAppFaq] = None
|
||||
if faq_pages:
|
||||
faq_setting = await FaqService.get_setting(
|
||||
db,
|
||||
requested_faq_language,
|
||||
fallback=True,
|
||||
)
|
||||
is_enabled = bool(faq_setting.is_enabled) if faq_setting else True
|
||||
|
||||
if is_enabled:
|
||||
ordered_pages = sorted(
|
||||
faq_pages,
|
||||
key=lambda page: (
|
||||
(page.display_order or 0),
|
||||
page.id,
|
||||
),
|
||||
)
|
||||
faq_items: List[MiniAppFaqItem] = []
|
||||
for page in ordered_pages:
|
||||
raw_content = (page.content or "").strip()
|
||||
if not raw_content:
|
||||
continue
|
||||
if not re.sub(r"<[^>]+>", "", raw_content).strip():
|
||||
continue
|
||||
faq_items.append(
|
||||
MiniAppFaqItem(
|
||||
id=page.id,
|
||||
title=page.title or None,
|
||||
content=page.content or "",
|
||||
display_order=getattr(page, "display_order", None),
|
||||
)
|
||||
)
|
||||
|
||||
if faq_items:
|
||||
resolved_language = (
|
||||
faq_setting.language
|
||||
if faq_setting and faq_setting.language
|
||||
else ordered_pages[0].language
|
||||
)
|
||||
faq_payload = MiniAppFaq(
|
||||
requested_language=requested_faq_language,
|
||||
language=resolved_language or requested_faq_language,
|
||||
is_enabled=is_enabled,
|
||||
total=len(faq_items),
|
||||
items=faq_items,
|
||||
)
|
||||
|
||||
legal_documents_payload: Optional[MiniAppLegalDocuments] = None
|
||||
|
||||
requested_offer_language = PublicOfferService.normalize_language(content_language_preference)
|
||||
public_offer = await PublicOfferService.get_active_offer(
|
||||
db,
|
||||
requested_offer_language,
|
||||
)
|
||||
if public_offer and (public_offer.content or "").strip():
|
||||
legal_documents_payload = legal_documents_payload or MiniAppLegalDocuments()
|
||||
legal_documents_payload.public_offer = MiniAppRichTextDocument(
|
||||
requested_language=requested_offer_language,
|
||||
language=public_offer.language,
|
||||
title=None,
|
||||
is_enabled=bool(public_offer.is_enabled),
|
||||
content=public_offer.content or "",
|
||||
created_at=public_offer.created_at,
|
||||
updated_at=public_offer.updated_at,
|
||||
)
|
||||
|
||||
requested_policy_language = PrivacyPolicyService.normalize_language(
|
||||
content_language_preference
|
||||
)
|
||||
privacy_policy = await PrivacyPolicyService.get_active_policy(
|
||||
db,
|
||||
requested_policy_language,
|
||||
)
|
||||
if privacy_policy and (privacy_policy.content or "").strip():
|
||||
legal_documents_payload = legal_documents_payload or MiniAppLegalDocuments()
|
||||
legal_documents_payload.privacy_policy = MiniAppRichTextDocument(
|
||||
requested_language=requested_policy_language,
|
||||
language=privacy_policy.language,
|
||||
title=None,
|
||||
is_enabled=bool(privacy_policy.is_enabled),
|
||||
content=privacy_policy.content or "",
|
||||
created_at=privacy_policy.created_at,
|
||||
updated_at=privacy_policy.updated_at,
|
||||
)
|
||||
|
||||
requested_rules_language = (content_language_preference or "ru").split("-")[0].lower()
|
||||
default_rules_language = (settings.DEFAULT_LANGUAGE or "ru").split("-")[0].lower()
|
||||
service_rules = await get_rules_by_language(db, requested_rules_language)
|
||||
if not service_rules and requested_rules_language != default_rules_language:
|
||||
service_rules = await get_rules_by_language(db, default_rules_language)
|
||||
|
||||
if service_rules and (service_rules.content or "").strip():
|
||||
legal_documents_payload = legal_documents_payload or MiniAppLegalDocuments()
|
||||
legal_documents_payload.service_rules = MiniAppRichTextDocument(
|
||||
requested_language=requested_rules_language,
|
||||
language=service_rules.language,
|
||||
title=getattr(service_rules, "title", None),
|
||||
is_enabled=bool(getattr(service_rules, "is_active", True)),
|
||||
content=service_rules.content or "",
|
||||
created_at=getattr(service_rules, "created_at", None),
|
||||
updated_at=getattr(service_rules, "updated_at", None),
|
||||
)
|
||||
|
||||
devices_count, devices = await _load_devices_info(user)
|
||||
lifetime_used = _bytes_to_gb(getattr(user, "lifetime_used_traffic_bytes", 0))
|
||||
|
||||
default_device_limit = settings.DEFAULT_DEVICE_LIMIT if settings.DEFAULT_DEVICE_LIMIT > 0 else None
|
||||
|
||||
response_user = MiniAppSubscriptionUser(
|
||||
telegram_id=user.telegram_id,
|
||||
username=user.username,
|
||||
first_name=user.first_name,
|
||||
last_name=user.last_name,
|
||||
display_name=_resolve_display_name(
|
||||
{
|
||||
"username": user.username,
|
||||
"first_name": user.first_name,
|
||||
"last_name": user.last_name,
|
||||
"telegram_id": user.telegram_id,
|
||||
}
|
||||
),
|
||||
language=user.language,
|
||||
status=user.status,
|
||||
subscription_status="inactive",
|
||||
subscription_actual_status="inactive",
|
||||
status_label=_status_label("inactive"),
|
||||
expires_at=None,
|
||||
device_limit=default_device_limit,
|
||||
traffic_used_gb=0.0,
|
||||
traffic_used_label=_format_gb_label(0.0),
|
||||
traffic_limit_gb=None,
|
||||
traffic_limit_label=_format_limit_label(None),
|
||||
lifetime_used_traffic_gb=lifetime_used,
|
||||
has_active_subscription=False,
|
||||
promo_offer_discount_percent=active_discount_percent,
|
||||
promo_offer_discount_expires_at=active_discount_expires_at,
|
||||
promo_offer_discount_source=promo_offer_source,
|
||||
)
|
||||
|
||||
referral_info = await _build_referral_info(db, user)
|
||||
|
||||
return MiniAppSubscriptionResponse(
|
||||
subscription_id=0,
|
||||
remnawave_short_uuid=None,
|
||||
user=response_user,
|
||||
subscription_url=None,
|
||||
subscription_crypto_link=None,
|
||||
subscription_purchase_url=purchase_url or None,
|
||||
links=[],
|
||||
ss_conf_links={},
|
||||
connected_squads=[],
|
||||
connected_servers=[],
|
||||
connected_devices_count=devices_count,
|
||||
connected_devices=devices,
|
||||
happ=None,
|
||||
happ_link=None,
|
||||
happ_crypto_link=None,
|
||||
happ_cryptolink_redirect_link=None,
|
||||
balance_kopeks=user.balance_kopeks,
|
||||
balance_rubles=round(getattr(user, "balance_rubles", user.balance_kopeks / 100), 2),
|
||||
balance_currency=balance_currency,
|
||||
transactions=[_serialize_transaction(tx) for tx in transactions],
|
||||
promo_offers=promo_offers,
|
||||
promo_group=(
|
||||
MiniAppPromoGroup(
|
||||
id=promo_group.id,
|
||||
name=promo_group.name,
|
||||
**_extract_promo_discounts(promo_group),
|
||||
)
|
||||
if promo_group
|
||||
else None
|
||||
),
|
||||
auto_assign_promo_groups=auto_promo_levels,
|
||||
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="none",
|
||||
autopay_enabled=False,
|
||||
branding=settings.get_miniapp_branding(),
|
||||
faq=faq_payload,
|
||||
legal_documents=legal_documents_payload,
|
||||
referral=referral_info,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/subscription", response_model=MiniAppSubscriptionResponse)
|
||||
async def get_subscription_details(
|
||||
payload: MiniAppSubscriptionRequest,
|
||||
@@ -2085,18 +2359,26 @@ 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 or not user.subscription:
|
||||
detail: Union[str, Dict[str, str]] = "Subscription not found"
|
||||
bot_username = settings.get_bot_username()
|
||||
bot_url = f"https://t.me/{bot_username}" if bot_username else None
|
||||
|
||||
if not user:
|
||||
detail: Dict[str, Optional[str]] = {
|
||||
"code": "user_not_registered",
|
||||
"message": "User is not registered in the bot",
|
||||
}
|
||||
if purchase_url:
|
||||
detail = {
|
||||
"message": "Subscription not found",
|
||||
"purchase_url": purchase_url,
|
||||
}
|
||||
detail["purchase_url"] = purchase_url
|
||||
if bot_url:
|
||||
detail["bot_url"] = bot_url
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=detail,
|
||||
)
|
||||
|
||||
if not user.subscription:
|
||||
return await _build_subscription_missing_response(db, user, purchase_url)
|
||||
|
||||
subscription = user.subscription
|
||||
traffic_used = _format_gb(subscription.traffic_used_gb)
|
||||
traffic_limit = subscription.traffic_limit_gb or 0
|
||||
|
||||
@@ -331,6 +331,46 @@
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.state-card {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: var(--radius-xl);
|
||||
padding: 32px 24px;
|
||||
text-align: center;
|
||||
box-shadow: var(--shadow-sm);
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.state-icon {
|
||||
font-size: 56px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.state-title {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 12px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.state-text {
|
||||
font-size: 15px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.state-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.state-actions .btn {
|
||||
width: auto;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.card {
|
||||
background: var(--bg-secondary);
|
||||
@@ -1882,6 +1922,11 @@
|
||||
color: #41464b;
|
||||
}
|
||||
|
||||
.status-inactive {
|
||||
background: linear-gradient(135deg, #e7e9ff, #f0f2ff);
|
||||
color: #3f3d56;
|
||||
}
|
||||
|
||||
/* Stats Grid */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
@@ -4223,11 +4268,31 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Registration State -->
|
||||
<div id="registerState" class="state-card hidden">
|
||||
<div class="state-icon">🤖</div>
|
||||
<div class="state-title" data-i18n="register_required.title">Registration required</div>
|
||||
<div class="state-text" data-i18n="register_required.description">Open the bot to create an account and start using the VPN.</div>
|
||||
<div class="state-actions">
|
||||
<button class="btn btn-primary" id="registerBtn" type="button" data-i18n="register_required.action">Open bot</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div id="mainContent" class="hidden">
|
||||
<!-- Promo Offers -->
|
||||
<div id="promoOffersContainer" class="promo-offers hidden"></div>
|
||||
|
||||
<!-- Empty Subscription State -->
|
||||
<div class="state-card hidden" id="subscriptionEmptyCard">
|
||||
<div class="state-icon">🛒</div>
|
||||
<div class="state-title" data-i18n="empty_subscription.title">Subscription inactive</div>
|
||||
<div class="state-text" data-i18n="empty_subscription.description">You don't have an active subscription yet. Purchase a plan or activate a trial in the bot.</div>
|
||||
<div class="state-actions">
|
||||
<button class="btn btn-primary" id="subscriptionEmptyAction" type="button" data-i18n="empty_subscription.action">Open bot</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Card -->
|
||||
<div class="card user-card animate-in">
|
||||
<div class="user-header">
|
||||
@@ -4972,6 +5037,12 @@
|
||||
'app.loading': 'Loading your subscription...',
|
||||
'error.default.title': 'Subscription Not Found',
|
||||
'error.default.message': 'Please contact support to activate your subscription.',
|
||||
'register_required.title': 'Registration required',
|
||||
'register_required.description': 'Open the bot to register and manage your subscription.',
|
||||
'register_required.action': 'Open bot',
|
||||
'empty_subscription.title': 'Subscription inactive',
|
||||
'empty_subscription.description': 'You do not have an active subscription yet. Purchase a plan or activate a trial in the bot.',
|
||||
'empty_subscription.action': 'Open bot',
|
||||
'stats.days_left': 'Days left',
|
||||
'stats.servers': 'Servers',
|
||||
'stats.devices': 'Devices',
|
||||
@@ -4985,6 +5056,7 @@
|
||||
'button.connect.default': 'Connect to VPN',
|
||||
'button.connect.happ': 'Connect',
|
||||
'button.copy': 'Copy subscription link',
|
||||
'button.open_bot': 'Open bot',
|
||||
'button.topup_balance': 'Top up balance',
|
||||
'topup.title': 'Top up balance',
|
||||
'topup.subtitle': 'Choose a payment method',
|
||||
@@ -5280,9 +5352,11 @@
|
||||
'status.trial': 'Trial',
|
||||
'status.expired': 'Expired',
|
||||
'status.disabled': 'Disabled',
|
||||
'status.inactive': 'Inactive',
|
||||
'status.unknown': 'Unknown',
|
||||
'subscription.type.trial': 'Trial',
|
||||
'subscription.type.paid': 'Paid',
|
||||
'subscription.type.none': 'No subscription',
|
||||
'autopay.enabled': 'Enabled',
|
||||
'autopay.disabled': 'Disabled',
|
||||
'platform.ios': 'iOS',
|
||||
@@ -5327,6 +5401,12 @@
|
||||
'app.loading': 'Загружаем вашу подписку...',
|
||||
'error.default.title': 'Подписка не найдена',
|
||||
'error.default.message': 'Свяжитесь с поддержкой, чтобы активировать подписку.',
|
||||
'register_required.title': 'Требуется регистрация',
|
||||
'register_required.description': 'Откройте бота, чтобы зарегистрироваться и управлять подпиской.',
|
||||
'register_required.action': 'Открыть бота',
|
||||
'empty_subscription.title': 'Подписка не активна',
|
||||
'empty_subscription.description': 'У вас нет активной подписки. Купите тариф или активируйте триал в боте.',
|
||||
'empty_subscription.action': 'Открыть бота',
|
||||
'stats.days_left': 'Осталось дней',
|
||||
'stats.servers': 'Серверы',
|
||||
'stats.devices': 'Устройства',
|
||||
@@ -5340,6 +5420,7 @@
|
||||
'button.connect.default': 'Подключиться к VPN',
|
||||
'button.connect.happ': 'Подключиться',
|
||||
'button.copy': 'Скопировать ссылку подписки',
|
||||
'button.open_bot': 'Открыть бота',
|
||||
'button.topup_balance': 'Пополнить баланс',
|
||||
'topup.title': 'Пополнение баланса',
|
||||
'topup.subtitle': 'Выберите способ оплаты',
|
||||
@@ -5635,9 +5716,11 @@
|
||||
'status.trial': 'Пробная',
|
||||
'status.expired': 'Истекла',
|
||||
'status.disabled': 'Отключена',
|
||||
'status.inactive': 'Неактивна',
|
||||
'status.unknown': 'Неизвестно',
|
||||
'subscription.type.trial': 'Триал',
|
||||
'subscription.type.paid': 'Платная',
|
||||
'subscription.type.none': 'Нет подписки',
|
||||
'autopay.enabled': 'Включен',
|
||||
'autopay.disabled': 'Выключен',
|
||||
'platform.ios': 'iOS',
|
||||
@@ -6459,6 +6542,7 @@
|
||||
|
||||
function getEffectivePurchaseUrl() {
|
||||
const candidates = [
|
||||
currentErrorState?.botUrl,
|
||||
currentErrorState?.purchaseUrl,
|
||||
subscriptionPurchaseUrl,
|
||||
configPurchaseUrl,
|
||||
@@ -6493,6 +6577,31 @@
|
||||
}
|
||||
}
|
||||
|
||||
function updateRegisterState() {
|
||||
const registerBtn = document.getElementById('registerBtn');
|
||||
if (!registerBtn) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sources = [
|
||||
currentErrorState?.botUrl,
|
||||
currentErrorState?.purchaseUrl,
|
||||
configPurchaseUrl,
|
||||
];
|
||||
|
||||
let link = null;
|
||||
for (const source of sources) {
|
||||
const normalized = normalizeUrl(source);
|
||||
if (normalized) {
|
||||
link = normalized;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
registerBtn.disabled = !link;
|
||||
registerBtn.dataset.link = link || '';
|
||||
}
|
||||
|
||||
function applyTranslations() {
|
||||
document.title = t('app.title');
|
||||
document.documentElement.setAttribute('lang', preferredLanguage);
|
||||
@@ -6516,6 +6625,7 @@
|
||||
languageSelect.setAttribute('aria-label', t('language.ariaLabel'));
|
||||
}
|
||||
updateErrorTexts();
|
||||
updateRegisterState();
|
||||
}
|
||||
|
||||
function updateConnectButtonLabel() {
|
||||
@@ -6571,7 +6681,7 @@
|
||||
setLanguage(event.target.value, { persist: true });
|
||||
});
|
||||
|
||||
function createError(title, message, status) {
|
||||
function createError(title, message, status, extra = null) {
|
||||
const error = new Error(message || title);
|
||||
if (title) {
|
||||
error.title = title;
|
||||
@@ -6579,6 +6689,9 @@
|
||||
if (status) {
|
||||
error.status = status;
|
||||
}
|
||||
if (extra && typeof extra === 'object') {
|
||||
Object.assign(error, extra);
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
@@ -6622,6 +6735,8 @@
|
||||
: 'Subscription not found';
|
||||
let title = response.status === 401 ? 'Authorization Error' : 'Subscription Not Found';
|
||||
let purchaseUrl = null;
|
||||
let detailCode = null;
|
||||
let botUrl = null;
|
||||
|
||||
try {
|
||||
const errorPayload = await response.json();
|
||||
@@ -6632,9 +6747,18 @@
|
||||
if (typeof errorPayload.detail.message === 'string') {
|
||||
detail = errorPayload.detail.message;
|
||||
}
|
||||
if (typeof errorPayload.detail.code === 'string') {
|
||||
detailCode = errorPayload.detail.code;
|
||||
}
|
||||
if (typeof errorPayload.detail.title === 'string') {
|
||||
title = errorPayload.detail.title;
|
||||
}
|
||||
purchaseUrl = errorPayload.detail.purchase_url
|
||||
|| errorPayload.detail.purchaseUrl
|
||||
|| purchaseUrl;
|
||||
botUrl = errorPayload.detail.bot_url
|
||||
|| errorPayload.detail.botUrl
|
||||
|| botUrl;
|
||||
}
|
||||
} else if (typeof errorPayload?.message === 'string') {
|
||||
detail = errorPayload.message;
|
||||
@@ -6644,6 +6768,14 @@
|
||||
title = errorPayload.title;
|
||||
}
|
||||
|
||||
if (typeof errorPayload?.code === 'string') {
|
||||
detailCode = detailCode || errorPayload.code;
|
||||
}
|
||||
|
||||
if (typeof errorPayload?.bot_url === 'string' || typeof errorPayload?.botUrl === 'string') {
|
||||
botUrl = errorPayload.bot_url || errorPayload.botUrl || botUrl;
|
||||
}
|
||||
|
||||
purchaseUrl = purchaseUrl
|
||||
|| errorPayload?.purchase_url
|
||||
|| errorPayload?.purchaseUrl
|
||||
@@ -6652,11 +6784,13 @@
|
||||
// ignore JSON parsing errors
|
||||
}
|
||||
|
||||
const errorObject = createError(title, detail, response.status);
|
||||
const normalizedPurchaseUrl = normalizeUrl(purchaseUrl);
|
||||
if (normalizedPurchaseUrl) {
|
||||
errorObject.purchaseUrl = normalizedPurchaseUrl;
|
||||
}
|
||||
const normalizedBotUrl = normalizeUrl(botUrl);
|
||||
const errorObject = createError(title, detail, response.status, {
|
||||
code: detailCode,
|
||||
purchaseUrl: normalizedPurchaseUrl || null,
|
||||
botUrl: normalizedBotUrl || null,
|
||||
});
|
||||
throw errorObject;
|
||||
}
|
||||
|
||||
@@ -6691,12 +6825,18 @@
|
||||
|
||||
currentErrorState = null;
|
||||
updateErrorTexts();
|
||||
updateRegisterState();
|
||||
|
||||
const errorState = document.getElementById('errorState');
|
||||
if (errorState) {
|
||||
errorState.classList.add('hidden');
|
||||
}
|
||||
|
||||
const registerState = document.getElementById('registerState');
|
||||
if (registerState) {
|
||||
registerState.classList.add('hidden');
|
||||
}
|
||||
|
||||
const loadingState = document.getElementById('loadingState');
|
||||
if (loadingState) {
|
||||
loadingState.classList.add('hidden');
|
||||
@@ -6777,6 +6917,8 @@
|
||||
);
|
||||
if (configUrl) {
|
||||
configPurchaseUrl = configUrl;
|
||||
updateRegisterState();
|
||||
updateSubscriptionEmptyState();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Unable to load apps configuration:', error);
|
||||
@@ -6784,6 +6926,30 @@
|
||||
}
|
||||
}
|
||||
|
||||
function updateSubscriptionEmptyState() {
|
||||
const card = document.getElementById('subscriptionEmptyCard');
|
||||
const userCard = document.querySelector('.user-card');
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasUser = Boolean(userData?.user);
|
||||
const hasActive = Boolean(userData?.user?.has_active_subscription);
|
||||
const shouldShow = hasUser && !hasActive;
|
||||
|
||||
card.classList.toggle('hidden', !shouldShow);
|
||||
if (userCard) {
|
||||
userCard.classList.toggle('hidden', shouldShow);
|
||||
}
|
||||
|
||||
const actionBtn = document.getElementById('subscriptionEmptyAction');
|
||||
if (actionBtn) {
|
||||
const link = getEffectivePurchaseUrl();
|
||||
actionBtn.disabled = !link;
|
||||
actionBtn.dataset.link = link || '';
|
||||
}
|
||||
}
|
||||
|
||||
function renderUserData() {
|
||||
if (!userData?.user) {
|
||||
return;
|
||||
@@ -6799,7 +6965,7 @@
|
||||
document.getElementById('userAvatar').textContent = avatarChar;
|
||||
document.getElementById('userName').textContent = fallbackName;
|
||||
|
||||
const knownStatuses = ['active', 'trial', 'expired', 'disabled'];
|
||||
const knownStatuses = ['active', 'trial', 'expired', 'disabled', 'inactive'];
|
||||
const statusValueRaw = (user.subscription_actual_status || user.subscription_status || 'active').toLowerCase();
|
||||
const statusClass = knownStatuses.includes(statusValueRaw) ? statusValueRaw : 'unknown';
|
||||
const statusBadge = document.getElementById('statusBadge');
|
||||
@@ -6879,6 +7045,7 @@
|
||||
: autopayLabel;
|
||||
}
|
||||
|
||||
updateSubscriptionEmptyState();
|
||||
renderSubscriptionPurchaseCard();
|
||||
renderSubscriptionRenewalCard();
|
||||
renderSubscriptionSettingsCard();
|
||||
@@ -10682,7 +10849,7 @@
|
||||
|| userData.user.subscription_status
|
||||
|| ''
|
||||
).toLowerCase();
|
||||
if (['trial', 'expired', 'disabled'].includes(statusRaw)) {
|
||||
if (['trial', 'expired', 'disabled', 'inactive'].includes(statusRaw)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -15280,9 +15447,22 @@
|
||||
title: error?.title,
|
||||
message: error?.message,
|
||||
purchaseUrl: normalizeUrl(error?.purchaseUrl) || null,
|
||||
botUrl: normalizeUrl(error?.botUrl) || null,
|
||||
code: error?.code || null,
|
||||
};
|
||||
updateErrorTexts();
|
||||
document.getElementById('errorState').classList.remove('hidden');
|
||||
updateRegisterState();
|
||||
|
||||
const errorState = document.getElementById('errorState');
|
||||
const registerState = document.getElementById('registerState');
|
||||
|
||||
if (error?.code === 'user_not_registered') {
|
||||
errorState?.classList.add('hidden');
|
||||
registerState?.classList.remove('hidden');
|
||||
} else {
|
||||
registerState?.classList.add('hidden');
|
||||
errorState?.classList.remove('hidden');
|
||||
}
|
||||
updateActionButtons();
|
||||
}
|
||||
|
||||
@@ -15300,6 +15480,22 @@
|
||||
openExternalLink(link);
|
||||
});
|
||||
|
||||
const registerBtn = document.getElementById('registerBtn');
|
||||
if (registerBtn) {
|
||||
registerBtn.addEventListener('click', () => {
|
||||
const link = registerBtn.dataset.link || getEffectivePurchaseUrl();
|
||||
openExternalLink(link);
|
||||
});
|
||||
}
|
||||
|
||||
const subscriptionEmptyAction = document.getElementById('subscriptionEmptyAction');
|
||||
if (subscriptionEmptyAction) {
|
||||
subscriptionEmptyAction.addEventListener('click', () => {
|
||||
const link = subscriptionEmptyAction.dataset.link || getEffectivePurchaseUrl();
|
||||
openExternalLink(link);
|
||||
});
|
||||
}
|
||||
|
||||
const topupButton = document.getElementById('topupBalanceBtn');
|
||||
if (topupButton) {
|
||||
topupButton.addEventListener('click', () => {
|
||||
|
||||
Reference in New Issue
Block a user