diff --git a/src/api/auth.ts b/src/api/auth.ts index a9b0552..4cf23a5 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -138,7 +138,7 @@ export const authApi = { provider: string, ): Promise<{ authorize_url: string; state: string }> => { const response = await apiClient.get<{ authorize_url: string; state: string }>( - `/cabinet/auth/oauth/${provider}/authorize`, + `/cabinet/auth/oauth/${encodeURIComponent(provider)}/authorize`, ); return response.data; }, @@ -146,7 +146,7 @@ export const authApi = { // OAuth: callback (exchange code for tokens) oauthCallback: async (provider: string, code: string, state: string): Promise => { const response = await apiClient.post( - `/cabinet/auth/oauth/${provider}/callback`, + `/cabinet/auth/oauth/${encodeURIComponent(provider)}/callback`, { code, state }, ); return response.data; diff --git a/src/components/ChangeEmailModal.tsx b/src/components/ChangeEmailModal.tsx deleted file mode 100644 index fe49917..0000000 --- a/src/components/ChangeEmailModal.tsx +++ /dev/null @@ -1,490 +0,0 @@ -import { useState, useRef, useEffect, useCallback } from 'react'; -import { createPortal } from 'react-dom'; -import { useTranslation } from 'react-i18next'; -import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { authApi } from '../api/auth'; -import { isValidEmail } from '../utils/validation'; -import { UI } from '../config/constants'; -import { useAuthStore } from '../store/auth'; -import { useTelegramSDK } from '../hooks/useTelegramSDK'; - -// Icons -const CloseIcon = () => ( - - - -); - -const EmailIcon = () => ( - - - -); - -const CheckIcon = () => ( - - - -); - -interface ChangeEmailModalProps { - onClose: () => void; - currentEmail: string; -} - -type Step = 'email' | 'code' | 'success'; - -function useIsMobile() { - const [isMobile, setIsMobile] = useState(() => { - if (typeof window === 'undefined') return false; - return window.innerWidth < 640; - }); - useEffect(() => { - const check = () => setIsMobile(window.innerWidth < 640); - window.addEventListener('resize', check); - return () => window.removeEventListener('resize', check); - }, []); - return isMobile; -} - -const RESEND_COOLDOWN = UI.RESEND_COOLDOWN_SEC; - -export default function ChangeEmailModal({ onClose, currentEmail }: ChangeEmailModalProps) { - const { t } = useTranslation(); - const queryClient = useQueryClient(); - const { setUser } = useAuthStore(); - const { isTelegramWebApp, safeAreaInset, contentSafeAreaInset } = useTelegramSDK(); - const isMobileScreen = useIsMobile(); - - const emailInputRef = useRef(null); - const codeInputRef = useRef(null); - - const [step, setStep] = useState('email'); - const [newEmail, setNewEmail] = useState(''); - const [code, setCode] = useState(''); - const [error, setError] = useState(null); - const [resendCooldown, setResendCooldown] = useState(0); - - const safeBottom = isTelegramWebApp - ? Math.max(safeAreaInset.bottom, contentSafeAreaInset.bottom) - : 0; - - const handleClose = useCallback(() => { - onClose(); - }, [onClose]); - - // Keyboard: Escape to close - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Escape') { - e.preventDefault(); - handleClose(); - } - }; - document.addEventListener('keydown', handleKeyDown); - return () => document.removeEventListener('keydown', handleKeyDown); - }, [handleClose]); - - // Scroll lock - useEffect(() => { - const scrollY = window.scrollY; - const preventScroll = (e: TouchEvent) => { - const target = e.target as HTMLElement; - if (target.closest('[data-modal-content]')) return; - e.preventDefault(); - }; - const preventWheel = (e: WheelEvent) => { - const target = e.target as HTMLElement; - if (target.closest('[data-modal-content]')) return; - e.preventDefault(); - }; - document.addEventListener('touchmove', preventScroll, { passive: false }); - document.addEventListener('wheel', preventWheel, { passive: false }); - document.body.style.overflow = 'hidden'; - return () => { - document.removeEventListener('touchmove', preventScroll); - document.removeEventListener('wheel', preventWheel); - document.body.style.overflow = ''; - window.scrollTo(0, scrollY); - }; - }, []); - - // Resend cooldown timer - useEffect(() => { - if (resendCooldown <= 0) return; - const timer = setInterval(() => { - setResendCooldown((prev) => Math.max(0, prev - 1)); - }, 1000); - return () => clearInterval(timer); - }, [resendCooldown]); - - // Auto-focus inputs - useEffect(() => { - const timer = setTimeout(() => { - if (step === 'email' && emailInputRef.current) { - emailInputRef.current.focus(); - } else if (step === 'code' && codeInputRef.current) { - codeInputRef.current.focus(); - } - }, 100); - return () => clearTimeout(timer); - }, [step]); - - // Request email change mutation - const requestChangeMutation = useMutation({ - mutationFn: (email: string) => authApi.requestEmailChange(email), - onSuccess: () => { - setError(null); - setStep('code'); - setResendCooldown(RESEND_COOLDOWN); - }, - onError: (err: { response?: { data?: { detail?: string } } }) => { - const detail = err.response?.data?.detail; - if (detail?.includes('already registered') || detail?.includes('already in use')) { - setError(t('profile.changeEmail.emailAlreadyUsed')); - } else if (detail?.includes('same as current')) { - setError(t('profile.changeEmail.sameEmail')); - } else if (detail?.includes('rate limit') || detail?.includes('too many')) { - setError(t('profile.changeEmail.tooManyRequests')); - } else { - setError(detail || t('common.error')); - } - }, - }); - - // Verify code mutation - const verifyCodeMutation = useMutation({ - mutationFn: (verificationCode: string) => authApi.verifyEmailChange(verificationCode), - onSuccess: async () => { - setError(null); - setStep('success'); - // Refresh user data - const updatedUser = await authApi.getMe(); - setUser(updatedUser); - queryClient.invalidateQueries({ queryKey: ['user'] }); - }, - onError: (err: { response?: { data?: { detail?: string } } }) => { - const detail = err.response?.data?.detail; - if (detail?.includes('invalid') || detail?.includes('wrong')) { - setError(t('profile.changeEmail.invalidCode')); - } else if (detail?.includes('expired')) { - setError(t('profile.changeEmail.codeExpired')); - } else { - setError(detail || t('common.error')); - } - }, - }); - - const handleSendCode = () => { - setError(null); - if (!newEmail.trim()) { - setError(t('profile.emailRequired')); - return; - } - if (!isValidEmail(newEmail.trim())) { - setError(t('profile.invalidEmail')); - return; - } - if (newEmail.toLowerCase().trim() === currentEmail.toLowerCase()) { - setError(t('profile.changeEmail.sameEmail')); - return; - } - requestChangeMutation.mutate(newEmail.trim()); - }; - - const handleVerifyCode = () => { - setError(null); - if (!code.trim()) { - setError(t('profile.changeEmail.enterCode')); - return; - } - if (code.trim().length < 4) { - setError(t('profile.changeEmail.invalidCode')); - return; - } - verifyCodeMutation.mutate(code.trim()); - }; - - const handleResendCode = () => { - if (resendCooldown > 0) return; - requestChangeMutation.mutate(newEmail.trim()); - }; - - const handleBack = () => { - setStep('email'); - setCode(''); - setError(null); - }; - - const isPending = requestChangeMutation.isPending || verifyCodeMutation.isPending; - - // Content JSX - const contentJSX = ( -
- {/* Header icon */} -
-
- -
-
-

{t('profile.changeEmail.title')}

-

- {step === 'email' && t('profile.changeEmail.description')} - {step === 'code' && t('profile.changeEmail.enterCodeDescription')} - {step === 'success' && t('profile.changeEmail.successDescription')} -

-
-
- - {/* Current email display */} - {step === 'email' && ( -
-

{t('profile.changeEmail.currentEmail')}

-

{currentEmail}

-
- )} - - {/* Email input step */} - {step === 'email' && ( -
-
- - setNewEmail(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter') { - e.preventDefault(); - handleSendCode(); - } - }} - placeholder="new@email.com" - className="input w-full" - autoComplete="email" - autoFocus - /> -
- - -
- )} - - {/* Code verification step */} - {step === 'code' && ( -
-
-

- {t('profile.changeEmail.codeSentTo', { email: newEmail })} -

-
- -
- - setCode(e.target.value.replace(/\D/g, ''))} - onKeyDown={(e) => { - if (e.key === 'Enter') { - e.preventDefault(); - handleVerifyCode(); - } - }} - placeholder="000000" - maxLength={6} - className="input w-full text-center text-2xl tracking-[0.5em]" - autoComplete="one-time-code" - autoFocus - /> -
- - - -
- - -
-
- )} - - {/* Success step */} - {step === 'success' && ( -
-
-
- -
-

- {t('profile.changeEmail.success')} -

-

{newEmail}

-
- - -
- )} - - {/* Error message */} - {error && ( -
- - - - {error} -
- )} -
- ); - - // Render modal based on screen size - const modalContent = isMobileScreen ? ( - <> - {/* Backdrop */} -
- {/* Bottom sheet */} -
e.stopPropagation()} - > - {/* Handle bar */} -
-
-
- - {/* Header */} -
-
- - - {t('profile.changeEmail.title')} - -
- -
- - {/* Divider */} -
- - {/* Content */} -
{contentJSX}
-
- - ) : ( -
-
e.stopPropagation()} - > - {/* Header */} -
-
-
- -
- - {t('profile.changeEmail.title')} - -
- -
- - {/* Content */} -
{contentJSX}
-
-
- ); - - if (typeof document !== 'undefined') { - return createPortal(modalContent, document.body); - } - return modalContent; -} diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index f35fb15..2f27756 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -15,7 +15,7 @@ import { type EmailAuthEnabled, } from '../api/branding'; import { getAndClearReturnUrl } from '../utils/token'; -import { isInTelegramWebApp, getTelegramInitData } from '../hooks/useTelegramSDK'; +import { isInTelegramWebApp, getTelegramInitData, useTelegramSDK } from '../hooks/useTelegramSDK'; import LanguageSwitcher from '../components/LanguageSwitcher'; import TelegramLoginButton from '../components/TelegramLoginButton'; import OAuthProviderIcon from '../components/OAuthProviderIcon'; @@ -54,6 +54,12 @@ export default function Login() { const [forgotPasswordSent, setForgotPasswordSent] = useState(false); const [forgotPasswordLoading, setForgotPasswordLoading] = useState(false); const [forgotPasswordError, setForgotPasswordError] = useState(''); + const [showEmailForm, setShowEmailForm] = useState(() => !!referralCode); + + // Telegram safe area insets + const { safeAreaInset, contentSafeAreaInset } = useTelegramSDK(); + const safeTop = Math.max(safeAreaInset.top, contentSafeAreaInset.top); + const safeBottom = Math.max(safeAreaInset.bottom, contentSafeAreaInset.bottom); // Получаем URL для возврата после авторизации const getReturnUrl = useCallback(() => { @@ -100,7 +106,7 @@ export default function Login() { queryFn: authApi.getOAuthProviders, staleTime: 60000, }); - const oauthProviders = oauthData?.providers ?? []; + const oauthProviders = Array.isArray(oauthData?.providers) ? oauthData.providers : []; const [oauthLoading, setOauthLoading] = useState(null); @@ -109,6 +115,17 @@ export default function Login() { setOauthLoading(provider); try { const { authorize_url, state } = await authApi.getOAuthAuthorizeUrl(provider); + + // Validate redirect URL — only allow HTTPS to prevent open redirect + try { + const parsed = new URL(authorize_url); + if (parsed.protocol !== 'https:') { + throw new Error('Invalid OAuth redirect URL'); + } + } catch { + throw new Error('Invalid OAuth redirect URL'); + } + saveOAuthState(state, provider); window.location.href = authorize_url; } catch { @@ -122,7 +139,7 @@ export default function Login() { // If email auth is disabled but user came with ref param, redirect to bot useEffect(() => { if (referralCode && emailAuthConfig?.enabled === false && botUsername) { - window.location.href = `https://t.me/${botUsername}?start=${referralCode}`; + window.location.href = `https://t.me/${botUsername}?start=${encodeURIComponent(referralCode)}`; } }, [referralCode, emailAuthConfig, botUsername]); @@ -165,7 +182,8 @@ export default function Login() { const error = err as { response?: { status?: number; data?: { detail?: string } } }; const status = error.response?.status; const detail = error.response?.data?.detail; - console.warn(`Telegram auth attempt ${attempt + 1} failed:`, status, detail); + if (import.meta.env.DEV) + console.warn(`Telegram auth attempt ${attempt + 1} failed:`, status, detail); if (status === 401 && attempt < MAX_RETRIES) { await new Promise((r) => setTimeout(r, 1500)); @@ -200,7 +218,7 @@ export default function Login() { const error = err as { response?: { status?: number; data?: { detail?: string } } }; const status = error.response?.status; const detail = error.response?.data?.detail; - console.warn('Telegram auth retry failed:', status, detail); + if (import.meta.env.DEV) console.warn('Telegram auth retry failed:', status, detail); setError( detail || t('auth.telegramRetryFailed', 'Authorization failed. Close the app and try again.'), @@ -301,45 +319,57 @@ export default function Login() { }; return ( -
+
0 ? `${safeTop + 16}px` : 'calc(1rem + env(safe-area-inset-top, 0px))', + paddingBottom: + safeBottom > 0 ? `${safeBottom + 16}px` : 'calc(1rem + env(safe-area-inset-bottom, 0px))', + }} + > {/* Background gradient */}
{/* Language switcher */} -
+
0 ? `${safeTop + 12}px` : 'calc(12px + env(safe-area-inset-top, 0px))', + }} + >
-
- {/* Logo */} +
+ {/* Logo & branding */}
-
- {/* Always show letter as fallback */} +
+ {/* Letter fallback */} {appLogo} - {/* Logo image with smooth fade-in */} + {/* Logo image */} {branding?.has_custom_logo && logoUrl && ( {appName setLogoLoaded(true)} /> )}
- {appName &&

{appName}

} -

