import { useState, useCallback, useMemo, useEffect, useRef } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import { wheelApi, type SpinResult, type SpinHistoryItem } from '../api/wheel' import FortuneWheel from '../components/wheel/FortuneWheel' import InsufficientBalancePrompt from '../components/InsufficientBalancePrompt' import { useCurrency } from '../hooks/useCurrency' // Icons const StarIcon = () => ( ) const CalendarIcon = () => ( ) const HistoryIcon = () => ( ) const CloseIcon = () => ( ) const SparklesIcon = () => ( ) const TrophyIcon = () => ( ) export default function Wheel() { const { t } = useTranslation() const queryClient = useQueryClient() const { formatAmount, currencySymbol } = useCurrency() const [isSpinning, setIsSpinning] = useState(false) const [targetRotation, setTargetRotation] = useState(null) const [spinResult, setSpinResult] = useState(null) const [showResultModal, setShowResultModal] = useState(false) const [paymentType, setPaymentType] = useState<'telegram_stars' | 'subscription_days'>('telegram_stars') const [showHistory, setShowHistory] = useState(false) const [isPayingStars, setIsPayingStars] = useState(false) const isTelegramMiniApp = useMemo(() => { // Check if we're in Telegram Mini App environment const webApp = window.Telegram?.WebApp return !!(webApp && typeof webApp.initData === 'string') }, []) const { data: config, isLoading, error } = useQuery({ queryKey: ['wheel-config'], queryFn: wheelApi.getConfig, }) const { data: history } = useQuery({ queryKey: ['wheel-history'], queryFn: () => wheelApi.getHistory(1, 10), enabled: showHistory, }) // Auto-select payment type based on availability useEffect(() => { if (!config) return const starsEnabled = config.spin_cost_stars_enabled && config.spin_cost_stars const daysEnabled = config.spin_cost_days_enabled && config.spin_cost_days const canPayBalance = starsEnabled && config.can_pay_stars const canPayDays = daysEnabled && config.can_pay_days if (isTelegramMiniApp) { // In Mini App: prefer days if available, Stars payment is separate button if (canPayDays) { setPaymentType('subscription_days') } } else { // In Web: prefer balance (Stars converted to rubles), fallback to days if (canPayBalance) { setPaymentType('telegram_stars') } else if (canPayDays) { setPaymentType('subscription_days') } } }, [config, isTelegramMiniApp]) // Function to poll for new spin result after Stars payment const pollForSpinResult = useCallback(async (maxAttempts = 15, delayMs = 800) => { // Wait a bit before first poll to give the bot time to process the payment await new Promise(resolve => setTimeout(resolve, 1500)) // Get current history to find the latest spin ID let historyBefore try { historyBefore = await wheelApi.getHistory(1, 1) } catch { historyBefore = { items: [], total: 0 } } const lastSpinIdBefore = historyBefore.items.length > 0 ? historyBefore.items[0].id : 0 for (let attempt = 0; attempt < maxAttempts; attempt++) { await new Promise(resolve => setTimeout(resolve, delayMs)) try { const historyAfter = await wheelApi.getHistory(1, 1) // Check if we have a new spin (either new item or higher ID) if (historyAfter.items.length > 0) { const latestSpin = historyAfter.items[0] // If we had no spins before, or this spin has a higher ID if (lastSpinIdBefore === 0 || latestSpin.id > lastSpinIdBefore) { // Found a new spin! Return it as SpinResult return { success: true, prize_id: latestSpin.id, prize_type: latestSpin.prize_type, prize_value: latestSpin.prize_value, prize_display_name: latestSpin.prize_display_name, emoji: latestSpin.emoji, color: latestSpin.color, rotation_degrees: 0, // Not needed for result display message: latestSpin.prize_type === 'nothing' ? t('wheel.noPrize') : `${t('wheel.youWon')} ${latestSpin.prize_display_name}!`, promocode: null, // Promocode is sent to bot chat error: null, } as SpinResult } } } catch { // Continue polling on error } } // Timeout - couldn't find new spin return null }, [t]) // Ref to store pending Stars payment result const pendingStarsResultRef = useRef(null) const isStarsSpinRef = useRef(false) const starsInvoiceMutation = useMutation({ mutationFn: wheelApi.createStarsInvoice, onSuccess: (data) => { if (window.Telegram?.WebApp?.openInvoice) { window.Telegram.WebApp.openInvoice(data.invoice_url, async (status) => { if (status === 'paid') { // Mark this as a Stars spin so handleSpinComplete knows to use the pending result isStarsSpinRef.current = true pendingStarsResultRef.current = null // Payment done - reset paying state immediately setIsPayingStars(false) // Start spinning animation (5 seconds duration in FortuneWheel) setIsSpinning(true) setTargetRotation(360 * 5 + Math.random() * 360) // Poll for the result in the background - don't await here! // The result will be stored and shown when animation completes pollForSpinResult().then((result) => { queryClient.invalidateQueries({ queryKey: ['wheel-config'] }) queryClient.invalidateQueries({ queryKey: ['wheel-history'] }) if (result) { pendingStarsResultRef.current = result } else { // Fallback: couldn't get result pendingStarsResultRef.current = { success: true, prize_id: null, prize_type: null, prize_value: 0, prize_display_name: '', emoji: '🎰', color: '#8B5CF6', rotation_degrees: 0, message: t('wheel.starsPaymentSuccessCheckHistory'), promocode: null, error: null, } } }).catch(() => { // Error polling, show generic success pendingStarsResultRef.current = { success: true, prize_id: null, prize_type: null, prize_value: 0, prize_display_name: '', emoji: '🎰', color: '#8B5CF6', rotation_degrees: 0, message: t('wheel.starsPaymentSuccessCheckHistory'), promocode: null, error: null, } }) } else if (status !== 'cancelled') { setIsPayingStars(false) setSpinResult({ success: false, prize_id: null, prize_type: null, prize_value: 0, prize_display_name: '', emoji: '😔', color: '#EF4444', rotation_degrees: 0, message: t('wheel.starsPaymentFailed'), promocode: null, error: 'payment_failed', }) setShowResultModal(true) } else { setIsPayingStars(false) } }) } else { // openInvoice not available - show error setIsPayingStars(false) setSpinResult({ success: false, prize_id: null, prize_type: null, prize_value: 0, prize_display_name: '', emoji: '😔', color: '#EF4444', rotation_degrees: 0, message: t('wheel.errors.starsNotAvailable'), promocode: null, error: 'stars_not_available', }) setShowResultModal(true) } }, onError: () => { setIsPayingStars(false) setSpinResult({ success: false, prize_id: null, prize_type: null, prize_value: 0, prize_display_name: '', emoji: '😔', color: '#EF4444', rotation_degrees: 0, message: t('wheel.errors.networkError'), promocode: null, error: 'network_error', }) setShowResultModal(true) }, }) const handleDirectStarsPay = () => { setIsPayingStars(true) starsInvoiceMutation.mutate() } const spinMutation = useMutation({ mutationFn: () => wheelApi.spin(paymentType), onSuccess: (result) => { if (result.success) { setTargetRotation(result.rotation_degrees) setSpinResult(result) } else { setIsSpinning(false) setSpinResult(result) setShowResultModal(true) } }, onError: () => { setIsSpinning(false) setSpinResult({ success: false, message: t('wheel.errors.networkError'), error: 'network_error', prize_id: null, prize_type: null, prize_value: 0, prize_display_name: '', emoji: '', color: '', rotation_degrees: 0, promocode: null, }) setShowResultModal(true) }, }) const handleSpin = () => { if (!config?.can_spin || isSpinning) return setIsSpinning(true) spinMutation.mutate() } const handleSpinComplete = useCallback(() => { setIsSpinning(false) // Check if this was a Stars payment spin if (isStarsSpinRef.current) { isStarsSpinRef.current = false // Use the pending result from polling, or show a fallback if (pendingStarsResultRef.current) { setSpinResult(pendingStarsResultRef.current) pendingStarsResultRef.current = null } else { // Polling still in progress or failed - show fallback setSpinResult({ success: true, prize_id: null, prize_type: null, prize_value: 0, prize_display_name: '', emoji: '🎰', color: '#8B5CF6', rotation_degrees: 0, message: t('wheel.starsPaymentSuccessCheckHistory'), promocode: null, error: null, }) } } setShowResultModal(true) queryClient.invalidateQueries({ queryKey: ['wheel-config'] }) queryClient.invalidateQueries({ queryKey: ['wheel-history'] }) }, [queryClient, t]) const closeResultModal = () => { setShowResultModal(false) setSpinResult(null) setTargetRotation(null) } if (isLoading) { return (
) } if (error || !config) { return (
😔

{t('wheel.errors.loadFailed')}

) } if (!config.is_enabled) { return (
🎡

{t('wheel.title')}

{t('wheel.disabled')}

) } const starsEnabled = config.spin_cost_stars_enabled && config.spin_cost_stars const daysEnabled = config.spin_cost_days_enabled && config.spin_cost_days const canPayBalance = starsEnabled && config.can_pay_stars // For web: pay with internal balance const canPayDays = daysEnabled && config.can_pay_days return (
{/* Hero Header */}
{/* Background decorations */}

