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