fix(web): debounce email check when change email (#36421)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: JzoNgKVO <27049666+JzoNgKVO@users.noreply.github.com>
This commit is contained in:
KVOJJJin
2026-05-20 15:09:15 +08:00
committed by GitHub
parent 7cb14cb4cc
commit 5cdf4e405b
2 changed files with 69 additions and 23 deletions

View File

@@ -157,11 +157,6 @@
"count": 2
}
},
"web/app/account/(commonLayout)/account-page/email-change-modal.tsx": {
"ts/no-explicit-any": {
"count": 5
}
},
"web/app/account/(commonLayout)/delete-account/components/verify-email.tsx": {
"react/set-state-in-effect": {
"count": 1

View File

@@ -3,6 +3,7 @@ import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast'
import { RiCloseLine } from '@remixicon/react'
import { useDebounceFn } from 'ahooks'
import { useCallback, useEffect, useRef, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
@@ -30,10 +31,35 @@ const STEP = {
type Step = typeof STEP[keyof typeof STEP]
const emailPattern = /^[\w.!#$%&'*+\-/=?^`{|}~]+@(?:[\w-]+\.)+[\w-]{2,}$/
type FetchResponseError = {
status: number
json: () => Promise<ResponseError>
}
function getErrorMessage(error: unknown) {
if (error instanceof Error)
return error.message
if (typeof error === 'object' && error !== null && 'message' in error) {
const message = (error as { message?: unknown }).message
return typeof message === 'string' ? message : ''
}
return ''
}
function isFetchResponseError(error: unknown): error is FetchResponseError {
if (typeof error !== 'object' || error === null)
return false
const maybeError = error as { status?: unknown, json?: unknown }
return typeof maybeError.status === 'number' && typeof maybeError.json === 'function'
}
const EmailChangeModal = ({ onClose, email }: Props) => {
const { t } = useTranslation()
const router = useRouter()
const [step, setStep] = useState<Step>(STEP.start)
const [step, setStep] = useState<Step>(STEP.newEmail)
const [code, setCode] = useState<string>('')
const [mail, setMail] = useState<string>('')
const [time, setTime] = useState<number>(0)
@@ -42,6 +68,7 @@ const EmailChangeModal = ({ onClose, email }: Props) => {
const [unAvailableEmail, setUnAvailableEmail] = useState<boolean>(false)
const [isCheckingEmail, setIsCheckingEmail] = useState<boolean>(false)
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null)
const latestEmailRef = useRef<string>('')
const clearCountdown = useCallback(() => {
if (!timerRef.current)
@@ -79,11 +106,11 @@ const EmailChangeModal = ({ onClose, email }: Props) => {
setStepToken(res.data)
}
catch (error) {
toast.error(`Error sending verification code: ${error ? (error as any).message : ''}`)
toast.error(`Error sending verification code: ${getErrorMessage(error)}`)
}
}
const verifyEmailAddress = async (email: string, code: string, token: string, callback?: (data?: any) => void) => {
const verifyEmailAddress = async (email: string, code: string, token: string, callback?: (token: string) => void) => {
try {
const res = await verifyEmail({
email,
@@ -99,7 +126,7 @@ const EmailChangeModal = ({ onClose, email }: Props) => {
}
}
catch (error) {
toast.error(`Error verifying email: ${error ? (error as any).message : ''}`)
toast.error(`Error verifying email: ${getErrorMessage(error)}`)
}
}
@@ -117,8 +144,7 @@ const EmailChangeModal = ({ onClose, email }: Props) => {
}
const isValidEmail = (email: string): boolean => {
const rfc5322emailRegex = /^[\w.!#$%&'*+/=?^`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/i
return rfc5322emailRegex.test(email) && email.length <= 254
return emailPattern.test(email)
}
const checkNewEmailExisted = async (email: string) => {
@@ -127,11 +153,15 @@ const EmailChangeModal = ({ onClose, email }: Props) => {
await checkEmailExisted({
email,
})
if (latestEmailRef.current !== email)
return
setNewEmailExited(false)
setUnAvailableEmail(false)
}
catch (e: any) {
if (e.status === 400) {
catch (e: unknown) {
if (latestEmailRef.current !== email)
return
if (isFetchResponseError(e) && e.status === 400) {
const [, errRespData] = await asyncRunSafe<ResponseError>(e.json())
const { code } = errRespData || {}
if (code === 'email_already_in_use')
@@ -141,24 +171,41 @@ const EmailChangeModal = ({ onClose, email }: Props) => {
}
}
finally {
setIsCheckingEmail(false)
if (latestEmailRef.current === email)
setIsCheckingEmail(false)
}
}
const {
run: checkNewEmailExistedDebounced,
cancel: cancelCheckNewEmailExisted,
} = useDebounceFn(checkNewEmailExisted, { wait: 500 })
useEffect(() => cancelCheckNewEmailExisted, [cancelCheckNewEmailExisted])
const handleNewEmailValueChange = (mailAddress: string) => {
const normalizedMailAddress = mailAddress.trim()
latestEmailRef.current = normalizedMailAddress
setMail(mailAddress)
setNewEmailExited(false)
if (isValidEmail(mailAddress))
checkNewEmailExisted(mailAddress)
setUnAvailableEmail(false)
if (isValidEmail(normalizedMailAddress)) {
setIsCheckingEmail(true)
checkNewEmailExistedDebounced(normalizedMailAddress)
return
}
cancelCheckNewEmailExisted()
setIsCheckingEmail(false)
}
const sendCodeToNewEmail = async () => {
if (!isValidEmail(mail)) {
const normalizedMail = mail.trim()
if (!isValidEmail(normalizedMail)) {
toast.error('Invalid email format')
return
}
await sendEmail(
mail,
normalizedMail,
false,
stepToken,
)
@@ -178,23 +225,27 @@ const EmailChangeModal = ({ onClose, email }: Props) => {
const updateEmail = async (lastToken: string) => {
try {
await resetEmail({
new_email: mail,
new_email: mail.trim(),
token: lastToken,
})
handleLogout()
}
catch (error) {
toast.error(`Error changing email: ${error ? (error as any).message : ''}`)
toast.error(`Error changing email: ${getErrorMessage(error)}`)
}
}
const submitNewEmail = async () => {
await verifyEmailAddress(mail, code, stepToken, updateEmail)
await verifyEmailAddress(mail.trim(), code, stepToken, updateEmail)
}
const normalizedMail = mail.trim()
const isMailValid = isValidEmail(normalizedMail)
const isSendCodeDisabled = !normalizedMail || newEmailExited || unAvailableEmail || isCheckingEmail || !isMailValid
return (
<Dialog open onOpenChange={open => !open && onClose()}>
<DialogContent className="w-[420px]! p-6!">
<DialogContent className="w-105! p-6!">
<div className="absolute top-5 right-5 cursor-pointer p-1.5" onClick={onClose}>
<RiCloseLine className="size-5 text-text-tertiary" />
</div>
@@ -304,7 +355,7 @@ const EmailChangeModal = ({ onClose, email }: Props) => {
</div>
<div className="mt-3 space-y-2">
<Button
disabled={!mail || newEmailExited || unAvailableEmail || isCheckingEmail || !isValidEmail(mail)}
disabled={isSendCodeDisabled}
className="w-full!"
variant="primary"
onClick={sendCodeToNewEmail}