From b6c684874ae4fffdb135522de2ae96b0bd7a3d3f Mon Sep 17 00:00:00 2001
From: simeng-li <simeng@silverhand.io>
Date: Wed, 11 Jan 2023 15:41:02 +0800
Subject: [PATCH] refactor(ui): simplify code verification hooks (#2898)

---
 packages/ui/src/apis/interaction.ts           |   2 +-
 .../src/containers/VerificationCode/index.tsx |  21 ++-
 .../use-continue-flow-code-verification.ts    |  81 ++++++++++++
 ...-set-email-verification-code-validation.ts |  64 ---------
 ...-set-phone-verification-code-validation.ts |  59 ---------
 ...word-email-verification-code-validation.ts |  60 ---------
 ...-forgot-password-flow-code-verification.ts |  71 ++++++++++
 ...word-phone-verification-code-validation.ts |  60 ---------
 ...eneral-verification-code-error-handler.ts} |   8 +-
 .../use-identifier-error-alert.ts             |  54 ++++----
 .../use-register-flow-code-verification.ts    | 122 ++++++++++++++++++
 ...with-email-verification-code-validation.ts | 108 ----------------
 ...with-phone-verification-code-validation.ts | 111 ----------------
 ... => use-sign-in-flow-code-verification.ts} |  86 ++++++------
 ...with-email-verification-code-validation.ts | 119 -----------------
 .../src/containers/VerificationCode/utils.ts  |  45 ++-----
 .../ui/src/pages/VerificationCode/index.tsx   |  11 +-
 packages/ui/src/types/guard.ts                |  12 +-
 packages/ui/src/types/index.ts                |   9 +-
 19 files changed, 407 insertions(+), 696 deletions(-)
 create mode 100644 packages/ui/src/containers/VerificationCode/use-continue-flow-code-verification.ts
 delete mode 100644 packages/ui/src/containers/VerificationCode/use-continue-set-email-verification-code-validation.ts
 delete mode 100644 packages/ui/src/containers/VerificationCode/use-continue-set-phone-verification-code-validation.ts
 delete mode 100644 packages/ui/src/containers/VerificationCode/use-forgot-password-email-verification-code-validation.ts
 create mode 100644 packages/ui/src/containers/VerificationCode/use-forgot-password-flow-code-verification.ts
 delete mode 100644 packages/ui/src/containers/VerificationCode/use-forgot-password-phone-verification-code-validation.ts
 rename packages/ui/src/containers/VerificationCode/{use-shared-error-handler.ts => use-general-verification-code-error-handler.ts} (72%)
 create mode 100644 packages/ui/src/containers/VerificationCode/use-register-flow-code-verification.ts
 delete mode 100644 packages/ui/src/containers/VerificationCode/use-register-with-email-verification-code-validation.ts
 delete mode 100644 packages/ui/src/containers/VerificationCode/use-register-with-phone-verification-code-validation.ts
 rename packages/ui/src/containers/VerificationCode/{use-sign-in-with-phone-verification-code-validation.ts => use-sign-in-flow-code-verification.ts} (50%)
 delete mode 100644 packages/ui/src/containers/VerificationCode/use-sign-in-with-email-verification-code-validation.ts