{t('auth.loginSubtitle')}

+ {appName &&

{appName}

} - {/* Referral Banner - only show when email auth is enabled */} + {/* Referral Banner */} {referralCode && isEmailAuthEnabled && ( -
-
+
+
- {t('auth.referralInvite')} + {t('auth.referralInvite')}
)} @@ -360,9 +390,9 @@ export default function Login() { {/* Check Email Screen */} {registeredEmail ? (
-
+
-

+

{t('auth.checkEmail', 'Check your email')}

-

+

{t('auth.verificationSent', 'We sent a verification link to:')}

-

{registeredEmail}

-

+

{registeredEmail}

+

{t( 'auth.clickLinkToVerify', 'Click the link in the email to verify your account and log in.', @@ -399,31 +429,29 @@ export default function Login() {

) : ( - /* Card */ + /* Main auth card */
{error && ( -
+
{error}
)} {/* Telegram auth section */} -
-

{t('auth.registerHint')}

- +
{isLoading && isTelegramWebApp ? ( -
+

{t('auth.authenticating')}

) : isTelegramWebApp && error ? ( -
+
))}
)} - {/* Email auth section - only when enabled */} + {/* Email auth section - collapsible */} {isEmailAuthEnabled && ( <> - {/* Divider */} -
+
- {t('auth.or', 'or')} +
-
- {/* Login / Register toggle */} -
- - -
- -
- {/* First name field - only for registration */} - {authMode === 'register' && ( -
- - setFirstName(e.target.value)} - /> -
- )} - -
- - setEmail(e.target.value)} - /> -
- -
- - setPassword(e.target.value)} - /> -
- - {/* Confirm password - only for registration */} - {authMode === 'register' && ( -
- - setConfirmPassword(e.target.value)} - /> -
- )} - - +
+ ) : ( +
+

+ {t( + 'auth.forgotPasswordHint', + 'Enter your email and we will send you instructions to reset your password.', + )} +

+ +
+ + setForgotPasswordEmail(e.target.value)} + placeholder="you@example.com" + className="input" + autoFocus + /> +
+ {forgotPasswordError && ( +

{forgotPasswordError}

+ )} + + +
+ +
+
+ ) ) : ( - t('auth.register', 'Register') - )} - - + /* Normal login / register */ + <> +
+ + +
- {/* Verification notice for registration */} - {authMode === 'register' && ( -

- {t( - 'auth.verificationEmailNotice', - 'After registration, a verification email will be sent to your address', - )} -

- )} +
+ {authMode === 'register' && ( +
+ + setFirstName(e.target.value)} + /> +
+ )} - {/* Forgot password link - only for login */} - {authMode === 'login' && ( -
- +
+ + setEmail(e.target.value)} + /> +
+ +
+ + setPassword(e.target.value)} + /> +
+ + {authMode === 'register' && ( +
+ + setConfirmPassword(e.target.value)} + /> +
+ )} + + + + + {authMode === 'register' && ( +

+ {t( + 'auth.verificationEmailNotice', + 'After registration, a verification email will be sent to your address', + )} +

+ )} + + {authMode === 'login' && ( +
+ +
+ )} + + )}
- )} +
)}
)}
- - {/* Forgot Password Modal */} - {showForgotPassword && ( -
-
-
- - - {forgotPasswordSent ? ( -
-
- - - -
-

- {t('auth.checkEmail', 'Check your email')} -

-

- {t( - 'auth.passwordResetSent', - 'If an account exists with this email, we sent password reset instructions.', - )} -

- -
- ) : ( - <> -

- {t('auth.forgotPassword', 'Forgot password?')} -

-

- {t( - 'auth.forgotPasswordHint', - 'Enter your email and we will send you instructions to reset your password.', - )} -

- -
-
- - setForgotPasswordEmail(e.target.value)} - placeholder="you@example.com" - className="input" - autoFocus - /> -
- - {forgotPasswordError && ( -
- {forgotPasswordError} -
- )} - - -
- - )} -
-
- )}
); } diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx index 75f7d2b..389c074 100644 --- a/src/pages/Profile.tsx +++ b/src/pages/Profile.tsx @@ -1,8 +1,8 @@ -import { useState } from 'react'; +import { useState, useRef, useEffect } from 'react'; import { Link } from 'react-router'; import { useTranslation } from 'react-i18next'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { motion } from 'framer-motion'; +import { motion, AnimatePresence } from 'framer-motion'; import { useAuthStore } from '../store/auth'; import { authApi } from '../api/auth'; import { isValidEmail } from '../utils/validation'; @@ -13,7 +13,7 @@ import { } from '../api/notifications'; import { referralApi } from '../api/referral'; import { brandingApi, type EmailAuthEnabled } from '../api/branding'; -import ChangeEmailModal from '../components/ChangeEmailModal'; +import { UI } from '../config/constants'; import { Card } from '@/components/data-display/Card'; import { Button } from '@/components/primitives/Button'; import { Switch } from '@/components/primitives/Switch'; @@ -70,7 +70,15 @@ export default function Profile() { const [error, setError] = useState(null); const [success, setSuccess] = useState(null); const [copied, setCopied] = useState(false); - const [showChangeEmailModal, setShowChangeEmailModal] = useState(false); + + // Inline email change flow + const [changeEmailStep, setChangeEmailStep] = useState<'email' | 'code' | 'success' | null>(null); + const [newEmail, setNewEmail] = useState(''); + const [changeCode, setChangeCode] = useState(''); + const [changeError, setChangeError] = useState(null); + const [resendCooldown, setResendCooldown] = useState(0); + const newEmailInputRef = useRef(null); + const codeInputRef = useRef(null); // Referral data const { data: referralInfo } = useQuery({ @@ -170,6 +178,117 @@ export default function Profile() { }, }); + // Email change mutations + const requestEmailChangeMutation = useMutation({ + mutationFn: (emailAddr: string) => authApi.requestEmailChange(emailAddr), + onSuccess: () => { + setChangeError(null); + setChangeEmailStep('code'); + setResendCooldown(UI.RESEND_COOLDOWN_SEC); + }, + onError: (err: { response?: { data?: { detail?: string } } }) => { + const detail = err.response?.data?.detail; + if (detail?.includes('already registered') || detail?.includes('already in use')) { + setChangeError(t('profile.changeEmail.emailAlreadyUsed')); + } else if (detail?.includes('same as current')) { + setChangeError(t('profile.changeEmail.sameEmail')); + } else if (detail?.includes('rate limit') || detail?.includes('too many')) { + setChangeError(t('profile.changeEmail.tooManyRequests')); + } else { + setChangeError(detail || t('common.error')); + } + }, + }); + + const verifyEmailChangeMutation = useMutation({ + mutationFn: (verificationCode: string) => authApi.verifyEmailChange(verificationCode), + onSuccess: async () => { + setChangeError(null); + setChangeEmailStep('success'); + const updatedUser = await authApi.getMe(); + setUser(updatedUser); + queryClient.invalidateQueries({ queryKey: ['user'] }); + }, + onError: (err: { response?: { data?: { detail?: string } } }) => { + const detail = err.response?.data?.detail; + if (detail?.includes('invalid') || detail?.includes('wrong')) { + setChangeError(t('profile.changeEmail.invalidCode')); + } else if (detail?.includes('expired')) { + setChangeError(t('profile.changeEmail.codeExpired')); + } else { + setChangeError(detail || t('common.error')); + } + }, + }); + + // Resend cooldown timer + useEffect(() => { + if (resendCooldown <= 0) return; + const timer = setInterval(() => { + setResendCooldown((prev) => Math.max(0, prev - 1)); + }, 1000); + return () => clearInterval(timer); + }, [resendCooldown]); + + // Auto-focus inputs on step change + useEffect(() => { + const timer = setTimeout(() => { + if (changeEmailStep === 'email') newEmailInputRef.current?.focus(); + else if (changeEmailStep === 'code') codeInputRef.current?.focus(); + }, 100); + return () => clearTimeout(timer); + }, [changeEmailStep]); + + // Auto-close success after 3s + useEffect(() => { + if (changeEmailStep !== 'success') return; + const timer = setTimeout(() => resetChangeEmail(), 3000); + return () => clearTimeout(timer); + }, [changeEmailStep]); + + const resetChangeEmail = () => { + setChangeEmailStep(null); + setNewEmail(''); + setChangeCode(''); + setChangeError(null); + setResendCooldown(0); + }; + + const handleSendChangeCode = () => { + setChangeError(null); + if (!newEmail.trim()) { + setChangeError(t('profile.emailRequired')); + return; + } + if (!isValidEmail(newEmail.trim())) { + setChangeError(t('profile.invalidEmail')); + return; + } + if (user?.email && newEmail.toLowerCase().trim() === user.email.toLowerCase()) { + setChangeError(t('profile.changeEmail.sameEmail')); + return; + } + requestEmailChangeMutation.mutate(newEmail.trim()); + }; + + const handleVerifyChangeCode = () => { + setChangeError(null); + if (!changeCode.trim()) { + setChangeError(t('profile.changeEmail.enterCode')); + return; + } + if (changeCode.trim().length < 4) { + setChangeError(t('profile.changeEmail.invalidCode')); + return; + } + verifyEmailChangeMutation.mutate(changeCode.trim()); + }; + + const handleResendChangeCode = () => { + if (resendCooldown > 0) return; + requestEmailChangeMutation.mutate(newEmail.trim()); + }; + const { data: notificationSettings, isLoading: notificationsLoading } = useQuery({ queryKey: ['notification-settings'], queryFn: notificationsApi.getSettings, @@ -324,27 +443,179 @@ export default function Profile() {

{t('profile.verificationRequired')}

- +
+ + {(user.auth_type === 'telegram' || user.auth_type === 'email') && ( + + )} +
)} - {user.email_verified && ( -
-

{t('profile.canLoginWithEmail')}

- +
+ )} + + {/* Inline email change flow */} + + {changeEmailStep === 'email' && ( + - - {t('profile.changeEmail.button')} - -
- )} +
+ + setNewEmail(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleSendChangeCode(); + } + }} + placeholder="new@email.com" + className="input w-full" + autoComplete="email" + /> + {changeError &&

{changeError}

} +
+ + +
+
+ + )} + + {changeEmailStep === 'code' && ( + +
+
+

+ {t('profile.changeEmail.codeSentTo', { email: newEmail })} +

+
+ + setChangeCode(e.target.value.replace(/\D/g, ''))} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleVerifyChangeCode(); + } + }} + placeholder="000000" + maxLength={6} + className="input w-full text-center text-2xl tracking-[0.5em]" + autoComplete="one-time-code" + /> + {changeError &&

{changeError}

} +
+
+ + +
+ +
+
+
+ )} + + {changeEmailStep === 'success' && ( + +
+
+ +
+

+ {t('profile.changeEmail.success')} +

+

{newEmail}

+
+
+
+
+ )} +
) : (
@@ -586,14 +857,6 @@ export default function Profile() { )} - - {/* Change Email Modal */} - {showChangeEmailModal && user?.email && ( - setShowChangeEmailModal(false)} - currentEmail={user.email} - /> - )} ); } diff --git a/src/pages/TopUpAmount.tsx b/src/pages/TopUpAmount.tsx index fb18324..3032224 100644 --- a/src/pages/TopUpAmount.tsx +++ b/src/pages/TopUpAmount.tsx @@ -87,7 +87,7 @@ export default function TopUpAmount() { const queryClient = useQueryClient(); const { formatAmount, currencySymbol, convertAmount, convertToRub, targetCurrency } = useCurrency(); - const { openInvoice } = usePlatform(); + const { openInvoice, openTelegramLink, openLink } = usePlatform(); const haptic = useHaptic(); const inputRef = useRef(null); @@ -281,6 +281,15 @@ export default function TopUpAmount() { : convertAmount(rub).toFixed(currencyDecimals); const isPending = topUpMutation.isPending || starsPaymentMutation.isPending; + const handleOpenPayment = () => { + if (!paymentUrl) return; + if (paymentUrl.includes('t.me/')) { + openTelegramLink(paymentUrl); + } else { + openLink(paymentUrl); + } + }; + const handleCopyUrl = async () => { if (!paymentUrl) return; try { @@ -478,15 +487,14 @@ export default function TopUpAmount() {

{t('balance.clickToOpenPayment')}

- {t('balance.openPaymentPage')} - +