From 39bfef58d0db9ebbe2bd27d4e7f327f02932b81b Mon Sep 17 00:00:00 2001 From: c0mrade Date: Wed, 3 Jun 2026 11:07:45 +0300 Subject: [PATCH 01/36] =?UTF-8?q?fix(subscriptions):=20=C2=AB+=20=D0=9A?= =?UTF-8?q?=D1=83=D0=BF=D0=B8=D1=82=D1=8C=20=D0=B5=D1=89=D1=91=C2=BB=20?= =?UTF-8?q?=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20=D0=BF=D1=80=D0=B8=20?= =?UTF-8?q?=D0=BF=D0=BB=D0=B0=D1=82=D0=BD=D0=BE=D0=B9=20=D0=BF=D0=BE=D0=B4?= =?UTF-8?q?=D0=BF=D0=B8=D1=81=D0=BA=D0=B5,=20=D1=8F=D0=B2=D0=BD=D1=8B?= =?UTF-8?q?=D0=B9=20CTA=20=D0=BF=D0=BE=D0=BA=D1=83=D0=BF=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit В мульти-тарифе кнопка «+ Купить ещё подписку» висела всегда, когда есть хоть какая-то подписка — даже если это только триал или всё истёкшее. Теперь она показывается только при наличии НАСТОЯЩЕЙ платной активной подписки (hasActivePaid: не триал + status active/limited). А «Посмотреть тарифы и купить подписку» из тусклой (пунктир + opacity-60, спрятана в триал-блоке) стала ЯВНОЙ primary-кнопкой (solid accent) и показывается, когда платной активной подписки ещё нет (триал/истёкшие/ новый юзер) — мы продаём подписки, путь к покупке должен быть очевиден. Затронуты Dashboard и Subscriptions. Добавлен ключ subscriptions.browsePlans (раньше рендерился только inline-фолбэк) в 4 локали. --- src/locales/en.json | 1 + src/locales/fa.json | 1 + src/locales/ru.json | 1 + src/locales/zh.json | 1 + src/pages/Dashboard.tsx | 32 +++++++++++++++++++++++++------- src/pages/Subscriptions.tsx | 25 ++++++++++++++++++++++--- 6 files changed, 51 insertions(+), 10 deletions(-) diff --git a/src/locales/en.json b/src/locales/en.json index 41c7c93..81f4f61 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -5103,6 +5103,7 @@ "subscriptions": { "buy": "Buy subscription", "buyAnother": "Buy another subscription", + "browsePlans": "View plans and subscribe", "empty": "You have no subscriptions yet", "emptyDesc": "Choose a tariff to get started", "title": "My subscriptions" diff --git a/src/locales/fa.json b/src/locales/fa.json index 00ef8c1..c471622 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -4737,6 +4737,7 @@ "subscriptions": { "buy": "خرید اشتراک", "buyAnother": "خرید اشتراک دیگر", + "browsePlans": "مشاهده تعرفه‌ها و خرید اشتراک", "empty": "هنوز اشتراکی ندارید", "emptyDesc": "برای شروع تعرفه‌ای انتخاب کنید", "title": "اشتراک‌های من" diff --git a/src/locales/ru.json b/src/locales/ru.json index d7bd867..3fefdf8 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -5665,6 +5665,7 @@ "subscriptions": { "buy": "Купить подписку", "buyAnother": "Купить ещё подписку", + "browsePlans": "Посмотреть тарифы и купить подписку", "empty": "У вас пока нет подписок", "emptyDesc": "Выберите тариф, чтобы начать пользоваться сервисом", "title": "Мои подписки" diff --git a/src/locales/zh.json b/src/locales/zh.json index 9143769..8e59695 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -4736,6 +4736,7 @@ "subscriptions": { "buy": "购买订阅", "buyAnother": "再购买一个订阅", + "browsePlans": "查看套餐并订阅", "empty": "您还没有订阅", "emptyDesc": "选择套餐以开始使用", "title": "我的订阅" diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index 18db455..c5c87ed 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -198,6 +198,12 @@ export default function Dashboard() { ? multiSubData !== undefined && (multiSubData.subscriptions?.length ?? 0) === 0 : subscriptionResponse?.has_subscription === false && !subLoading; + // Есть ли НАСТОЯЩАЯ (платная, не триал) живая подписка — от этого зависит CTA: + // «+ Купить ещё» только при наличии платной; иначе явная «Посмотреть тарифы». + const hasActivePaid = (multiSubData?.subscriptions ?? []).some( + (s) => !s.is_trial && (s.status === 'active' || s.status === 'limited'), + ); + // Show onboarding for new users after data loads useEffect(() => { if (!isOnboardingCompleted && !subLoading && !refLoading && !blockingType) { @@ -299,12 +305,23 @@ export default function Dashboard() { {t('dashboard.showAll', 'Показать все')} ({multiSubData.subscriptions.length}) )} - - + {t('subscriptions.buyAnother', 'Купить ещё тариф')} - + {hasActivePaid ? ( + + +{' '} + {t('subscriptions.buyAnother', 'Купить ещё тариф')} + + ) : ( + + +{' '} + {t('subscriptions.browsePlans', 'Посмотреть тарифы и купить подписку')} + + )} )} @@ -360,8 +377,9 @@ export default function Dashboard() { (вход по умолчанию) юзер оставался заперт (Telegram-баг #605056/#605063). */} + +{' '} {t('subscriptions.browsePlans', 'Посмотреть тарифы и купить подписку')} diff --git a/src/pages/Subscriptions.tsx b/src/pages/Subscriptions.tsx index 67b64e8..03d33b4 100644 --- a/src/pages/Subscriptions.tsx +++ b/src/pages/Subscriptions.tsx @@ -62,6 +62,12 @@ export default function Subscriptions() { const subscriptions = data?.subscriptions ?? []; const isMultiTariff = data?.multi_tariff_enabled ?? false; const hasNoSubscriptions = !isLoading && subscriptions.length === 0; + // Есть ли хотя бы одна НАСТОЯЩАЯ (платная, не триал) живая подписка. От этого + // зависит CTA: «+ Купить ещё» — только если уже есть платная; иначе показываем + // явную «Посмотреть тарифы и купить подписку» (триал/истёкшие — это ещё не покупка). + const hasActivePaid = subscriptions.some( + (s) => !s.is_trial && (s.status === 'active' || s.status === 'limited'), + ); // Если у юзера нет подписок — проверяем доступность триала, иначе // (в multi-tariff) ему вообще негде увидеть оффер. @@ -107,7 +113,8 @@ export default function Subscriptions() {

{t('subscriptions.title', 'Мои подписки')}

- {!isLoading && subscriptions.length > 0 && ( + {/* «+ Купить ещё» — только если уже есть платная активная подписка */} + {!isLoading && hasActivePaid && ( + )} + {/* Loading */} {isLoading && (
@@ -152,9 +171,9 @@ export default function Subscriptions() { (Telegram-баг #605056/#605063). */}
From cecfe7ec038a26eb580404957e70859e0df27e26 Mon Sep 17 00:00:00 2001 From: c0mrade Date: Wed, 3 Jun 2026 11:17:00 +0300 Subject: [PATCH 02/36] =?UTF-8?q?fix(dashboard):=20=D1=83=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D0=B4=D1=83=D0=B1=D0=BB=D1=8C=20=D0=BA?= =?UTF-8?q?=D0=BD=D0=BE=D0=BF=D0=BA=D0=B8=20=D0=BF=D0=BE=D0=BA=D1=83=D0=BF?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=BF=D1=80=D0=B8=20=D0=BF=D1=83=D1=81=D1=82?= =?UTF-8?q?=D0=BE=D0=BC=20=D1=81=D0=BF=D0=B8=D1=81=D0=BA=D0=B5=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=B4=D0=BF=D0=B8=D1=81=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Мульти-тариф блок рендерился даже при пустом списке подписок (пустой массив truthy) — показывал заголовок «Подписки» и кнопку покупки, которая дублировала кнопку из блока «нет подписок» (триал-оффер). Получалось две одинаковые «Посмотреть тарифы и купить подписку». Теперь мульти-тариф блок рендерится только когда подписки реально есть (length > 0), а блок «нет подписок» показывает триал (если доступен) и ВСЕГДА одну явную кнопку покупки — заодно закрывает случай «нет подписок и триал недоступен» (раньше его прикрывал пустой мульти-тариф блок). --- src/pages/Dashboard.tsx | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index c5c87ed..41557a5 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -279,8 +279,10 @@ export default function Dashboard() { {/* Pending Gift Activations */} {pendingGifts && pendingGifts.length > 0 && } - {/* Multi-tariff: show subscription cards (max 3) */} - {isMultiTariff && multiSubData?.subscriptions && ( + {/* Multi-tariff: show subscription cards (max 3) — только когда подписки + реально есть. Пустой случай (нет подписок) ведёт блок ниже (триал/покупка), + иначе кнопка покупки дублировалась. */} + {isMultiTariff && multiSubData?.subscriptions && multiSubData.subscriptions.length > 0 && (
@@ -361,20 +363,22 @@ export default function Dashboard() { )} - {/* Trial Activation */} - {hasNoSubscription && !trialLoading && trialInfo?.is_available && ( + {/* Нет подписок: показываем триал (если доступен) и ВСЕГДА одну явную + кнопку покупки. Триал не обязателен, чтобы попасть в витрину — раньше + при доступном триале это был единственный экран без кнопки покупки + (Telegram-баг #605056/#605063). Единственная кнопка тут (вместо дубля + с мульти-тариф блоком). */} + {hasNoSubscription && !trialLoading && (
- - {/* Новый пользователь не обязан активировать триал, чтобы попасть в - витрину — даём явный путь к покупке подписки. Раньше при доступном - триале это был единственный экран без кнопки покупки, и на дашборде - (вход по умолчанию) юзер оставался заперт (Telegram-баг #605056/#605063). */} + {trialInfo?.is_available && ( + + )} Date: Wed, 3 Jun 2026 11:54:23 +0300 Subject: [PATCH 03/36] =?UTF-8?q?fix(landing):=20=D0=B7=D0=B0=D0=B3=D0=BE?= =?UTF-8?q?=D0=BB=D0=BE=D0=B2=D0=BE=D0=BA=20=C2=ABLoading...=C2=BB=20?= =?UTF-8?q?=D0=B8=20=D0=BF=D1=83=D1=81=D1=82=D0=B0=D1=8F=20=D0=B8=D0=BA?= =?UTF-8?q?=D0=BE=D0=BD=D0=BA=D0=B0=20=D0=B2=D0=BA=D0=BB=D0=B0=D0=B4=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=BD=D0=B0=20=D0=BB=D0=B5=D0=BD=D0=B4=D0=B8=D0=BD?= =?UTF-8?q?=D0=B3=D0=B0=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Публичный лендинг (QuickPurchase) рендерится вне AppShell, а useBranding с заголовком/иконкой завязан на авторизацию — поэтому на лендинге вкладка оставалась с дефолтным «Loading...» из index.html (тайтл обновлялся только при заданном meta_title) и без favicon (href="data:,"). - index.html: нейтральная дефолтная иконка-монограмма вместо пустого data-URI и тайтл «VPN» вместо «Loading...». - QuickPurchase: тайтл берётся из meta_title || title лендинга; favicon ставится из брендинга (кастомный лого-блоб или буквенная монограмма), брендинг тянется из публичного эндпоинта. - favicon-хелперы вынесены в utils/favicon.ts; useBranding переведён на них (DRY) + добавлен буквенный фолбэк, чтобы иконка была всегда. - Тип LandingTariff дополнен is_daily/daily_price_kopeks (синхронно с бэком). --- index.html | 14 +++++++----- src/api/landings.ts | 3 +++ src/hooks/useBranding.ts | 16 +++++--------- src/pages/QuickPurchase.tsx | 44 +++++++++++++++++++++++++++++++++---- src/utils/favicon.ts | 35 +++++++++++++++++++++++++++++ 5 files changed, 92 insertions(+), 20 deletions(-) create mode 100644 src/utils/favicon.ts diff --git a/index.html b/index.html index 6c22bd0..e48da2d 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,10 @@ - + - Loading... + VPN