From 4833f51dc790c5e26d7852ffa4ff1c6719068aea Mon Sep 17 00:00:00 2001 From: Egor Date: Mon, 19 Jan 2026 01:18:02 +0300 Subject: [PATCH] Add files via upload --- src/components/InsufficientBalancePrompt.tsx | 87 +++++----- src/components/TopUpModal.tsx | 167 +++++++------------ 2 files changed, 97 insertions(+), 157 deletions(-) diff --git a/src/components/InsufficientBalancePrompt.tsx b/src/components/InsufficientBalancePrompt.tsx index f5d1887..d1101bd 100644 --- a/src/components/InsufficientBalancePrompt.tsx +++ b/src/components/InsufficientBalancePrompt.tsx @@ -145,22 +145,22 @@ function PaymentMethodModal({ paymentMethods, onSelect, onClose }: PaymentMethod const { formatAmount, currencySymbol } = useCurrency() return ( -
+
-
- {/* Compact Header */} -
-

{t('balance.selectPaymentMethod')}

-
- {/* Scrollable content */} -
+ {/* Content */} +
{!paymentMethods ? (
@@ -170,49 +170,40 @@ function PaymentMethodModal({ paymentMethods, onSelect, onClose }: PaymentMethod {t('balance.noPaymentMethods')}
) : ( -
- {paymentMethods.map((method) => { - const methodKey = method.id.toLowerCase().replace(/-/g, '_') - const translatedName = t(`balance.paymentMethods.${methodKey}.name`, { defaultValue: '' }) + paymentMethods.map((method) => { + const methodKey = method.id.toLowerCase().replace(/-/g, '_') + const translatedName = t(`balance.paymentMethods.${methodKey}.name`, { defaultValue: '' }) - return ( - - ) - })} -
+
+
+
{translatedName || method.name}
+
+ {formatAmount(method.min_amount_kopeks / 100, 0)} – {formatAmount(method.max_amount_kopeks / 100, 0)} {currencySymbol} +
+
+ + + + + ) + }) )}
- - {/* Compact Footer */} -
- -
) diff --git a/src/components/TopUpModal.tsx b/src/components/TopUpModal.tsx index 1ce3615..822860f 100644 --- a/src/components/TopUpModal.tsx +++ b/src/components/TopUpModal.tsx @@ -7,7 +7,6 @@ import { checkRateLimit, getRateLimitResetTime, RATE_LIMIT_KEYS } from '../utils 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) => { @@ -17,16 +16,13 @@ const openPaymentLink = (url: string, reservedWindow?: Window | null) => { 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 @@ -46,10 +42,9 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top const getInitialAmount = (): string => { if (!initialAmountRubles || initialAmountRubles <= 0) return '' const converted = convertAmount(initialAmountRubles) - if (targetCurrency === 'IRR' || targetCurrency === 'RUB') { - return Math.ceil(converted).toString() - } - return converted.toFixed(2) + return (targetCurrency === 'IRR' || targetCurrency === 'RUB') + ? Math.ceil(converted).toString() + : converted.toFixed(2) } const [amount, setAmount] = useState(getInitialAmount) @@ -57,7 +52,6 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top const [selectedOption, setSelectedOption] = useState( method.options && method.options.length > 0 ? method.options[0].id : null ) - const [isInputFocused, setIsInputFocused] = useState(false) const popupRef = useRef(null) const hasOptions = method.options && method.options.length > 0 @@ -78,24 +72,18 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top webApp.openInvoice(data.invoice_url, (status) => { if (status === 'paid') { setError(null); onClose() } else if (status === 'failed') { setError(t('wheel.starsPaymentFailed')) } - else if (status === 'cancelled') { setError(null) } }) - } catch (e) { setError('Ошибка открытия окна оплаты: ' + String(e)) } + } catch (e) { setError('Ошибка: ' + String(e)) } }, - onError: (error: unknown) => { - const axiosError = error as { response?: { data?: { detail?: string }, status?: number } } - setError(`Ошибка API (${axiosError?.response?.status || 'network'}): ${axiosError?.response?.data?.detail || 'Не удалось создать счёт'}`) + 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 + 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) => { @@ -104,12 +92,11 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top popupRef.current = null onClose() }, - onError: (error: unknown) => { + onError: (err: unknown) => { try { if (popupRef.current && !popupRef.current.closed) popupRef.current.close() } catch {} popupRef.current = null - const detail = (error as { response?: { data?: { detail?: string } } })?.response?.data?.detail || '' - if (detail.includes('not yet implemented')) setError(t('balance.useBot')) - else setError(detail || t('common.error')) + const detail = (err as { response?: { data?: { detail?: string } } })?.response?.data?.detail || '' + setError(detail.includes('not yet implemented') ? t('balance.useBot') : (detail || t('common.error'))) }, }) @@ -118,76 +105,63 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top inputRef.current?.blur() if (!checkRateLimit(RATE_LIMIT_KEYS.PAYMENT, 3, 30000)) { - setError(t('balance.tooManyRequests', { seconds: getRateLimitResetTime(RATE_LIMIT_KEYS.PAYMENT) }) || 'Подождите...') + setError('Подождите ' + getRateLimitResetTime(RATE_LIMIT_KEYS.PAYMENT) + ' сек.') return } - - if (hasOptions && !selectedOption) { setError(t('balance.selectPaymentOption', 'Выберите способ')); return } + if (hasOptions && !selectedOption) { setError('Выберите способ'); return } const amountCurrency = parseFloat(amount) - if (isNaN(amountCurrency) || amountCurrency <= 0) { setError(t('balance.invalidAmount', 'Введите сумму')); return } + if (isNaN(amountCurrency) || amountCurrency <= 0) { setError('Введите сумму'); return } const amountRubles = convertToRub(amountCurrency) - if (amountRubles < minRubles) { setError(t('balance.minAmountError', { amount: minRubles })); return } - if (amountRubles > maxRubles) { setError(t('balance.maxAmountError', { amount: maxRubles })); return } + 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); return } - topUpMutation.mutate(amountKopeks) + 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 getQuickAmountValue = (rubAmount: number): string => { - if (targetCurrency === 'IRR') return Math.round(convertAmount(rubAmount)).toString() - return convertAmount(rubAmount).toFixed(currencyDecimals) - } - const inputStep = currencyDecimals === 0 ? 1 : 0.01 + 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 - // When input is focused in Telegram Mini App, position modal at top - const modalPosition = isInputFocused && isTelegramMiniApp ? 'items-start pt-2' : 'items-end sm:items-center' - return ( -
-
{ inputRef.current?.blur(); onClose() }} /> +
+
-
- {/* Header - hide when keyboard open on mobile */} -
-
-
- - - -
-

