diff --git a/packages/experience/src/App.tsx b/packages/experience/src/App.tsx
index 3880823a57b..56e03faa518 100644
--- a/packages/experience/src/App.tsx
+++ b/packages/experience/src/App.tsx
@@ -7,12 +7,14 @@ import LoadingLayerProvider from './Providers/LoadingLayerProvider';
import PageContextProvider from './Providers/PageContextProvider';
import SettingsProvider from './Providers/SettingsProvider';
import UserInteractionContextProvider from './Providers/UserInteractionContextProvider';
+import { isDevFeaturesEnabled } from './constants/env';
import Callback from './pages/Callback';
import Consent from './pages/Consent';
import Continue from './pages/Continue';
import DirectSignIn from './pages/DirectSignIn';
import ErrorPage from './pages/ErrorPage';
import ForgotPassword from './pages/ForgotPassword';
+import IdentifierSignIn from './pages/IdentifierSignIn';
import MfaBinding from './pages/MfaBinding';
import BackupCodeBinding from './pages/MfaBinding/BackupCodeBinding';
import TotpBinding from './pages/MfaBinding/TotpBinding';
@@ -120,6 +122,16 @@ const App = () => {
{/* Consent */}
} />
+ {isDevFeaturesEnabled && (
+ <>
+ {/* Identifier sign-in */}
+ }
+ />
+ >
+ )}
+
} />
diff --git a/packages/experience/src/Layout/FirstScreenLayout/index.module.scss b/packages/experience/src/Layout/FirstScreenLayout/index.module.scss
new file mode 100644
index 00000000000..18ee6754532
--- /dev/null
+++ b/packages/experience/src/Layout/FirstScreenLayout/index.module.scss
@@ -0,0 +1,25 @@
+@use '@/scss/underscore' as _;
+
+.wrapper {
+ @include _.full-page;
+ @include _.flex-column(normal, normal);
+ @include _.full-width;
+
+ > *:last-child {
+ margin-bottom: 0;
+ }
+}
+
+:global(body.desktop) {
+ .wrapper {
+ padding: _.unit(6) 0;
+ }
+
+ .placeholderTop {
+ flex: 3;
+ }
+
+ .placeholderBottom {
+ flex: 5;
+ }
+}
diff --git a/packages/experience/src/Layout/FirstScreenLayout/index.tsx b/packages/experience/src/Layout/FirstScreenLayout/index.tsx
new file mode 100644
index 00000000000..8aaa76165f2
--- /dev/null
+++ b/packages/experience/src/Layout/FirstScreenLayout/index.tsx
@@ -0,0 +1,28 @@
+import { type ReactNode, useContext } from 'react';
+
+import PageContext from '@/Providers/PageContextProvider/PageContext';
+
+import PageMeta from '../../components/PageMeta';
+import type { Props as PageMetaProps } from '../../components/PageMeta';
+
+import styles from './index.module.scss';
+
+type Props = {
+ readonly children: ReactNode;
+ readonly pageMeta: PageMetaProps;
+};
+
+const FirstScreenLayout = ({ children, pageMeta }: Props) => {
+ const { platform } = useContext(PageContext);
+
+ return (
+ <>
+
+ {platform === 'web' &&
}
+ {children}
+ {platform === 'web' && }
+ >
+ );
+};
+
+export default FirstScreenLayout;
diff --git a/packages/experience/src/Layout/IdentifierPageLayout/index.module.scss b/packages/experience/src/Layout/IdentifierPageLayout/index.module.scss
new file mode 100644
index 00000000000..bf8b5680d9f
--- /dev/null
+++ b/packages/experience/src/Layout/IdentifierPageLayout/index.module.scss
@@ -0,0 +1,33 @@
+@use '@/scss/underscore' as _;
+
+.header {
+ margin: _.unit(6) 0;
+}
+
+.description {
+ margin-top: _.unit(2);
+ @include _.text-hint;
+}
+
+.terms {
+ margin-top: _.unit(4);
+ @include _.text-hint;
+ text-align: center;
+ font: var(--font-body-3);
+}
+
+.link {
+ margin-top: _.unit(7);
+}
+
+:global(body.mobile) {
+ .title {
+ @include _.title;
+ }
+}
+
+:global(body.desktop) {
+ .title {
+ @include _.title_desktop;
+ }
+}
diff --git a/packages/experience/src/Layout/IdentifierPageLayout/index.tsx b/packages/experience/src/Layout/IdentifierPageLayout/index.tsx
new file mode 100644
index 00000000000..2f73d9c9d7e
--- /dev/null
+++ b/packages/experience/src/Layout/IdentifierPageLayout/index.tsx
@@ -0,0 +1,55 @@
+import { type AgreeToTermsPolicy } from '@logto/schemas';
+import { type TFuncKey } from 'i18next';
+import { useMemo, type ReactNode } from 'react';
+
+import DynamicT from '@/components/DynamicT';
+import type { Props as PageMetaProps } from '@/components/PageMeta';
+import type { Props as TextLinkProps } from '@/components/TextLink';
+import TextLink from '@/components/TextLink';
+import TermsAndPrivacyLinks from '@/containers/TermsAndPrivacyLinks';
+import useTerms from '@/hooks/use-terms';
+
+import FirstScreenLayout from '../FirstScreenLayout';
+
+import styles from './index.module.scss';
+
+type Props = {
+ readonly children: ReactNode;
+ readonly pageMeta: PageMetaProps;
+ readonly title: TFuncKey;
+ readonly description: string;
+ readonly footerTermsDisplayPolicies: AgreeToTermsPolicy[];
+ readonly authOptionsLink: TextLinkProps;
+};
+
+const IdentifierPageLayout = ({
+ children,
+ pageMeta,
+ title,
+ description,
+ footerTermsDisplayPolicies,
+ authOptionsLink,
+}: Props) => {
+ const { agreeToTermsPolicy } = useTerms();
+
+ const shouldDisplayFooterTerms = useMemo(
+ () => agreeToTermsPolicy && footerTermsDisplayPolicies.includes(agreeToTermsPolicy),
+ [agreeToTermsPolicy, footerTermsDisplayPolicies]
+ );
+
+ return (
+
+
+ {children}
+ {shouldDisplayFooterTerms && }
+
+
+ );
+};
+
+export default IdentifierPageLayout;
diff --git a/packages/experience/src/Layout/LandingPageLayout/index.tsx b/packages/experience/src/Layout/LandingPageLayout/index.tsx
index 06cb52fcb78..dc40e0006a4 100644
--- a/packages/experience/src/Layout/LandingPageLayout/index.tsx
+++ b/packages/experience/src/Layout/LandingPageLayout/index.tsx
@@ -6,30 +6,24 @@ import { useContext } from 'react';
import PageContext from '@/Providers/PageContextProvider/PageContext';
import BrandingHeader from '@/components/BrandingHeader';
-import PageMeta from '@/components/PageMeta';
import { layoutClassNames } from '@/utils/consts';
import { getBrandingLogoUrl } from '@/utils/logo';
+import FirstScreenLayout from '../FirstScreenLayout';
+
import styles from './index.module.scss';
type ThirdPartyBranding = ConsentInfoResponse['application']['branding'];
type Props = {
readonly children: ReactNode;
- readonly className?: string;
readonly title: TFuncKey;
readonly titleInterpolation?: Record;
readonly thirdPartyBranding?: ThirdPartyBranding;
};
-const LandingPageLayout = ({
- children,
- className,
- title,
- titleInterpolation,
- thirdPartyBranding,
-}: Props) => {
- const { experienceSettings, theme, platform } = useContext(PageContext);
+const LandingPageLayout = ({ children, title, titleInterpolation, thirdPartyBranding }: Props) => {
+ const { experienceSettings, theme } = useContext(PageContext);
if (!experienceSettings) {
return null;
@@ -41,24 +35,19 @@ const LandingPageLayout = ({
} = experienceSettings;
return (
- <>
-
- {platform === 'web' && }
-
-
- {children}
-
- {platform === 'web' && }
- >
+
+
+ {children}
+
);
};
diff --git a/packages/experience/src/components/PageMeta/index.tsx b/packages/experience/src/components/PageMeta/index.tsx
index 04414bedb40..72a6d31ad94 100644
--- a/packages/experience/src/components/PageMeta/index.tsx
+++ b/packages/experience/src/components/PageMeta/index.tsx
@@ -2,7 +2,7 @@ import { type TFuncKey } from 'i18next';
import { Helmet } from 'react-helmet';
import { useTranslation } from 'react-i18next';
-type Props = {
+export type Props = {
readonly titleKey: TFuncKey;
readonly titleKeyInterpolation?: Record;
};
diff --git a/packages/experience/src/constants/env.ts b/packages/experience/src/constants/env.ts
new file mode 100644
index 00000000000..d30c92c950e
--- /dev/null
+++ b/packages/experience/src/constants/env.ts
@@ -0,0 +1,4 @@
+import { yes } from '@silverhand/essentials';
+
+export const isDevFeaturesEnabled =
+ process.env.NODE_ENV !== 'production' || yes(process.env.DEV_FEATURES_ENABLED);
diff --git a/packages/experience/src/hooks/use-identifier-params.ts b/packages/experience/src/hooks/use-identifier-params.ts
new file mode 100644
index 00000000000..80e95c56c69
--- /dev/null
+++ b/packages/experience/src/hooks/use-identifier-params.ts
@@ -0,0 +1,22 @@
+import { useSearchParams } from 'react-router-dom';
+
+import { identifierSearchParamGuard } from '@/types/guard';
+/**
+ * Extracts and validates sign-in identifiers from URL search parameters.
+ *
+ * Functionality:
+ * 1. Extracts all 'identifier' values from the URL search parameters.
+ * 2. Validates these values to ensure they are valid `SignInIdentifier`.
+ * 3. Returns an array of validated sign-in identifiers.
+ */
+const useIdentifierParams = () => {
+ const [searchParams] = useSearchParams();
+
+ // Todo @xiaoyijun use a constant for the key
+ const rawIdentifiers = searchParams.getAll('identifier');
+ const [, identifiers = []] = identifierSearchParamGuard.validate(rawIdentifiers);
+
+ return { identifiers };
+};
+
+export default useIdentifierParams;
diff --git a/packages/experience/src/pages/IdentifierSignIn/index.tsx b/packages/experience/src/pages/IdentifierSignIn/index.tsx
new file mode 100644
index 00000000000..d4e55797d2a
--- /dev/null
+++ b/packages/experience/src/pages/IdentifierSignIn/index.tsx
@@ -0,0 +1,56 @@
+import { AgreeToTermsPolicy, experience } from '@logto/schemas';
+import { useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Navigate } from 'react-router-dom';
+
+import IdentifierPageLayout from '@/Layout/IdentifierPageLayout';
+import { identifierInputDescriptionMap } from '@/utils/form';
+
+import IdentifierSignInForm from '../SignIn/IdentifierSignInForm';
+import PasswordSignInForm from '../SignIn/PasswordSignInForm';
+
+import useIdentifierSignInMethods from './use-identifier-sign-in-methods';
+
+const IdentifierSignIn = () => {
+ const { t } = useTranslation();
+
+ const signInMethods = useIdentifierSignInMethods();
+
+ const isPasswordOnly = useMemo(
+ () =>
+ signInMethods.length > 0 &&
+ signInMethods.every(({ password, verificationCode }) => password && !verificationCode),
+ [signInMethods]
+ );
+
+ // Fallback to sign-in page if no sign-in methods are available
+ if (signInMethods.length === 0) {
+ return ;
+ }
+
+ return (
+ t(identifierInputDescriptionMap[identifier])),
+ })}
+ footerTermsDisplayPolicies={[
+ AgreeToTermsPolicy.Automatic,
+ AgreeToTermsPolicy.ManualRegistrationOnly,
+ ]}
+ authOptionsLink={{
+ to: `/${experience.routes.signIn}`,
+ text: 'description.all_sign_in_options',
+ }}
+ >
+ {isPasswordOnly ? (
+ identifier)} />
+ ) : (
+
+ )}
+
+ );
+};
+
+export default IdentifierSignIn;
diff --git a/packages/experience/src/pages/IdentifierSignIn/use-identifier-sign-in-methods.ts b/packages/experience/src/pages/IdentifierSignIn/use-identifier-sign-in-methods.ts
new file mode 100644
index 00000000000..a5fd57d88e7
--- /dev/null
+++ b/packages/experience/src/pages/IdentifierSignIn/use-identifier-sign-in-methods.ts
@@ -0,0 +1,35 @@
+import { useMemo } from 'react';
+
+import useIdentifierParams from '@/hooks/use-identifier-params';
+import { useSieMethods } from '@/hooks/use-sie';
+
+/**
+ * Read sign-in methods from sign-in experience config and URL identifier parameters.
+ *
+ * Sign-in methods fallback logic:
+ * 1. If no identifiers are provided in the URL, return all sign-in methods from sign-in experience config.
+ * 2. If identifiers are provided in the URL but all of them are not supported by the sign-in experience config, return all sign-in methods from sign-in experience config.
+ * 3. If identifiers are provided in the URL and supported by the sign-in experience config, return the intersection of the two.
+ */
+const useIdentifierSignInMethods = () => {
+ const { signInMethods } = useSieMethods();
+ const { identifiers } = useIdentifierParams();
+
+ return useMemo(() => {
+ // Fallback to all sign-in methods if no identifiers are provided
+ if (identifiers.length === 0) {
+ return signInMethods;
+ }
+
+ const methods = signInMethods.filter(({ identifier }) => identifiers.includes(identifier));
+
+ // Fallback to all sign-in methods if no identifiers are supported
+ if (methods.length === 0) {
+ return signInMethods;
+ }
+
+ return methods;
+ }, [identifiers, signInMethods]);
+};
+
+export default useIdentifierSignInMethods;
diff --git a/packages/experience/src/types/guard.ts b/packages/experience/src/types/guard.ts
index b9b13e1b772..03837d4d68e 100644
--- a/packages/experience/src/types/guard.ts
+++ b/packages/experience/src/types/guard.ts
@@ -106,6 +106,11 @@ export const ssoConnectorMetadataGuard: s.Describe = s.obj
connectorName: s.string(),
});
+const identifierEnumGuard = s.enums([
+ SignInIdentifier.Email,
+ SignInIdentifier.Phone,
+ SignInIdentifier.Username,
+]);
/**
* Defines the type guard for user identifier input value caching.
*
@@ -117,8 +122,11 @@ export const ssoConnectorMetadataGuard: s.Describe = s.obj
* page or the password page, the identifier they entered will not be cleared.
*/
export const identifierInputValueGuard: s.Describe = s.object({
- type: s.optional(
- s.enums([SignInIdentifier.Email, SignInIdentifier.Phone, SignInIdentifier.Username])
- ),
+ type: s.optional(identifierEnumGuard),
value: s.string(),
});
+
+/**
+ * Type guard for the `identifier` search param config on the identifier sign-in/register page.
+ */
+export const identifierSearchParamGuard = s.array(identifierEnumGuard);
diff --git a/packages/phrases-experience/src/locales/de/description.ts b/packages/phrases-experience/src/locales/de/description.ts
index 1a6780ade4d..bde66dd9c3c 100644
--- a/packages/phrases-experience/src/locales/de/description.ts
+++ b/packages/phrases-experience/src/locales/de/description.ts
@@ -102,6 +102,9 @@ const description = {
/** UNTRANSLATED */
redirect_to: 'You will be redirected to {{name}}.',
auto_agreement: 'Indem Sie fortfahren, stimmen Sie den zu.',
+ identifier_sign_in_description:
+ 'Geben Sie Ihre {{types, list(type: disjunction;)}} ein, um sich anzumelden.',
+ all_sign_in_options: 'Alle Anmeldeoptionen',
};
export default Object.freeze(description);
diff --git a/packages/phrases-experience/src/locales/en/description.ts b/packages/phrases-experience/src/locales/en/description.ts
index b22b4db72ad..8689e7e1e2a 100644
--- a/packages/phrases-experience/src/locales/en/description.ts
+++ b/packages/phrases-experience/src/locales/en/description.ts
@@ -88,6 +88,8 @@ const description = {
user_id: 'User ID: {{id}}',
redirect_to: 'You will be redirected to {{name}}.',
auto_agreement: 'By continuing, you agree to the .',
+ identifier_sign_in_description: 'Enter you {{types, list(type: disjunction;)}} to sign in.',
+ all_sign_in_options: 'All sign-in options',
};
export default Object.freeze(description);
diff --git a/packages/phrases-experience/src/locales/es/description.ts b/packages/phrases-experience/src/locales/es/description.ts
index d9c91a7738f..fbec1edf258 100644
--- a/packages/phrases-experience/src/locales/es/description.ts
+++ b/packages/phrases-experience/src/locales/es/description.ts
@@ -102,6 +102,9 @@ const description = {
/** UNTRANSLATED */
redirect_to: 'You will be redirected to {{name}}.',
auto_agreement: 'Al continuar, acepta los .',
+ identifier_sign_in_description:
+ 'Ingrese su {{types, list(type: disjunction;)}} para iniciar sesión.',
+ all_sign_in_options: 'Todas las opciones de inicio de sesión',
};
export default Object.freeze(description);
diff --git a/packages/phrases-experience/src/locales/fr/description.ts b/packages/phrases-experience/src/locales/fr/description.ts
index aaed9bbf9bb..867e96ec756 100644
--- a/packages/phrases-experience/src/locales/fr/description.ts
+++ b/packages/phrases-experience/src/locales/fr/description.ts
@@ -102,6 +102,9 @@ const description = {
/** UNTRANSLATED */
redirect_to: 'You will be redirected to {{name}}.',
auto_agreement: 'En continuant, vous acceptez les .',
+ identifier_sign_in_description:
+ 'Entrez votre {{types, list(type: disjunction;)}} pour vous connecter.',
+ all_sign_in_options: 'Toutes les options de connexion',
};
export default Object.freeze(description);
diff --git a/packages/phrases-experience/src/locales/it/description.ts b/packages/phrases-experience/src/locales/it/description.ts
index 13fc169e5f1..8c5fa999e46 100644
--- a/packages/phrases-experience/src/locales/it/description.ts
+++ b/packages/phrases-experience/src/locales/it/description.ts
@@ -99,6 +99,9 @@ const description = {
/** UNTRANSLATED */
redirect_to: 'You will be redirected to {{name}}.',
auto_agreement: 'Continuando, accetti i .',
+ identifier_sign_in_description:
+ 'Inserisci il tuo {{types, list(type: disjunction;)}} per accedere.',
+ all_sign_in_options: 'Tutte le opzioni di accesso',
};
export default Object.freeze(description);
diff --git a/packages/phrases-experience/src/locales/ja/description.ts b/packages/phrases-experience/src/locales/ja/description.ts
index b7b034cac0c..4bf41550720 100644
--- a/packages/phrases-experience/src/locales/ja/description.ts
+++ b/packages/phrases-experience/src/locales/ja/description.ts
@@ -99,6 +99,8 @@ const description = {
/** UNTRANSLATED */
redirect_to: 'You will be redirected to {{name}}.',
auto_agreement: '続行することで、に同意したことになります。',
+ identifier_sign_in_description: '{{types, list(type: disjunction;)}}を入力してサインインします。',
+ all_sign_in_options: 'すべてのサインインオプション',
};
export default Object.freeze(description);
diff --git a/packages/phrases-experience/src/locales/ko/description.ts b/packages/phrases-experience/src/locales/ko/description.ts
index 0216c0dbdd8..460663a2c99 100644
--- a/packages/phrases-experience/src/locales/ko/description.ts
+++ b/packages/phrases-experience/src/locales/ko/description.ts
@@ -93,6 +93,9 @@ const description = {
/** UNTRANSLATED */
redirect_to: 'You will be redirected to {{name}}.',
auto_agreement: '계속 진행하면 에 동의하는 것입니다.',
+ identifier_sign_in_description:
+ '로그인하려면 {{types, list(type: disjunction;)}}을(를) 입력하세요.',
+ all_sign_in_options: '모든 로그인 옵션',
};
export default Object.freeze(description);
diff --git a/packages/phrases-experience/src/locales/pl-pl/description.ts b/packages/phrases-experience/src/locales/pl-pl/description.ts
index fc06aaf41ad..8ec9d862dcd 100644
--- a/packages/phrases-experience/src/locales/pl-pl/description.ts
+++ b/packages/phrases-experience/src/locales/pl-pl/description.ts
@@ -100,6 +100,9 @@ const description = {
/** UNTRANSLATED */
redirect_to: 'You will be redirected to {{name}}.',
auto_agreement: 'Kontynuując, zgadzasz się na .',
+ identifier_sign_in_description:
+ 'Wprowadź swoje {{types, list(type: disjunction;)}} aby się zalogować.',
+ all_sign_in_options: 'Wszystkie opcje logowania',
};
export default Object.freeze(description);
diff --git a/packages/phrases-experience/src/locales/pt-br/description.ts b/packages/phrases-experience/src/locales/pt-br/description.ts
index 26f34d2e3a2..2bfa3756165 100644
--- a/packages/phrases-experience/src/locales/pt-br/description.ts
+++ b/packages/phrases-experience/src/locales/pt-br/description.ts
@@ -97,6 +97,8 @@ const description = {
/** UNTRANSLATED */
redirect_to: 'You will be redirected to {{name}}.',
auto_agreement: 'Ao continuar, você concorda com os .',
+ identifier_sign_in_description: 'Digite seu {{types, list(type: disjunction;)}} para entrar.',
+ all_sign_in_options: 'Todas as opções de login',
};
export default Object.freeze(description);
diff --git a/packages/phrases-experience/src/locales/pt-pt/description.ts b/packages/phrases-experience/src/locales/pt-pt/description.ts
index b5b480d9931..70a3d64ad0a 100644
--- a/packages/phrases-experience/src/locales/pt-pt/description.ts
+++ b/packages/phrases-experience/src/locales/pt-pt/description.ts
@@ -97,6 +97,9 @@ const description = {
/** UNTRANSLATED */
redirect_to: 'You will be redirected to {{name}}.',
auto_agreement: 'Ao continuar, você concorda com os .',
+ identifier_sign_in_description:
+ 'Introduza o seu {{types, list(type: disjunction;)}} para iniciar sessão.',
+ all_sign_in_options: 'Todas as opções de início de sessão',
};
export default Object.freeze(description);
diff --git a/packages/phrases-experience/src/locales/ru/description.ts b/packages/phrases-experience/src/locales/ru/description.ts
index 91ab68a3310..dab9966c37c 100644
--- a/packages/phrases-experience/src/locales/ru/description.ts
+++ b/packages/phrases-experience/src/locales/ru/description.ts
@@ -101,6 +101,8 @@ const description = {
/** UNTRANSLATED */
redirect_to: 'You will be redirected to {{name}}.',
auto_agreement: 'Продолжая, вы соглашаетесь с .',
+ identifier_sign_in_description: 'Введите свои {{types, list(type: disjunction;)}} для входа.',
+ all_sign_in_options: 'Все варианты входа',
};
export default Object.freeze(description);
diff --git a/packages/phrases-experience/src/locales/tr-tr/description.ts b/packages/phrases-experience/src/locales/tr-tr/description.ts
index 2e6f98ffb66..cecb875900e 100644
--- a/packages/phrases-experience/src/locales/tr-tr/description.ts
+++ b/packages/phrases-experience/src/locales/tr-tr/description.ts
@@ -97,6 +97,8 @@ const description = {
/** UNTRANSLATED */
redirect_to: 'You will be redirected to {{name}}.',
auto_agreement: 'Devam ederek kabul etmiş oluyorsunuz.',
+ identifier_sign_in_description: 'Oturum açmak için {{types, list(type: disjunction;)}} girin.',
+ all_sign_in_options: 'Tüm oturum açma seçenekleri',
};
export default Object.freeze(description);
diff --git a/packages/phrases-experience/src/locales/zh-cn/description.ts b/packages/phrases-experience/src/locales/zh-cn/description.ts
index 884b0c7964d..f0c9b23ed5b 100644
--- a/packages/phrases-experience/src/locales/zh-cn/description.ts
+++ b/packages/phrases-experience/src/locales/zh-cn/description.ts
@@ -77,6 +77,8 @@ const description = {
user_id: '用户 ID: {{id}}',
redirect_to: '你将被重定向到 {{name}}。',
auto_agreement: '继续即表示您同意。',
+ identifier_sign_in_description: '输入您的{{types, list(type: disjunction;)}}以登录。',
+ all_sign_in_options: '所有登录选项',
};
export default Object.freeze(description);
diff --git a/packages/phrases-experience/src/locales/zh-hk/description.ts b/packages/phrases-experience/src/locales/zh-hk/description.ts
index 1e28053f98f..1ef2ce8bd30 100644
--- a/packages/phrases-experience/src/locales/zh-hk/description.ts
+++ b/packages/phrases-experience/src/locales/zh-hk/description.ts
@@ -89,6 +89,8 @@ const description = {
/** UNTRANSLATED */
redirect_to: 'You will be redirected to {{name}}.',
auto_agreement: '繼續即表示您同意。',
+ identifier_sign_in_description: '輸入您的{{types, list(type: disjunction;)}}以登入。',
+ all_sign_in_options: '所有登入選項',
};
export default Object.freeze(description);
diff --git a/packages/phrases-experience/src/locales/zh-tw/description.ts b/packages/phrases-experience/src/locales/zh-tw/description.ts
index f4cbe0fa059..23c706afda8 100644
--- a/packages/phrases-experience/src/locales/zh-tw/description.ts
+++ b/packages/phrases-experience/src/locales/zh-tw/description.ts
@@ -89,6 +89,8 @@ const description = {
/** UNTRANSLATED */
redirect_to: 'You will be redirected to {{name}}.',
auto_agreement: '繼續即表示您同意。',
+ identifier_sign_in_description: '輸入您的{{types, list(type: disjunction;)}}以登入。',
+ all_sign_in_options: '所有登入選項',
};
export default Object.freeze(description);
diff --git a/packages/schemas/src/consts/experience.ts b/packages/schemas/src/consts/experience.ts
index bc84b6fa15e..c992636b6ed 100644
--- a/packages/schemas/src/consts/experience.ts
+++ b/packages/schemas/src/consts/experience.ts
@@ -3,6 +3,7 @@ const routes = Object.freeze({
register: 'register',
sso: 'single-sign-on',
consent: 'consent',
+ identifierSignIn: 'identifier-sign-in',
});
export const experience = Object.freeze({