fix(subscriptions): «+ Купить ещё» только при платной подписке, явный CTA покупки

В мульти-тарифе кнопка «+ Купить ещё подписку» висела всегда, когда есть
хоть какая-то подписка — даже если это только триал или всё истёкшее.
Теперь она показывается только при наличии НАСТОЯЩЕЙ платной активной
подписки (hasActivePaid: не триал + status active/limited).

А «Посмотреть тарифы и купить подписку» из тусклой (пунктир + opacity-60,
спрятана в триал-блоке) стала ЯВНОЙ primary-кнопкой (solid accent) и
показывается, когда платной активной подписки ещё нет (триал/истёкшие/
новый юзер) — мы продаём подписки, путь к покупке должен быть очевиден.
Затронуты Dashboard и Subscriptions. Добавлен ключ subscriptions.browsePlans
(раньше рендерился только inline-фолбэк) в 4 локали.
This commit is contained in:
c0mrade
2026-06-03 11:07:45 +03:00
parent 7efe069be0
commit 39bfef58d0
6 changed files with 51 additions and 10 deletions

View File

@@ -5103,6 +5103,7 @@
"subscriptions": { "subscriptions": {
"buy": "Buy subscription", "buy": "Buy subscription",
"buyAnother": "Buy another subscription", "buyAnother": "Buy another subscription",
"browsePlans": "View plans and subscribe",
"empty": "You have no subscriptions yet", "empty": "You have no subscriptions yet",
"emptyDesc": "Choose a tariff to get started", "emptyDesc": "Choose a tariff to get started",
"title": "My subscriptions" "title": "My subscriptions"

View File

@@ -4737,6 +4737,7 @@
"subscriptions": { "subscriptions": {
"buy": "خرید اشتراک", "buy": "خرید اشتراک",
"buyAnother": "خرید اشتراک دیگر", "buyAnother": "خرید اشتراک دیگر",
"browsePlans": "مشاهده تعرفه‌ها و خرید اشتراک",
"empty": "هنوز اشتراکی ندارید", "empty": "هنوز اشتراکی ندارید",
"emptyDesc": "برای شروع تعرفه‌ای انتخاب کنید", "emptyDesc": "برای شروع تعرفه‌ای انتخاب کنید",
"title": "اشتراک‌های من" "title": "اشتراک‌های من"

View File

@@ -5665,6 +5665,7 @@
"subscriptions": { "subscriptions": {
"buy": "Купить подписку", "buy": "Купить подписку",
"buyAnother": "Купить ещё подписку", "buyAnother": "Купить ещё подписку",
"browsePlans": "Посмотреть тарифы и купить подписку",
"empty": "У вас пока нет подписок", "empty": "У вас пока нет подписок",
"emptyDesc": "Выберите тариф, чтобы начать пользоваться сервисом", "emptyDesc": "Выберите тариф, чтобы начать пользоваться сервисом",
"title": "Мои подписки" "title": "Мои подписки"

View File

@@ -4736,6 +4736,7 @@
"subscriptions": { "subscriptions": {
"buy": "购买订阅", "buy": "购买订阅",
"buyAnother": "再购买一个订阅", "buyAnother": "再购买一个订阅",
"browsePlans": "查看套餐并订阅",
"empty": "您还没有订阅", "empty": "您还没有订阅",
"emptyDesc": "选择套餐以开始使用", "emptyDesc": "选择套餐以开始使用",
"title": "我的订阅" "title": "我的订阅"

View File

@@ -198,6 +198,12 @@ export default function Dashboard() {
? multiSubData !== undefined && (multiSubData.subscriptions?.length ?? 0) === 0 ? multiSubData !== undefined && (multiSubData.subscriptions?.length ?? 0) === 0
: subscriptionResponse?.has_subscription === false && !subLoading; : 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 // Show onboarding for new users after data loads
useEffect(() => { useEffect(() => {
if (!isOnboardingCompleted && !subLoading && !refLoading && !blockingType) { if (!isOnboardingCompleted && !subLoading && !refLoading && !blockingType) {
@@ -299,12 +305,23 @@ export default function Dashboard() {
{t('dashboard.showAll', 'Показать все')} ({multiSubData.subscriptions.length}) {t('dashboard.showAll', 'Показать все')} ({multiSubData.subscriptions.length})
</Link> </Link>
)} )}
{hasActivePaid ? (
<Link <Link
to="/subscription/purchase" to="/subscription/purchase"
className="flex w-full items-center justify-center gap-2 rounded-2xl bg-accent-500/15 p-3.5 text-sm font-medium text-accent-400 transition-all hover:bg-accent-500/25" className="flex w-full items-center justify-center gap-2 rounded-2xl bg-accent-500/15 p-3.5 text-sm font-medium text-accent-400 transition-all hover:bg-accent-500/25"
> >
<span className="text-base">+</span> {t('subscriptions.buyAnother', 'Купить ещё тариф')} <span className="text-base">+</span>{' '}
{t('subscriptions.buyAnother', 'Купить ещё тариф')}
</Link> </Link>
) : (
<Link
to="/subscription/purchase"
className="flex w-full items-center justify-center gap-2 rounded-2xl bg-accent-500 p-3.5 text-sm font-semibold text-white transition-colors hover:bg-accent-600"
>
<span className="text-base">+</span>{' '}
{t('subscriptions.browsePlans', 'Посмотреть тарифы и купить подписку')}
</Link>
)}
</div> </div>
)} )}
@@ -360,8 +377,9 @@ export default function Dashboard() {
(вход по умолчанию) юзер оставался заперт (Telegram-баг #605056/#605063). */} (вход по умолчанию) юзер оставался заперт (Telegram-баг #605056/#605063). */}
<Link <Link
to="/subscription/purchase" to="/subscription/purchase"
className="flex w-full items-center justify-center rounded-2xl border border-dashed border-white/15 p-3.5 text-sm font-medium opacity-60 transition-opacity hover:opacity-90" className="flex w-full items-center justify-center gap-2 rounded-2xl bg-accent-500 p-3.5 text-sm font-semibold text-white transition-colors hover:bg-accent-600"
> >
<span className="text-base">+</span>{' '}
{t('subscriptions.browsePlans', 'Посмотреть тарифы и купить подписку')} {t('subscriptions.browsePlans', 'Посмотреть тарифы и купить подписку')}
</Link> </Link>
</div> </div>

View File

@@ -62,6 +62,12 @@ export default function Subscriptions() {
const subscriptions = data?.subscriptions ?? []; const subscriptions = data?.subscriptions ?? [];
const isMultiTariff = data?.multi_tariff_enabled ?? false; const isMultiTariff = data?.multi_tariff_enabled ?? false;
const hasNoSubscriptions = !isLoading && subscriptions.length === 0; const hasNoSubscriptions = !isLoading && subscriptions.length === 0;
// Есть ли хотя бы одна НАСТОЯЩАЯ (платная, не триал) живая подписка. От этого
// зависит CTA: «+ Купить ещё» — только если уже есть платная; иначе показываем
// явную «Посмотреть тарифы и купить подписку» (триал/истёкшие — это ещё не покупка).
const hasActivePaid = subscriptions.some(
(s) => !s.is_trial && (s.status === 'active' || s.status === 'limited'),
);
// Если у юзера нет подписок — проверяем доступность триала, иначе // Если у юзера нет подписок — проверяем доступность триала, иначе
// (в multi-tariff) ему вообще негде увидеть оффер. // (в multi-tariff) ему вообще негде увидеть оффер.
@@ -107,7 +113,8 @@ export default function Subscriptions() {
<h1 className="text-2xl font-bold" style={{ color: g.text }}> <h1 className="text-2xl font-bold" style={{ color: g.text }}>
{t('subscriptions.title', 'Мои подписки')} {t('subscriptions.title', 'Мои подписки')}
</h1> </h1>
{!isLoading && subscriptions.length > 0 && ( {/* «+ Купить ещё» — только если уже есть платная активная подписка */}
{!isLoading && hasActivePaid && (
<button <button
onClick={() => navigate('/subscription/purchase')} onClick={() => navigate('/subscription/purchase')}
className="flex items-center gap-1.5 rounded-xl px-4 py-2 text-sm font-medium transition-colors" className="flex items-center gap-1.5 rounded-xl px-4 py-2 text-sm font-medium transition-colors"
@@ -123,6 +130,18 @@ export default function Subscriptions() {
)} )}
</div> </div>
{/* Есть подписки, но платной активной нет (только триал/истёкшие) —
даём ЯВНУЮ primary-кнопку покупки: мы продаём подписки. */}
{!isLoading && subscriptions.length > 0 && !hasActivePaid && (
<button
onClick={() => navigate('/subscription/purchase')}
className="flex w-full items-center justify-center gap-2 rounded-2xl bg-accent-500 p-3.5 text-sm font-semibold text-white transition-colors hover:bg-accent-600"
>
<PlusIcon className="h-5 w-5" />
{t('subscriptions.browsePlans', 'Посмотреть тарифы и купить подписку')}
</button>
)}
{/* Loading */} {/* Loading */}
{isLoading && ( {isLoading && (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2"> <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
@@ -152,9 +171,9 @@ export default function Subscriptions() {
(Telegram-баг #605056/#605063). */} (Telegram-баг #605056/#605063). */}
<button <button
onClick={() => navigate('/subscription/purchase')} onClick={() => navigate('/subscription/purchase')}
className="w-full rounded-xl border px-6 py-3 text-sm font-medium transition-colors" className="flex w-full items-center justify-center gap-2 rounded-xl bg-accent-500 px-6 py-3 text-sm font-semibold text-white transition-colors hover:bg-accent-600"
style={{ background: g.innerBg, borderColor: g.cardBorder, color: g.text }}
> >
<PlusIcon className="h-5 w-5" />
{t('subscriptions.browsePlans', 'Посмотреть тарифы и купить подписку')} {t('subscriptions.browsePlans', 'Посмотреть тарифы и купить подписку')}
</button> </button>
</div> </div>