From e6aead2fb0f4ecc1a4f683e51a06a1f6def4240c Mon Sep 17 00:00:00 2001 From: simeng-li Date: Sun, 6 Nov 2022 20:55:38 +0800 Subject: [PATCH] refactor(ui): refactor passcodevalidation refactor passcode validation --- packages/phrases-ui/src/locales/en.ts | 3 +- packages/phrases-ui/src/locales/fr.ts | 3 +- packages/phrases-ui/src/locales/ko.ts | 3 +- packages/phrases-ui/src/locales/pt-pt.ts | 3 +- packages/phrases-ui/src/locales/tr-tr.ts | 3 +- packages/phrases-ui/src/locales/zh-cn.ts | 3 +- .../PasscodeValidation/index.test.tsx | 188 +++++++++++++++--- .../containers/PasscodeValidation/index.tsx | 97 ++------- ...rgot-password-email-passcode-validation.ts | 54 +++++ ...forgot-password-sms-passcode-validation.ts | 53 +++++ ...orgot-password-with-email-error-handler.ts | 37 ---- ...-forgot-password-with-sms-error-handler.ts | 38 ---- .../use-identifier-error-alert.ts | 35 ++++ .../use-passcode-validation-error-handler.ts | 43 ---- ...register-with-email-passcode-validation.ts | 95 +++++++++ .../use-register-with-sms-error-handler.ts | 54 ----- ...e-register-with-sms-passcode-validation.ts | 95 +++++++++ .../PasscodeValidation/use-resend-passcode.ts | 50 +++++ .../use-shared-error-handler.ts | 26 +++ .../use-sign-in-with-email-error-handler.ts | 64 ------ ...-sign-in-with-email-passcode-validation.ts | 100 ++++++++++ .../use-sign-in-with-sms-error-handler.ts | 65 ------ ...se-sign-in-with-sms-passcode-validation.ts | 100 ++++++++++ .../user-register-with-email-error-handler.ts | 53 ----- .../containers/PasscodeValidation/utils.ts | 35 ++++ packages/ui/src/types/index.ts | 1 + 26 files changed, 829 insertions(+), 472 deletions(-) create mode 100644 packages/ui/src/containers/PasscodeValidation/use-forgot-password-email-passcode-validation.ts create mode 100644 packages/ui/src/containers/PasscodeValidation/use-forgot-password-sms-passcode-validation.ts delete mode 100644 packages/ui/src/containers/PasscodeValidation/use-forgot-password-with-email-error-handler.ts delete mode 100644 packages/ui/src/containers/PasscodeValidation/use-forgot-password-with-sms-error-handler.ts create mode 100644 packages/ui/src/containers/PasscodeValidation/use-identifier-error-alert.ts delete mode 100644 packages/ui/src/containers/PasscodeValidation/use-passcode-validation-error-handler.ts create mode 100644 packages/ui/src/containers/PasscodeValidation/use-register-with-email-passcode-validation.ts delete mode 100644 packages/ui/src/containers/PasscodeValidation/use-register-with-sms-error-handler.ts create mode 100644 packages/ui/src/containers/PasscodeValidation/use-register-with-sms-passcode-validation.ts create mode 100644 packages/ui/src/containers/PasscodeValidation/use-resend-passcode.ts create mode 100644 packages/ui/src/containers/PasscodeValidation/use-shared-error-handler.ts delete mode 100644 packages/ui/src/containers/PasscodeValidation/use-sign-in-with-email-error-handler.ts create mode 100644 packages/ui/src/containers/PasscodeValidation/use-sign-in-with-email-passcode-validation.ts delete mode 100644 packages/ui/src/containers/PasscodeValidation/use-sign-in-with-sms-error-handler.ts create mode 100644 packages/ui/src/containers/PasscodeValidation/use-sign-in-with-sms-passcode-validation.ts delete mode 100644 packages/ui/src/containers/PasscodeValidation/user-register-with-email-error-handler.ts create mode 100644 packages/ui/src/containers/PasscodeValidation/utils.ts diff --git a/packages/phrases-ui/src/locales/en.ts b/packages/phrases-ui/src/locales/en.ts index a402d30bf..898ca024a 100644 --- a/packages/phrases-ui/src/locales/en.ts +++ b/packages/phrases-ui/src/locales/en.ts @@ -50,7 +50,8 @@ const translation = { 'The account with {{type}} {{value}} already exists, would you like to sign in?', sign_in_id_does_not_exists: 'The account with {{type}} {{value}} does not exist, would you like to create a new account?', - forgot_password_id_does_not_exits: 'The account with {{type}} {{value}} does not exist.', + sign_in_id_does_not_exists_alert: 'The account with {{type}} {{value}} does not exist.', + create_account_id_exists_alert: 'The account with {{type}} {{value}} already exists', bind_account_title: 'Link account', social_create_account: 'No account? You can create a new account and link.', social_bind_account: 'Already have an account? Sign in to link it with your social identity.', diff --git a/packages/phrases-ui/src/locales/fr.ts b/packages/phrases-ui/src/locales/fr.ts index 80636aa7a..1d7ce5e81 100644 --- a/packages/phrases-ui/src/locales/fr.ts +++ b/packages/phrases-ui/src/locales/fr.ts @@ -52,7 +52,8 @@ const translation = { 'Le compte avec {{type}} {{value}} existe déjà, voulez-vous vous connecter ?', sign_in_id_does_not_exists: "Le compte avec {{type}} {{value}} n'existe pas, voulez-vous créer un nouveau compte ?", - forgot_password_id_does_not_exits: 'The account with {{type}} {{value}} does not exist.', // UNTRANSLATED + sign_in_id_does_not_exists_alert: 'The account with {{type}} {{value}} does not exist.', // UNTRANSLATED + create_account_id_exists_alert: 'The account with {{type}} {{value}} already exists', // UNTRANSLATED bind_account_title: 'Lier le compte', social_create_account: 'Pas de compte ? Vous pouvez créer un nouveau compte et un lien.', social_bind_account: diff --git a/packages/phrases-ui/src/locales/ko.ts b/packages/phrases-ui/src/locales/ko.ts index 7abd09af0..9553d7e5d 100644 --- a/packages/phrases-ui/src/locales/ko.ts +++ b/packages/phrases-ui/src/locales/ko.ts @@ -50,7 +50,8 @@ const translation = { continue_with: '계속하기', create_account_id_exists: '{{type}} {{value}} 계정이 이미 존재해요. 로그인하시겠어요?', sign_in_id_does_not_exists: '{type}} {{value}} 계정이 존재하지 않아요. 새로 만드시겠어요?', - forgot_password_id_does_not_exits: 'The account with {{type}} {{value}} does not exist.', // UNTRANSLATED + sign_in_id_does_not_exists_alert: 'The account with {{type}} {{value}} does not exist.', // UNTRANSLATED + create_account_id_exists_alert: 'The account with {{type}} {{value}} already exists', // UNTRANSLATED bind_account_title: '계정 연동', social_create_account: '계정이 없으신가요? 새로운 계정을 만들고 연동해보세요.', social_bind_account: '계정이 이미 있으신가요? 로그인하여 다른 계정과 연동해보세요.', diff --git a/packages/phrases-ui/src/locales/pt-pt.ts b/packages/phrases-ui/src/locales/pt-pt.ts index 7fc3ea200..3be06c774 100644 --- a/packages/phrases-ui/src/locales/pt-pt.ts +++ b/packages/phrases-ui/src/locales/pt-pt.ts @@ -50,7 +50,8 @@ const translation = { continue_with: 'Continuar com', create_account_id_exists: 'A conta com {{type}} {{value}} já existe, gostaria de fazer login?', sign_in_id_does_not_exists: 'A conta com {{type}} {{value}} não existe, gostaria de criar uma?', - forgot_password_id_does_not_exits: 'The account with {{type}} {{value}} does not exist.', // UNTRANSLATED + sign_in_id_does_not_exists_alert: 'The account with {{type}} {{value}} does not exist.', // UNTRANSLATED + create_account_id_exists_alert: 'The account with {{type}} {{value}} already exists', // UNTRANSLATED bind_account_title: 'Agregar conta', social_create_account: 'Sem conta? Pode criar uma nova e agregar.', social_bind_account: 'Já tem uma conta? Faça login para agregar a sua identidade social.', diff --git a/packages/phrases-ui/src/locales/tr-tr.ts b/packages/phrases-ui/src/locales/tr-tr.ts index b0ea4efd8..081e2d62a 100644 --- a/packages/phrases-ui/src/locales/tr-tr.ts +++ b/packages/phrases-ui/src/locales/tr-tr.ts @@ -51,7 +51,8 @@ const translation = { create_account_id_exists: '{{type}} {{value}} ile hesap mevcut, giriş yapmak ister misiniz?', sign_in_id_does_not_exists: '{{type}} {{value}} ile hesap mevcut değil, yeni bir hesap oluşturmak ister misiniz?', - forgot_password_id_does_not_exits: 'The account with {{type}} {{value}} does not exist.', // UNTRANSLATED + sign_in_id_does_not_exists_alert: 'The account with {{type}} {{value}} does not exist.', // UNTRANSLATED + create_account_id_exists_alert: 'The account with {{type}} {{value}} already exists', // UNTRANSLATED bind_account_title: 'Hesap bağla', social_create_account: 'Hesabınız yok mu? Yeni bir hesap ve bağlantı oluşturabilirsiniz.', social_bind_account: 'Hesabınız zaten var mı? Hesabınıza bağlanmak için giriş yapınız.', diff --git a/packages/phrases-ui/src/locales/zh-cn.ts b/packages/phrases-ui/src/locales/zh-cn.ts index 1e2bd6870..7877cab8b 100644 --- a/packages/phrases-ui/src/locales/zh-cn.ts +++ b/packages/phrases-ui/src/locales/zh-cn.ts @@ -50,7 +50,8 @@ const translation = { continue_with: '通过以下方式继续', create_account_id_exists: '{{ type }}为 {{ value }} 的帐号已存在,你要登录吗?', sign_in_id_does_not_exists: '{{ type }}为 {{ value }} 的帐号不存在,你要创建一个新帐号吗?', - forgot_password_id_does_not_exits: '{{ type }}为 {{ value }} 的帐号不存在。', + sign_in_id_does_not_exists_alert: '{{ type }}为 {{ value }} 的帐号不存在。', + create_account_id_exists_alert: '{{ type }}为 {{ value }} 的帐号已存在', bind_account_title: '绑定帐号', social_create_account: '没有帐号?你可以创建一个帐号并绑定。', social_bind_account: '已有帐号?登录以绑定社交身份。', diff --git a/packages/ui/src/containers/PasscodeValidation/index.test.tsx b/packages/ui/src/containers/PasscodeValidation/index.test.tsx index 219212d4a..35978c4ac 100644 --- a/packages/ui/src/containers/PasscodeValidation/index.test.tsx +++ b/packages/ui/src/containers/PasscodeValidation/index.test.tsx @@ -2,6 +2,12 @@ import { SignInIdentifier } from '@logto/schemas'; import { act, fireEvent, waitFor } from '@testing-library/react'; import renderWithPageContext from '@/__mocks__/RenderWithPageContext'; +import { + verifyForgotPasswordEmailPasscode, + verifyForgotPasswordSmsPasscode, +} from '@/apis/forgot-password'; +import { verifyRegisterEmailPasscode, verifyRegisterSmsPasscode } from '@/apis/register'; +import { verifySignInEmailPasscode, verifySignInSmsPasscode } from '@/apis/sign-in'; import { UserFlow } from '@/types'; import PasscodeValidation from '.'; @@ -9,7 +15,6 @@ import PasscodeValidation from '.'; jest.useFakeTimers(); const sendPasscodeApi = jest.fn(); -const verifyPasscodeApi = jest.fn(); const mockedNavigate = jest.fn(); @@ -20,11 +25,26 @@ jest.mock('react-router-dom', () => ({ jest.mock('@/apis/utils', () => ({ getSendPasscodeApi: () => sendPasscodeApi, - getVerifyPasscodeApi: () => verifyPasscodeApi, +})); + +jest.mock('@/apis/sign-in', () => ({ + verifySignInEmailPasscode: jest.fn(), + verifySignInSmsPasscode: jest.fn(), +})); + +jest.mock('@/apis/register', () => ({ + verifyRegisterEmailPasscode: jest.fn(), + verifyRegisterSmsPasscode: jest.fn(), +})); + +jest.mock('@/apis/forgot-password', () => ({ + verifyForgotPasswordEmailPasscode: jest.fn(), + verifyForgotPasswordSmsPasscode: jest.fn(), })); describe('', () => { const email = 'foo@logto.io'; + const phone = '18573333333'; const originalLocation = window.location; beforeAll(() => { @@ -75,68 +95,170 @@ describe('', () => { expect(sendPasscodeApi).toBeCalledWith(email); }); - it('fire validate passcode event', async () => { - const { container } = renderWithPageContext( - - ); - const inputs = container.querySelectorAll('input'); + describe('sign-in', () => { + it('fire email sign-in validate passcode event', async () => { + (verifySignInEmailPasscode as jest.Mock).mockImplementationOnce(() => ({ + redirectTo: 'foo.com', + })); + + const { container } = renderWithPageContext( + + ); + const inputs = container.querySelectorAll('input'); - await waitFor(() => { for (const input of inputs) { act(() => { fireEvent.input(input, { target: { value: '1' } }); }); } - expect(verifyPasscodeApi).toBeCalledWith(email, '111111', undefined); + await waitFor(() => { + expect(verifySignInEmailPasscode).toBeCalledWith(email, '111111', undefined); + }); + + await waitFor(() => { + expect(window.location.replace).toBeCalledWith('foo.com'); + }); }); - }); - it('should redirect with success redirectUri response', async () => { - verifyPasscodeApi.mockImplementationOnce(() => ({ redirectTo: 'foo.com' })); + it('fire sms sign-in validate passcode event', async () => { + (verifySignInSmsPasscode as jest.Mock).mockImplementationOnce(() => ({ + redirectTo: 'foo.com', + })); - const { container } = renderWithPageContext( - - ); + const { container } = renderWithPageContext( + + ); + const inputs = container.querySelectorAll('input'); - const inputs = container.querySelectorAll('input'); - - await waitFor(() => { for (const input of inputs) { act(() => { fireEvent.input(input, { target: { value: '1' } }); }); } - expect(verifyPasscodeApi).toBeCalledWith(email, '111111', undefined); - }); + await waitFor(() => { + expect(verifySignInSmsPasscode).toBeCalledWith(phone, '111111', undefined); + }); - await waitFor(() => { - expect(window.location.replace).toBeCalledWith('foo.com'); + await waitFor(() => { + expect(window.location.replace).toBeCalledWith('foo.com'); + }); }); }); - it('should redirect to reset password page if the flow is forgot-password', async () => { - verifyPasscodeApi.mockImplementationOnce(() => ({ success: true })); + describe('register', () => { + it('fire email register validate passcode event', async () => { + (verifyRegisterEmailPasscode as jest.Mock).mockImplementationOnce(() => ({ + redirectTo: 'foo.com', + })); + + const { container } = renderWithPageContext( + + ); + const inputs = container.querySelectorAll('input'); + + for (const input of inputs) { + act(() => { + fireEvent.input(input, { target: { value: '1' } }); + }); + } + + await waitFor(() => { + expect(verifyRegisterEmailPasscode).toBeCalledWith(email, '111111'); + }); + + await waitFor(() => { + expect(window.location.replace).toBeCalledWith('foo.com'); + }); + }); + + it('fire sms register validate passcode event', async () => { + (verifyRegisterSmsPasscode as jest.Mock).mockImplementationOnce(() => ({ + redirectTo: 'foo.com', + })); + + const { container } = renderWithPageContext( + + ); + const inputs = container.querySelectorAll('input'); + + for (const input of inputs) { + act(() => { + fireEvent.input(input, { target: { value: '1' } }); + }); + } + + await waitFor(() => { + expect(verifyRegisterSmsPasscode).toBeCalledWith(phone, '111111'); + }); + + await waitFor(() => { + expect(window.location.replace).toBeCalledWith('foo.com'); + }); + }); + }); + + describe('forgot password', () => { + it('fire email forgot-password validate passcode event', async () => { + (verifyForgotPasswordEmailPasscode as jest.Mock).mockImplementationOnce(() => ({ + success: true, + })); + + const { container } = renderWithPageContext( + + ); + + const inputs = container.querySelectorAll('input'); + + for (const input of inputs) { + act(() => { + fireEvent.input(input, { target: { value: '1' } }); + }); + } + + await waitFor(() => { + expect(verifyForgotPasswordEmailPasscode).toBeCalledWith(email, '111111'); + }); + + await waitFor(() => { + expect(window.location.replace).not.toBeCalled(); + expect(mockedNavigate).toBeCalledWith('/forgot-password/reset', { replace: true }); + }); + }); + }); + + it('fire Sms forgot-password validate passcode event', async () => { + (verifyForgotPasswordSmsPasscode as jest.Mock).mockImplementationOnce(() => ({ + success: true, + })); const { container } = renderWithPageContext( ); const inputs = container.querySelectorAll('input'); - await waitFor(() => { - for (const input of inputs) { - act(() => { - fireEvent.input(input, { target: { value: '1' } }); - }); - } + for (const input of inputs) { + act(() => { + fireEvent.input(input, { target: { value: '1' } }); + }); + } - expect(verifyPasscodeApi).toBeCalledWith(email, '111111', undefined); + await waitFor(() => { + expect(verifyForgotPasswordSmsPasscode).toBeCalledWith(phone, '111111'); }); await waitFor(() => { diff --git a/packages/ui/src/containers/PasscodeValidation/index.tsx b/packages/ui/src/containers/PasscodeValidation/index.tsx index 307f13647..8a859132b 100644 --- a/packages/ui/src/containers/PasscodeValidation/index.tsx +++ b/packages/ui/src/containers/PasscodeValidation/index.tsx @@ -1,22 +1,15 @@ import type { SignInIdentifier } from '@logto/schemas'; import classNames from 'classnames'; -import { useState, useEffect, useContext, useCallback, useMemo } from 'react'; +import { useState, useEffect } from 'react'; import { useTranslation, Trans } from 'react-i18next'; -import { useNavigate } from 'react-router-dom'; -import { useTimer } from 'react-timer-hook'; -import { getSendPasscodeApi, getVerifyPasscodeApi } from '@/apis/utils'; import Passcode, { defaultLength } from '@/components/Passcode'; import TextLink from '@/components/TextLink'; -import type { ErrorHandlers } from '@/hooks/use-api'; -import useApi from '@/hooks/use-api'; -import { PageContext } from '@/hooks/use-page-context'; import type { UserFlow } from '@/types'; -import { SearchParameters } from '@/types'; -import { getSearchParameters } from '@/utils'; import * as styles from './index.module.scss'; -import usePasscodeValidationErrorHandler from './use-passcode-validation-error-handler'; +import useResendPasscode from './use-resend-passcode'; +import { getPasscodeValidationHook } from './utils'; type Props = { type: UserFlow; @@ -25,90 +18,30 @@ type Props = { className?: string; }; -export const timeRange = 59; - -const getTimeout = () => { - const now = new Date(); - now.setSeconds(now.getSeconds() + timeRange); - - return now; -}; - const PasscodeValidation = ({ type, method, className, target }: Props) => { const [code, setCode] = useState([]); - const [error, setError] = useState(); - const { setToast } = useContext(PageContext); const { t } = useTranslation(); - const navigate = useNavigate(); + const usePasscodeValidation = getPasscodeValidationHook(type, method); - const { seconds, isRunning, restart } = useTimer({ - autoStart: true, - expiryTimestamp: getTimeout(), + const { errorMessage, clearErrorMessage, onSubmit } = usePasscodeValidation(target, () => { + setCode([]); }); - // Get the flow specific error handler hook - const { errorHandler } = usePasscodeValidationErrorHandler(type, method, target); - - const verifyPasscodeErrorHandlers: ErrorHandlers = useMemo( - () => ({ - ...errorHandler, - 'passcode.expired': (error) => { - setError(error.message); - }, - 'passcode.code_mismatch': (error) => { - setError(error.message); - }, - callback: () => { - setCode([]); - }, - }), - [errorHandler] - ); - - const { result: verifyPasscodeResult, run: verifyPassCode } = useApi( - getVerifyPasscodeApi(type, method), - verifyPasscodeErrorHandlers - ); - - const { run: sendPassCode } = useApi(getSendPasscodeApi(type, method)); - - const resendPasscodeHandler = useCallback(async () => { - setError(undefined); - - const result = await sendPassCode(target); - - if (result) { - setToast(t('description.passcode_sent')); - restart(getTimeout(), true); - } - }, [restart, sendPassCode, setToast, t, target]); + const { seconds, isRunning, onResendPasscode } = useResendPasscode(type, method, target); useEffect(() => { if (code.length === defaultLength && code.every(Boolean)) { - const socialToBind = getSearchParameters(location.search, SearchParameters.bindWithSocial); - void verifyPassCode(target, code.join(''), socialToBind); + void onSubmit(code.join('')); } - }, [code, target, verifyPassCode]); - - useEffect(() => { - if (verifyPasscodeResult?.redirectTo) { - window.location.replace(verifyPasscodeResult.redirectTo); - - return; - } - - if (verifyPasscodeResult && type === 'forgot-password') { - navigate('/forgot-password/reset', { replace: true }); - } - }, [navigate, type, verifyPasscodeResult]); + }, [code, onSubmit, target]); return (
{isRunning ? ( @@ -118,7 +51,13 @@ const PasscodeValidation = ({ type, method, className, target }: Props) => { ) : ( - + { + clearErrorMessage(); + void onResendPasscode(); + }} + /> )} ); diff --git a/packages/ui/src/containers/PasscodeValidation/use-forgot-password-email-passcode-validation.ts b/packages/ui/src/containers/PasscodeValidation/use-forgot-password-email-passcode-validation.ts new file mode 100644 index 000000000..c66fb6c08 --- /dev/null +++ b/packages/ui/src/containers/PasscodeValidation/use-forgot-password-email-passcode-validation.ts @@ -0,0 +1,54 @@ +import { SignInIdentifier } from '@logto/schemas'; +import { useMemo, useEffect, useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { verifyForgotPasswordEmailPasscode } from '@/apis/forgot-password'; +import type { ErrorHandlers } from '@/hooks/use-api'; +import useApi from '@/hooks/use-api'; +import { UserFlow } from '@/types'; + +import useIdentifierErrorAlert from './use-identifier-error-alert'; +import useSharedErrorHandler from './use-shared-error-handler'; + +const useForgotPasswordEmailPasscodeValidation = (email: string, errorCallback?: () => void) => { + const navigate = useNavigate(); + const { sharedErrorHandlers, errorMessage, clearErrorMessage } = useSharedErrorHandler(); + + const identifierNotExistErrorHandler = useIdentifierErrorAlert( + UserFlow.forgotPassword, + SignInIdentifier.Email, + email + ); + + const errorHandlers: ErrorHandlers = useMemo( + () => ({ + 'user.email_not_exists': identifierNotExistErrorHandler, + ...sharedErrorHandlers, + callback: errorCallback, + }), + [identifierNotExistErrorHandler, sharedErrorHandlers, errorCallback] + ); + + const { result, run: verifyPasscode } = useApi(verifyForgotPasswordEmailPasscode, errorHandlers); + + const onSubmit = useCallback( + async (code: string) => { + return verifyPasscode(email, code); + }, + [email, verifyPasscode] + ); + + useEffect(() => { + if (result) { + navigate(`/${UserFlow.forgotPassword}/reset`, { replace: true }); + } + }, [navigate, result]); + + return { + errorMessage, + clearErrorMessage, + onSubmit, + }; +}; + +export default useForgotPasswordEmailPasscodeValidation; diff --git a/packages/ui/src/containers/PasscodeValidation/use-forgot-password-sms-passcode-validation.ts b/packages/ui/src/containers/PasscodeValidation/use-forgot-password-sms-passcode-validation.ts new file mode 100644 index 000000000..685f67a97 --- /dev/null +++ b/packages/ui/src/containers/PasscodeValidation/use-forgot-password-sms-passcode-validation.ts @@ -0,0 +1,53 @@ +import { SignInIdentifier } from '@logto/schemas'; +import { useMemo, useEffect, useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { verifyForgotPasswordSmsPasscode } from '@/apis/forgot-password'; +import type { ErrorHandlers } from '@/hooks/use-api'; +import useApi from '@/hooks/use-api'; +import { UserFlow } from '@/types'; + +import useIdentifierErrorAlert from './use-identifier-error-alert'; +import useSharedErrorHandler from './use-shared-error-handler'; + +const useForgotPasswordSmsPasscodeValidation = (phone: string, errorCallback?: () => void) => { + const navigate = useNavigate(); + const { sharedErrorHandlers, errorMessage, clearErrorMessage } = useSharedErrorHandler(); + const identifierNotExistErrorHandler = useIdentifierErrorAlert( + UserFlow.forgotPassword, + SignInIdentifier.Sms, + phone + ); + + const errorHandlers: ErrorHandlers = useMemo( + () => ({ + 'user.phone_not_exists': identifierNotExistErrorHandler, + ...sharedErrorHandlers, + callback: errorCallback, + }), + [sharedErrorHandlers, errorCallback, identifierNotExistErrorHandler] + ); + + const { result, run: verifyPasscode } = useApi(verifyForgotPasswordSmsPasscode, errorHandlers); + + const onSubmit = useCallback( + async (code: string) => { + return verifyPasscode(phone, code); + }, + [phone, verifyPasscode] + ); + + useEffect(() => { + if (result) { + navigate(`/${UserFlow.forgotPassword}/reset`, { replace: true }); + } + }, [navigate, result]); + + return { + errorMessage, + clearErrorMessage, + onSubmit, + }; +}; + +export default useForgotPasswordSmsPasscodeValidation; diff --git a/packages/ui/src/containers/PasscodeValidation/use-forgot-password-with-email-error-handler.ts b/packages/ui/src/containers/PasscodeValidation/use-forgot-password-with-email-error-handler.ts deleted file mode 100644 index 1e5f3a19f..000000000 --- a/packages/ui/src/containers/PasscodeValidation/use-forgot-password-with-email-error-handler.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useNavigate } from 'react-router-dom'; - -import type { ErrorHandlers } from '@/hooks/use-api'; -import { useConfirmModal } from '@/hooks/use-confirm-modal'; - -const useForgotPasswordWithEmailErrorHandler = (email: string) => { - const { t } = useTranslation(); - const { show } = useConfirmModal(); - const navigate = useNavigate(); - - const emailNotExistForgotPasswordHandler = useCallback(async () => { - await show({ - type: 'alert', - ModalContent: t('description.forgot_password_id_does_not_exits', { - type: t(`description.email`), - value: email, - }), - cancelText: 'action.got_it', - }); - navigate(-1); - }, [navigate, show, t, email]); - - const errorHandler = useMemo( - () => ({ - 'user.email_not_exists': emailNotExistForgotPasswordHandler, - }), - [emailNotExistForgotPasswordHandler] - ); - - return { - errorHandler, - }; -}; - -export default useForgotPasswordWithEmailErrorHandler; diff --git a/packages/ui/src/containers/PasscodeValidation/use-forgot-password-with-sms-error-handler.ts b/packages/ui/src/containers/PasscodeValidation/use-forgot-password-with-sms-error-handler.ts deleted file mode 100644 index cf4b28c79..000000000 --- a/packages/ui/src/containers/PasscodeValidation/use-forgot-password-with-sms-error-handler.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useNavigate } from 'react-router-dom'; - -import type { ErrorHandlers } from '@/hooks/use-api'; -import { useConfirmModal } from '@/hooks/use-confirm-modal'; -import { formatPhoneNumberWithCountryCallingCode } from '@/utils/country-code'; - -const useForgotPasswordWithSmsErrorHandler = (phone: string) => { - const { t } = useTranslation(); - const { show } = useConfirmModal(); - const navigate = useNavigate(); - - const phoneNotExistForgotPasswordHandler = useCallback(async () => { - await show({ - type: 'alert', - ModalContent: t('description.forgot_password_id_does_not_exits', { - type: t(`description.phone_number`), - value: formatPhoneNumberWithCountryCallingCode(phone), - }), - cancelText: 'action.got_it', - }); - navigate(-1); - }, [navigate, show, t, phone]); - - const errorHandler = useMemo( - () => ({ - 'user.phone_not_exists': phoneNotExistForgotPasswordHandler, - }), - [phoneNotExistForgotPasswordHandler] - ); - - return { - errorHandler, - }; -}; - -export default useForgotPasswordWithSmsErrorHandler; diff --git a/packages/ui/src/containers/PasscodeValidation/use-identifier-error-alert.ts b/packages/ui/src/containers/PasscodeValidation/use-identifier-error-alert.ts new file mode 100644 index 000000000..4940dd45d --- /dev/null +++ b/packages/ui/src/containers/PasscodeValidation/use-identifier-error-alert.ts @@ -0,0 +1,35 @@ +import { SignInIdentifier } from '@logto/schemas'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; + +import { useConfirmModal } from '@/hooks/use-confirm-modal'; +import { UserFlow } from '@/types'; + +const useIdentifierErrorAlert = ( + flow: UserFlow, + method: SignInIdentifier.Email | SignInIdentifier.Sms, + value: string +) => { + const { show } = useConfirmModal(); + const navigate = useNavigate(); + const { t } = useTranslation(); + + return async () => { + await show({ + type: 'alert', + ModalContent: t( + flow === UserFlow.register + ? 'description.create_account_id_exists_alert' + : 'description.sign_in_id_does_not_exists_alert', + { + type: t(`description.${method === SignInIdentifier.Email ? 'email' : 'phone_number'}`), + value, + } + ), + cancelText: 'action.got_it', + }); + navigate(-1); + }; +}; + +export default useIdentifierErrorAlert; diff --git a/packages/ui/src/containers/PasscodeValidation/use-passcode-validation-error-handler.ts b/packages/ui/src/containers/PasscodeValidation/use-passcode-validation-error-handler.ts deleted file mode 100644 index 9bb5dda9e..000000000 --- a/packages/ui/src/containers/PasscodeValidation/use-passcode-validation-error-handler.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { UserFlow } from '@/types'; - -import useForgotPasswordWithEmailErrorHandler from './use-forgot-password-with-email-error-handler'; -import useForgotPasswordWithSmsErrorHandler from './use-forgot-password-with-sms-error-handler'; -import useRegisterWithSmsErrorHandler from './use-register-with-sms-error-handler'; -import useSignInWithEmailErrorHandler from './use-sign-in-with-email-error-handler'; -import useSignInWithSmsErrorHandler from './use-sign-in-with-sms-error-handler'; -import useRegisterWithEmailErrorHandler from './user-register-with-email-error-handler'; - -type Method = 'email' | 'sms'; - -const getPasscodeValidationErrorHandlersByFlowAndMethod = (flow: UserFlow, method: Method) => { - if (flow === UserFlow.signIn && method === 'email') { - return useSignInWithEmailErrorHandler; - } - - if (flow === UserFlow.signIn && method === 'sms') { - return useSignInWithSmsErrorHandler; - } - - if (flow === UserFlow.register && method === 'email') { - return useRegisterWithEmailErrorHandler; - } - - if (flow === UserFlow.register && method === 'sms') { - return useRegisterWithSmsErrorHandler; - } - - if (flow === UserFlow.forgotPassword && method === 'email') { - return useForgotPasswordWithEmailErrorHandler; - } - - return useForgotPasswordWithSmsErrorHandler; -}; - -const usePasscodeValidationErrorHandler = (type: UserFlow, method: Method, target: string) => { - const useFlowErrorHandler = getPasscodeValidationErrorHandlersByFlowAndMethod(type, method); - const { errorHandler } = useFlowErrorHandler(target); - - return { errorHandler }; -}; - -export default usePasscodeValidationErrorHandler; diff --git a/packages/ui/src/containers/PasscodeValidation/use-register-with-email-passcode-validation.ts b/packages/ui/src/containers/PasscodeValidation/use-register-with-email-passcode-validation.ts new file mode 100644 index 000000000..17e35ced0 --- /dev/null +++ b/packages/ui/src/containers/PasscodeValidation/use-register-with-email-passcode-validation.ts @@ -0,0 +1,95 @@ +import { SignInIdentifier, SignInMode } from '@logto/schemas'; +import { useMemo, useCallback, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; + +import { verifyRegisterEmailPasscode } from '@/apis/register'; +import { signInWithEmail } from '@/apis/sign-in'; +import type { ErrorHandlers } from '@/hooks/use-api'; +import useApi from '@/hooks/use-api'; +import { useConfirmModal } from '@/hooks/use-confirm-modal'; +import { useSieMethods } from '@/hooks/use-sie'; +import { UserFlow } from '@/types'; + +import useIdentifierErrorAlert from './use-identifier-error-alert'; +import useSharedErrorHandler from './use-shared-error-handler'; + +const useRegisterWithEmailPasscodeValidation = (email: string, errorCallback?: () => void) => { + const { t } = useTranslation(); + const { show } = useConfirmModal(); + const navigate = useNavigate(); + const { errorMessage, clearErrorMessage, sharedErrorHandlers } = useSharedErrorHandler(); + + const { signInMode } = useSieMethods(); + + const { run: signInWithEmailAsync } = useApi(signInWithEmail); + + const identifierExistErrorHandler = useIdentifierErrorAlert( + UserFlow.register, + SignInIdentifier.Email, + email + ); + + const emailExistSignInErrorHandler = useCallback(async () => { + const [confirm] = await show({ + confirmText: 'action.sign_in', + ModalContent: t('description.create_account_id_exists', { + type: t(`description.email`), + value: email, + }), + }); + + if (!confirm) { + navigate(-1); + + return; + } + + const result = await signInWithEmailAsync(); + + if (result?.redirectTo) { + window.location.replace(result.redirectTo); + } + }, [email, navigate, show, signInWithEmailAsync, t]); + + const errorHandlers = useMemo( + () => ({ + 'user.email_exists_register': + signInMode === SignInMode.Register + ? identifierExistErrorHandler + : emailExistSignInErrorHandler, + ...sharedErrorHandlers, + callback: errorCallback, + }), + [ + emailExistSignInErrorHandler, + errorCallback, + identifierExistErrorHandler, + sharedErrorHandlers, + signInMode, + ] + ); + + const { result, run: verifyPasscode } = useApi(verifyRegisterEmailPasscode, errorHandlers); + + const onSubmit = useCallback( + async (code: string) => { + return verifyPasscode(email, code); + }, + [email, verifyPasscode] + ); + + useEffect(() => { + if (result?.redirectTo) { + window.location.replace(result.redirectTo); + } + }, [result]); + + return { + errorMessage, + clearErrorMessage, + onSubmit, + }; +}; + +export default useRegisterWithEmailPasscodeValidation; diff --git a/packages/ui/src/containers/PasscodeValidation/use-register-with-sms-error-handler.ts b/packages/ui/src/containers/PasscodeValidation/use-register-with-sms-error-handler.ts deleted file mode 100644 index af70f2323..000000000 --- a/packages/ui/src/containers/PasscodeValidation/use-register-with-sms-error-handler.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useNavigate } from 'react-router-dom'; - -import { signInWithSms } from '@/apis/sign-in'; -import type { ErrorHandlers } from '@/hooks/use-api'; -import useApi from '@/hooks/use-api'; -import { useConfirmModal } from '@/hooks/use-confirm-modal'; -import { formatPhoneNumberWithCountryCallingCode } from '@/utils/country-code'; - -const useRegisterWithSmsErrorHandler = (phone: string) => { - const { t } = useTranslation(); - const { show } = useConfirmModal(); - const navigate = useNavigate(); - - const { run: signInWithSmsAsync } = useApi(signInWithSms); - - const phoneExistRegisterHandler = useCallback(async () => { - const [confirm] = await show({ - confirmText: 'action.sign_in', - ModalContent: t('description.create_account_id_exists', { - type: t(`description.phone_number`), - value: formatPhoneNumberWithCountryCallingCode(phone), - }), - }); - - if (!confirm) { - navigate(-1); - - return; - } - - const result = await signInWithSmsAsync(); - - if (result?.redirectTo) { - window.location.replace(result.redirectTo); - } - }, [navigate, phone, show, signInWithSmsAsync, t]); - - const errorHandler = useMemo( - () => ({ - 'user.phone_exists_register': async () => { - await phoneExistRegisterHandler(); - }, - }), - [phoneExistRegisterHandler] - ); - - return { - errorHandler, - }; -}; - -export default useRegisterWithSmsErrorHandler; diff --git a/packages/ui/src/containers/PasscodeValidation/use-register-with-sms-passcode-validation.ts b/packages/ui/src/containers/PasscodeValidation/use-register-with-sms-passcode-validation.ts new file mode 100644 index 000000000..293793635 --- /dev/null +++ b/packages/ui/src/containers/PasscodeValidation/use-register-with-sms-passcode-validation.ts @@ -0,0 +1,95 @@ +import { SignInIdentifier, SignInMode } from '@logto/schemas'; +import { useMemo, useCallback, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; + +import { verifyRegisterSmsPasscode } from '@/apis/register'; +import { signInWithSms } from '@/apis/sign-in'; +import type { ErrorHandlers } from '@/hooks/use-api'; +import useApi from '@/hooks/use-api'; +import { useConfirmModal } from '@/hooks/use-confirm-modal'; +import { useSieMethods } from '@/hooks/use-sie'; +import { UserFlow } from '@/types'; +import { formatPhoneNumberWithCountryCallingCode } from '@/utils/country-code'; + +import useIdentifierErrorAlert from './use-identifier-error-alert'; +import useSharedErrorHandler from './use-shared-error-handler'; + +const useRegisterWithSmsPasscodeValidation = (phone: string, errorCallback?: () => void) => { + const { t } = useTranslation(); + const { show } = useConfirmModal(); + const navigate = useNavigate(); + const { errorMessage, clearErrorMessage, sharedErrorHandlers } = useSharedErrorHandler(); + const { signInMode } = useSieMethods(); + + const { run: signInWithSmsAsync } = useApi(signInWithSms); + + const identifierExistErrorHandler = useIdentifierErrorAlert( + UserFlow.register, + SignInIdentifier.Sms, + formatPhoneNumberWithCountryCallingCode(phone) + ); + + const phoneExistSignInErrorHandler = useCallback(async () => { + const [confirm] = await show({ + confirmText: 'action.sign_in', + ModalContent: t('description.create_account_id_exists', { + type: t(`description.phone_number`), + value: phone, + }), + }); + + if (!confirm) { + navigate(-1); + + return; + } + + const result = await signInWithSmsAsync(); + + if (result?.redirectTo) { + window.location.replace(result.redirectTo); + } + }, [phone, navigate, show, signInWithSmsAsync, t]); + + const errorHandlers = useMemo( + () => ({ + 'user.phone_exists_register': + signInMode === SignInMode.Register + ? identifierExistErrorHandler + : phoneExistSignInErrorHandler, + ...sharedErrorHandlers, + callback: errorCallback, + }), + [ + phoneExistSignInErrorHandler, + errorCallback, + identifierExistErrorHandler, + sharedErrorHandlers, + signInMode, + ] + ); + + const { result, run: verifyPasscode } = useApi(verifyRegisterSmsPasscode, errorHandlers); + + useEffect(() => { + if (result?.redirectTo) { + window.location.replace(result.redirectTo); + } + }, [result]); + + const onSubmit = useCallback( + async (code: string) => { + return verifyPasscode(phone, code); + }, + [phone, verifyPasscode] + ); + + return { + errorMessage, + clearErrorMessage, + onSubmit, + }; +}; + +export default useRegisterWithSmsPasscodeValidation; diff --git a/packages/ui/src/containers/PasscodeValidation/use-resend-passcode.ts b/packages/ui/src/containers/PasscodeValidation/use-resend-passcode.ts new file mode 100644 index 000000000..d39330915 --- /dev/null +++ b/packages/ui/src/containers/PasscodeValidation/use-resend-passcode.ts @@ -0,0 +1,50 @@ +import type { SignInIdentifier } from '@logto/schemas'; +import { t } from 'i18next'; +import { useCallback, useContext } from 'react'; +import { useTimer } from 'react-timer-hook'; + +import { getSendPasscodeApi } from '@/apis/utils'; +import useApi from '@/hooks/use-api'; +import { PageContext } from '@/hooks/use-page-context'; +import type { UserFlow } from '@/types'; + +export const timeRange = 59; + +const getTimeout = () => { + const now = new Date(); + now.setSeconds(now.getSeconds() + timeRange); + + return now; +}; + +const useResendPasscode = ( + type: UserFlow, + method: SignInIdentifier.Email | SignInIdentifier.Sms, + target: string +) => { + const { setToast } = useContext(PageContext); + + const { seconds, isRunning, restart } = useTimer({ + autoStart: true, + expiryTimestamp: getTimeout(), + }); + + const { run: sendPassCode } = useApi(getSendPasscodeApi(type, method)); + + const onResendPasscode = useCallback(async () => { + const result = await sendPassCode(target); + + if (result) { + setToast(t('description.passcode_sent')); + restart(getTimeout(), true); + } + }, [restart, sendPassCode, setToast, target]); + + return { + seconds, + isRunning, + onResendPasscode, + }; +}; + +export default useResendPasscode; diff --git a/packages/ui/src/containers/PasscodeValidation/use-shared-error-handler.ts b/packages/ui/src/containers/PasscodeValidation/use-shared-error-handler.ts new file mode 100644 index 000000000..b1fcae8c2 --- /dev/null +++ b/packages/ui/src/containers/PasscodeValidation/use-shared-error-handler.ts @@ -0,0 +1,26 @@ +import { useState } from 'react'; + +import type { ErrorHandlers } from '@/hooks/use-api'; + +const useSharedErrorHandler = () => { + const [errorMessage, setErrorMessage] = useState(); + + const sharedErrorHandlers: ErrorHandlers = { + 'passcode.expired': (error) => { + setErrorMessage(error.message); + }, + 'passcode.code_mismatch': (error) => { + setErrorMessage(error.message); + }, + }; + + return { + errorMessage, + sharedErrorHandlers, + clearErrorMessage: () => { + setErrorMessage(''); + }, + }; +}; + +export default useSharedErrorHandler; diff --git a/packages/ui/src/containers/PasscodeValidation/use-sign-in-with-email-error-handler.ts b/packages/ui/src/containers/PasscodeValidation/use-sign-in-with-email-error-handler.ts deleted file mode 100644 index 058404280..000000000 --- a/packages/ui/src/containers/PasscodeValidation/use-sign-in-with-email-error-handler.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { useCallback, useMemo, useContext } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useNavigate } from 'react-router-dom'; - -import { registerWithEmail } from '@/apis/register'; -import type { ErrorHandlers } from '@/hooks/use-api'; -import useApi from '@/hooks/use-api'; -import { useConfirmModal } from '@/hooks/use-confirm-modal'; -import { PageContext } from '@/hooks/use-page-context'; -import { SearchParameters } from '@/types'; -import { getSearchParameters } from '@/utils'; - -const useSignInWithEmailErrorHandler = (email: string) => { - const { t } = useTranslation(); - const { show } = useConfirmModal(); - const navigate = useNavigate(); - const { setToast } = useContext(PageContext); - - const { run: registerWithEmailAsync } = useApi(registerWithEmail); - - const socialToBind = getSearchParameters(location.search, SearchParameters.bindWithSocial); - - const emailNotExistSignInHandler = useCallback(async () => { - const [confirm] = await show({ - confirmText: 'action.create', - ModalContent: t('description.sign_in_id_does_not_exists', { - type: t(`description.email`), - value: email, - }), - }); - - if (!confirm) { - navigate(-1); - - return; - } - - const result = await registerWithEmailAsync(); - - if (result?.redirectTo) { - window.location.replace(result.redirectTo); - } - }, [navigate, registerWithEmailAsync, show, t, email]); - - const errorHandler = useMemo( - () => ({ - 'user.email_not_exists': async (error) => { - // Directly display the error if user is trying to bind with social - if (socialToBind) { - setToast(error.message); - } - - await emailNotExistSignInHandler(); - }, - }), - [emailNotExistSignInHandler, setToast, socialToBind] - ); - - return { - errorHandler, - }; -}; - -export default useSignInWithEmailErrorHandler; diff --git a/packages/ui/src/containers/PasscodeValidation/use-sign-in-with-email-passcode-validation.ts b/packages/ui/src/containers/PasscodeValidation/use-sign-in-with-email-passcode-validation.ts new file mode 100644 index 000000000..94de5d7c4 --- /dev/null +++ b/packages/ui/src/containers/PasscodeValidation/use-sign-in-with-email-passcode-validation.ts @@ -0,0 +1,100 @@ +import { SignInIdentifier, SignInMode } from '@logto/schemas'; +import { useMemo, useCallback, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; + +import { registerWithEmail } from '@/apis/register'; +import { verifySignInEmailPasscode } from '@/apis/sign-in'; +import type { ErrorHandlers } from '@/hooks/use-api'; +import useApi from '@/hooks/use-api'; +import { useConfirmModal } from '@/hooks/use-confirm-modal'; +import { useSieMethods } from '@/hooks/use-sie'; +import { UserFlow, SearchParameters } from '@/types'; +import { getSearchParameters } from '@/utils'; + +import useIdentifierErrorAlert from './use-identifier-error-alert'; +import useSharedErrorHandler from './use-shared-error-handler'; + +const useSignInWithEmailPasscodeValidation = (email: string, errorCallback?: () => void) => { + const { t } = useTranslation(); + const { show } = useConfirmModal(); + const navigate = useNavigate(); + const { errorMessage, clearErrorMessage, sharedErrorHandlers } = useSharedErrorHandler(); + + const { signInMode } = useSieMethods(); + + const { run: registerWithEmailAsync } = useApi(registerWithEmail); + + const socialToBind = getSearchParameters(location.search, SearchParameters.bindWithSocial); + + const identifierNotExistErrorHandler = useIdentifierErrorAlert( + UserFlow.signIn, + SignInIdentifier.Email, + email + ); + + const emailNotExistRegisterErrorHandler = useCallback(async () => { + const [confirm] = await show({ + confirmText: 'action.create', + ModalContent: t('description.sign_in_id_does_not_exists', { + type: t(`description.email`), + value: email, + }), + }); + + if (!confirm) { + navigate(-1); + + return; + } + + const result = await registerWithEmailAsync(); + + if (result?.redirectTo) { + window.location.replace(result.redirectTo); + } + }, [email, navigate, show, registerWithEmailAsync, t]); + + const errorHandlers = useMemo( + () => ({ + 'user.email_not_exists': + // Block user auto register if is bind social or sign-in only flow + signInMode === SignInMode.SignIn || socialToBind + ? identifierNotExistErrorHandler + : emailNotExistRegisterErrorHandler, + ...sharedErrorHandlers, + callback: errorCallback, + }), + [ + emailNotExistRegisterErrorHandler, + errorCallback, + identifierNotExistErrorHandler, + sharedErrorHandlers, + signInMode, + socialToBind, + ] + ); + + const { result, run: verifyPasscode } = useApi(verifySignInEmailPasscode, errorHandlers); + + useEffect(() => { + if (result?.redirectTo) { + window.location.replace(result.redirectTo); + } + }, [result]); + + const onSubmit = useCallback( + async (code: string) => { + return verifyPasscode(email, code, socialToBind); + }, + [email, socialToBind, verifyPasscode] + ); + + return { + errorMessage, + clearErrorMessage, + onSubmit, + }; +}; + +export default useSignInWithEmailPasscodeValidation; diff --git a/packages/ui/src/containers/PasscodeValidation/use-sign-in-with-sms-error-handler.ts b/packages/ui/src/containers/PasscodeValidation/use-sign-in-with-sms-error-handler.ts deleted file mode 100644 index 84c414554..000000000 --- a/packages/ui/src/containers/PasscodeValidation/use-sign-in-with-sms-error-handler.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { useCallback, useMemo, useContext } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useNavigate } from 'react-router-dom'; - -import { registerWithSms } from '@/apis/register'; -import type { ErrorHandlers } from '@/hooks/use-api'; -import useApi from '@/hooks/use-api'; -import { useConfirmModal } from '@/hooks/use-confirm-modal'; -import { PageContext } from '@/hooks/use-page-context'; -import { SearchParameters } from '@/types'; -import { getSearchParameters } from '@/utils'; -import { formatPhoneNumberWithCountryCallingCode } from '@/utils/country-code'; - -const useSignInWithSmsErrorHandler = (phone: string) => { - const { t } = useTranslation(); - const { show } = useConfirmModal(); - const navigate = useNavigate(); - const { setToast } = useContext(PageContext); - - const { run: registerWithSmsAsync } = useApi(registerWithSms); - - const socialToBind = getSearchParameters(location.search, SearchParameters.bindWithSocial); - - const phoneNotExistSignInHandler = useCallback(async () => { - const [confirm] = await show({ - ModalContent: t('description.sign_in_id_does_not_exists', { - confirmText: 'action.create', - type: t(`description.phone_number`), - value: formatPhoneNumberWithCountryCallingCode(phone), - }), - }); - - if (!confirm) { - navigate(-1); - - return; - } - - const result = await registerWithSmsAsync(); - - if (result?.redirectTo) { - window.location.replace(result.redirectTo); - } - }, [navigate, registerWithSmsAsync, show, t, phone]); - - const errorHandler = useMemo( - () => ({ - 'user.phone_not_exists': async (error) => { - // Directly display the error if user is trying to bind with social - if (socialToBind) { - setToast(error.message); - } - - await phoneNotExistSignInHandler(); - }, - }), - [phoneNotExistSignInHandler, setToast, socialToBind] - ); - - return { - errorHandler, - }; -}; - -export default useSignInWithSmsErrorHandler; diff --git a/packages/ui/src/containers/PasscodeValidation/use-sign-in-with-sms-passcode-validation.ts b/packages/ui/src/containers/PasscodeValidation/use-sign-in-with-sms-passcode-validation.ts new file mode 100644 index 000000000..15d5f9af7 --- /dev/null +++ b/packages/ui/src/containers/PasscodeValidation/use-sign-in-with-sms-passcode-validation.ts @@ -0,0 +1,100 @@ +import { SignInIdentifier, SignInMode } from '@logto/schemas'; +import { useMemo, useCallback, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; + +import { registerWithSms } from '@/apis/register'; +import { verifySignInSmsPasscode } from '@/apis/sign-in'; +import type { ErrorHandlers } from '@/hooks/use-api'; +import useApi from '@/hooks/use-api'; +import { useConfirmModal } from '@/hooks/use-confirm-modal'; +import { useSieMethods } from '@/hooks/use-sie'; +import { UserFlow, SearchParameters } from '@/types'; +import { getSearchParameters } from '@/utils'; + +import useIdentifierErrorAlert from './use-identifier-error-alert'; +import useSharedErrorHandler from './use-shared-error-handler'; + +const useSignInWithSmsPasscodeValidation = (phone: string, errorCallback?: () => void) => { + const { t } = useTranslation(); + const { show } = useConfirmModal(); + const navigate = useNavigate(); + const { errorMessage, clearErrorMessage, sharedErrorHandlers } = useSharedErrorHandler(); + + const { signInMode } = useSieMethods(); + + const { run: registerWithSmsAsync } = useApi(registerWithSms); + + const socialToBind = getSearchParameters(location.search, SearchParameters.bindWithSocial); + + const identifierNotExistErrorHandler = useIdentifierErrorAlert( + UserFlow.signIn, + SignInIdentifier.Sms, + phone + ); + + const phoneNotExistRegisterErrorHandler = useCallback(async () => { + const [confirm] = await show({ + confirmText: 'action.create', + ModalContent: t('description.sign_in_id_does_not_exists', { + type: t(`description.phone_number`), + value: phone, + }), + }); + + if (!confirm) { + navigate(-1); + + return; + } + + const result = await registerWithSmsAsync(); + + if (result?.redirectTo) { + window.location.replace(result.redirectTo); + } + }, [phone, navigate, show, registerWithSmsAsync, t]); + + const errorHandlers = useMemo( + () => ({ + 'user.phone_not_exists': + // Block user auto register if is bind social or sign-in only flow + signInMode === SignInMode.SignIn || socialToBind + ? identifierNotExistErrorHandler + : phoneNotExistRegisterErrorHandler, + ...sharedErrorHandlers, + callback: errorCallback, + }), + [ + phoneNotExistRegisterErrorHandler, + errorCallback, + identifierNotExistErrorHandler, + sharedErrorHandlers, + signInMode, + socialToBind, + ] + ); + + const { result, run: verifyPasscode } = useApi(verifySignInSmsPasscode, errorHandlers); + + useEffect(() => { + if (result?.redirectTo) { + window.location.replace(result.redirectTo); + } + }, [result]); + + const onSubmit = useCallback( + async (code: string) => { + return verifyPasscode(phone, code, socialToBind); + }, + [phone, socialToBind, verifyPasscode] + ); + + return { + errorMessage, + clearErrorMessage, + onSubmit, + }; +}; + +export default useSignInWithSmsPasscodeValidation; diff --git a/packages/ui/src/containers/PasscodeValidation/user-register-with-email-error-handler.ts b/packages/ui/src/containers/PasscodeValidation/user-register-with-email-error-handler.ts deleted file mode 100644 index d10aa1fd3..000000000 --- a/packages/ui/src/containers/PasscodeValidation/user-register-with-email-error-handler.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useNavigate } from 'react-router-dom'; - -import { signInWithEmail } from '@/apis/sign-in'; -import type { ErrorHandlers } from '@/hooks/use-api'; -import useApi from '@/hooks/use-api'; -import { useConfirmModal } from '@/hooks/use-confirm-modal'; - -const useRegisterWithEmailErrorHandler = (email: string) => { - const { t } = useTranslation(); - const { show } = useConfirmModal(); - const navigate = useNavigate(); - - const { run: signInWithEmailAsync } = useApi(signInWithEmail); - - const emailExistRegisterHandler = useCallback(async () => { - const [confirm] = await show({ - ModalContent: t('description.create_account_id_exists', { - confirmText: 'action.sign_in', - type: t(`description.email`), - value: email, - }), - }); - - if (!confirm) { - navigate(-1); - - return; - } - - const result = await signInWithEmailAsync(); - - if (result?.redirectTo) { - window.location.replace(result.redirectTo); - } - }, [navigate, show, signInWithEmailAsync, t, email]); - - const errorHandler = useMemo( - () => ({ - 'user.email_exists_register': async () => { - await emailExistRegisterHandler(); - }, - }), - [emailExistRegisterHandler] - ); - - return { - errorHandler, - }; -}; - -export default useRegisterWithEmailErrorHandler; diff --git a/packages/ui/src/containers/PasscodeValidation/utils.ts b/packages/ui/src/containers/PasscodeValidation/utils.ts new file mode 100644 index 000000000..8d243bd8b --- /dev/null +++ b/packages/ui/src/containers/PasscodeValidation/utils.ts @@ -0,0 +1,35 @@ +import { SignInIdentifier } from '@logto/schemas'; + +import { UserFlow } from '@/types'; + +import useForgotPasswordEmailPasscodeValidation from './use-forgot-password-email-passcode-validation'; +import useForgotPasswordSmsPasscodeValidation from './use-forgot-password-sms-passcode-validation'; +import useRegisterWithEmailPasscodeValidation from './use-register-with-email-passcode-validation'; +import useRegisterWithSmsPasscodeValidation from './use-register-with-sms-passcode-validation'; +import useSignInWithEmailPasscodeValidation from './use-sign-in-with-email-passcode-validation'; +import useSignInWithSmsPasscodeValidation from './use-sign-in-with-sms-passcode-validation'; + +export const getPasscodeValidationHook = ( + type: UserFlow, + method: SignInIdentifier.Email | SignInIdentifier.Sms +) => { + switch (type) { + case UserFlow.signIn: + return method === SignInIdentifier.Email + ? useSignInWithEmailPasscodeValidation + : useSignInWithSmsPasscodeValidation; + case UserFlow.register: + return method === SignInIdentifier.Email + ? useRegisterWithEmailPasscodeValidation + : useRegisterWithSmsPasscodeValidation; + case UserFlow.forgotPassword: + return method === SignInIdentifier.Email + ? useForgotPasswordEmailPasscodeValidation + : useForgotPasswordSmsPasscodeValidation; + default: + // TODO: continue flow hook + return method === SignInIdentifier.Email + ? useRegisterWithEmailPasscodeValidation + : useRegisterWithSmsPasscodeValidation; + } +}; diff --git a/packages/ui/src/types/index.ts b/packages/ui/src/types/index.ts index deac9d761..ee2ade2d9 100644 --- a/packages/ui/src/types/index.ts +++ b/packages/ui/src/types/index.ts @@ -9,6 +9,7 @@ export enum UserFlow { signIn = 'sign-in', register = 'register', forgotPassword = 'forgot-password', + continue = 'continue', } export enum SearchParameters {