{methodName}

-
-
- {/* Content */}
- {/* Payment options - hide when keyboard open */} - {hasOptions && method.options && !isInputFocused && ( -
- {method.options.map((option) => ( + {/* Payment options */} + {hasOptions && method.options && ( +
+ {method.options.map((opt) => ( ))}
@@ -201,36 +175,27 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top inputMode="decimal" value={amount} onChange={(e) => setAmount(e.target.value)} - onFocus={() => setIsInputFocused(true)} - onBlur={() => setTimeout(() => setIsInputFocused(false), 150)} placeholder={`${formatAmount(minRubles, 0)} – ${formatAmount(maxRubles, 0)}`} - step={inputStep} - className="w-full h-14 px-4 pr-16 text-xl 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 focus:ring-1 focus:ring-accent-500" + 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 - always show, they help dismiss keyboard */} + {/* Quick amounts */} {quickAmounts.length > 0 && ( -
+
{quickAmounts.map((a) => { - const quickValue = getQuickAmountValue(a) - const isSelected = amount === quickValue + const val = getQuickValue(a) return (
)} - {/* Error - hide when keyboard open */} - {error && !isInputFocused && ( -
- {error} -
+ {/* Error */} + {error && ( +
{error}
)} - {/* Submit button - always visible */} + {/* Submit */} - - {/* Cancel - hide when keyboard open */} - {!isInputFocused && ( - - )}
- - {/* Safe area for iPhone */} -
)