fix: block wheel spin without active subscription

- Add has_subscription to WheelConfig type
- Disable spin button when user has no subscription
- Add noSubscription guard in handleUnifiedSpin handler
- Show warning banner directing user to activate subscription
- Prevent banner stacking (noSubscription takes priority over daily limit)
- Add noSubscription i18n keys for ru, en, zh, fa
This commit is contained in:
Fringg
2026-02-27 00:53:50 +03:00
parent 7e89ccea5c
commit 821e991f51
6 changed files with 38 additions and 18 deletions

View File

@@ -26,6 +26,7 @@ export interface WheelConfig {
can_pay_days: boolean; can_pay_days: boolean;
user_balance_kopeks: number; user_balance_kopeks: number;
required_balance_kopeks: number; required_balance_kopeks: number;
has_subscription: boolean;
} }
export interface SpinAvailability { export interface SpinAvailability {

View File

@@ -858,7 +858,8 @@
"cannotSpin": "Cannot spin right now", "cannotSpin": "Cannot spin right now",
"insufficientBalance": "Insufficient balance", "insufficientBalance": "Insufficient balance",
"topUpRequired": "Top up your balance to pay for spin", "topUpRequired": "Top up your balance to pay for spin",
"starsNotAvailable": "Stars payment not available. Open the app via Telegram." "starsNotAvailable": "Stars payment not available. Open the app via Telegram.",
"noSubscription": "An active subscription is required to spin the wheel. Activate your subscription first."
}, },
"payWithStars": "Pay with Stars", "payWithStars": "Pay with Stars",
"payWithDays": "Pay with days", "payWithDays": "Pay with days",

View File

@@ -694,7 +694,8 @@
"cannotSpin": "فعلاً امکان چرخش نیست", "cannotSpin": "فعلاً امکان چرخش نیست",
"insufficientBalance": "موجودی کافی نیست", "insufficientBalance": "موجودی کافی نیست",
"topUpRequired": "برای پرداخت شارژ کنید", "topUpRequired": "برای پرداخت شارژ کنید",
"starsNotAvailable": "پرداخت Stars در دسترس نیست. برنامه را از طریق تلگرام باز کنید." "starsNotAvailable": "پرداخت Stars در دسترس نیست. برنامه را از طریق تلگرام باز کنید.",
"noSubscription": "برای چرخاندن گردونه نیاز به اشتراک فعال دارید. ابتدا اشتراک خود را فعال کنید."
}, },
"payWithStars": "پرداخت {stars} Stars", "payWithStars": "پرداخت {stars} Stars",
"processingPayment": "در حال پردازش...", "processingPayment": "در حال پردازش...",

View File

@@ -887,7 +887,8 @@
"cannotSpin": "Сейчас нельзя крутить", "cannotSpin": "Сейчас нельзя крутить",
"insufficientBalance": "Недостаточно средств на балансе", "insufficientBalance": "Недостаточно средств на балансе",
"topUpRequired": "Пополните баланс для оплаты спина", "topUpRequired": "Пополните баланс для оплаты спина",
"starsNotAvailable": "Оплата Stars недоступна. Откройте приложение через Telegram." "starsNotAvailable": "Оплата Stars недоступна. Откройте приложение через Telegram.",
"noSubscription": "Для вращения колеса необходима активная подписка. Активируйте подписку в разделе «Подписка»."
}, },
"payWithStars": "Оплатить Stars", "payWithStars": "Оплатить Stars",
"payWithDays": "Оплатить днями", "payWithDays": "Оплатить днями",

View File

@@ -698,7 +698,8 @@
"cannotSpin": "当前无法抽奖", "cannotSpin": "当前无法抽奖",
"insufficientBalance": "余额不足", "insufficientBalance": "余额不足",
"topUpRequired": "请充值以支付抽奖费用", "topUpRequired": "请充值以支付抽奖费用",
"starsNotAvailable": "Stars支付不可用。请通过Telegram打开应用。" "starsNotAvailable": "Stars支付不可用。请通过Telegram打开应用。",
"noSubscription": "需要有效订阅才能转动轮盘。请先激活订阅。"
}, },
"payWithStars": "支付 {stars} Stars", "payWithStars": "支付 {stars} Stars",
"processingPayment": "处理中...", "processingPayment": "处理中...",

