From b89be4e6528cc856029265be4d400889fb3032ba Mon Sep 17 00:00:00 2001 From: Nixieboluo Date: Wed, 23 Jul 2025 18:14:22 +0800 Subject: [PATCH] fix(desktop): email/phone binding/unbinding ux improvments (#5717) * fix(desktop): remove cftoken verif for email/phone binding Signed-off-by: Nixieboluo * fix(desktop): turnstile widget refresh causes unintended sms resends Signed-off-by: Nixieboluo * fix(desktop): captcha timer starts before code was sent Signed-off-by: Nixieboluo * feat(desktop): clear input field and error state when resending sms Signed-off-by: Nixieboluo * fix(desktop): page state not reset on account settings modal close Signed-off-by: Nixieboluo * fix(desktop): incorrect title on email/phone change/binding/unbinding modal Signed-off-by: Nixieboluo * chore(desktop): clean up filterCf Signed-off-by: Nixieboluo * chore(desktop): minor i18n phrase casing fix Signed-off-by: Nixieboluo --------- Signed-off-by: Nixieboluo --- frontend/desktop/package.json | 4 +- .../desktop/public/locales/en/common.json | 6 ++- frontend/desktop/public/locales/en/v2.json | 1 + .../desktop/public/locales/zh/common.json | 4 ++ frontend/desktop/public/locales/zh/v2.json | 3 +- .../account/AccountCenter/index.tsx | 33 +++++++++++------ .../desktop/src/components/v2/EmailCheck.tsx | 37 +++++++++++++++++-- .../desktop/src/components/v2/PhoneCheck.tsx | 30 ++++++++++++++- .../src/pages/api/auth/email/bind/sms.ts | 16 ++++---- .../src/pages/api/auth/email/unbind/sms.ts | 16 ++++---- .../src/pages/api/auth/phone/bind/sms.ts | 20 +++++----- .../desktop/src/pages/api/auth/phone/sms.ts | 2 - .../src/pages/api/auth/phone/unbind/sms.ts | 34 ++++++++--------- frontend/pnpm-lock.yaml | 12 +++--- 14 files changed, 143 insertions(+), 75 deletions(-) diff --git a/frontend/desktop/package.json b/frontend/desktop/package.json index 49fea3f3b..a30c008ea 100644 --- a/frontend/desktop/package.json +++ b/frontend/desktop/package.json @@ -31,7 +31,7 @@ "@emotion/styled": "^11.11.0", "@hookform/resolvers": "^3.9.0", "@kubernetes/client-node": "^0.18.1", - "@marsidev/react-turnstile": "^0.5.3", + "@marsidev/react-turnstile": "^1.1.0", "@prisma/client": "^5.10.2", "@sealos/driver": "workspace:^", "@sealos/ui": "workspace:^", @@ -104,4 +104,4 @@ "jest-environment-jsdom": "^29.7.0", "prettier": "^2.8.8" } -} \ No newline at end of file +} diff --git a/frontend/desktop/public/locales/en/common.json b/frontend/desktop/public/locales/en/common.json index 7914f93de..5dce3e4a9 100644 --- a/frontend/desktop/public/locales/en/common.json +++ b/frontend/desktop/public/locales/en/common.json @@ -33,6 +33,8 @@ "billing": "Billing", "bind": "Link", "bind_success": "Binding successful", + "bindemail": "Bind Email", + "bindphone": "Bind Phone", "bonus": "Bonus", "bound": "Bound", "business_license": "business license", @@ -40,7 +42,7 @@ "captcha_init_failed": "Failed to initialize CAPTCHA", "change": "Change", "change_binding": "Change Binding", - "changeemail": "Modify email", + "changeemail": "Modify Email", "changepassword": "Change Password", "changephone": "Change Phone", "charge": "Charge", @@ -284,6 +286,8 @@ "transAmt_not_match": "The verification amount does not match. Please note that the verification amount is in cents. For example, if the received amount is 0.23 yuan, enter 23.", "unbind": "Unbind", "unbind_success": "Unbinding successfully", + "unbindemail": "Unbind Email", + "unbindphone": "Unbind Phone", "unbound": "Not Linked", "under_active_development": "Under active development 🚧", "unread": "Unread", diff --git a/frontend/desktop/public/locales/en/v2.json b/frontend/desktop/public/locales/en/v2.json index 6ce052dc3..c1a912cd7 100644 --- a/frontend/desktop/public/locales/en/v2.json +++ b/frontend/desktop/public/locales/en/v2.json @@ -91,6 +91,7 @@ "register": "Register", "remember_me": "Remember me", "request_new_link": "Resend it", + "sending_code": "Sending verification code...", "settings": "Settings", "show_me": "Show me", "sign_in": "Sign in", diff --git a/frontend/desktop/public/locales/zh/common.json b/frontend/desktop/public/locales/zh/common.json index 596d4d1fd..3da15507a 100644 --- a/frontend/desktop/public/locales/zh/common.json +++ b/frontend/desktop/public/locales/zh/common.json @@ -33,6 +33,8 @@ "billing": "费用中心", "bind": "绑定", "bind_success": "绑定成功", + "bindemail": "绑定电子邮箱", + "bindphone": "绑定手机号", "bonus": "赠", "bound": "已绑定", "business_license": "营业执照", @@ -276,6 +278,8 @@ "transAmt_not_match": "验证金额不匹配,注意验证金额单位是分,例如收到的打款金额为 0.23 元 则输入23。", "unbind": "解绑", "unbind_success": "解绑成功", + "unbindemail": "解绑电子邮箱", + "unbindphone": "解绑手机号", "unbound": "未绑定", "under_active_development": "正在积极开发中 🚧", "unread": "未读", diff --git a/frontend/desktop/public/locales/zh/v2.json b/frontend/desktop/public/locales/zh/v2.json index c3579c4dd..376355e28 100644 --- a/frontend/desktop/public/locales/zh/v2.json +++ b/frontend/desktop/public/locales/zh/v2.json @@ -91,6 +91,7 @@ "register": "注册", "remember_me": "记住我", "request_new_link": "重新发送验证码", + "sending_code": "正在发送验证码...", "settings": "设置", "show_me": "展示", "sign_in": "登录", @@ -130,4 +131,4 @@ "worspace_heading_description": "计算资源、应用数据和团队权限一体化", "you_may_invite_memebers_later": "稍后可邀请成员共同协作", "your_current_plan": "当前套餐" -} \ No newline at end of file +} diff --git a/frontend/desktop/src/components/account/AccountCenter/index.tsx b/frontend/desktop/src/components/account/AccountCenter/index.tsx index 04bccc240..36b0aca2c 100644 --- a/frontend/desktop/src/components/account/AccountCenter/index.tsx +++ b/frontend/desktop/src/components/account/AccountCenter/index.tsx @@ -85,17 +85,6 @@ export default function AccountCenter(props: AccountCenterProps) { infoData.refetch(); }; - const modalTitle = useMemo(() => { - if (pageState === PageState.INDEX) return t('common:account_settings'); - else if (pageState === PageState.PASSWORD) return t('common:changepassword'); - else if (Object.values(PhoneState).includes(pageState as PhoneState)) - return t('common:changephone'); - else if (Object.values(EmailState).includes(pageState as EmailState)) - return t('common:changeemail'); - else if (pageState === PageState.REALNAME_AUTH) return t('common:realName_verification'); - else return ''; - }, [t, pageState]); - const infoData = useQuery({ queryFn: UserInfo, queryKey: [session?.token, 'UserInfo'], @@ -146,6 +135,19 @@ export default function AccountCenter(props: AccountCenterProps) { return state; }, [infoData.data?.oauthProvider]); + const modalTitle = useMemo(() => { + if (pageState === PageState.INDEX) return t('common:account_settings'); + else if (pageState === PageState.PASSWORD) return t('common:changepassword'); + else if (pageState === PageState.EMAIL_BIND) return t('common:bindemail'); + else if (pageState === PageState.EMAIL_UNBIND) return t('common:unbindemail'); + else if (pageState === PageState.EMAIL_CHANGE_BIND) return t('common:changeemail'); + else if (pageState === PageState.PHONE_BIND) return t('common:bindphone'); + else if (pageState === PageState.PHONE_UNBIND) return t('common:unbindphone'); + else if (pageState === PageState.PHONE_CHANGE_BIND) return t('common:changephone'); + else if (pageState === PageState.REALNAME_AUTH) return t('common:realName_verification'); + else return ''; + }, [t, pageState]); + return ( <> {children ? ( @@ -172,7 +174,14 @@ export default function AccountCenter(props: AccountCenterProps) { icon={} /> )} - + { + resetPageState(); + onClose(); + }} + isCentered + > { if (!signupData) { router.push('/signin'); @@ -106,7 +107,6 @@ export default function EmailCheckComponent() { const sendCode = async (cfToken?: string) => { setIsLoading(true); const oldTime = startTime; - updateStartTime(); setCanResend(false); try { @@ -120,6 +120,8 @@ export default function EmailCheckComponent() { }); if (result.code !== 200) { throw Error(result.message); + } else { + updateStartTime(); } } catch (error) { // rollout @@ -142,6 +144,11 @@ export default function EmailCheckComponent() { const onSubmit = async (force = false) => { if ((!canResend || isLoading) && !force) return; + // Clear error state + verifyMutation.reset(); + // Clear input field + setPinValue(''); + if (authConfig?.turnstile.enabled) { turnstileRef.current?.reset(); } else { @@ -182,7 +189,9 @@ export default function EmailCheckComponent() { {!!authConfig?.turnstile.enabled && ( { console.log('Verification code:', value); @@ -221,8 +232,28 @@ export default function EmailCheckComponent() { + {isLoading && ( + + {t('v2:sending_code')} + + )} + {verifyMutation.isLoading ? ( - {t('v2:verifying')} + + {t('v2:verifying')} + ) : ( {verifyMutation.isError && ( diff --git a/frontend/desktop/src/components/v2/PhoneCheck.tsx b/frontend/desktop/src/components/v2/PhoneCheck.tsx index 7483429d5..34603e46b 100644 --- a/frontend/desktop/src/components/v2/PhoneCheck.tsx +++ b/frontend/desktop/src/components/v2/PhoneCheck.tsx @@ -38,6 +38,7 @@ export default function PhoneCheckComponent() { const [isLoading, setIsLoading] = useState(false); const { captchaIsLoaded } = useScriptStore(); const { signupData, clearSignupData, startTime, updateStartTime } = useSignupStore(); + const [pinValue, setPinValue] = useState(''); const { setToken } = useSessionStore(); const getRemainTime = () => 60000 - new Date().getTime() + startTime; @@ -185,6 +186,11 @@ export default function PhoneCheckComponent() { const onSubmit = async (force = false) => { if ((!canResend || isLoading) && !force) return; + // Clear error state + verifyMutation.reset(); + // Clear input field + setPinValue(''); + setIsLoading(true); try { if (!signupData || signupData.providerType !== 'PHONE') { @@ -269,6 +275,8 @@ export default function PhoneCheckComponent() { placeholder="" focusBorderColor="#18181B" autoFocus + value={pinValue} + onChange={setPinValue} isDisabled={verifyMutation.isLoading} onComplete={(value) => { verifyMutation.mutate({ code: value, id: signupData?.providerId || '' }); @@ -287,8 +295,28 @@ export default function PhoneCheckComponent() { + {sendCodeMutation.isLoading && ( + + {t('v2:sending_code')} + + )} + {verifyMutation.isLoading ? ( - {t('v2:verifying')} + + {t('v2:verifying')} + ) : ( {verifyMutation.isError && ( diff --git a/frontend/desktop/src/pages/api/auth/email/bind/sms.ts b/frontend/desktop/src/pages/api/auth/email/bind/sms.ts index 27189c57d..67c0398ce 100644 --- a/frontend/desktop/src/pages/api/auth/email/bind/sms.ts +++ b/frontend/desktop/src/pages/api/auth/email/bind/sms.ts @@ -2,20 +2,18 @@ import { NextApiRequest, NextApiResponse } from 'next'; import { filterAccessToken } from '@/services/backend/middleware/access'; import { enableEmailSms } from '@/services/enable'; import { ErrorHandler } from '@/services/backend/middleware/error'; -import { filterCf, filterEmailParams, sendSmsCodeGuard } from '@/services/backend/middleware/sms'; +import { filterEmailParams, sendSmsCodeGuard } from '@/services/backend/middleware/sms'; import { sendEmailCodeSvc } from '@/services/backend/svc/sms'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { if (!enableEmailSms()) { throw new Error('SMS is not enabled'); } - await filterCf(req, res, async () => { - await filterAccessToken(req, res, () => - filterEmailParams(req, res, ({ email }) => - sendSmsCodeGuard({ id: email, smsType: 'email_bind' })(req, res, () => - sendEmailCodeSvc(email, 'email_bind')(res) - ) + await filterAccessToken(req, res, () => + filterEmailParams(req, res, ({ email }) => + sendSmsCodeGuard({ id: email, smsType: 'email_bind' })(req, res, () => + sendEmailCodeSvc(email, 'email_bind')(res) ) - ); - }); + ) + ); }); diff --git a/frontend/desktop/src/pages/api/auth/email/unbind/sms.ts b/frontend/desktop/src/pages/api/auth/email/unbind/sms.ts index 58b92fe73..ee3914938 100644 --- a/frontend/desktop/src/pages/api/auth/email/unbind/sms.ts +++ b/frontend/desktop/src/pages/api/auth/email/unbind/sms.ts @@ -1,7 +1,7 @@ import { filterAccessToken } from '@/services/backend/middleware/access'; import { ErrorHandler } from '@/services/backend/middleware/error'; import { unbindEmailGuard } from '@/services/backend/middleware/oauth'; -import { filterCf, filterEmailParams, sendSmsCodeGuard } from '@/services/backend/middleware/sms'; +import { filterEmailParams, sendSmsCodeGuard } from '@/services/backend/middleware/sms'; import { sendEmailCodeSvc } from '@/services/backend/svc/sms'; import { enableEmailSms } from '@/services/enable'; import { NextApiRequest, NextApiResponse } from 'next'; @@ -11,14 +11,12 @@ export default ErrorHandler(async function handler(req: NextApiRequest, res: Nex throw new Error('SMS is not enabled'); } await filterAccessToken(req, res, ({ userUid }) => - filterCf(req, res, async () => - filterEmailParams(req, res, ({ email }) => - unbindEmailGuard(email, userUid)(res, () => - sendSmsCodeGuard({ - id: email, - smsType: 'email_unbind' - })(req, res, () => sendEmailCodeSvc(email, 'email_unbind')(res)) - ) + filterEmailParams(req, res, ({ email }) => + unbindEmailGuard(email, userUid)(res, () => + sendSmsCodeGuard({ + id: email, + smsType: 'email_unbind' + })(req, res, () => sendEmailCodeSvc(email, 'email_unbind')(res)) ) ) ); diff --git a/frontend/desktop/src/pages/api/auth/phone/bind/sms.ts b/frontend/desktop/src/pages/api/auth/phone/bind/sms.ts index bc0512150..3257243aa 100644 --- a/frontend/desktop/src/pages/api/auth/phone/bind/sms.ts +++ b/frontend/desktop/src/pages/api/auth/phone/bind/sms.ts @@ -1,6 +1,6 @@ import { filterAccessToken } from '@/services/backend/middleware/access'; import { ErrorHandler } from '@/services/backend/middleware/error'; -import { filterCf, filterPhoneParams, sendSmsCodeGuard } from '@/services/backend/middleware/sms'; +import { filterPhoneParams, sendSmsCodeGuard } from '@/services/backend/middleware/sms'; import { cnVersionMiddleware } from '@/services/backend/middleware/version'; import { sendPhoneCodeSvc } from '@/services/backend/svc/sms'; import { enablePhoneSms } from '@/services/enable'; @@ -11,15 +11,13 @@ export default ErrorHandler(async function handler(req: NextApiRequest, res: Nex if (!enablePhoneSms()) { throw new Error('SMS is not enabled'); } - await filterCf(req, res, async () => { - await filterAccessToken(req, res, () => - filterPhoneParams(req, res, ({ phoneNumbers: phone }) => - sendSmsCodeGuard({ - id: phone, - smsType: 'phone_bind' - })(req, res, () => sendPhoneCodeSvc(phone, 'phone_bind')(res)) - ) - ); - }); + await filterAccessToken(req, res, () => + filterPhoneParams(req, res, ({ phoneNumbers: phone }) => + sendSmsCodeGuard({ + id: phone, + smsType: 'phone_bind' + })(req, res, () => sendPhoneCodeSvc(phone, 'phone_bind')(res)) + ) + ); }); }); diff --git a/frontend/desktop/src/pages/api/auth/phone/sms.ts b/frontend/desktop/src/pages/api/auth/phone/sms.ts index 97de5cd07..d2324eed4 100644 --- a/frontend/desktop/src/pages/api/auth/phone/sms.ts +++ b/frontend/desktop/src/pages/api/auth/phone/sms.ts @@ -14,7 +14,6 @@ export default ErrorHandler(async function handler(req: NextApiRequest, res: Nex if (!enablePhoneSms()) { throw new Error('SMS is not enabled'); } - // await filterCf(req, res, async () => { await filterCaptcha(req, res, () => filterPhoneParams(req, res, ({ phoneNumbers: phone }) => sendSmsCodeGuard({ @@ -23,6 +22,5 @@ export default ErrorHandler(async function handler(req: NextApiRequest, res: Nex })(req, res, () => sendPhoneCodeSvc(phone, 'phone_login')(res)) ) ); - // }); }); }); diff --git a/frontend/desktop/src/pages/api/auth/phone/unbind/sms.ts b/frontend/desktop/src/pages/api/auth/phone/unbind/sms.ts index d46643fa0..c0c3e98b3 100644 --- a/frontend/desktop/src/pages/api/auth/phone/unbind/sms.ts +++ b/frontend/desktop/src/pages/api/auth/phone/unbind/sms.ts @@ -1,7 +1,7 @@ import { filterAccessToken } from '@/services/backend/middleware/access'; import { ErrorHandler } from '@/services/backend/middleware/error'; import { unbindPhoneGuard } from '@/services/backend/middleware/oauth'; -import { filterCf, filterPhoneParams, sendSmsCodeGuard } from '@/services/backend/middleware/sms'; +import { filterPhoneParams, sendSmsCodeGuard } from '@/services/backend/middleware/sms'; import { cnVersionMiddleware } from '@/services/backend/middleware/version'; import { sendPhoneCodeSvc } from '@/services/backend/svc/sms'; import { enablePhoneSms } from '@/services/enable'; @@ -13,23 +13,21 @@ export default ErrorHandler(async function handler(req: NextApiRequest, res: Nex throw new Error('SMS is not enabled'); } await filterAccessToken(req, res, async ({ userUid }) => { - await filterCf(req, res, async () => { - await filterPhoneParams( - req, - res, - async ({ phoneNumbers }) => - await unbindPhoneGuard(phoneNumbers, userUid)( - res, - async () => - await sendSmsCodeGuard({ - id: phoneNumbers, - smsType: 'phone_unbind' - })(req, res, async () => { - await sendPhoneCodeSvc(phoneNumbers, 'phone_unbind')(res); - }) - ) - ); - }); + await filterPhoneParams( + req, + res, + async ({ phoneNumbers }) => + await unbindPhoneGuard(phoneNumbers, userUid)( + res, + async () => + await sendSmsCodeGuard({ + id: phoneNumbers, + smsType: 'phone_unbind' + })(req, res, async () => { + await sendPhoneCodeSvc(phoneNumbers, 'phone_unbind')(res); + }) + ) + ); }); }); }); diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 8f97d0180..bfa468141 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -103,8 +103,8 @@ importers: specifier: ^0.18.1 version: 0.18.1 '@marsidev/react-turnstile': - specifier: ^0.5.3 - version: 0.5.3(react-dom@18.2.0)(react@18.2.0) + specifier: ^1.1.0 + version: 1.1.0(react-dom@18.2.0)(react@18.2.0) '@prisma/client': specifier: ^5.10.2 version: 5.10.2(prisma@5.10.2) @@ -8383,11 +8383,11 @@ packages: resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} dev: false - /@marsidev/react-turnstile@0.5.3(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-lx3p2/56esPt8Ksr37K8uhPt/K4Mg8xaIfCV8MPKmE/1X4aHesRqZok1+L1ySQwsdWoEe5+KJOhBXka8lFBwNg==} + /@marsidev/react-turnstile@1.1.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-X7bP9ZYutDd+E+klPYF+/BJHqEyyVkN4KKmZcNRr84zs3DcMoftlMAuoKqNSnqg0HE7NQ1844+TLFSJoztCdSA==} peerDependencies: - react: '>=16.8.0' - react-dom: '>=16.8.0' + react: ^17.0.2 || ^18.0.0 || ^19.0 + react-dom: ^17.0.2 || ^18.0.0 || ^19.0 dependencies: react: 18.2.0 react-dom: 18.2.0(react@18.2.0)