From 981e33ecfabdf7d2560531bfe5d025c302154f28 Mon Sep 17 00:00:00 2001 From: Egor Date: Mon, 19 Jan 2026 00:53:18 +0300 Subject: [PATCH] Add files via upload --- src/components/InsufficientBalancePrompt.tsx | 124 ++++++---- src/components/TopUpModal.tsx | 245 +++++++++++++------ 2 files changed, 245 insertions(+), 124 deletions(-) diff --git a/src/components/InsufficientBalancePrompt.tsx b/src/components/InsufficientBalancePrompt.tsx index 243f508..270cb92 100644 --- a/src/components/InsufficientBalancePrompt.tsx +++ b/src/components/InsufficientBalancePrompt.tsx @@ -145,55 +145,93 @@ function PaymentMethodModal({ paymentMethods, onSelect, onClose }: PaymentMethod const { formatAmount, currencySymbol } = useCurrency() return ( -
-
-
-

{t('balance.selectPaymentMethod')}

-
- {!paymentMethods ? ( -
-
-
- ) : paymentMethods.length === 0 ? ( -
- {t('balance.noPaymentMethods')} -
- ) : ( -
- {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: '' }) + {/* Scrollable content */} +
+ {!paymentMethods ? ( +
+
+
+ ) : paymentMethods.length === 0 ? ( +
+ {t('balance.noPaymentMethods')} +
+ ) : ( +
+ {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 ( - - ) - })} -
- )} + return ( + + ) + })} +
+ )} +
+ + {/* Footer */} +
+ +
) diff --git a/src/components/TopUpModal.tsx b/src/components/TopUpModal.tsx index 772c949..25e9d12 100644 --- a/src/components/TopUpModal.tsx +++ b/src/components/TopUpModal.tsx @@ -1,4 +1,4 @@ -import { useState, useRef } from 'react' +import { useState, useRef, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { useMutation } from '@tanstack/react-query' import { balanceApi } from '../api/balance' @@ -45,6 +45,8 @@ interface TopUpModalProps { export default function TopUpModal({ method, onClose, initialAmountRubles }: TopUpModalProps) { const { t } = useTranslation() const { formatAmount, currencySymbol, convertAmount, convertToRub, targetCurrency } = useCurrency() + const formRef = useRef(null) + const inputRef = useRef(null) // Calculate initial amount in user's currency const getInitialAmount = (): string => { @@ -73,6 +75,23 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top const methodName = t(`balance.paymentMethods.${methodKey}.name`, { defaultValue: '' }) || method.name const isTelegramMiniApp = typeof window !== 'undefined' && Boolean(window.Telegram?.WebApp?.initData) + // Handle visual viewport changes (keyboard appearance on mobile) + useEffect(() => { + const handleResize = () => { + // Scroll input into view when keyboard appears + if (document.activeElement === inputRef.current) { + setTimeout(() => { + inputRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' }) + }, 100) + } + } + + if (typeof window !== 'undefined' && window.visualViewport) { + window.visualViewport.addEventListener('resize', handleResize) + return () => window.visualViewport?.removeEventListener('resize', handleResize) + } + }, []) + // Stars payment using the same approach as Wheel.tsx const starsPaymentMutation = useMutation({ mutationFn: (amountKopeks: number) => balanceApi.createStarsInvoice(amountKopeks), @@ -140,7 +159,11 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top }) const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); setError(null) + e.preventDefault() + setError(null) + + // Hide keyboard on mobile + inputRef.current?.blur() // Rate limit check: max 3 payment attempts per 30 seconds if (!checkRateLimit(RATE_LIMIT_KEYS.PAYMENT, 3, 30000)) { @@ -176,103 +199,163 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top const minInputValue = targetCurrency === 'RUB' ? minRubles : targetCurrency === 'IRR' ? Math.round(convertAmount(minRubles)) : Number(convertAmount(minRubles).toFixed(currencyDecimals)) const maxInputValue = targetCurrency === 'RUB' ? maxRubles : targetCurrency === 'IRR' ? Math.round(convertAmount(maxRubles)) : Number(convertAmount(maxRubles).toFixed(currencyDecimals)) + const isPending = topUpMutation.isPending || starsPaymentMutation.isPending + return ( -
-
-
-

{t('balance.topUp')} - {methodName}

-
-
- {hasOptions && method.options && ( -
- -
- {method.options.map((option) => ( - - ))} + {/* Scrollable content */} +
+ + {/* Payment options */} + {hasOptions && method.options && ( +
+ +
+ {method.options.map((option) => ( + + ))} +
+ )} + + {/* Amount input */} +
+ +
+ setAmount(e.target.value)} + placeholder={`${formatAmount(minRubles, currencyDecimals)} - ${formatAmount(maxRubles, currencyDecimals)}`} + min={minInputValue} + max={maxInputValue} + step={inputStep} + className="input text-lg pr-14 h-14" + autoComplete="off" + /> + + {currencySymbol} + +
+

+ {t('balance.minAmount')}: {formatAmount(minRubles, currencyDecimals)} — {t('balance.maxAmount')}: {formatAmount(maxRubles, currencyDecimals)} {currencySymbol} +

- )} -
- - setAmount(e.target.value)} - placeholder={`${formatAmount(minRubles, currencyDecimals)} - ${formatAmount(maxRubles, currencyDecimals)}`} - min={minInputValue} - max={maxInputValue} - step={inputStep} - className="input" - /> -
- {t('balance.minAmount')}: {formatAmount(minRubles, currencyDecimals)} {currencySymbol} | {t('balance.maxAmount')}: {formatAmount(maxRubles, currencyDecimals)} {currencySymbol} -
-
+ {/* Quick amounts */} + {quickAmounts.length > 0 && ( +
+ +
+ {quickAmounts.map((a) => { + const quickValue = getQuickAmountValue(a) + const isSelected = amount === quickValue + return ( + + ) + })} +
+
+ )} - {quickAmounts.length > 0 && ( -
- {quickAmounts.map((a) => { - const quickValue = getQuickAmountValue(a) - return ( - - ) - })} -
- )} + {/* Error message */} + {error && ( +
+ {error} +
+ )} + +
- {error && ( -
{error}
- )} - -
- + -
- +
) } -