diff --git a/packages/flat-components/src/components/LoginPage/BindingPhonePanel/index.tsx b/packages/flat-components/src/components/LoginPage/BindingPhonePanel/index.tsx index 5fbbdb31bed..a16f034870b 100644 --- a/packages/flat-components/src/components/LoginPage/BindingPhonePanel/index.tsx +++ b/packages/flat-components/src/components/LoginPage/BindingPhonePanel/index.tsx @@ -1,7 +1,7 @@ import "./index.less"; -import { useTranslate } from "@netless/flat-i18n"; -import { Button, message, Form } from "antd"; +import { FlatI18nTFunction, useTranslate } from "@netless/flat-i18n"; +import { Button, message, Form, Modal } from "antd"; import React, { useCallback, useState } from "react"; import { useSafePromise } from "../../../utils/hooks"; @@ -10,11 +10,16 @@ import { LoginAccount, PasswordLoginType, defaultCountryCode } from "../LoginAcc import { LoginSendCode } from "../LoginSendCode"; import { codeValidator } from "../LoginWithCode/validators"; import { phoneValidator } from "../LoginWithPassword/validators"; +import { BindingPhoneSendCodeResult } from "@netless/flat-server-api"; export interface BindingPhonePanelProps { cancelBindingPhone: () => void; bindingPhone: (countryCode: string, phone: string, code: string) => Promise; - sendBindingPhoneCode: (countryCode: string, phone: string) => Promise; + sendBindingPhoneCode: ( + countryCode: string, + phone: string, + ) => Promise; + needRebindingPhone: () => void; } interface BindingFormValues { @@ -26,6 +31,7 @@ export const BindingPhonePanel: React.FC = ({ sendBindingPhoneCode, cancelBindingPhone, bindingPhone, + needRebindingPhone, }) => { const sp = useSafePromise(); const t = useTranslate(); @@ -57,7 +63,7 @@ export const BindingPhonePanel: React.FC = ({ } }, [bindingPhone, form, countryCode, isFormValidated, sp, t]); - const handleSendVerificationCode = async (): Promise => { + const sendVerificationCode = async (): Promise => { const { phone } = form.getFieldsValue(); return sendBindingPhoneCode(countryCode, phone); }; @@ -75,6 +81,12 @@ export const BindingPhonePanel: React.FC = ({ } }, [form]); + const handleSendVerificationCode = async (): Promise => { + if (await requestRebinding({ t })) { + needRebindingPhone(); + } + }; + return (
@@ -96,8 +108,9 @@ export const BindingPhonePanel: React.FC = ({ @@ -118,3 +131,17 @@ export const BindingPhonePanel: React.FC = ({
); }; + +export interface RequestRebindingParams { + t: FlatI18nTFunction; +} + +export function requestRebinding({ t }: RequestRebindingParams): Promise { + return new Promise(resolve => + Modal.confirm({ + content:
{t("rebinding-phone-tips")}
, + onOk: () => resolve(true), + onCancel: () => resolve(false), + }), + ); +} diff --git a/packages/flat-components/src/components/LoginPage/LoginSendCode/index.tsx b/packages/flat-components/src/components/LoginPage/LoginSendCode/index.tsx index cd81908571d..a9959863e64 100644 --- a/packages/flat-components/src/components/LoginPage/LoginSendCode/index.tsx +++ b/packages/flat-components/src/components/LoginPage/LoginSendCode/index.tsx @@ -7,17 +7,23 @@ import { useTranslate } from "@netless/flat-i18n"; import { useIsUnMounted, useSafePromise } from "../../../utils/hooks"; import { PasswordLoginType, isPhone } from "../LoginAccount"; import checkedSVG from "../icons/checked.svg"; +import { BindingPhoneSendCodeResult, RequestErrorCode } from "@netless/flat-server-api"; export interface LoginSendCodeProps { isAccountValidated: boolean; type: PasswordLoginType; - sendVerificationCode: () => Promise; + // BindingPhoneSendCodeResult for binding phone page + sendVerificationCode: () => Promise; + + // for rebinding phone + handleSendVerificationCode?: () => void; } export const LoginSendCode: React.FC = ({ type, isAccountValidated, sendVerificationCode, + handleSendVerificationCode, ...restProps }) => { const isUnMountRef = useIsUnMounted(); @@ -29,10 +35,11 @@ export const LoginSendCode: React.FC = ({ const sendCode = useCallback(async () => { if (isAccountValidated) { - setSendingCode(true); - const sent = await sp(sendVerificationCode()); - setSendingCode(false); - if (sent) { + try { + setSendingCode(true); + await sp(sendVerificationCode()); + + setSendingCode(false); void message.info( t(isPhone(type) ? "sent-verify-code-to-phone" : "sent-verify-code-to-email"), ); @@ -48,11 +55,33 @@ export const LoginSendCode: React.FC = ({ clearInterval(timer); } }, 1000); - } else { + } catch (error) { + if (!isUnMountRef.current) { + setSendingCode(false); + } + + // we say the phone is already binding when error message contains `RequestErrorCode.SMSAlreadyBinding` + // and then we can enter rebinding page to rebind. + if ( + error.message.indexOf(RequestErrorCode.SMSAlreadyBinding) > -1 && + handleSendVerificationCode + ) { + handleSendVerificationCode(); + return; + } + message.error(t("send-verify-code-failed")); } } - }, [isUnMountRef, isAccountValidated, type, sendVerificationCode, sp, t]); + }, [ + isAccountValidated, + sp, + sendVerificationCode, + t, + type, + isUnMountRef, + handleSendVerificationCode, + ]); return ( = props => { + return ; +}; + +Overview.args = { + cancelRebindingPhone: () => { + message.info("back to previous step"); + }, + rebindingPhone: (countryCode: string, phone: string, code: string) => { + message.info("merge phone with " + countryCode + " " + phone + " " + code); + return new Promise(resolve => setTimeout(() => resolve(false), 1000)); + }, + sendRebindingPhoneCode: (countryCode: string, phone: string) => { + message.info("send verification code with " + countryCode + " " + phone); + return new Promise(resolve => setTimeout(() => resolve(false), 1000)); + }, +}; diff --git a/packages/flat-components/src/components/LoginPage/RebindingPhonePanel/index.less b/packages/flat-components/src/components/LoginPage/RebindingPhonePanel/index.less new file mode 100644 index 00000000000..b9647788206 --- /dev/null +++ b/packages/flat-components/src/components/LoginPage/RebindingPhonePanel/index.less @@ -0,0 +1,9 @@ +.login-with-phone-rebinding { + padding-top: 184px; + padding-bottom: 32px; +} + +.login-countdown { + user-select: none; + -webkit-user-select: none; +} \ No newline at end of file diff --git a/packages/flat-components/src/components/LoginPage/RebindingPhonePanel/index.tsx b/packages/flat-components/src/components/LoginPage/RebindingPhonePanel/index.tsx new file mode 100644 index 00000000000..81f48de5553 --- /dev/null +++ b/packages/flat-components/src/components/LoginPage/RebindingPhonePanel/index.tsx @@ -0,0 +1,120 @@ +import "./index.less"; + +import { useTranslate } from "@netless/flat-i18n"; +import { Button, message, Form } from "antd"; +import React, { useCallback, useState } from "react"; + +import { useSafePromise } from "../../../utils/hooks"; +import { LoginTitle } from "../LoginTitle"; +import { LoginAccount, PasswordLoginType, defaultCountryCode } from "../LoginAccount"; +import { LoginSendCode } from "../LoginSendCode"; +import { codeValidator } from "../LoginWithCode/validators"; +import { phoneValidator } from "../LoginWithPassword/validators"; + +export interface RebindingPhonePanelProps { + cancelRebindingPhone: () => void; + rebindingPhone: (countryCode: string, phone: string, code: string) => Promise; + sendRebindingPhoneCode: (countryCode: string, phone: string) => Promise; +} + +interface RebindingFormValues { + phone: string; + code: string; +} + +export const RebindingPhonePanel: React.FC = ({ + sendRebindingPhoneCode, + rebindingPhone, + cancelRebindingPhone, +}) => { + const sp = useSafePromise(); + const t = useTranslate(); + + const [form] = Form.useForm(); + const [isFormValidated, setIsFormValidated] = useState(false); + const [isAccountValidated, setIsAccountValidated] = useState(false); + const type = PasswordLoginType.Phone; + + const defaultValues = { + phone: "", + code: "", + }; + + const [countryCode, setCountryCode] = useState(defaultCountryCode); + const [clickedRebinding, setClickedRebinding] = useState(false); + + const handleRebindingPhone = useCallback(async () => { + if (isFormValidated && rebindingPhone) { + setClickedRebinding(true); + const { phone, code } = form.getFieldsValue(); + const success = await sp(rebindingPhone(countryCode, phone, code)); + if (success) { + await sp(new Promise(resolve => setTimeout(resolve, 60000))); + } else { + message.error(t("rebinding-phone-failed")); + } + setClickedRebinding(false); + } + }, [isFormValidated, form, sp, countryCode, rebindingPhone, t]); + + const sendVerificationCode = async (): Promise => { + const { phone } = form.getFieldsValue(); + return sendRebindingPhoneCode(countryCode, phone); + }; + + const formValidateStatus = useCallback(() => { + setIsFormValidated( + form.getFieldsError().every(field => field.errors.length <= 0) && + Object.values(form.getFieldsValue()).every(v => !!v), + ); + + if (form.getFieldValue("phone") && !form.getFieldError("phone").length) { + setIsAccountValidated(true); + } else { + setIsAccountValidated(false); + } + }, [form]); + + return ( +
+
+ + +
+ + setCountryCode(code)} + placeholder={t("enter-phone")} + /> + + + + + +
+ + +
+
+ ); +}; diff --git a/packages/flat-components/src/components/LoginPage/index.tsx b/packages/flat-components/src/components/LoginPage/index.tsx index 6e0b75fcff8..ab5a7da6617 100644 --- a/packages/flat-components/src/components/LoginPage/index.tsx +++ b/packages/flat-components/src/components/LoginPage/index.tsx @@ -9,6 +9,7 @@ import { useLanguage } from "@netless/flat-i18n"; export * from "./LoginWithPassword"; export * from "./LoginWithCode"; export * from "./BindingPhonePanel"; +export * from "./RebindingPhonePanel"; export * from "./QRCodePanel"; export * from "./LoginAccount"; export * from "./LoginPassword"; diff --git a/packages/flat-i18n/locales/en.json b/packages/flat-i18n/locales/en.json index b0d6122214e..9659425759a 100644 --- a/packages/flat-i18n/locales/en.json +++ b/packages/flat-i18n/locales/en.json @@ -133,9 +133,12 @@ "login-using-other-methods": "Use other methods to log in", "login-phone-verification-code-invalid": "Invalid verification code", "login-phone-already-exist": "The phone number already exists", + "rebinding-phone-tips": "Input mobile phone number has been bound to other accounts, whether the current New Account and existing account merger? The merge operation will erase all data from the current New Account.", "phone-already-binding": "The phone is already bound", "bind-phone-failed": "Failed to bind phone", + "rebinding-phone-failed": "Failed to rebind phone", "bind-phone": "Bind phone", + "rebinding-phone": "Merge account", "need-bind-phone": "Need to bind phone", "online-interaction-to-synchronize-ideas": "Interact online to keep ideas in sync", "privacy-agreement": "Privacy Policy", diff --git a/packages/flat-i18n/locales/zh-CN.json b/packages/flat-i18n/locales/zh-CN.json index c7f39306463..89db14467b8 100644 --- a/packages/flat-i18n/locales/zh-CN.json +++ b/packages/flat-i18n/locales/zh-CN.json @@ -138,9 +138,12 @@ "login-using-other-methods": "使用其他方式登录", "login-phone-verification-code-invalid": "无效的验证码", "login-phone-already-exist": "该手机号已注册", + "rebinding-phone-tips": "输入手机号已绑定其他账号,是否将当前新建账号和已有账号合并?合并操作将会清除当前新建账号所有数据。", "phone-already-binding": "该手机号已绑定", "bind-phone-failed": "绑定手机号失败", + "rebinding-phone-failed": "账号合并失败", "bind-phone": "绑定手机号", + "rebinding-phone": "合并账号", "need-bind-phone": "根据相关政策法规,需要绑定手机号", "back": "返回", "remove-room": "移除房间", diff --git a/packages/flat-pages/src/LoginPage/index.tsx b/packages/flat-pages/src/LoginPage/index.tsx index 7496565f46d..bf3af35f72b 100644 --- a/packages/flat-pages/src/LoginPage/index.tsx +++ b/packages/flat-pages/src/LoginPage/index.tsx @@ -20,6 +20,7 @@ import { LoginKeyType, PasswordLoginType, LoginButtonProviderType, + RebindingPhonePanel, } from "flat-components"; import { bindingPhone, @@ -36,6 +37,8 @@ import { registerPhone, registerEmailSendCode, registerPhoneSendCode, + rebindingPhoneSendCode, + rebindingPhone, } from "@netless/flat-server-api"; import { globalStore } from "@netless/flat-stores"; @@ -130,8 +133,28 @@ export const LoginPage = observer(function LoginPage() { onLoginResult(null); setCurrentState("SWITCH_TO_PASSWORD"); }} + needRebindingPhone={() => setCurrentState("SWITCH_TO_REBINDING_PHONE")} sendBindingPhoneCode={async (countryCode, phone) => - wrap(bindingPhoneSendCode(countryCode + phone)) + bindingPhoneSendCode(countryCode + phone) + } + /> + ); + } + case "rebindingPhone": { + return ( + { + setCurrentState("SWITCH_TO_BINDING_PHONE"); + }} + rebindingPhone={async (countryCode, phone, code) => + wrap( + rebindingPhone(countryCode + phone, Number(code)).then( + onLoginResult, + ), + ) + } + sendRebindingPhoneCode={async (countryCode, phone) => + rebindingPhoneSendCode(countryCode + phone) } /> ); diff --git a/packages/flat-pages/src/LoginPage/utils/machine.ts b/packages/flat-pages/src/LoginPage/utils/machine.ts index da1056dba9b..8ddc63fbe5c 100644 --- a/packages/flat-pages/src/LoginPage/utils/machine.ts +++ b/packages/flat-pages/src/LoginPage/utils/machine.ts @@ -7,7 +7,8 @@ export type ToggleEventsType = | "SWITCH_TO_REGISTER" | "SWITCH_TO_QRCODE" | "SWITCH_TO_PASSWORD" - | "SWITCH_TO_BINDING_PHONE"; + | "SWITCH_TO_BINDING_PHONE" + | "SWITCH_TO_REBINDING_PHONE"; export const loginMachine = createMachine({ id: "login-page", @@ -44,6 +45,12 @@ export const loginMachine = createMachine { // update state to binding phone - // we can not update state to binding phone here if state have already been binding phone state + // we can not update state to binding phone here if state have already been binding phone state or rebinding phone state if ( globalStore.needPhoneBinding && (loginResult ? !loginResult.hasPhone : false) && - currentState.value !== "bindingPhone" + currentState.value !== "bindingPhone" && + currentState.value !== "rebindingPhone" ) { setCurrentState("SWITCH_TO_BINDING_PHONE"); } diff --git a/packages/flat-server-api/src/login.ts b/packages/flat-server-api/src/login.ts index bfd243470d0..02daa408d29 100644 --- a/packages/flat-server-api/src/login.ts +++ b/packages/flat-server-api/src/login.ts @@ -273,6 +273,27 @@ export async function resetEmailSendCode( ); } +export interface RebindingPhonePayload { + phone: string; // +8612345678901 + code: number; // 123456 +} + +export async function rebindingPhone(phone: string, code: number): Promise { + return await postV2("user/rebind-phone", { + phone, + code, + }); +} + +export async function rebindingPhoneSendCode(phone: string): Promise { + return await postV2( + "user/rebind-phone/send-message", + { + phone, + }, + ); +} + export type BindingPhoneSendCodeResult = {}; export async function bindingPhoneSendCode(phone: string): Promise {