diff --git a/src/components/TopUpModal.tsx b/src/components/TopUpModal.tsx index d92270c..7c67612 100644 --- a/src/components/TopUpModal.tsx +++ b/src/components/TopUpModal.tsx @@ -9,28 +9,6 @@ import { checkRateLimit, getRateLimitResetTime, RATE_LIMIT_KEYS } from '../utils import type { PaymentMethod } from '../types' import BentoCard from './ui/BentoCard' -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, try_browser: true }); 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 -} - // Icons const CloseIcon = () => ( @@ -68,6 +46,24 @@ const SparklesIcon = () => ( ) +const ExternalLinkIcon = () => ( + + + +) + +const CopyIcon = () => ( + + + +) + +const CheckIcon = () => ( + + + +) + interface TopUpModalProps { method: PaymentMethod onClose: () => void @@ -117,7 +113,8 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top const [selectedOption, setSelectedOption] = useState( method.options && method.options.length > 0 ? method.options[0].id : null ) - const popupRef = useRef(null) + const [paymentUrl, setPaymentUrl] = useState(null) + const [copied, setCopied] = useState(false) const [isInputFocused, setIsInputFocused] = useState(false) const handleClose = useCallback(() => { @@ -177,7 +174,6 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top 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), @@ -205,13 +201,23 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top 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() + if (redirectUrl) { + // In Telegram Mini App, try to open directly + const webApp = window.Telegram?.WebApp + if (webApp?.openLink) { + try { + webApp.openLink(redirectUrl, { try_instant_view: false, try_browser: true }) + onClose() + return + } catch (e) { + console.warn('[TopUpModal] webApp.openLink failed:', e) + } + } + // Otherwise show the link for user to click + setPaymentUrl(redirectUrl) + } }, 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'))) }, @@ -219,6 +225,7 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top const handleSubmit = () => { setError(null) + setPaymentUrl(null) inputRef.current?.blur() if (!checkRateLimit(RATE_LIMIT_KEYS.PAYMENT, 3, 30000)) { @@ -234,9 +241,6 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top } 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) } } @@ -248,6 +252,22 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top : convertAmount(rub).toFixed(currencyDecimals) const isPending = topUpMutation.isPending || starsPaymentMutation.isPending + const handleCopyUrl = async () => { + if (!paymentUrl) return + try { + await navigator.clipboard.writeText(paymentUrl) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } catch (e) { + console.warn('Failed to copy:', e) + } + } + + const handleOpenUrl = () => { + if (!paymentUrl) return + window.open(paymentUrl, '_blank', 'noopener,noreferrer') + } + // Auto-focus input - works on mobile in Telegram WebApp useEffect(() => { const timer = setTimeout(() => { @@ -261,9 +281,6 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top return () => clearTimeout(timer) }, []) - // Calculate display amount for preview - const displayAmount = amount && parseFloat(amount) > 0 ? parseFloat(amount) : 0 - // Content JSX - shared between mobile and desktop const contentJSX = (
@@ -314,33 +331,55 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top
)} - {/* Amount input - modern design */} + {/* Amount input + Submit button - inline */}
-
- setAmount(e.target.value)} - onFocus={() => setIsInputFocused(true)} - onBlur={() => setIsInputFocused(false)} - onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); handleSubmit() } }} - placeholder="0" - className="w-full h-16 px-5 pr-16 text-2xl font-bold bg-transparent text-dark-100 placeholder:text-dark-600 focus:outline-none" - style={{ fontSize: '24px' }} - autoComplete="off" - autoFocus - /> - - {currencySymbol} - +
+
+ setAmount(e.target.value)} + onFocus={() => setIsInputFocused(true)} + onBlur={() => setIsInputFocused(false)} + onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); handleSubmit() } }} + placeholder="0" + className="w-full h-14 px-4 pr-12 text-xl font-bold bg-transparent text-dark-100 placeholder:text-dark-600 focus:outline-none" + autoComplete="off" + autoFocus + /> + + {currencySymbol} + +
+
@@ -386,36 +425,53 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top
)} - {/* Submit button */} - - )} - + + )} )