{t('wheel.title')}

{config.daily_limit > 0 && (
{t('wheel.spinsRemaining')}: {Math.max(0, config.daily_limit - config.user_spins_today)} / {config.daily_limit}
)}
{/* Main Content Grid */}
{/* Wheel Section */}
{/* Wheel Container with glow background */}
{/* Background glow */}
{/* Wheel */}
{/* Spin Button */}
{/* Payment Options - Different for Web vs Mini App */} {isTelegramMiniApp ? ( // Mini App: Show Stars button (direct Telegram payment) and Days button
{/* Direct Stars payment via Telegram */} {starsEnabled && ( )} {/* Days payment option */} {daysEnabled && ( )}
) : ( // Web: Show Balance button (Stars converted to rubles) and Days button (starsEnabled || daysEnabled) && (
{starsEnabled && ( )} {daysEnabled && ( )}
) )} {/* Main Spin Button - Show for web always, for mini app only if days available */} {(!isTelegramMiniApp || canPayDays) && ( )} {/* Cannot spin hint */} {!config.can_spin && !isSpinning && ( <> {config.can_spin_reason === 'daily_limit_reached' ? (

{t('wheel.errors.dailyLimitReached')}

) : config.can_spin_reason === 'insufficient_balance' ? ( ) : (

{t('wheel.errors.cannotSpin')}

)} )}
{/* History Sidebar */} {showHistory && (

{t('wheel.recentSpins')}

{history && history.items.length > 0 ? (
{history.items.map((item: SpinHistoryItem, index: number) => (
{item.emoji}
{item.prize_display_name}
{new Date(item.created_at).toLocaleDateString()}
-{item.payment_type === 'telegram_stars' ? `${item.payment_amount} ⭐` : `${item.payment_amount}${t('wheel.days').charAt(0)}`}
))}
) : (
🎰
{t('wheel.noHistory')}
)}
)}
{/* Result Modal */} {showResultModal && spinResult && (
{/* Decorative elements */} {spinResult.success && ( <>
{/* Confetti effect */}
{Array.from({ length: 20 }).map((_, i) => (
))}
)} {/* Close button */} {/* Content */}
{/* Prize icon */}
{spinResult.success ? (spinResult.emoji || '🎉') : '😔'}
{/* Title */}

{spinResult.success ? spinResult.prize_type === 'nothing' ? t('wheel.noLuck') : t('wheel.congratulations') : t('wheel.oops')}

{/* Message */}

{spinResult.message}

{/* Promocode if won */} {spinResult.promocode && (

{t('wheel.yourPromoCode')}

{spinResult.promocode}

)} {/* Close button */}
)}
) }