diff --git a/packages/ui/src/apis/interaction.ts b/packages/ui/src/apis/interaction.ts
index cf66711c7..c917ce7c6 100644
--- a/packages/ui/src/apis/interaction.ts
+++ b/packages/ui/src/apis/interaction.ts
@@ -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`, {
diff --git a/packages/ui/src/containers/VerificationCode/index.tsx b/packages/ui/src/containers/VerificationCode/index.tsx
index 0c356a1f1..bad802940 100644
--- a/packages/ui/src/containers/VerificationCode/index.tsx
+++ b/packages/ui/src/containers/VerificationCode/index.tsx
@@ -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)}>
diff --git a/packages/ui/src/containers/VerificationCode/use-continue-flow-code-verification.ts b/packages/ui/src/containers/VerificationCode/use-continue-flow-code-verification.ts
new file mode 100644
index 000000000..abb4d67a4
--- /dev/null
+++ b/packages/ui/src/containers/VerificationCode/use-continue-flow-code-verification.ts
@@ -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;
diff --git a/packages/ui/src/containers/VerificationCode/use-continue-set-email-verification-code-validation.ts b/packages/ui/src/containers/VerificationCode/use-continue-set-email-verification-code-validation.ts
deleted file mode 100644
index 3f3938e26..000000000
--- a/packages/ui/src/containers/VerificationCode/use-continue-set-email-verification-code-validation.ts
+++ /dev/null
@@ -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;
diff --git a/packages/ui/src/containers/VerificationCode/use-continue-set-phone-verification-code-validation.ts b/packages/ui/src/containers/VerificationCode/use-continue-set-phone-verification-code-validation.ts
deleted file mode 100644
index 541253441..000000000
--- a/packages/ui/src/containers/VerificationCode/use-continue-set-phone-verification-code-validation.ts
+++ /dev/null
@@ -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;
diff --git a/packages/ui/src/containers/VerificationCode/use-forgot-password-email-verification-code-validation.ts b/packages/ui/src/containers/VerificationCode/use-forgot-password-email-verification-code-validation.ts
deleted file mode 100644
index a2993652c..000000000
--- a/packages/ui/src/containers/VerificationCode/use-forgot-password-email-verification-code-validation.ts
+++ /dev/null
@@ -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;
diff --git a/packages/ui/src/containers/VerificationCode/use-forgot-password-flow-code-verification.ts b/packages/ui/src/containers/VerificationCode/use-forgot-password-flow-code-verification.ts
new file mode 100644
index 000000000..0f0fe64c9
--- /dev/null
+++ b/packages/ui/src/containers/VerificationCode/use-forgot-password-flow-code-verification.ts
@@ -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;
diff --git a/packages/ui/src/containers/VerificationCode/use-forgot-password-phone-verification-code-validation.ts b/packages/ui/src/containers/VerificationCode/use-forgot-password-phone-verification-code-validation.ts
deleted file mode 100644
index 7fcc7cc64..000000000
--- a/packages/ui/src/containers/VerificationCode/use-forgot-password-phone-verification-code-validation.ts
+++ /dev/null
@@ -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;
diff --git a/packages/ui/src/containers/VerificationCode/use-shared-error-handler.ts b/packages/ui/src/containers/VerificationCode/use-general-verification-code-error-handler.ts
similarity index 72%
rename from packages/ui/src/containers/VerificationCode/use-shared-error-handler.ts
rename to packages/ui/src/containers/VerificationCode/use-general-verification-code-error-handler.ts
index 71f87f946..9c5fd75a7 100644
--- a/packages/ui/src/containers/VerificationCode/use-shared-error-handler.ts
+++ b/packages/ui/src/containers/VerificationCode/use-general-verification-code-error-handler.ts
@@ -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;
diff --git a/packages/ui/src/containers/VerificationCode/use-identifier-error-alert.ts b/packages/ui/src/containers/VerificationCode/use-identifier-error-alert.ts
index 39b9a4d64..240bd666b 100644
--- a/packages/ui/src/containers/VerificationCode/use-identifier-error-alert.ts
+++ b/packages/ui/src/containers/VerificationCode/use-identifier-error-alert.ts
@@ -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;
diff --git a/packages/ui/src/containers/VerificationCode/use-register-flow-code-verification.ts b/packages/ui/src/containers/VerificationCode/use-register-flow-code-verification.ts
new file mode 100644
index 000000000..e33cde0c2
--- /dev/null
+++ b/packages/ui/src/containers/VerificationCode/use-register-flow-code-verification.ts
@@ -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;
diff --git a/packages/ui/src/containers/VerificationCode/use-register-with-email-verification-code-validation.ts b/packages/ui/src/containers/VerificationCode/use-register-with-email-verification-code-validation.ts
deleted file mode 100644
index e709bbd05..000000000
--- a/packages/ui/src/containers/VerificationCode/use-register-with-email-verification-code-validation.ts
+++ /dev/null
@@ -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;
diff --git a/packages/ui/src/containers/VerificationCode/use-register-with-phone-verification-code-validation.ts b/packages/ui/src/containers/VerificationCode/use-register-with-phone-verification-code-validation.ts
deleted file mode 100644
index 977f4cc79..000000000
--- a/packages/ui/src/containers/VerificationCode/use-register-with-phone-verification-code-validation.ts
+++ /dev/null
@@ -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;
diff --git a/packages/ui/src/containers/VerificationCode/use-sign-in-with-phone-verification-code-validation.ts b/packages/ui/src/containers/VerificationCode/use-sign-in-flow-code-verification.ts
similarity index 50%
rename from packages/ui/src/containers/VerificationCode/use-sign-in-with-phone-verification-code-validation.ts
rename to packages/ui/src/containers/VerificationCode/use-sign-in-flow-code-verification.ts
index d11ba4483..f422cf79c 100644
--- a/packages/ui/src/containers/VerificationCode/use-sign-in-with-phone-verification-code-validation.ts
+++ b/packages/ui/src/containers/VerificationCode/use-sign-in-flow-code-verification.ts
@@ -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;
diff --git a/packages/ui/src/containers/VerificationCode/use-sign-in-with-email-verification-code-validation.ts b/packages/ui/src/containers/VerificationCode/use-sign-in-with-email-verification-code-validation.ts
deleted file mode 100644
index bb834c0ab..000000000
--- a/packages/ui/src/containers/VerificationCode/use-sign-in-with-email-verification-code-validation.ts
+++ /dev/null
@@ -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;
diff --git a/packages/ui/src/containers/VerificationCode/utils.ts b/packages/ui/src/containers/VerificationCode/utils.ts
index 5a04246d9..f78d5ac43 100644
--- a/packages/ui/src/containers/VerificationCode/utils.ts
+++ b/packages/ui/src/containers/VerificationCode/utils.ts
@@ -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];
diff --git a/packages/ui/src/pages/VerificationCode/index.tsx b/packages/ui/src/pages/VerificationCode/index.tsx
index 590e17c6f..34448a45a 100644
--- a/packages/ui/src/pages/VerificationCode/index.tsx
+++ b/packages/ui/src/pages/VerificationCode/index.tsx
@@ -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}
diff --git a/packages/ui/src/types/guard.ts b/packages/ui/src/types/guard.ts
index 11bfa1aa4..a1a48c94a 100644
--- a/packages/ui/src/types/guard.ts
+++ b/packages/ui/src/types/guard.ts
@@ -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([
diff --git a/packages/ui/src/types/index.ts b/packages/ui/src/types/index.ts
index 78e52188a..ede6e284b 100644
--- a/packages/ui/src/types/index.ts
+++ b/packages/ui/src/types/index.ts
@@ -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[];