import { useState, useRef } from 'react' import { useTranslation } from 'react-i18next' import { useMutation } from '@tanstack/react-query' import { balanceApi } from '../api/balance' import { useCurrency } from '../hooks/useCurrency' import { checkRateLimit, getRateLimitResetTime, RATE_LIMIT_KEYS } from '../utils/rateLimit' import type { PaymentMethod } from '../types' const TELEGRAM_LINK_REGEX = /^https?:\/\/t\.me\//i const isTelegramPaymentLink = (url: string): boolean => TELEGRAM_LINK_REGEX.test(url) const openPaymentLink = (url: string, reservedWindow?: Window | null) => { if (typeof window === 'undefined' || !url) return const webApp = window.Telegram?.WebApp if (isTelegramPaymentLink(url) && webApp?.openTelegramLink) { try { webApp.openTelegramLink(url); return } catch (e) { console.warn('[TopUpModal] openTelegramLink failed:', e) } } if (webApp?.openLink) { try { webApp.openLink(url, { try_instant_view: false }); return } catch (e) { console.warn('[TopUpModal] webApp.openLink failed:', e) } } if (reservedWindow && !reservedWindow.closed) { try { reservedWindow.location.href = url; reservedWindow.focus?.() } catch (e) { console.warn('[TopUpModal] Failed to use reserved window:', e) } return } const w2 = window.open(url, '_blank', 'noopener,noreferrer') if (w2) { w2.opener = null; return } window.location.href = url } interface TopUpModalProps { method: PaymentMethod onClose: () => void initialAmountRubles?: number } export default function TopUpModal({ method, onClose, initialAmountRubles }: TopUpModalProps) { const { t } = useTranslation() const { formatAmount, currencySymbol, convertAmount, convertToRub, targetCurrency } = useCurrency() const inputRef = useRef(null) const getInitialAmount = (): string => { if (!initialAmountRubles || initialAmountRubles <= 0) return '' const converted = convertAmount(initialAmountRubles) return (targetCurrency === 'IRR' || targetCurrency === 'RUB') ? Math.ceil(converted).toString() : converted.toFixed(2) } const [amount, setAmount] = useState(getInitialAmount) const [error, setError] = useState(null) const [selectedOption, setSelectedOption] = useState( method.options && method.options.length > 0 ? method.options[0].id : null ) const popupRef = useRef(null) const hasOptions = method.options && method.options.length > 0 const minRubles = method.min_amount_kopeks / 100 const maxRubles = method.max_amount_kopeks / 100 const methodKey = method.id.toLowerCase().replace(/-/g, '_') const isStarsMethod = methodKey.includes('stars') const methodName = t(`balance.paymentMethods.${methodKey}.name`, { defaultValue: '' }) || method.name const isTelegramMiniApp = typeof window !== 'undefined' && Boolean(window.Telegram?.WebApp?.initData) const starsPaymentMutation = useMutation({ mutationFn: (amountKopeks: number) => balanceApi.createStarsInvoice(amountKopeks), onSuccess: (data) => { const webApp = window.Telegram?.WebApp if (!data.invoice_url) { setError('Сервер не вернул ссылку на оплату'); return } if (!webApp?.openInvoice) { setError('Оплата Stars доступна только в Telegram Mini App'); return } try { webApp.openInvoice(data.invoice_url, (status) => { if (status === 'paid') { setError(null); onClose() } else if (status === 'failed') { setError(t('wheel.starsPaymentFailed')) } }) } catch (e) { setError('Ошибка: ' + String(e)) } }, onError: (err: unknown) => { const axiosError = err as { response?: { data?: { detail?: string }, status?: number } } setError(`Ошибка: ${axiosError?.response?.data?.detail || 'Не удалось создать счёт'}`) }, }) const topUpMutation = useMutation<{ payment_id: string; payment_url?: string; invoice_url?: string amount_kopeks: number; amount_rubles: number; status: string; expires_at: string | null }, unknown, number>({ mutationFn: (amountKopeks: number) => balanceApi.createTopUp(amountKopeks, method.id, selectedOption || undefined), onSuccess: (data) => { const redirectUrl = data.payment_url || (data as any).invoice_url if (redirectUrl) openPaymentLink(redirectUrl, popupRef.current) popupRef.current = null onClose() }, onError: (err: unknown) => { try { if (popupRef.current && !popupRef.current.closed) popupRef.current.close() } catch {} popupRef.current = null const detail = (err as { response?: { data?: { detail?: string } } })?.response?.data?.detail || '' setError(detail.includes('not yet implemented') ? t('balance.useBot') : (detail || t('common.error'))) }, }) const handleSubmit = () => { setError(null) inputRef.current?.blur() if (!checkRateLimit(RATE_LIMIT_KEYS.PAYMENT, 3, 30000)) { setError('Подождите ' + getRateLimitResetTime(RATE_LIMIT_KEYS.PAYMENT) + ' сек.') return } if (hasOptions && !selectedOption) { setError('Выберите способ'); return } const amountCurrency = parseFloat(amount) if (isNaN(amountCurrency) || amountCurrency <= 0) { setError('Введите сумму'); return } const amountRubles = convertToRub(amountCurrency) if (amountRubles < minRubles || amountRubles > maxRubles) { setError(`Сумма: ${minRubles} – ${maxRubles} ₽`); return } const amountKopeks = Math.round(amountRubles * 100) if (!isTelegramMiniApp) { try { popupRef.current = window.open('', '_blank') } catch { popupRef.current = null } } if (isStarsMethod) { starsPaymentMutation.mutate(amountKopeks) } else { topUpMutation.mutate(amountKopeks) } } const quickAmounts = [100, 300, 500, 1000].filter((a) => a >= minRubles && a <= maxRubles) const currencyDecimals = (targetCurrency === 'IRR' || targetCurrency === 'RUB') ? 0 : 2 const getQuickValue = (rub: number) => (targetCurrency === 'IRR') ? Math.round(convertAmount(rub)).toString() : convertAmount(rub).toFixed(currencyDecimals) const isPending = topUpMutation.isPending || starsPaymentMutation.isPending return (
{/* Header */}
{methodName}
{/* Payment options */} {hasOptions && method.options && (
{method.options.map((opt) => ( ))}
)} {/* Amount input */}
setAmount(e.target.value)} placeholder={`${formatAmount(minRubles, 0)} – ${formatAmount(maxRubles, 0)}`} className="w-full h-12 px-4 pr-12 text-lg font-semibold bg-dark-800 border border-dark-700 rounded-xl text-dark-100 placeholder:text-dark-500 focus:outline-none focus:border-accent-500" autoComplete="off" /> {currencySymbol}
{/* Quick amounts */} {quickAmounts.length > 0 && (
{quickAmounts.map((a) => { const val = getQuickValue(a) return ( ) })}
)} {/* Error */} {error && (
{error}
)} {/* Submit */}
) }