View File

@@ -403,6 +403,7 @@ export default function Wheel() {
}; };
const handleUnifiedSpin = () => { const handleUnifiedSpin = () => {
if (noSubscription) return;
if (paymentType === 'telegram_stars') { if (paymentType === 'telegram_stars') {
if (!config?.spin_cost_stars_enabled || !config?.spin_cost_stars) { if (!config?.spin_cost_stars_enabled || !config?.spin_cost_stars) {
notify.warning(t('wheel.starsNotAvailable')); notify.warning(t('wheel.starsNotAvailable'));
@@ -505,10 +506,12 @@ export default function Wheel() {
// Stars via Telegram invoice don't require ruble balance, so only check daily limit // Stars via Telegram invoice don't require ruble balance, so only check daily limit
const dailyLimitReached = config.daily_limit > 0 && config.user_spins_today >= config.daily_limit; const dailyLimitReached = config.daily_limit > 0 && config.user_spins_today >= config.daily_limit;
const noSubscription = !config.has_subscription;
const spinDisabled = const spinDisabled =
isSpinning || isSpinning ||
isPayingStars || isPayingStars ||
dailyLimitReached || dailyLimitReached ||
noSubscription ||
(paymentType === 'telegram_stars' ? !starsEnabled : !config.can_spin); (paymentType === 'telegram_stars' ? !starsEnabled : !config.can_spin);
return ( return (
@@ -614,22 +617,34 @@ export default function Wheel() {
</Button> </Button>
)} )}
{/* No subscription hint */}
{!isSpinning && noSubscription && (
<div className="rounded-linear border border-warning-500/30 bg-warning-500/5 p-4 text-center">
<p className="text-warning-400">{t('wheel.errors.noSubscription')}</p>
</div>
)}
{/* Cannot spin hint — only show for days payment (Stars via invoice always works) */} {/* Cannot spin hint — only show for days payment (Stars via invoice always works) */}
{!isSpinning && paymentType !== 'telegram_stars' && !config.can_spin && ( {!isSpinning &&
<div className="rounded-linear border border-dark-700/30 bg-dark-800/30 p-4 text-center"> !noSubscription &&
<p className="text-dark-400"> paymentType !== 'telegram_stars' &&
{config.can_spin_reason === 'daily_limit_reached' !config.can_spin && (
? t('wheel.errors.dailyLimitReached') <div className="rounded-linear border border-dark-700/30 bg-dark-800/30 p-4 text-center">
: t('wheel.errors.cannotSpin')} <p className="text-dark-400">
</p> {config.can_spin_reason === 'daily_limit_reached'
</div> ? t('wheel.errors.dailyLimitReached')
)} : t('wheel.errors.cannotSpin')}
</p>
</div>
)}
{/* Daily limit hint for Stars payment (not covered by can_spin check) */} {/* Daily limit hint for Stars payment (not covered by can_spin check) */}
{!isSpinning && paymentType === 'telegram_stars' && dailyLimitReached && ( {!isSpinning &&
<div className="rounded-linear border border-dark-700/30 bg-dark-800/30 p-4 text-center"> !noSubscription &&
<p className="text-dark-400">{t('wheel.errors.dailyLimitReached')}</p> paymentType === 'telegram_stars' &&
</div> dailyLimitReached && (
)} <div className="rounded-linear border border-dark-700/30 bg-dark-800/30 p-4 text-center">
<p className="text-dark-400">{t('wheel.errors.dailyLimitReached')}</p>
</div>
)}
{/* Inline Result Card */} {/* Inline Result Card */}
{spinResult && !isSpinning && ( {spinResult && !isSpinning && (