import React, { useState } from 'react' import { Trans, useTranslation } from 'react-i18next' import { useRouter } from 'next/navigation' import { useContext } from 'use-context-selector' import { ToastContext } from '@/app/components/base/toast' import { RiCloseLine } from '@remixicon/react' import Modal from '@/app/components/base/modal' import Button from '@/app/components/base/button' import Input from '@/app/components/base/input' import { checkEmailExisted, resetEmail, sendVerifyCode, verifyEmail, } from '@/service/common' import { noop } from 'lodash-es' import { asyncRunSafe } from '@/utils' import type { ResponseError } from '@/service/fetch' import { useLogout } from '@/service/use-common' type Props = { show: boolean onClose: () => void email: string } enum STEP { start = 'start', verifyOrigin = 'verifyOrigin', newEmail = 'newEmail', verifyNew = 'verifyNew', } const EmailChangeModal = ({ onClose, email, show }: Props) => { const { t } = useTranslation() const { notify } = useContext(ToastContext) const router = useRouter() const [step, setStep] = useState(STEP.start) const [code, setCode] = useState('') const [mail, setMail] = useState('') const [time, setTime] = useState(0) const [stepToken, setStepToken] = useState('') const [newEmailExited, setNewEmailExited] = useState(false) const [unAvailableEmail, setUnAvailableEmail] = useState(false) const [isCheckingEmail, setIsCheckingEmail] = useState(false) const startCount = () => { setTime(60) const timer = setInterval(() => { setTime((prev) => { if (prev <= 0) { clearInterval(timer) return 0 } return prev - 1 }) }, 1000) } const sendEmail = async (email: string, isOrigin: boolean, token?: string) => { try { const res = await sendVerifyCode({ email, phase: isOrigin ? 'old_email' : 'new_email', token, }) startCount() if (res.data) setStepToken(res.data) } catch (error) { notify({ type: 'error', message: `Error sending verification code: ${error ? (error as any).message : ''}`, }) } } const verifyEmailAddress = async (email: string, code: string, token: string, callback?: (data?: any) => void) => { try { const res = await verifyEmail({ email, code, token, }) if (res.is_valid) { setStepToken(res.token) callback?.(res.token) } else { notify({ type: 'error', message: 'Verifying email failed', }) } } catch (error) { notify({ type: 'error', message: `Error verifying email: ${error ? (error as any).message : ''}`, }) } } const sendCodeToOriginEmail = async () => { await sendEmail( email, true, ) setStep(STEP.verifyOrigin) } const handleVerifyOriginEmail = async () => { await verifyEmailAddress(email, code, stepToken, () => setStep(STEP.newEmail)) setCode('') } const isValidEmail = (email: string): boolean => { const rfc5322emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/ return rfc5322emailRegex.test(email) && email.length <= 254 } const checkNewEmailExisted = async (email: string) => { setIsCheckingEmail(true) try { await checkEmailExisted({ email, }) setNewEmailExited(false) setUnAvailableEmail(false) } catch (e: any) { if (e.status === 400) { const [, errRespData] = await asyncRunSafe(e.json()) const { code } = errRespData || {} if (code === 'email_already_in_use') setNewEmailExited(true) if (code === 'account_in_freeze') setUnAvailableEmail(true) } } finally { setIsCheckingEmail(false) } } const handleNewEmailValueChange = (mailAddress: string) => { setMail(mailAddress) setNewEmailExited(false) if (isValidEmail(mailAddress)) checkNewEmailExisted(mailAddress) } const sendCodeToNewEmail = async () => { if (!isValidEmail(mail)) { notify({ type: 'error', message: 'Invalid email format', }) return } await sendEmail( mail, false, stepToken, ) setStep(STEP.verifyNew) } const { mutateAsync: logout } = useLogout() const handleLogout = async () => { await logout() localStorage.removeItem('setup_status') // Tokens are now stored in cookies and cleared by backend router.push('/signin') } const updateEmail = async (lastToken: string) => { try { await resetEmail({ new_email: mail, token: lastToken, }) handleLogout() } catch (error) { notify({ type: 'error', message: `Error changing email: ${error ? (error as any).message : ''}`, }) } } const submitNewEmail = async () => { await verifyEmailAddress(mail, code, stepToken, updateEmail) } return (
{step === STEP.start && ( <>
{t('common.account.changeEmail.title')}
{t('common.account.changeEmail.authTip')}
}} values={{ email }} />
)} {step === STEP.verifyOrigin && ( <>
{t('common.account.changeEmail.verifyEmail')}
}} values={{ email }} />
{t('common.account.changeEmail.codeLabel')}
setCode(e.target.value)} maxLength={6} />
{t('common.account.changeEmail.resendTip')} {time > 0 && ( {t('common.account.changeEmail.resendCount', { count: time })} )} {!time && ( {t('common.account.changeEmail.resend')} )}
)} {step === STEP.newEmail && ( <>
{t('common.account.changeEmail.newEmail')}
{t('common.account.changeEmail.content3')}
{t('common.account.changeEmail.emailLabel')}
handleNewEmailValueChange(e.target.value)} destructive={newEmailExited || unAvailableEmail} /> {newEmailExited && (
{t('common.account.changeEmail.existingEmail')}
)} {unAvailableEmail && (
{t('common.account.changeEmail.unAvailableEmail')}
)}
)} {step === STEP.verifyNew && ( <>
{t('common.account.changeEmail.verifyNew')}
}} values={{ email: mail }} />
{t('common.account.changeEmail.codeLabel')}
setCode(e.target.value)} maxLength={6} />
{t('common.account.changeEmail.resendTip')} {time > 0 && ( {t('common.account.changeEmail.resendCount', { count: time })} )} {!time && ( {t('common.account.changeEmail.resend')} )}
)}
) } export default EmailChangeModal