mirror of
https://github.com/logto-io/logto.git
synced 2025-03-24 22:41:28 -05:00
refactor(ui): refactor passcodevalidation
refactor passcode validation
This commit is contained in:
parent
6f58f30ed0
commit
e6aead2fb0
26 changed files with 829 additions and 472 deletions
|
@ -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.',
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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: '계정이 이미 있으신가요? 로그인하여 다른 계정과 연동해보세요.',
|
||||
|
|
|
@ -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.',
|
||||
|
|
|
@ -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.',
|
||||
|
|
|
@ -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: '已有帐号?登录以绑定社交身份。',
|
||||
|
|
|
@ -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('<PasscodeValidation />', () => {
|
||||
const email = 'foo@logto.io';
|
||||
const phone = '18573333333';
|
||||
const originalLocation = window.location;
|
||||
|
||||
beforeAll(() => {
|
||||
|
@ -75,68 +95,170 @@ describe('<PasscodeValidation />', () => {
|
|||
expect(sendPasscodeApi).toBeCalledWith(email);
|
||||
});
|
||||
|
||||
it('fire validate passcode event', async () => {
|
||||
const { container } = renderWithPageContext(
|
||||
<PasscodeValidation type={UserFlow.signIn} method={SignInIdentifier.Email} target={email} />
|
||||
);
|
||||
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(
|
||||
<PasscodeValidation type={UserFlow.signIn} method={SignInIdentifier.Email} target={email} />
|
||||
);
|
||||
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(
|
||||
<PasscodeValidation type={UserFlow.signIn} method={SignInIdentifier.Email} target={email} />
|
||||
);
|
||||
const { container } = renderWithPageContext(
|
||||
<PasscodeValidation type={UserFlow.signIn} method={SignInIdentifier.Sms} target={phone} />
|
||||
);
|
||||
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(
|
||||
<PasscodeValidation
|
||||
type={UserFlow.register}
|
||||
method={SignInIdentifier.Email}
|
||||
target={email}
|
||||
/>
|
||||
);
|
||||
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(
|
||||
<PasscodeValidation type={UserFlow.register} method={SignInIdentifier.Sms} target={phone} />
|
||||
);
|
||||
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(
|
||||
<PasscodeValidation
|
||||
type={UserFlow.forgotPassword}
|
||||
method={SignInIdentifier.Email}
|
||||
target={email}
|
||||
/>
|
||||
);
|
||||
|
||||
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(
|
||||
<PasscodeValidation
|
||||
type={UserFlow.forgotPassword}
|
||||
method={SignInIdentifier.Email}
|
||||
target={email}
|
||||
method={SignInIdentifier.Sms}
|
||||
target={phone}
|
||||
/>
|
||||
);
|
||||
|
||||
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(() => {
|
||||
|
|
|
@ -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<string[]>([]);
|
||||
const [error, setError] = useState<string>();
|
||||
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 (
|
||||
<form className={classNames(styles.form, className)}>
|
||||
<Passcode
|
||||
name="passcode"
|
||||
className={classNames(styles.inputField, error && styles.withError)}
|
||||
className={classNames(styles.inputField, errorMessage && styles.withError)}
|
||||
value={code}
|
||||
error={error}
|
||||
error={errorMessage}
|
||||
onChange={setCode}
|
||||
/>
|
||||
{isRunning ? (
|
||||
|
@ -118,7 +51,13 @@ const PasscodeValidation = ({ type, method, className, target }: Props) => {
|
|||
</Trans>
|
||||
</div>
|
||||
) : (
|
||||
<TextLink text="description.resend_passcode" onClick={resendPasscodeHandler} />
|
||||
<TextLink
|
||||
text="description.resend_passcode"
|
||||
onClick={() => {
|
||||
clearErrorMessage();
|
||||
void onResendPasscode();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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<ErrorHandlers>(
|
||||
() => ({
|
||||
'user.email_not_exists': emailNotExistForgotPasswordHandler,
|
||||
}),
|
||||
[emailNotExistForgotPasswordHandler]
|
||||
);
|
||||
|
||||
return {
|
||||
errorHandler,
|
||||
};
|
||||
};
|
||||
|
||||
export default useForgotPasswordWithEmailErrorHandler;
|
|
@ -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<ErrorHandlers>(
|
||||
() => ({
|
||||
'user.phone_not_exists': phoneNotExistForgotPasswordHandler,
|
||||
}),
|
||||
[phoneNotExistForgotPasswordHandler]
|
||||
);
|
||||
|
||||
return {
|
||||
errorHandler,
|
||||
};
|
||||
};
|
||||
|
||||
export default useForgotPasswordWithSmsErrorHandler;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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<ErrorHandlers>(
|
||||
() => ({
|
||||
'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;
|
|
@ -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<ErrorHandlers>(
|
||||
() => ({
|
||||
'user.phone_exists_register': async () => {
|
||||
await phoneExistRegisterHandler();
|
||||
},
|
||||
}),
|
||||
[phoneExistRegisterHandler]
|
||||
);
|
||||
|
||||
return {
|
||||
errorHandler,
|
||||
};
|
||||
};
|
||||
|
||||
export default useRegisterWithSmsErrorHandler;
|
|
@ -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<ErrorHandlers>(
|
||||
() => ({
|
||||
'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;
|
|
@ -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;
|
|
@ -0,0 +1,26 @@
|
|||
import { useState } from 'react';
|
||||
|
||||
import type { ErrorHandlers } from '@/hooks/use-api';
|
||||
|
||||
const useSharedErrorHandler = () => {
|
||||
const [errorMessage, setErrorMessage] = useState<string>();
|
||||
|
||||
const sharedErrorHandlers: ErrorHandlers = {
|
||||
'passcode.expired': (error) => {
|
||||
setErrorMessage(error.message);
|
||||
},
|
||||
'passcode.code_mismatch': (error) => {
|
||||
setErrorMessage(error.message);
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
errorMessage,
|
||||
sharedErrorHandlers,
|
||||
clearErrorMessage: () => {
|
||||
setErrorMessage('');
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default useSharedErrorHandler;
|
|
@ -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<ErrorHandlers>(
|
||||
() => ({
|
||||
'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;
|
|
@ -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<ErrorHandlers>(
|
||||
() => ({
|
||||
'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;
|
|
@ -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<ErrorHandlers>(
|
||||
() => ({
|
||||
'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;
|
|
@ -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<ErrorHandlers>(
|
||||
() => ({
|
||||
'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;
|
|
@ -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<ErrorHandlers>(
|
||||
() => ({
|
||||
'user.email_exists_register': async () => {
|
||||
await emailExistRegisterHandler();
|
||||
},
|
||||
}),
|
||||
[emailExistRegisterHandler]
|
||||
);
|
||||
|
||||
return {
|
||||
errorHandler,
|
||||
};
|
||||
};
|
||||
|
||||
export default useRegisterWithEmailErrorHandler;
|
35
packages/ui/src/containers/PasscodeValidation/utils.ts
Normal file
35
packages/ui/src/containers/PasscodeValidation/utils.ts
Normal file
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -9,6 +9,7 @@ export enum UserFlow {
|
|||
signIn = 'sign-in',
|
||||
register = 'register',
|
||||
forgotPassword = 'forgot-password',
|
||||
continue = 'continue',
|
||||
}
|
||||
|
||||
export enum SearchParameters {
|
||||
|
|
Loading…
Add table
Reference in a new issue