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 { useAuthStore } from '../store/auth'; import { useTelegramWebApp } from '../hooks/useTelegramWebApp'; import { useBackButton } from '@/platform'; // 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 = 60; // seconds export default function ChangeEmailModal({ onClose, currentEmail }: ChangeEmailModalProps) { const { t } = useTranslation(); const queryClient = useQueryClient(); const { setUser } = useAuthStore(); const { isTelegramWebApp, safeAreaInset, contentSafeAreaInset } = useTelegramWebApp(); 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]); // Telegram back button - using platform hook useBackButton(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 validateEmail = (email: string): boolean => { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email.trim()); }; const handleSendCode = () => { setError(null); if (!newEmail.trim()) { setError(t('profile.emailRequired')); return; } if (!validateEmail(newEmail)) { 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; }