import { useState, useEffect, useRef } from 'react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { useSearchParams, useNavigate } from 'react-router'; import { motion, AnimatePresence } from 'framer-motion'; import { useAuthStore } from '../store/auth'; import { balanceApi } from '../api/balance'; import { useCurrency } from '../hooks/useCurrency'; import { API } from '../config/constants'; import { useToast } from '../components/Toast'; import type { PaginatedResponse, Transaction } from '../types'; import { Card } from '@/components/data-display/Card'; import { Button } from '@/components/primitives/Button'; import { staggerContainer, staggerItem } from '@/components/motion/transitions'; // Icons const ChevronDownIcon = ({ className = 'h-5 w-5' }: { className?: string }) => ( ); const WalletIcon = ({ className = 'h-8 w-8' }: { className?: string }) => ( ); export default function Balance() { const { t } = useTranslation(); const refreshUser = useAuthStore((state) => state.refreshUser); const queryClient = useQueryClient(); const { formatAmount, currencySymbol } = useCurrency(); const [searchParams] = useSearchParams(); const navigate = useNavigate(); const { showToast } = useToast(); const paymentHandledRef = useRef(false); // Fetch balance from API const { data: balanceData, refetch: refetchBalance } = useQuery({ queryKey: ['balance'], queryFn: balanceApi.getBalance, staleTime: API.BALANCE_STALE_TIME_MS, refetchOnMount: 'always', }); // Refresh user data on mount to sync balance in store useEffect(() => { refreshUser(); }, [refreshUser]); // Handle payment return from payment gateway useEffect(() => { if (paymentHandledRef.current) return; const paymentStatus = searchParams.get('payment') || searchParams.get('status'); const isSuccess = paymentStatus === 'success' || paymentStatus === 'paid' || paymentStatus === 'completed' || searchParams.get('success') === 'true'; if (isSuccess) { paymentHandledRef.current = true; refetchBalance(); refreshUser(); queryClient.invalidateQueries({ queryKey: ['transactions'] }); queryClient.invalidateQueries({ queryKey: ['subscription'] }); showToast({ type: 'success', title: t('balance.paymentSuccess.title'), message: t('balance.paymentSuccess.message'), duration: 6000, }); navigate('/balance', { replace: true }); } }, [searchParams, navigate, refetchBalance, refreshUser, queryClient, showToast, t]); const [promocode, setPromocode] = useState(''); const [promocodeLoading, setPromocodeLoading] = useState(false); const [promocodeError, setPromocodeError] = useState(null); const [promocodeSuccess, setPromocodeSuccess] = useState<{ message: string; amount: number; } | null>(null); const [transactionsPage, setTransactionsPage] = useState(1); const [isHistoryOpen, setIsHistoryOpen] = useState(false); const { data: transactions, isLoading } = useQuery>({ queryKey: ['transactions', transactionsPage], queryFn: () => balanceApi.getTransactions({ per_page: 20, page: transactionsPage }), placeholderData: (previousData) => previousData, }); const { data: paymentMethods } = useQuery({ queryKey: ['payment-methods'], queryFn: balanceApi.getPaymentMethods, }); const normalizeType = (type: string) => type?.toUpperCase?.() ?? type; const getTypeBadge = (type: string) => { switch (normalizeType(type)) { case 'DEPOSIT': return 'badge-success'; case 'SUBSCRIPTION_PAYMENT': return 'badge-info'; case 'REFERRAL_REWARD': return 'badge-warning'; case 'WITHDRAWAL': return 'badge-error'; default: return 'badge-neutral'; } }; const getTypeLabel = (type: string) => { switch (normalizeType(type)) { case 'DEPOSIT': return t('balance.deposit'); case 'SUBSCRIPTION_PAYMENT': return t('balance.subscriptionPayment'); case 'REFERRAL_REWARD': return t('balance.referralReward'); case 'WITHDRAWAL': return t('balance.withdrawal'); default: return type; } }; const handlePromocodeActivate = async () => { if (!promocode.trim()) return; setPromocodeLoading(true); setPromocodeError(null); setPromocodeSuccess(null); try { const result = await balanceApi.activatePromocode(promocode.trim()); if (result.success) { const bonusAmount = result.balance_after - result.balance_before; setPromocodeSuccess({ message: result.bonus_description || t('balance.promocode.success'), amount: bonusAmount, }); setTransactionsPage(1); setPromocode(''); await refetchBalance(); await refreshUser(); queryClient.invalidateQueries({ queryKey: ['transactions'] }); } } catch (error: unknown) { const axiosError = error as { response?: { data?: { detail?: string } } }; const errorDetail = axiosError.response?.data?.detail || 'server_error'; const errorKey = errorDetail.toLowerCase().includes('not found') ? 'not_found' : errorDetail.toLowerCase().includes('expired') ? 'expired' : errorDetail.toLowerCase().includes('fully used') ? 'used' : errorDetail.toLowerCase().includes('already used') ? 'already_used_by_user' : 'server_error'; setPromocodeError(t(`balance.promocode.errors.${errorKey}`)); } finally { setPromocodeLoading(false); } }; return (

{t('balance.title')}

{/* Balance Card */}
{t('balance.currentBalance')}
{formatAmount(balanceData?.balance_rubles || 0)} {currencySymbol}
{/* Promo Code Section */}

{t('balance.promocode.title')}

setPromocode(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handlePromocodeActivate()} placeholder={t('balance.promocode.placeholder')} className="input flex-1" disabled={promocodeLoading} />
{promocodeError && ( {promocodeError} )} {promocodeSuccess && (
{promocodeSuccess.message}
{promocodeSuccess.amount > 0 && (
{t('balance.promocode.balanceAdded', { amount: promocodeSuccess.amount.toFixed(2), })}
)}
)}
{/* Payment Methods */} {paymentMethods && paymentMethods.length > 0 && (

{t('balance.topUpBalance')}

{paymentMethods.map((method) => { const methodKey = method.id.toLowerCase().replace(/-/g, '_'); const translatedName = t(`balance.paymentMethods.${methodKey}.name`, { defaultValue: '', }); const translatedDesc = t(`balance.paymentMethods.${methodKey}.description`, { defaultValue: '', }); return ( method.is_available && navigate(`/balance/top-up/${method.id}`)} >
{translatedName || method.name}
{(translatedDesc || method.description) && (
{translatedDesc || method.description}
)}
{formatAmount(method.min_amount_kopeks / 100, 0)} –{' '} {formatAmount(method.max_amount_kopeks / 100, 0)} {currencySymbol}
); })}
)} {/* Transaction History */} {isHistoryOpen && (
{isLoading ? (
) : transactions?.items && transactions.items.length > 0 ? ( {transactions.items.map((tx) => { const isPositive = tx.amount_rubles >= 0; const displayAmount = Math.abs(tx.amount_rubles); const sign = isPositive ? '+' : '-'; const colorClass = isPositive ? 'text-success-400' : 'text-error-400'; return (
{getTypeLabel(tx.type)} {new Date(tx.created_at).toLocaleDateString()}
{tx.description && (
{tx.description}
)}
{sign} {formatAmount(displayAmount)} {currencySymbol}
); })}
) : (
{t('balance.noTransactions')}
)} {transactions && transactions.pages > 1 && (
{t('balance.page', { current: transactions.page, total: transactions.pages, })}
)}
)} ); }