From 0eba4cfc8f7117879d0e73891a8d8473674bcc73 Mon Sep 17 00:00:00 2001 From: Egor Date: Wed, 7 Jan 2026 17:25:06 +0300 Subject: [PATCH 01/28] Update remnawave_api.py --- app/external/remnawave_api.py | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/app/external/remnawave_api.py b/app/external/remnawave_api.py index 73501edb..d975141a 100644 --- a/app/external/remnawave_api.py +++ b/app/external/remnawave_api.py @@ -538,10 +538,25 @@ class RemnaWaveAPI: user = self._parse_user(response['response']) return await self.enrich_user_with_happ_link(user) - async def revoke_user_subscription(self, uuid: str, new_short_uuid: Optional[str] = None) -> RemnaWaveUser: + async def revoke_user_subscription( + self, + uuid: str, + new_short_uuid: Optional[str] = None, + revoke_only_passwords: bool = False + ) -> RemnaWaveUser: + """ + Отзывает подписку пользователя (меняет ссылку/пароли). + + Args: + uuid: UUID пользователя + new_short_uuid: Новый короткий UUID (опционально, рекомендуется генерировать автоматически) + revoke_only_passwords: Если True, меняются только пароли без изменения URL подписки + """ data = {} if new_short_uuid: data['shortUuid'] = new_short_uuid + if revoke_only_passwords: + data['revokeOnlyPasswords'] = True response = await self._make_request('POST', f'/api/users/{uuid}/actions/revoke', data) user = self._parse_user(response['response']) @@ -809,6 +824,19 @@ class RemnaWaveAPI: async def get_system_stats(self) -> Dict[str, Any]: response = await self._make_request('GET', '/api/system/stats') return response['response'] + + async def get_system_metadata(self) -> Dict[str, Any]: + """ + Получает метаданные системы Remnawave. + + Returns: + Dict с полями: + - version: версия Remnawave + - build: {time, number} - информация о сборке + - git: {backend: {commitSha}, node: {commitSha}} - информация о коммитах + """ + response = await self._make_request('GET', '/api/system/metadata') + return response['response'] async def get_bandwidth_stats(self) -> Dict[str, Any]: response = await self._make_request('GET', '/api/system/stats/bandwidth') From d9f0d6496dd56d822b0e3e8c018692a7c63686d3 Mon Sep 17 00:00:00 2001 From: Egor Date: Wed, 7 Jan 2026 17:33:56 +0300 Subject: [PATCH 02/28] Update miniapp.py --- app/webapi/routes/miniapp.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/webapi/routes/miniapp.py b/app/webapi/routes/miniapp.py index f6beb775..2f6acade 100644 --- a/app/webapi/routes/miniapp.py +++ b/app/webapi/routes/miniapp.py @@ -5052,6 +5052,8 @@ async def get_subscription_renewal_options_endpoint( autopay_days_options=renewal_autopay_days_options, autopay=renewal_autopay_payload, autopay_settings=renewal_autopay_payload, + is_trial=bool(getattr(subscription, "is_trial", False)), + sales_mode=settings.get_sales_mode(), **renewal_autopay_extras, ) From cfdfe1ccd12a35a95d0176d5f428a64e720ffdc9 Mon Sep 17 00:00:00 2001 From: Egor Date: Wed, 7 Jan 2026 17:34:39 +0300 Subject: [PATCH 03/28] Update miniapp.py --- app/webapi/schemas/miniapp.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/webapi/schemas/miniapp.py b/app/webapi/schemas/miniapp.py index 73360418..e33ad8b1 100644 --- a/app/webapi/schemas/miniapp.py +++ b/app/webapi/schemas/miniapp.py @@ -201,6 +201,9 @@ class MiniAppSubscriptionRenewalOptionsResponse(BaseModel): autopay_days_options: List[int] = Field(default_factory=list) autopay: Optional[MiniAppSubscriptionAutopay] = None autopay_settings: Optional[MiniAppSubscriptionAutopay] = None + # Флаги для определения типа действия (покупка vs продление) + is_trial: bool = Field(default=False, alias="isTrial") + sales_mode: str = Field(default="classic", alias="salesMode") model_config = ConfigDict(populate_by_name=True, extra="allow") From 571018982f4d45d66557ecd178c8995899880974 Mon Sep 17 00:00:00 2001 From: Egor Date: Wed, 7 Jan 2026 17:35:30 +0300 Subject: [PATCH 04/28] Update index.html --- miniapp/index.html | 69 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 64 insertions(+), 5 deletions(-) diff --git a/miniapp/index.html b/miniapp/index.html index e07f5459..69e65b06 100644 --- a/miniapp/index.html +++ b/miniapp/index.html @@ -5903,7 +5903,11 @@ 'subscription_settings.confirm.months.one': '{count} month', 'subscription_settings.confirm.months.other': '{count} months', 'subscription_renewal.title': 'Renew subscription', + 'subscription_renewal.title.trial_classic': 'Buy subscription', + 'subscription_renewal.title.trial_tariffs': 'Buy tariff', 'subscription_renewal.subtitle': 'Choose how long you want to extend access.', + 'subscription_renewal.subtitle.trial_classic': 'Choose a period to get started.', + 'subscription_renewal.subtitle.trial_tariffs': 'Choose a period to get started.', 'subscription_renewal.status.loading': 'Loading renewal options…', 'subscription_renewal.status.empty': 'No renewal options are currently available.', 'subscription_renewal.error.generic': 'Unable to load renewal options. Please try again later.', @@ -5915,9 +5919,12 @@ 'subscription_renewal.summary.discount': 'You save {percent}%', 'subscription_renewal.summary.insufficient': 'Not enough balance — missing {missing}.', 'subscription_renewal.submit': 'Renew', + 'subscription_renewal.submit.trial': 'Buy', 'subscription_renewal.submit.loading': 'Processing…', 'subscription_renewal.confirm.title': 'Confirm renewal', + 'subscription_renewal.confirm.title.trial': 'Confirm purchase', 'subscription_renewal.confirm.message': 'Renew the subscription for {period} and pay {amount}?', + 'subscription_renewal.confirm.message.trial': 'Buy subscription for {period} and pay {amount}?', 'subscription_renewal.confirm.balance': 'Current balance: {balance}', 'subscription_renewal.confirm.accept': 'Pay', 'subscription_renewal.confirm.cancel': 'Cancel', @@ -6327,7 +6334,11 @@ 'subscription_settings.confirm.months.one': '{count} месяц', 'subscription_settings.confirm.months.other': '{count} месяцев', 'subscription_renewal.title': 'Продление подписки', + 'subscription_renewal.title.trial_classic': 'Покупка подписки', + 'subscription_renewal.title.trial_tariffs': 'Покупка тарифа', 'subscription_renewal.subtitle': 'Выберите период продления.', + 'subscription_renewal.subtitle.trial_classic': 'Выберите период для начала.', + 'subscription_renewal.subtitle.trial_tariffs': 'Выберите период для начала.', 'subscription_renewal.status.loading': 'Загружаем доступные периоды…', 'subscription_renewal.status.empty': 'Доступных периодов продления нет.', 'subscription_renewal.error.generic': 'Не удалось загрузить параметры продления. Попробуйте позже.', @@ -6339,9 +6350,12 @@ 'subscription_renewal.summary.discount': 'Выгода {percent}%', 'subscription_renewal.summary.insufficient': 'Недостаточно средств — не хватает {missing}.', 'subscription_renewal.submit': 'Продлить', + 'subscription_renewal.submit.trial': 'Купить', 'subscription_renewal.submit.loading': 'Продление…', 'subscription_renewal.confirm.title': 'Подтвердите продление', + 'subscription_renewal.confirm.title.trial': 'Подтвердите покупку', 'subscription_renewal.confirm.message': 'Продлить подписку на {period} и списать {amount}?', + 'subscription_renewal.confirm.message.trial': 'Купить подписку на {period} и списать {amount}?', 'subscription_renewal.confirm.balance': 'Текущий баланс: {balance}', 'subscription_renewal.confirm.accept': 'Оплатить', 'subscription_renewal.confirm.cancel': 'Отмена', @@ -14919,6 +14933,44 @@ statusText.classList.add('hidden'); } + // Обновляем заголовок, подзаголовок и кнопку в зависимости от типа подписки + const isTrial = subscriptionRenewalData.isTrial || subscriptionRenewalData.is_trial || false; + const salesMode = subscriptionRenewalData.salesMode || subscriptionRenewalData.sales_mode || 'classic'; + + // Обновляем заголовок карточки + const titleEl = card.querySelector('[data-i18n="subscription_renewal.title"]'); + if (titleEl) { + let titleKey = 'subscription_renewal.title'; + if (isTrial) { + titleKey = salesMode === 'tariffs' + ? 'subscription_renewal.title.trial_tariffs' + : 'subscription_renewal.title.trial_classic'; + } + const titleText = t(titleKey); + titleEl.textContent = titleText !== titleKey ? titleText : (isTrial ? (salesMode === 'tariffs' ? 'Buy tariff' : 'Buy subscription') : 'Renew subscription'); + } + + // Обновляем подзаголовок + const subtitleEl = card.querySelector('[data-i18n="subscription_renewal.subtitle"]'); + if (subtitleEl) { + let subtitleKey = 'subscription_renewal.subtitle'; + if (isTrial) { + subtitleKey = salesMode === 'tariffs' + ? 'subscription_renewal.subtitle.trial_tariffs' + : 'subscription_renewal.subtitle.trial_classic'; + } + const subtitleText = t(subtitleKey); + subtitleEl.textContent = subtitleText !== subtitleKey ? subtitleText : (isTrial ? 'Choose a period to get started.' : 'Choose how long you want to extend access.'); + } + + // Обновляем текст кнопки + const submitBtn = document.getElementById('subscriptionRenewalSubmit'); + if (submitBtn && !subscriptionRenewalSubmitting) { + const btnKey = isTrial ? 'subscription_renewal.submit.trial' : 'subscription_renewal.submit'; + const btnText = t(btnKey); + submitBtn.textContent = btnText !== btnKey ? btnText : (isTrial ? 'Buy' : 'Renew'); + } + renderSubscriptionRenewalMeta(subscriptionRenewalData); renderSubscriptionRenewalOptions(subscriptionRenewalData); renderSubscriptionRenewalSummary(subscriptionRenewalData); @@ -14930,10 +14982,17 @@ || (option.priceKopeks !== null ? formatPriceFromKopeks(option.priceKopeks, data.currency) : ''); const periodLabel = buildRenewalPeriodLabel(option); - const messageTemplate = t('subscription_renewal.confirm.message'); - const message = messageTemplate && messageTemplate !== 'subscription_renewal.confirm.message' - ? messageTemplate.replace('{period}', periodLabel).replace('{amount}', amountLabel) + // Определяем, триал ли это + const isTrial = data?.isTrial || data?.is_trial || false; + + const messageKey = isTrial ? 'subscription_renewal.confirm.message.trial' : 'subscription_renewal.confirm.message'; + const messageTemplate = t(messageKey); + const defaultMessage = isTrial + ? `Buy subscription for ${periodLabel} and pay ${amountLabel}?` : `Renew the subscription for ${periodLabel} and pay ${amountLabel}?`; + const message = messageTemplate && messageTemplate !== messageKey + ? messageTemplate.replace('{period}', periodLabel).replace('{amount}', amountLabel) + : defaultMessage; const balanceLabel = data?.balanceLabel || (data.balanceKopeks !== null ? formatPriceFromKopeks(data.balanceKopeks, data.currency) : null); @@ -14947,7 +15006,7 @@ finalMessage = `${message}\n${balanceText}`; } - const titleKey = 'subscription_renewal.confirm.title'; + const titleKey = isTrial ? 'subscription_renewal.confirm.title.trial' : 'subscription_renewal.confirm.title'; const confirmKey = 'subscription_renewal.confirm.accept'; const cancelKey = 'subscription_renewal.confirm.cancel'; @@ -14956,7 +15015,7 @@ const cancelText = t(cancelKey); return showConfirmationPopup({ - title: title === titleKey ? 'Confirm renewal' : title, + title: title === titleKey ? (isTrial ? 'Confirm purchase' : 'Confirm renewal') : title, message: finalMessage, confirmText: confirmText === confirmKey ? 'Pay' : confirmText, cancelText: cancelText === cancelKey ? 'Cancel' : cancelText, From 50dd5a5fb398af5391350bc9dc14e08d718d8e21 Mon Sep 17 00:00:00 2001 From: Egor Date: Wed, 7 Jan 2026 17:52:50 +0300 Subject: [PATCH 05/28] Update index.html --- miniapp/index.html | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/miniapp/index.html b/miniapp/index.html index 69e65b06..80ba6e0c 100644 --- a/miniapp/index.html +++ b/miniapp/index.html @@ -14251,6 +14251,27 @@ payload.autopaySettings, ); + // Определяем, триал ли это (из API или из userData) + const isTrial = Boolean( + root.is_trial + ?? root.isTrial + ?? payload.is_trial + ?? payload.isTrial + ?? (userData?.subscription_type === 'trial') + ?? (userData?.user?.subscription_type === 'trial') + ); + + // Режим продаж + const salesMode = ( + root.sales_mode + ?? root.salesMode + ?? payload.sales_mode + ?? payload.salesMode + ?? userData?.sales_mode + ?? userData?.salesMode + ?? 'classic' + ); + return { subscriptionId, currency, @@ -14263,6 +14284,8 @@ missingAmountKopeks, statusMessage, autopay: autopayData, + isTrial, + salesMode, }; } From dae5c073180a8f13315a0cd3a690ccd735ead0de Mon Sep 17 00:00:00 2001 From: Egor Date: Wed, 7 Jan 2026 18:35:43 +0300 Subject: [PATCH 06/28] Update index.html --- miniapp/index.html | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/miniapp/index.html b/miniapp/index.html index 80ba6e0c..8ee28d4c 100644 --- a/miniapp/index.html +++ b/miniapp/index.html @@ -13708,6 +13708,30 @@ return typeRaw === 'paid'; } + function isTrialSubscription() { + if (!userData?.user) { + return false; + } + + const statusRaw = String( + userData.user.subscription_actual_status + || userData.user.subscription_status + || '' + ).toLowerCase(); + + if (['trial', 'trialing'].includes(statusRaw)) { + return true; + } + + const typeRaw = String( + userData.subscription_type + ?? userData.user.subscription_type + ?? '' + ).toLowerCase(); + + return typeRaw === 'trial'; + } + function normalizeServerEntry(entry) { if (!entry) { return null; @@ -14896,7 +14920,8 @@ return; } - const shouldShow = hasActiveSubscription(); + // Для триала НЕ показываем карточку продления — юзер должен купить подписку + const shouldShow = hasActiveSubscription() && !isTrialSubscription(); card.classList.toggle('hidden', !shouldShow); if (!shouldShow) { return; @@ -15369,7 +15394,8 @@ return; } - const shouldShow = hasActiveSubscription(); + // Для триала НЕ показываем настройки подписки — юзер должен сначала купить + const shouldShow = hasActiveSubscription() && !isTrialSubscription(); card.classList.toggle('hidden', !shouldShow); if (!shouldShow) { return; @@ -16062,7 +16088,8 @@ if (subscriptionPurchaseModalOpen) { return true; } - return Boolean(userData?.user) && !hasPaidSubscription(); + // Показываем для пользователей без оплаченной подписки ИЛИ с триалом + return Boolean(userData?.user) && (!hasPaidSubscription() || isTrialSubscription()); } function resolvePurchasePeriodId(period) { From 0e5b115d64899a0557e816c320f74a8f32ebfdf3 Mon Sep 17 00:00:00 2001 From: Egor Date: Thu, 8 Jan 2026 00:52:25 +0300 Subject: [PATCH 07/28] Update index.html --- miniapp/index.html | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/miniapp/index.html b/miniapp/index.html index 8ee28d4c..68b506ed 100644 --- a/miniapp/index.html +++ b/miniapp/index.html @@ -5960,6 +5960,10 @@ 'promo_code.error.generic': 'Unable to activate the promo code.', 'promo_code.error.unauthorized': 'Authorization failed. Please reopen the mini app from Telegram.', 'promo_code.error.network': 'Network error. Please try again later.', + 'tariffs.title': 'Tariffs', + 'tariffs.select': 'Select tariff', + 'tariffs.current': 'Current tariff', + 'tariffs.no_tariffs': 'No tariffs available', 'card.referral.title': 'Referral Program', 'card.history.title': 'Transaction History', 'card.servers.title': 'Connected Servers', @@ -6391,6 +6395,10 @@ 'promo_code.error.generic': 'Не удалось активировать промокод.', 'promo_code.error.unauthorized': 'Ошибка авторизации. Откройте мини-приложение из Telegram.', 'promo_code.error.network': 'Ошибка сети. Попробуйте ещё раз.', + 'tariffs.title': 'Тарифы', + 'tariffs.select': 'Выбрать тариф', + 'tariffs.current': 'Текущий тариф', + 'tariffs.no_tariffs': 'Нет доступных тарифов', 'card.referral.title': 'Реферальная программа', 'card.history.title': 'История операций', 'card.servers.title': 'Подключённые серверы', @@ -19081,7 +19089,7 @@ body?.classList.add('hidden'); try { - const initData = getInitData(); + const initData = tg.initData || ''; const response = await fetch('/miniapp/subscription/tariffs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -19212,7 +19220,7 @@ } try { - const initData = getInitData(); + const initData = tg.initData || ''; const periodDays = selectedTariffPeriod.days || selectedTariffPeriod.period_days || selectedTariffPeriod.periodDays; const response = await fetch('/miniapp/subscription/tariff/purchase', { From 233f6c3490c7839b58f4b636fd27ece292075210 Mon Sep 17 00:00:00 2001 From: Egor Date: Thu, 8 Jan 2026 01:02:18 +0300 Subject: [PATCH 08/28] Update index.html --- miniapp/index.html | 245 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 201 insertions(+), 44 deletions(-) diff --git a/miniapp/index.html b/miniapp/index.html index 68b506ed..f3e50f76 100644 --- a/miniapp/index.html +++ b/miniapp/index.html @@ -5160,12 +5160,28 @@ @@ -19111,6 +19127,8 @@ } } + let selectedTariffData = null; + function renderTariffs() { const loading = document.getElementById('tariffsLoading'); const body = document.getElementById('tariffsBody'); @@ -19127,8 +19145,8 @@ currentBlock?.classList.remove('hidden'); if (currentName) { currentName.innerHTML = ` - ${escapeHtml(current.name)}
- + ${escapeHtml(current.name)} + ${current.device_limit || current.deviceLimit} устр. • ${current.traffic_limit_label || current.trafficLimitLabel || (current.traffic_limit_gb || current.trafficLimitGb) + ' ГБ'} • ${current.servers_count || current.serversCount || 0} серв. @@ -19145,66 +19163,208 @@ const tariffs = tariffsData?.tariffs || []; if (tariffs.length === 0) { - list.innerHTML = '
Нет доступных тарифов
'; + list.innerHTML = '
' + t('tariffs.no_tariffs') + '
'; return; } tariffs.forEach(tariff => { const isCurrent = tariff.is_current || tariff.isCurrent; const periods = tariff.periods || []; - const firstPeriod = periods[0]; + const minPrice = periods.length > 0 + ? periods.reduce((min, p) => { + const price = p.price_kopeks || p.priceKopeks || 0; + return price < min ? price : min; + }, Infinity) + : null; + const minPriceLabel = minPrice !== null && minPrice !== Infinity + ? formatPriceFromKopeks(minPrice, tariffsData?.currency || 'RUB') + : null; const div = document.createElement('div'); - div.className = 'subscription-settings-toggle' + (isCurrent ? ' active' : ''); - div.style.cssText = 'padding: 16px; border-radius: var(--radius); border: 2px solid var(--border-color); cursor: pointer; transition: all 0.2s;'; - if (selectedTariffId === tariff.id) { - div.style.borderColor = 'var(--primary)'; - div.style.background = 'rgba(var(--primary-rgb), 0.05)'; - } + div.className = 'subscription-settings-toggle' + (selectedTariffId === tariff.id ? ' active' : ''); + + const trafficLabel = tariff.traffic_limit_label || tariff.trafficLimitLabel + || ((tariff.traffic_limit_gb || tariff.trafficLimitGb) === 0 ? '∞' : (tariff.traffic_limit_gb || tariff.trafficLimitGb) + ' ГБ'); div.innerHTML = ` -
-
-
- ${isCurrent ? '✅ ' : ''}${escapeHtml(tariff.name)} -
-
- ${tariff.device_limit || tariff.deviceLimit} устр. • - ${tariff.traffic_limit_label || tariff.trafficLimitLabel || (tariff.traffic_limit_gb || tariff.trafficLimitGb) + ' ГБ'} • - ${tariff.servers_count || tariff.serversCount || 0} серв. -
- ${tariff.description ? `
${escapeHtml(tariff.description)}
` : ''} +
+
+ ${isCurrent ? '✅ ' : ''}${escapeHtml(tariff.name)}
-
- ${firstPeriod ? ` -
- от ${firstPeriod.price_label || firstPeriod.priceLabel || '—'} -
-
- ${firstPeriod.label || firstPeriod.days + ' дн.'} -
- ` : ''} +
+ ${tariff.device_limit || tariff.deviceLimit} устр. • ${trafficLabel} • ${tariff.servers_count || tariff.serversCount || 0} серв.
+ ${minPriceLabel ? ` +
+ от ${minPriceLabel} +
+ ` : ''} `; div.addEventListener('click', () => selectTariff(tariff)); list.appendChild(div); }); + + // Обновляем периоды если тариф выбран + renderTariffPeriods(); } function selectTariff(tariff) { selectedTariffId = tariff.id; - selectedTariffPeriod = (tariff.periods || [])[0]; + selectedTariffData = tariff; + selectedTariffPeriod = null; - const btn = document.getElementById('tariffsSelectBtn'); - if (btn) { - btn.disabled = false; - const price = selectedTariffPeriod?.price_label || selectedTariffPeriod?.priceLabel || ''; - btn.textContent = `Купить ${tariff.name} ${price ? '(' + price + ')' : ''}`; + // Обновляем UI + document.querySelectorAll('#tariffsList .subscription-settings-toggle').forEach(el => { + el.classList.remove('active'); + }); + event?.target?.closest('.subscription-settings-toggle')?.classList.add('active'); + + renderTariffPeriods(); + updateTariffButton(); + } + + function renderTariffPeriods() { + const section = document.getElementById('tariffPeriodsSection'); + const list = document.getElementById('tariffPeriodsList'); + const summary = document.getElementById('tariffsSummary'); + + if (!selectedTariffData || !section || !list) { + section?.classList.add('hidden'); + summary?.classList.add('hidden'); + return; } - renderTariffs(); + const periods = selectedTariffData.periods || []; + if (periods.length === 0) { + section.classList.add('hidden'); + summary?.classList.add('hidden'); + return; + } + + section.classList.remove('hidden'); + list.innerHTML = ''; + + periods.forEach((period, index) => { + const days = period.days || period.period_days || period.periodDays; + const priceKopeks = period.price_kopeks || period.priceKopeks || period.final_price || period.finalPrice || 0; + const originalKopeks = period.original_price || period.originalPrice || period.base_price || period.basePrice || priceKopeks; + const hasDiscount = originalKopeks > priceKopeks; + const discountPercent = hasDiscount ? Math.round((1 - priceKopeks / originalKopeks) * 100) : 0; + + const priceLabel = formatPriceFromKopeks(priceKopeks, tariffsData?.currency || 'RUB'); + const originalLabel = hasDiscount ? formatPriceFromKopeks(originalKopeks, tariffsData?.currency || 'RUB') : null; + + let periodLabel = period.label || period.name; + if (!periodLabel) { + if (days === 30) periodLabel = '1 месяц'; + else if (days === 90) periodLabel = '3 месяца'; + else if (days === 180) periodLabel = '6 месяцев'; + else if (days === 365) periodLabel = '12 месяцев'; + else periodLabel = days + ' дней'; + } + + const isSelected = selectedTariffPeriod && + (selectedTariffPeriod.id === period.id || + (selectedTariffPeriod.days || selectedTariffPeriod.period_days) === days); + + const div = document.createElement('div'); + div.className = 'subscription-settings-toggle' + (isSelected ? ' active' : ''); + + div.innerHTML = ` +
+
${periodLabel}
+ ${hasDiscount ? `
-${discountPercent}%
` : ''} +
+
+ ${originalLabel ? `${originalLabel}` : ''} + ${priceLabel} +
+ `; + + div.addEventListener('click', () => selectTariffPeriod(period)); + list.appendChild(div); + + // Автовыбор первого периода + if (index === 0 && !selectedTariffPeriod) { + selectedTariffPeriod = period; + div.classList.add('active'); + } + }); + + updateTariffSummary(); + } + + function selectTariffPeriod(period) { + selectedTariffPeriod = period; + + document.querySelectorAll('#tariffPeriodsList .subscription-settings-toggle').forEach(el => { + el.classList.remove('active'); + }); + event?.target?.closest('.subscription-settings-toggle')?.classList.add('active'); + + updateTariffSummary(); + updateTariffButton(); + } + + function updateTariffSummary() { + const summary = document.getElementById('tariffsSummary'); + const priceEl = document.getElementById('tariffPriceCurrent'); + const originalEl = document.getElementById('tariffPriceOriginal'); + const discountEl = document.getElementById('tariffPriceDiscount'); + + if (!selectedTariffPeriod || !summary) { + summary?.classList.add('hidden'); + return; + } + + summary.classList.remove('hidden'); + + const priceKopeks = selectedTariffPeriod.price_kopeks || selectedTariffPeriod.priceKopeks || + selectedTariffPeriod.final_price || selectedTariffPeriod.finalPrice || 0; + const originalKopeks = selectedTariffPeriod.original_price || selectedTariffPeriod.originalPrice || + selectedTariffPeriod.base_price || selectedTariffPeriod.basePrice || priceKopeks; + const hasDiscount = originalKopeks > priceKopeks; + + if (priceEl) { + priceEl.textContent = formatPriceFromKopeks(priceKopeks, tariffsData?.currency || 'RUB'); + } + + if (originalEl) { + if (hasDiscount) { + originalEl.textContent = formatPriceFromKopeks(originalKopeks, tariffsData?.currency || 'RUB'); + originalEl.classList.remove('hidden'); + } else { + originalEl.classList.add('hidden'); + } + } + + if (discountEl) { + if (hasDiscount) { + const percent = Math.round((1 - priceKopeks / originalKopeks) * 100); + discountEl.textContent = `Выгода ${percent}%`; + discountEl.classList.remove('hidden'); + } else { + discountEl.classList.add('hidden'); + } + } + } + + function updateTariffButton() { + const btn = document.getElementById('tariffsSelectBtn'); + if (!btn) return; + + if (selectedTariffData && selectedTariffPeriod) { + btn.disabled = false; + const priceKopeks = selectedTariffPeriod.price_kopeks || selectedTariffPeriod.priceKopeks || + selectedTariffPeriod.final_price || selectedTariffPeriod.finalPrice || 0; + const priceLabel = formatPriceFromKopeks(priceKopeks, tariffsData?.currency || 'RUB'); + btn.textContent = `Купить за ${priceLabel}`; + } else { + btn.disabled = true; + btn.textContent = t('tariffs.select'); + } } async function purchaseTariff() { @@ -19245,10 +19405,7 @@ console.error('Tariff purchase failed:', err); showPopup(err.message || 'Не удалось купить тариф', 'Ошибка'); } finally { - if (btn) { - btn.disabled = !selectedTariffId; - btn.textContent = 'Выбрать тариф'; - } + updateTariffButton(); } } From 6191a7a395e704438375f46a8fae66c1ced4d6ce Mon Sep 17 00:00:00 2001 From: Egor Date: Thu, 8 Jan 2026 01:25:27 +0300 Subject: [PATCH 09/28] Update index.html --- miniapp/index.html | 124 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 108 insertions(+), 16 deletions(-) diff --git a/miniapp/index.html b/miniapp/index.html index f3e50f76..63d87909 100644 --- a/miniapp/index.html +++ b/miniapp/index.html @@ -19144,13 +19144,44 @@ const current = tariffsData.current_tariff || tariffsData.currentTariff; currentBlock?.classList.remove('hidden'); if (currentName) { + const trafficLabel = current.traffic_limit_label || current.trafficLimitLabel + || ((current.traffic_limit_gb || current.trafficLimitGb) === 0 ? '∞' : (current.traffic_limit_gb || current.trafficLimitGb) + ' ГБ'); + const deviceLimit = current.device_limit || current.deviceLimit || 1; + const servers = current.servers || []; + const serversCount = current.servers_count || current.serversCount || servers.length || 0; + + let serverTags = ''; + if (servers.length > 0) { + const serverNames = servers.slice(0, 3).map(s => s.name || s.display_name).filter(Boolean); + serverTags = serverNames.map(name => + `${escapeHtml(name)}` + ).join(''); + if (serversCount > serverNames.length) { + serverTags += `+${serversCount - serverNames.length}`; + } + } else { + serverTags = `🌍 Все серверы`; + } + currentName.innerHTML = ` - ${escapeHtml(current.name)} - - ${current.device_limit || current.deviceLimit} устр. • - ${current.traffic_limit_label || current.trafficLimitLabel || (current.traffic_limit_gb || current.trafficLimitGb) + ' ГБ'} • - ${current.servers_count || current.serversCount || 0} серв. - +
✓ ${escapeHtml(current.name)}
+
+ + + + + ${deviceLimit} + + + + + + ${trafficLabel} + +
+
+ ${serverTags} +
`; } } else { @@ -19186,20 +19217,81 @@ const trafficLabel = tariff.traffic_limit_label || tariff.trafficLimitLabel || ((tariff.traffic_limit_gb || tariff.trafficLimitGb) === 0 ? '∞' : (tariff.traffic_limit_gb || tariff.trafficLimitGb) + ' ГБ'); + const deviceLimit = tariff.device_limit || tariff.deviceLimit || 1; + + // Серверы - показываем названия или количество + const servers = tariff.servers || []; + const serversCount = tariff.servers_count || tariff.serversCount || servers.length || 0; + let serversText = ''; + if (servers.length > 0) { + const serverNames = servers.slice(0, 3).map(s => s.name || s.display_name || s.displayName).filter(Boolean); + if (serverNames.length > 0) { + serversText = serverNames.join(', '); + if (serversCount > serverNames.length) { + serversText += ` +${serversCount - serverNames.length}`; + } + } else { + serversText = serversCount + ' серв.'; + } + } else if (serversCount > 0) { + serversText = serversCount + ' серв.'; + } else { + serversText = 'Все серверы'; + } + + const description = tariff.description || ''; + + // Серверные теги + let serverTags = ''; + if (servers.length > 0) { + const serverNames = servers.slice(0, 4).map(s => s.name || s.display_name || s.displayName).filter(Boolean); + serverTags = serverNames.map(name => + `${escapeHtml(name)}` + ).join(''); + if (serversCount > serverNames.length) { + serverTags += `+${serversCount - serverNames.length}`; + } + } else if (serversCount === 0) { + serverTags = `🌍 Все серверы`; + } + div.innerHTML = ` -
-
- ${isCurrent ? '✅ ' : ''}${escapeHtml(tariff.name)} +
+
+
+
+ ${isCurrent ? '' : ''} + ${escapeHtml(tariff.name)} +
+ ${description ? `
${escapeHtml(description)}
` : ''} +
+ ${minPriceLabel ? ` +
+
от
+
${minPriceLabel}
+
+ ` : ''}
-
- ${tariff.device_limit || tariff.deviceLimit} устр. • ${trafficLabel} • ${tariff.servers_count || tariff.serversCount || 0} серв. +
+ + + + + ${deviceLimit} + + + + + + ${trafficLabel} +
+ ${serverTags ? ` +
+ ${serverTags} +
+ ` : ''}
- ${minPriceLabel ? ` -
- от ${minPriceLabel} -
- ` : ''} `; div.addEventListener('click', () => selectTariff(tariff)); From 464df9689ea26635b43a5243e6bff99a200f7918 Mon Sep 17 00:00:00 2001 From: Egor Date: Thu, 8 Jan 2026 01:33:36 +0300 Subject: [PATCH 10/28] Update index.html --- miniapp/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miniapp/index.html b/miniapp/index.html index 63d87909..11e788a7 100644 --- a/miniapp/index.html +++ b/miniapp/index.html @@ -5159,7 +5159,7 @@