mirror of
https://github.com/langgenius/dify.git
synced 2026-05-25 19:00:43 -04:00
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:
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user