feat: wheel subscription picker for multi-tariff mode

- API: spin() accepts subscriptionId, WheelConfig has eligible_subscriptions
- Auto-select subscription when only one eligible
- Show subscription picker when multiple eligible (days payment)
- Disable spin button until subscription selected (when >1 eligible)
- Daily tariffs excluded from eligible list
This commit is contained in:
c0mrade
2026-03-24 14:41:20 +03:00
parent 31d0953c23
commit 2921d8cae6
2 changed files with 58 additions and 2 deletions

View File

@@ -8,6 +8,12 @@ export interface WheelPrize {
prize_type: string;
}
export interface EligibleSubscription {
id: number;
tariff_name: string | null;
days_left: number;
}
export interface WheelConfig {
is_enabled: boolean;
name: string;
@@ -25,6 +31,7 @@ export interface WheelConfig {
user_balance_kopeks: number;
required_balance_kopeks: number;
has_subscription: boolean;
eligible_subscriptions: EligibleSubscription[] | null;
}
export interface SpinAvailability {
@@ -187,9 +194,13 @@ export const wheelApi = {
return response.data;
},
spin: async (paymentType: 'telegram_stars' | 'subscription_days'): Promise<SpinResult> => {
spin: async (
paymentType: 'telegram_stars' | 'subscription_days',
subscriptionId?: number,
): Promise<SpinResult> => {
const response = await apiClient.post<SpinResult>('/cabinet/wheel/spin', {
payment_type: paymentType,
...(subscriptionId != null && { subscription_id: subscriptionId }),
});
return response.data;
},

View File

@@ -107,6 +107,7 @@ export default function Wheel() {
const [isPayingStars, setIsPayingStars] = useState(false);
const [historyExpanded, setHistoryExpanded] = useState(false);
const [showStarsConfirm, setShowStarsConfirm] = useState(false);
const [selectedSubscriptionId, setSelectedSubscriptionId] = useState<number | null>(null);
const paymentTypeInitialized = useRef(false);
const {
@@ -136,6 +137,11 @@ export default function Wheel() {
} else if (daysEnabled) {
setPaymentType('subscription_days');
}
// Auto-select subscription if only one eligible
if (config.eligible_subscriptions?.length === 1) {
setSelectedSubscriptionId(config.eligible_subscriptions[0].id);
}
}, [config]);
// Function to poll for new spin result after Stars payment
@@ -368,7 +374,7 @@ export default function Wheel() {
};
const spinMutation = useMutation({
mutationFn: () => wheelApi.spin(paymentType),
mutationFn: () => wheelApi.spin(paymentType, selectedSubscriptionId ?? undefined),
onSuccess: (result) => {
if (result.success) {
setTargetRotation(result.rotation_degrees);
@@ -506,11 +512,18 @@ export default function Wheel() {
// 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 noSubscription = !config.has_subscription;
const needsSubscriptionPick =
paymentType === 'subscription_days' &&
config.eligible_subscriptions &&
config.eligible_subscriptions.length > 1 &&
!selectedSubscriptionId;
const spinDisabled =
isSpinning ||
isPayingStars ||
dailyLimitReached ||
noSubscription ||
needsSubscriptionPick ||
(paymentType === 'telegram_stars' ? !starsEnabled : !config.can_spin);
return (
@@ -582,6 +595,38 @@ export default function Wheel() {
</div>
)}
{/* Subscription selector for days payment in multi-tariff */}
{paymentType === 'subscription_days' &&
config.eligible_subscriptions &&
config.eligible_subscriptions.length > 1 && (
<div className="rounded-xl border border-dark-700/30 bg-dark-800/30 p-3">
<p className="mb-2 text-center text-xs text-dark-400">
{t('wheel.selectSubscription', 'Выберите подписку')}
</p>
<div className="space-y-1.5">
{config.eligible_subscriptions.map((sub) => (
<button
key={sub.id}
onClick={() => setSelectedSubscriptionId(sub.id)}
disabled={isSpinning}
className={`flex w-full items-center justify-between rounded-lg px-3 py-2 text-sm transition-all ${
selectedSubscriptionId === sub.id
? 'bg-accent-500/15 text-accent-400'
: 'text-dark-400 hover:text-dark-200'
}`}
>
<span className="font-medium">
{sub.tariff_name || t('subscription.defaultName', 'Подписка')}
</span>
<span className="text-xs opacity-60">
{sub.days_left} {t('common.units.days', 'дней')}
</span>
</button>
))}
</div>
</div>
)}
{/* Stars confirmation panel */}
{showStarsConfirm && !isSpinning && !isPayingStars ? (
<div className="space-y-3 rounded-xl border border-accent-500/30 bg-accent-500/5 p-4">