fix(desktop): email/phone binding/unbinding ux improvments (#5717)

* fix(desktop): remove cftoken verif for email/phone binding

Signed-off-by: Nixieboluo <me@sagirii.me>

* fix(desktop): turnstile widget refresh causes unintended sms resends

Signed-off-by: Nixieboluo <me@sagirii.me>

* fix(desktop): captcha timer starts before code was sent

Signed-off-by: Nixieboluo <me@sagirii.me>

* feat(desktop): clear input field and error state when resending sms

Signed-off-by: Nixieboluo <me@sagirii.me>

* fix(desktop): page state not reset on account settings modal close

Signed-off-by: Nixieboluo <me@sagirii.me>

* fix(desktop): incorrect title on email/phone change/binding/unbinding modal

Signed-off-by: Nixieboluo <me@sagirii.me>

* chore(desktop): clean up filterCf

Signed-off-by: Nixieboluo <me@sagirii.me>

* chore(desktop): minor i18n phrase casing fix

Signed-off-by: Nixieboluo <me@sagirii.me>

---------

Signed-off-by: Nixieboluo <me@sagirii.me>
This commit is contained in:
Nixieboluo
2025-07-23 18:14:22 +08:00
committed by GitHub
parent f6fc0c2e53
commit b89be4e652
14 changed files with 143 additions and 75 deletions
+2 -2
View File
@@ -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"
}
}
}
@@ -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",
@@ -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",
@@ -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": "未读",
+2 -1
View File
@@ -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": "当前套餐"
}
}
@@ -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={<SettingIcon boxSize={'16px'} fill={'rgba(255, 255, 255, 0.7)'} />}
/>
)}
<Modal isOpen={isOpen} onClose={onClose} isCentered>
<Modal
isOpen={isOpen}
onClose={() => {
resetPageState();
onClose();
}}
isCentered
>
<ModalOverlay />
<ModalContent
borderRadius={'12px'}
@@ -37,6 +37,7 @@ export default function EmailCheckComponent() {
const { signupData, clearSignupData, startTime, updateStartTime, setStartTime } =
useSignupStore();
const { setToken } = useSessionStore();
const [pinValue, setPinValue] = useState('');
useEffect(() => {
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 && (
<Turnstile
options={{
size: 'normal'
size: 'normal',
refreshExpired: 'never',
refreshTimeout: 'never'
}}
ref={turnstileRef}
siteKey={authConfig?.turnstile.cloudflare.siteKey}
@@ -202,6 +211,8 @@ export default function EmailCheckComponent() {
placeholder=""
focusBorderColor="#18181B"
autoFocus
value={pinValue}
onChange={setPinValue}
isDisabled={verifyMutation.isLoading}
onComplete={(value) => {
console.log('Verification code:', value);
@@ -221,8 +232,28 @@ export default function EmailCheckComponent() {
</PinInput>
</FormControl>
{isLoading && (
<Text
style={{
fontWeight: 400,
fontSize: '14px',
lineHeight: '20px'
}}
>
{t('v2:sending_code')}
</Text>
)}
{verifyMutation.isLoading ? (
<Text>{t('v2:verifying')}</Text>
<Text
style={{
fontWeight: 400,
fontSize: '14px',
lineHeight: '20px'
}}
>
{t('v2:verifying')}
</Text>
) : (
<Flex>
{verifyMutation.isError && (
@@ -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() {
</PinInput>
</FormControl>
{sendCodeMutation.isLoading && (
<Text
style={{
fontWeight: 400,
fontSize: '14px',
lineHeight: '20px'
}}
>
{t('v2:sending_code')}
</Text>
)}
{verifyMutation.isLoading ? (
<Text>{t('v2:verifying')}</Text>
<Text
style={{
fontWeight: 400,
fontSize: '14px',
lineHeight: '20px'
}}
>
{t('v2:verifying')}
</Text>
) : (
<Flex>
{verifyMutation.isError && (
@@ -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)
)
);
});
)
);
});
@@ -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))
)
)
);
@@ -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))
)
);
});
});
@@ -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))
)
);
// });
});
});
@@ -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);
})
)
);
});
});
});
+6 -6
View File
@@ -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)