0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-31 22:51:25 -05:00

refactor(ui): simplify code verification hooks (#2898)

This commit is contained in:
simeng-li 2023-01-11 15:41:02 +08:00 committed by GitHub
parent fe14f0563c
commit b6c684874a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 407 additions and 696 deletions

View file

@ -134,7 +134,7 @@ export const verifyForgotPasswordVerificationCodeIdentifier = async (
return api.post(`${interactionPrefix}/submit`).json<Response>();
};
export const signInWithVerifierIdentifier = async () => {
export const signInWithVerifiedIdentifier = async () => {
await api.delete(`${interactionPrefix}/profile`);
await api.put(`${interactionPrefix}/event`, {

View file

@ -1,4 +1,4 @@
import type { SignInIdentifier } from '@logto/schemas';
import { SignInIdentifier } from '@logto/schemas';
import classNames from 'classnames';
import { useState, useEffect, useCallback } from 'react';
import { useTranslation, Trans } from 'react-i18next';
@ -10,7 +10,7 @@ import { UserFlow } from '@/types';
import PasswordSignInLink from './PasswordSignInLink';
import * as styles from './index.module.scss';
import useResendVerificationCode from './use-resend-verification-code';
import { getVerificationCodeHook } from './utils';
import { getCodeVerificationHookByFlow } from './utils';
type Props = {
type: UserFlow;
@ -23,13 +23,18 @@ type Props = {
const VerificationCode = ({ type, method, className, hasPasswordButton, target }: Props) => {
const [code, setCode] = useState<string[]>([]);
const { t } = useTranslation();
const useVerificationCode = getVerificationCodeHook(type, method);
const useVerificationCode = getCodeVerificationHookByFlow(type);
const errorCallback = useCallback(() => {
setCode([]);
}, []);
const { errorMessage, clearErrorMessage, onSubmit } = useVerificationCode(target, errorCallback);
const { errorMessage, clearErrorMessage, onSubmit } = useVerificationCode(
method,
target,
errorCallback
);
const { seconds, isRunning, onResendVerificationCode } = useResendVerificationCode(
type,
@ -39,9 +44,13 @@ const VerificationCode = ({ type, method, className, hasPasswordButton, target }
useEffect(() => {
if (code.length === defaultLength && code.every(Boolean)) {
void onSubmit(code.join(''));
const payload =
method === SignInIdentifier.Email
? { email: target, verificationCode: code.join('') }
: { phone: target, verificationCode: code.join('') };
void onSubmit(payload);
}
}, [code, onSubmit, target]);
}, [code, method, onSubmit, target]);
return (
<form className={classNames(styles.form, className)}>

View file

@ -0,0 +1,81 @@
import type { EmailVerificationCodePayload, PhoneVerificationCodePayload } from '@logto/schemas';
import { SignInIdentifier } from '@logto/schemas';
import { useMemo, useCallback } from 'react';
import { addProfileWithVerificationCodeIdentifier } from '@/apis/interaction';
import type { ErrorHandlers } from '@/hooks/use-api';
import useApi from '@/hooks/use-api';
import useRequiredProfileErrorHandler from '@/hooks/use-required-profile-error-handler';
import type { VerificationCodeIdentifier } from '@/types';
import { SearchParameters } from '@/types';
import { getSearchParameters } from '@/utils';
import useGeneralVerificationCodeErrorHandler from './use-general-verification-code-error-handler';
import useIdentifierErrorAlert, { IdentifierErrorType } from './use-identifier-error-alert';
const useContinueFlowCodeVerification = (
_method: VerificationCodeIdentifier,
target: string,
errorCallback?: () => void
) => {
const { generalVerificationCodeErrorHandlers, errorMessage, clearErrorMessage } =
useGeneralVerificationCodeErrorHandler();
const requiredProfileErrorHandler = useRequiredProfileErrorHandler(true);
const identifierErrorHandler = useIdentifierErrorAlert();
const verifyVerificationCodeErrorHandlers: ErrorHandlers = useMemo(
() => ({
'user.phone_already_in_use': () => {
void identifierErrorHandler(
IdentifierErrorType.IdentifierAlreadyExists,
SignInIdentifier.Phone,
target
);
},
'user.email_already_in_use': () => {
void identifierErrorHandler(
IdentifierErrorType.IdentifierAlreadyExists,
SignInIdentifier.Email,
target
);
},
...requiredProfileErrorHandler,
...generalVerificationCodeErrorHandlers,
callback: errorCallback,
}),
[
errorCallback,
target,
identifierErrorHandler,
requiredProfileErrorHandler,
generalVerificationCodeErrorHandlers,
]
);
const { run: verifyVerificationCode } = useApi(
addProfileWithVerificationCodeIdentifier,
verifyVerificationCodeErrorHandlers
);
const onSubmit = useCallback(
async (payload: EmailVerificationCodePayload | PhoneVerificationCodePayload) => {
const socialToBind = getSearchParameters(location.search, SearchParameters.bindWithSocial);
const result = await verifyVerificationCode(payload, socialToBind);
if (result?.redirectTo) {
window.location.replace(result.redirectTo);
}
},
[verifyVerificationCode]
);
return {
errorMessage,
clearErrorMessage,
onSubmit,
};
};
export default useContinueFlowCodeVerification;

View file

@ -1,64 +0,0 @@
import { SignInIdentifier } from '@logto/schemas';
import { useMemo, useCallback } from 'react';
import { addProfileWithVerificationCodeIdentifier } from '@/apis/interaction';
import type { ErrorHandlers } from '@/hooks/use-api';
import useApi from '@/hooks/use-api';
import useRequiredProfileErrorHandler from '@/hooks/use-required-profile-error-handler';
import { UserFlow, SearchParameters } from '@/types';
import { getSearchParameters } from '@/utils';
import useIdentifierErrorAlert from './use-identifier-error-alert';
import useSharedErrorHandler from './use-shared-error-handler';
const useContinueSetEmailVerificationCode = (email: string, errorCallback?: () => void) => {
const { sharedErrorHandlers, errorMessage, clearErrorMessage } = useSharedErrorHandler();
const requiredProfileErrorHandler = useRequiredProfileErrorHandler(true);
const identifierNotExistErrorHandler = useIdentifierErrorAlert(
UserFlow.continue,
SignInIdentifier.Email,
email
);
const verifyVerificationCodeErrorHandlers: ErrorHandlers = useMemo(
() => ({
'user.email_already_in_use': identifierNotExistErrorHandler,
...requiredProfileErrorHandler,
...sharedErrorHandlers,
callback: errorCallback,
}),
[
errorCallback,
identifierNotExistErrorHandler,
requiredProfileErrorHandler,
sharedErrorHandlers,
]
);
const { run: verifyVerificationCode } = useApi(
addProfileWithVerificationCodeIdentifier,
verifyVerificationCodeErrorHandlers
);
const onSubmit = useCallback(
async (verificationCode: string) => {
const socialToBind = getSearchParameters(location.search, SearchParameters.bindWithSocial);
const result = await verifyVerificationCode({ email, verificationCode }, socialToBind);
if (result?.redirectTo) {
window.location.replace(result.redirectTo);
}
},
[email, verifyVerificationCode]
);
return {
errorMessage,
clearErrorMessage,
onSubmit,
};
};
export default useContinueSetEmailVerificationCode;

View file

@ -1,59 +0,0 @@
import { SignInIdentifier } from '@logto/schemas';
import { useMemo, useCallback } from 'react';
import { addProfileWithVerificationCodeIdentifier } from '@/apis/interaction';
import type { ErrorHandlers } from '@/hooks/use-api';
import useApi from '@/hooks/use-api';
import useRequiredProfileErrorHandler from '@/hooks/use-required-profile-error-handler';
import { UserFlow, SearchParameters } from '@/types';
import { getSearchParameters } from '@/utils';
import useIdentifierErrorAlert from './use-identifier-error-alert';
import useSharedErrorHandler from './use-shared-error-handler';
const useContinueSetPhoneVerificationCode = (phone: string, errorCallback?: () => void) => {
const { sharedErrorHandlers, errorMessage, clearErrorMessage } = useSharedErrorHandler();
const requiredProfileErrorHandler = useRequiredProfileErrorHandler(true);
const identifierExistErrorHandler = useIdentifierErrorAlert(
UserFlow.continue,
SignInIdentifier.Phone,
phone
);
const verifyVerificationCodeErrorHandlers: ErrorHandlers = useMemo(
() => ({
'user.phone_already_in_use': identifierExistErrorHandler,
...requiredProfileErrorHandler,
...sharedErrorHandlers,
callback: errorCallback,
}),
[errorCallback, identifierExistErrorHandler, requiredProfileErrorHandler, sharedErrorHandlers]
);
const { run: verifyVerificationCode } = useApi(
addProfileWithVerificationCodeIdentifier,
verifyVerificationCodeErrorHandlers
);
const onSubmit = useCallback(
async (verificationCode: string) => {
const socialToBind = getSearchParameters(location.search, SearchParameters.bindWithSocial);
const result = await verifyVerificationCode({ phone, verificationCode }, socialToBind);
if (result?.redirectTo) {
window.location.replace(result.redirectTo);
}
},
[phone, verifyVerificationCode]
);
return {
errorMessage,
clearErrorMessage,
onSubmit,
};
};
export default useContinueSetPhoneVerificationCode;

View file

@ -1,60 +0,0 @@
import { SignInIdentifier } from '@logto/schemas';
import { useMemo, useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { verifyForgotPasswordVerificationCodeIdentifier } from '@/apis/interaction';
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 useForgotPasswordEmailVerificationCode = (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.user_not_exist': identifierNotExistErrorHandler,
'user.new_password_required_in_profile': () => {
navigate(`/${UserFlow.forgotPassword}/reset`, { replace: true });
},
...sharedErrorHandlers,
callback: errorCallback,
}),
[identifierNotExistErrorHandler, sharedErrorHandlers, errorCallback, navigate]
);
const { result, run: verifyVerificationCode } = useApi(
verifyForgotPasswordVerificationCodeIdentifier,
errorHandlers
);
const onSubmit = useCallback(
async (verificationCode: string) => {
return verifyVerificationCode({ email, verificationCode });
},
[email, verifyVerificationCode]
);
useEffect(() => {
if (result) {
navigate(`/${UserFlow.signIn}`, { replace: true });
}
}, [navigate, result]);
return {
errorMessage,
clearErrorMessage,
onSubmit,
};
};
export default useForgotPasswordEmailVerificationCode;

View file

@ -0,0 +1,71 @@
import type { EmailVerificationCodePayload, PhoneVerificationCodePayload } from '@logto/schemas';
import { useMemo, useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { verifyForgotPasswordVerificationCodeIdentifier } from '@/apis/interaction';
import type { ErrorHandlers } from '@/hooks/use-api';
import useApi from '@/hooks/use-api';
import type { VerificationCodeIdentifier } from '@/types';
import { UserFlow } from '@/types';
import useGeneralVerificationCodeErrorHandler from './use-general-verification-code-error-handler';
import useIdentifierErrorAlert, { IdentifierErrorType } from './use-identifier-error-alert';
const useForgotPasswordFlowCodeVerification = (
method: VerificationCodeIdentifier,
target: string,
errorCallback?: () => void
) => {
const navigate = useNavigate();
const { generalVerificationCodeErrorHandlers, errorMessage, clearErrorMessage } =
useGeneralVerificationCodeErrorHandler();
const identifierErrorHandler = useIdentifierErrorAlert();
const errorHandlers: ErrorHandlers = useMemo(
() => ({
'user.user_not_exist': () => {
void identifierErrorHandler(IdentifierErrorType.IdentifierNotExist, method, target);
},
'user.new_password_required_in_profile': () => {
navigate(`/${UserFlow.forgotPassword}/reset`, { replace: true });
},
...generalVerificationCodeErrorHandlers,
callback: errorCallback,
}),
[
generalVerificationCodeErrorHandlers,
errorCallback,
identifierErrorHandler,
method,
target,
navigate,
]
);
const { result, run: verifyVerificationCode } = useApi(
verifyForgotPasswordVerificationCodeIdentifier,
errorHandlers
);
const onSubmit = useCallback(
async (payload: EmailVerificationCodePayload | PhoneVerificationCodePayload) => {
return verifyVerificationCode(payload);
},
[verifyVerificationCode]
);
useEffect(() => {
if (result) {
navigate(`/${UserFlow.signIn}`, { replace: true });
}
}, [navigate, result]);
return {
errorMessage,
clearErrorMessage,
onSubmit,
};
};
export default useForgotPasswordFlowCodeVerification;

View file

@ -1,60 +0,0 @@
import { SignInIdentifier } from '@logto/schemas';
import { useMemo, useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { verifyForgotPasswordVerificationCodeIdentifier } from '@/apis/interaction';
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 useForgotPasswordPhoneVerificationCode = (phone: string, errorCallback?: () => void) => {
const navigate = useNavigate();
const { sharedErrorHandlers, errorMessage, clearErrorMessage } = useSharedErrorHandler();
const identifierNotExistErrorHandler = useIdentifierErrorAlert(
UserFlow.forgotPassword,
SignInIdentifier.Phone,
phone
);
const errorHandlers: ErrorHandlers = useMemo(
() => ({
'user.user_not_exist': identifierNotExistErrorHandler,
'user.new_password_required_in_profile': () => {
navigate(`/${UserFlow.forgotPassword}/reset`, { replace: true });
},
...sharedErrorHandlers,
callback: errorCallback,
}),
[identifierNotExistErrorHandler, sharedErrorHandlers, errorCallback, navigate]
);
const { result, run: verifyVerificationCode } = useApi(
verifyForgotPasswordVerificationCodeIdentifier,
errorHandlers
);
const onSubmit = useCallback(
async (verificationCode: string) => {
return verifyVerificationCode({ phone, verificationCode });
},
[phone, verifyVerificationCode]
);
useEffect(() => {
if (result) {
navigate(`/${UserFlow.signIn}`, { replace: true });
}
}, [navigate, result]);
return {
errorMessage,
clearErrorMessage,
onSubmit,
};
};
export default useForgotPasswordPhoneVerificationCode;

View file

@ -2,11 +2,11 @@ import { useState, useMemo } from 'react';
import type { ErrorHandlers } from '@/hooks/use-api';
const useSharedErrorHandler = () => {
const useGeneralVerificationCodeErrorHandler = () => {
const [errorMessage, setErrorMessage] = useState<string>();
// Have to wrap up in a useMemo hook otherwise the handler updates on every cycle
const sharedErrorHandlers: ErrorHandlers = useMemo(
const generalVerificationCodeErrorHandlers: ErrorHandlers = useMemo(
() => ({
'verification_code.expired': (error) => {
setErrorMessage(error.message);
@ -20,11 +20,11 @@ const useSharedErrorHandler = () => {
return {
errorMessage,
sharedErrorHandlers,
generalVerificationCodeErrorHandlers,
clearErrorMessage: () => {
setErrorMessage('');
},
};
};
export default useSharedErrorHandler;
export default useGeneralVerificationCodeErrorHandler;

View file

@ -4,34 +4,44 @@ import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { useConfirmModal } from '@/hooks/use-confirm-modal';
import { UserFlow } from '@/types';
import type { VerificationCodeIdentifier } from '@/types';
const useIdentifierErrorAlert = (
flow: UserFlow,
method: SignInIdentifier.Email | SignInIdentifier.Phone,
value: string
) => {
export enum IdentifierErrorType {
IdentifierNotExist = 'IdentifierNotExist',
IdentifierAlreadyExists = 'IdentifierAlreadyExists',
}
const useIdentifierErrorAlert = () => {
const { show } = useConfirmModal();
const navigate = useNavigate();
const { t } = useTranslation();
// Have to wrap up in a useCallback hook otherwise the handler updates on every cycle
return useCallback(async () => {
await show({
type: 'alert',
ModalContent: t(
flow === UserFlow.register || flow === UserFlow.continue
? 'description.create_account_id_exists_alert'
: 'description.sign_in_id_does_not_exist_alert',
{
type: t(`description.${method === SignInIdentifier.Email ? 'email' : 'phone_number'}`),
value,
}
),
cancelText: 'action.got_it',
});
navigate(-1);
}, [flow, method, navigate, show, t, value]);
return useCallback(
async (
errorType: IdentifierErrorType,
identifierType: VerificationCodeIdentifier,
identifier: string
) => {
await show({
type: 'alert',
ModalContent: t(
errorType === IdentifierErrorType.IdentifierAlreadyExists
? 'description.create_account_id_exists_alert'
: 'description.sign_in_id_does_not_exist_alert',
{
type: t(
`description.${identifierType === SignInIdentifier.Email ? 'email' : 'phone_number'}`
),
identifier,
}
),
cancelText: 'action.got_it',
});
navigate(-1);
},
[navigate, show, t]
);
};
export default useIdentifierErrorAlert;

View file

@ -0,0 +1,122 @@
import type { EmailVerificationCodePayload, PhoneVerificationCodePayload } from '@logto/schemas';
import { SignInIdentifier, SignInMode } from '@logto/schemas';
import { useMemo, useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import {
addProfileWithVerificationCodeIdentifier,
signInWithVerifiedIdentifier,
} from '@/apis/interaction';
import type { ErrorHandlers } from '@/hooks/use-api';
import useApi from '@/hooks/use-api';
import { useConfirmModal } from '@/hooks/use-confirm-modal';
import useRequiredProfileErrorHandler from '@/hooks/use-required-profile-error-handler';
import { useSieMethods } from '@/hooks/use-sie';
import type { VerificationCodeIdentifier } from '@/types';
import useGeneralVerificationCodeErrorHandler from './use-general-verification-code-error-handler';
import useIdentifierErrorAlert, { IdentifierErrorType } from './use-identifier-error-alert';
const useRegisterFlowCodeVerification = (
method: VerificationCodeIdentifier,
target: string,
errorCallback?: () => void
) => {
const { t } = useTranslation();
const { show } = useConfirmModal();
const navigate = useNavigate();
const { errorMessage, clearErrorMessage, generalVerificationCodeErrorHandlers } =
useGeneralVerificationCodeErrorHandler();
const { signInMode } = useSieMethods();
const requiredProfileErrorHandlers = useRequiredProfileErrorHandler(true);
const { run: signInWithIdentifierAsync } = useApi(
signInWithVerifiedIdentifier,
requiredProfileErrorHandlers
);
const showIdentifierErrorAlert = useIdentifierErrorAlert();
const identifierExistErrorHandler = useCallback(async () => {
// Should not redirect user to sign-in if is register-only mode
if (signInMode === SignInMode.Register) {
void showIdentifierErrorAlert(IdentifierErrorType.IdentifierAlreadyExists, method, target);
return;
}
const [confirm] = await show({
confirmText: 'action.sign_in',
ModalContent: t('description.create_account_id_exists', {
type: t(`description.${method === SignInIdentifier.Email ? 'email' : 'phone_number'}`),
value: target,
}),
});
if (!confirm) {
navigate(-1);
return;
}
const result = await signInWithIdentifierAsync();
if (result?.redirectTo) {
window.location.replace(result.redirectTo);
}
}, [
method,
navigate,
show,
showIdentifierErrorAlert,
signInMode,
signInWithIdentifierAsync,
t,
target,
]);
const errorHandlers = useMemo<ErrorHandlers>(
() => ({
'user.email_already_in_use': identifierExistErrorHandler,
'user.phone_already_in_use': identifierExistErrorHandler,
...generalVerificationCodeErrorHandlers,
...requiredProfileErrorHandlers,
callback: errorCallback,
}),
[
errorCallback,
identifierExistErrorHandler,
requiredProfileErrorHandlers,
generalVerificationCodeErrorHandlers,
]
);
const { result, run: verifyVerificationCode } = useApi(
addProfileWithVerificationCodeIdentifier,
errorHandlers
);
const onSubmit = useCallback(
async (payload: EmailVerificationCodePayload | PhoneVerificationCodePayload) => {
return verifyVerificationCode(payload);
},
[verifyVerificationCode]
);
useEffect(() => {
if (result?.redirectTo) {
window.location.replace(result.redirectTo);
}
}, [result]);
return {
errorMessage,
clearErrorMessage,
onSubmit,
};
};
export default useRegisterFlowCodeVerification;

View file

@ -1,108 +0,0 @@
import { SignInIdentifier, SignInMode } from '@logto/schemas';
import { useMemo, useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import {
addProfileWithVerificationCodeIdentifier,
signInWithVerifierIdentifier,
} from '@/apis/interaction';
import type { ErrorHandlers } from '@/hooks/use-api';
import useApi from '@/hooks/use-api';
import { useConfirmModal } from '@/hooks/use-confirm-modal';
import useRequiredProfileErrorHandler from '@/hooks/use-required-profile-error-handler';
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 useRegisterWithEmailVerificationCode = (email: string, errorCallback?: () => void) => {
const { t } = useTranslation();
const { show } = useConfirmModal();
const navigate = useNavigate();
const { errorMessage, clearErrorMessage, sharedErrorHandlers } = useSharedErrorHandler();
const { signInMode } = useSieMethods();
const requiredProfileErrorHandlers = useRequiredProfileErrorHandler(true);
const { run: signInWithEmailAsync } = useApi(
signInWithVerifierIdentifier,
requiredProfileErrorHandlers
);
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_already_in_use':
signInMode === SignInMode.Register
? identifierExistErrorHandler
: emailExistSignInErrorHandler,
...sharedErrorHandlers,
...requiredProfileErrorHandlers,
callback: errorCallback,
}),
[
emailExistSignInErrorHandler,
errorCallback,
identifierExistErrorHandler,
requiredProfileErrorHandlers,
sharedErrorHandlers,
signInMode,
]
);
const { result, run: verifyVerificationCode } = useApi(
addProfileWithVerificationCodeIdentifier,
errorHandlers
);
const onSubmit = useCallback(
async (verificationCode: string) => {
return verifyVerificationCode({ email, verificationCode });
},
[email, verifyVerificationCode]
);
useEffect(() => {
if (result?.redirectTo) {
window.location.replace(result.redirectTo);
}
}, [result]);
return {
errorMessage,
clearErrorMessage,
onSubmit,
};
};
export default useRegisterWithEmailVerificationCode;

View file

@ -1,111 +0,0 @@
import { SignInIdentifier, SignInMode } from '@logto/schemas';
import { useMemo, useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import {
addProfileWithVerificationCodeIdentifier,
signInWithVerifierIdentifier,
} from '@/apis/interaction';
import type { ErrorHandlers } from '@/hooks/use-api';
import useApi from '@/hooks/use-api';
import { useConfirmModal } from '@/hooks/use-confirm-modal';
import useRequiredProfileErrorHandler from '@/hooks/use-required-profile-error-handler';
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 useRegisterWithPhoneVerificationCode = (phone: string, errorCallback?: () => void) => {
const { t } = useTranslation();
const { show } = useConfirmModal();
const navigate = useNavigate();
const { errorMessage, clearErrorMessage, sharedErrorHandlers } = useSharedErrorHandler();
const { signInMode } = useSieMethods();
const requiredProfileErrorHandlers = useRequiredProfileErrorHandler(true);
const { run: signInWithPhoneAsync } = useApi(
signInWithVerifierIdentifier,
requiredProfileErrorHandlers
);
const identifierExistErrorHandler = useIdentifierErrorAlert(
UserFlow.register,
SignInIdentifier.Phone,
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 signInWithPhoneAsync();
if (result?.redirectTo) {
window.location.replace(result.redirectTo);
}
}, [phone, navigate, show, signInWithPhoneAsync, t]);
const errorHandlers = useMemo<ErrorHandlers>(
() => ({
'user.phone_already_in_use':
signInMode === SignInMode.Register
? identifierExistErrorHandler
: phoneExistSignInErrorHandler,
...sharedErrorHandlers,
...requiredProfileErrorHandlers,
callback: errorCallback,
}),
[
signInMode,
identifierExistErrorHandler,
phoneExistSignInErrorHandler,
sharedErrorHandlers,
requiredProfileErrorHandlers,
errorCallback,
]
);
const { result, run: verifyVerificationCode } = useApi(
addProfileWithVerificationCodeIdentifier,
errorHandlers
);
useEffect(() => {
if (result?.redirectTo) {
window.location.replace(result.redirectTo);
}
}, [result]);
const onSubmit = useCallback(
async (verificationCode: string) => {
return verifyVerificationCode({
phone,
verificationCode,
});
},
[phone, verifyVerificationCode]
);
return {
errorMessage,
clearErrorMessage,
onSubmit,
};
};
export default useRegisterWithPhoneVerificationCode;

View file

@ -1,3 +1,4 @@
import type { EmailVerificationCodePayload, PhoneVerificationCodePayload } from '@logto/schemas';
import { SignInIdentifier, SignInMode } from '@logto/schemas';
import { useMemo, useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
@ -12,41 +13,51 @@ import useApi from '@/hooks/use-api';
import { useConfirmModal } from '@/hooks/use-confirm-modal';
import useRequiredProfileErrorHandler from '@/hooks/use-required-profile-error-handler';
import { useSieMethods } from '@/hooks/use-sie';
import { UserFlow, SearchParameters } from '@/types';
import type { VerificationCodeIdentifier } from '@/types';
import { SearchParameters } from '@/types';
import { getSearchParameters } from '@/utils';
import useIdentifierErrorAlert from './use-identifier-error-alert';
import useSharedErrorHandler from './use-shared-error-handler';
import useGeneralVerificationCodeErrorHandler from './use-general-verification-code-error-handler';
import useIdentifierErrorAlert, { IdentifierErrorType } from './use-identifier-error-alert';
const useSignInWithPhoneVerificationCode = (phone: string, errorCallback?: () => void) => {
const useSignInFlowCodeVerification = (
method: VerificationCodeIdentifier,
target: string,
errorCallback?: () => void
) => {
const { t } = useTranslation();
const { show } = useConfirmModal();
const navigate = useNavigate();
const { errorMessage, clearErrorMessage, sharedErrorHandlers } = useSharedErrorHandler();
const { errorMessage, clearErrorMessage, generalVerificationCodeErrorHandlers } =
useGeneralVerificationCodeErrorHandler();
const { signInMode } = useSieMethods();
const requiredProfileErrorHandlers = useRequiredProfileErrorHandler(true);
const { run: registerWithPhoneAsync } = useApi(
const { run: registerWithIdentifierAsync } = useApi(
registerWithVerifiedIdentifier,
requiredProfileErrorHandlers
);
const socialToBind = getSearchParameters(location.search, SearchParameters.bindWithSocial);
const identifierNotExistErrorHandler = useIdentifierErrorAlert(
UserFlow.signIn,
SignInIdentifier.Phone,
phone
);
const showIdentifierErrorAlert = useIdentifierErrorAlert();
const identifierNotExistErrorHandler = useCallback(async () => {
// Should not redirect user to register if is sign-in only mode or bind social flow
if (signInMode === SignInMode.SignIn || socialToBind) {
void showIdentifierErrorAlert(IdentifierErrorType.IdentifierNotExist, method, target);
return;
}
const phoneNotExistRegisterErrorHandler = useCallback(async () => {
const [confirm] = await show({
confirmText: 'action.create',
ModalContent: t('description.sign_in_id_does_not_exist', {
type: t(`description.phone_number`),
value: phone,
ype: t(`description.${method === SignInIdentifier.Email ? 'email' : 'phone_number'}`),
value: target,
}),
});
@ -56,32 +67,37 @@ const useSignInWithPhoneVerificationCode = (phone: string, errorCallback?: () =>
return;
}
const result = await registerWithPhoneAsync({ phone });
const result = await registerWithIdentifierAsync(
method === SignInIdentifier.Email ? { email: target } : { phone: target }
);
if (result?.redirectTo) {
window.location.replace(result.redirectTo);
}
}, [phone, navigate, show, registerWithPhoneAsync, t]);
}, [
method,
navigate,
registerWithIdentifierAsync,
show,
showIdentifierErrorAlert,
signInMode,
socialToBind,
t,
target,
]);
const errorHandlers = useMemo<ErrorHandlers>(
() => ({
'user.user_not_exist':
// Block user auto register if is bind social or sign-in only flow
signInMode === SignInMode.SignIn || socialToBind
? identifierNotExistErrorHandler
: phoneNotExistRegisterErrorHandler,
...sharedErrorHandlers,
'user.user_not_exist': identifierNotExistErrorHandler,
...generalVerificationCodeErrorHandlers,
...requiredProfileErrorHandlers,
callback: errorCallback,
}),
[
signInMode,
socialToBind,
identifierNotExistErrorHandler,
phoneNotExistRegisterErrorHandler,
sharedErrorHandlers,
requiredProfileErrorHandlers,
errorCallback,
identifierNotExistErrorHandler,
requiredProfileErrorHandlers,
generalVerificationCodeErrorHandlers,
]
);
@ -97,16 +113,10 @@ const useSignInWithPhoneVerificationCode = (phone: string, errorCallback?: () =>
}, [result]);
const onSubmit = useCallback(
async (verificationCode: string) => {
return asyncSignInWithVerificationCodeIdentifier(
{
phone,
verificationCode,
},
socialToBind
);
async (payload: EmailVerificationCodePayload | PhoneVerificationCodePayload) => {
return asyncSignInWithVerificationCodeIdentifier(payload, socialToBind);
},
[phone, socialToBind, asyncSignInWithVerificationCodeIdentifier]
[asyncSignInWithVerificationCodeIdentifier, socialToBind]
);
return {
@ -116,4 +126,4 @@ const useSignInWithPhoneVerificationCode = (phone: string, errorCallback?: () =>
};
};
export default useSignInWithPhoneVerificationCode;
export default useSignInFlowCodeVerification;

View file

@ -1,119 +0,0 @@
import { SignInIdentifier, SignInMode } from '@logto/schemas';
import { useMemo, useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import {
signInWithVerificationCodeIdentifier,
registerWithVerifiedIdentifier,
} from '@/apis/interaction';
import type { ErrorHandlers } from '@/hooks/use-api';
import useApi from '@/hooks/use-api';
import { useConfirmModal } from '@/hooks/use-confirm-modal';
import useRequiredProfileErrorHandler from '@/hooks/use-required-profile-error-handler';
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 useSignInWithEmailVerificationCode = (email: string, errorCallback?: () => void) => {
const { t } = useTranslation();
const { show } = useConfirmModal();
const navigate = useNavigate();
const { errorMessage, clearErrorMessage, sharedErrorHandlers } = useSharedErrorHandler();
const { signInMode } = useSieMethods();
const requiredProfileErrorHandlers = useRequiredProfileErrorHandler(true);
const { run: registerWithEmailAsync } = useApi(
registerWithVerifiedIdentifier,
requiredProfileErrorHandlers
);
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_exist', {
type: t(`description.email`),
value: email,
}),
});
if (!confirm) {
navigate(-1);
return;
}
const result = await registerWithEmailAsync({ email });
if (result?.redirectTo) {
window.location.replace(result.redirectTo);
}
}, [email, navigate, show, registerWithEmailAsync, t]);
const errorHandlers = useMemo<ErrorHandlers>(
() => ({
'user.user_not_exist':
// Block user auto register if is bind social or sign-in only flow
signInMode === SignInMode.SignIn || socialToBind
? identifierNotExistErrorHandler
: emailNotExistRegisterErrorHandler,
...sharedErrorHandlers,
...requiredProfileErrorHandlers,
callback: errorCallback,
}),
[
emailNotExistRegisterErrorHandler,
errorCallback,
identifierNotExistErrorHandler,
requiredProfileErrorHandlers,
sharedErrorHandlers,
signInMode,
socialToBind,
]
);
const { result, run: asyncSignInWithVerificationCodeIdentifier } = useApi(
signInWithVerificationCodeIdentifier,
errorHandlers
);
useEffect(() => {
if (result?.redirectTo) {
window.location.replace(result.redirectTo);
}
}, [result]);
const onSubmit = useCallback(
async (verificationCode: string) => {
return asyncSignInWithVerificationCodeIdentifier(
{
email,
verificationCode,
},
socialToBind
);
},
[asyncSignInWithVerificationCodeIdentifier, email, socialToBind]
);
return {
errorMessage,
clearErrorMessage,
onSubmit,
};
};
export default useSignInWithEmailVerificationCode;

View file

@ -1,36 +1,15 @@
import { SignInIdentifier } from '@logto/schemas';
import { UserFlow } from '@/types';
import useContinueSetEmailVerificationCode from './use-continue-set-email-verification-code-validation';
import useContinueSetPhoneVerificationCode from './use-continue-set-phone-verification-code-validation';
import useForgotPasswordEmailVerificationCode from './use-forgot-password-email-verification-code-validation';
import useForgotPasswordPhoneVerificationCode from './use-forgot-password-phone-verification-code-validation';
import useRegisterWithEmailVerificationCode from './use-register-with-email-verification-code-validation';
import useRegisterWithPhoneVerificationCode from './use-register-with-phone-verification-code-validation';
import useSignInWithEmailVerificationCode from './use-sign-in-with-email-verification-code-validation';
import useSignInWithPhoneVerificationCode from './use-sign-in-with-phone-verification-code-validation';
import useContinueFlowCodeVerification from './use-continue-flow-code-verification';
import useForgotPasswordFlowCodeVerification from './use-forgot-password-flow-code-verification';
import useRegisterFlowCodeVerification from './use-register-flow-code-verification';
import useSignInFlowCodeVerification from './use-sign-in-flow-code-verification';
export const getVerificationCodeHook = (
type: UserFlow,
method: SignInIdentifier.Email | SignInIdentifier.Phone
) => {
switch (type) {
case UserFlow.signIn:
return method === SignInIdentifier.Email
? useSignInWithEmailVerificationCode
: useSignInWithPhoneVerificationCode;
case UserFlow.register:
return method === SignInIdentifier.Email
? useRegisterWithEmailVerificationCode
: useRegisterWithPhoneVerificationCode;
case UserFlow.forgotPassword:
return method === SignInIdentifier.Email
? useForgotPasswordEmailVerificationCode
: useForgotPasswordPhoneVerificationCode;
default:
return method === SignInIdentifier.Email
? useContinueSetEmailVerificationCode
: useContinueSetPhoneVerificationCode;
}
};
export const codeVerificationHooks = Object.freeze({
[UserFlow.signIn]: useSignInFlowCodeVerification,
[UserFlow.register]: useRegisterFlowCodeVerification,
[UserFlow.forgotPassword]: useForgotPasswordFlowCodeVerification,
[UserFlow.continue]: useContinueFlowCodeVerification,
});
export const getCodeVerificationHookByFlow = (flow: UserFlow) => codeVerificationHooks[flow];

View file

@ -1,6 +1,6 @@
import { t } from 'i18next';
import { useParams, useLocation } from 'react-router-dom';
import { is } from 'superstruct';
import { is, validate } from 'superstruct';
import SecondaryPageWrapper from '@/components/SecondaryPageWrapper';
import VerificationCodeContainer from '@/containers/VerificationCode';
@ -15,7 +15,7 @@ import {
import { formatPhoneNumberWithCountryCallingCode } from '@/utils/country-code';
type Parameters = {
type: UserFlow;
type: string;
method: string;
};
@ -24,11 +24,12 @@ const VerificationCode = () => {
const { signInMethods } = useSieMethods();
const { state } = useLocation();
const invalidType = !is(type, userFlowGuard);
const invalidMethod = !is(method, verificationCodeMethodGuard);
const invalidState = !is(state, verificationCodeStateGuard);
if (invalidType || invalidMethod) {
const [, flow] = validate(type, userFlowGuard);
if (!flow || invalidMethod) {
return <ErrorPage />;
}
@ -55,7 +56,7 @@ const VerificationCode = () => {
}}
>
<VerificationCodeContainer
type={type}
type={flow}
method={method}
target={target}
hasPasswordButton={type === UserFlow.signIn && methodSettings?.password}

View file

@ -1,6 +1,8 @@
import { SignInIdentifier, MissingProfile } from '@logto/schemas';
import * as s from 'superstruct';
import { UserFlow } from '.';
export const bindSocialStateGuard = s.object({
relatedUser: s.object({
type: s.union([s.literal('email'), s.literal('phone')]),
@ -24,11 +26,11 @@ export const SignInMethodGuard = s.union([
s.literal(SignInIdentifier.Username),
]);
export const userFlowGuard = s.union([
s.literal('sign-in'),
s.literal('register'),
s.literal('forgot-password'),
s.literal('continue'),
export const userFlowGuard = s.enums([
UserFlow.signIn,
UserFlow.register,
UserFlow.forgotPassword,
UserFlow.continue,
]);
export const continueMethodGuard = s.union([

View file

@ -1,4 +1,9 @@
import type { SignInExperience, ConnectorMetadata, AppearanceMode } from '@logto/schemas';
import type {
SignInExperience,
ConnectorMetadata,
AppearanceMode,
SignInIdentifier,
} from '@logto/schemas';
export enum UserFlow {
signIn = 'sign-in',
@ -18,6 +23,8 @@ export type Platform = 'web' | 'mobile';
// TODO: @simeng, @sijie, @charles should we combine this with admin console?
export type Theme = 'dark' | 'light';
export type VerificationCodeIdentifier = SignInIdentifier.Email | SignInIdentifier.Phone;
// Omit socialSignInConnectorTargets since it is being translated into socialConnectors
export type SignInExperienceResponse = Omit<SignInExperience, 'socialSignInConnectorTargets'> & {
socialConnectors: ConnectorMetadata[];