) => {
clearErrorMessage();
void handleSubmit(async ({ identifier: { type, value }, password }) => {
+ if (showSingleSignOn) {
+ navigateToSingleSignOn();
+ return;
+ }
+
if (!type) {
return;
}
@@ -62,7 +70,7 @@ const PasswordSignInForm = ({ className, autoFocus, signInMethods }: Props) => {
});
})(event);
},
- [clearErrorMessage, handleSubmit, onSubmit]
+ [clearErrorMessage, handleSubmit, navigateToSingleSignOn, onSubmit, showSingleSignOn]
);
useEffect(() => {
@@ -98,19 +106,24 @@ const PasswordSignInForm = ({ className, autoFocus, signInMethods }: Props) => {
/>
)}
/>
+ {showSingleSignOn && (
+ {t('description.single_sign_on_enabled')}
+ )}
-
+ {!showSingleSignOn && (
+
+ )}
{errorMessage && {errorMessage}}
- {isForgotPasswordEnabled && (
+ {isForgotPasswordEnabled && !showSingleSignOn && (
{
/>
)}
-
+
diff --git a/packages/experience/src/pages/SignIn/PasswordSignInForm/use-single-sign-on-watch.ts b/packages/experience/src/pages/SignIn/PasswordSignInForm/use-single-sign-on-watch.ts
new file mode 100644
index 000000000..922066a3d
--- /dev/null
+++ b/packages/experience/src/pages/SignIn/PasswordSignInForm/use-single-sign-on-watch.ts
@@ -0,0 +1,92 @@
+import { SignInIdentifier, type SsoConnectorMetadata } from '@logto/schemas';
+import { useEffect, useState, useCallback, useContext } from 'react';
+import { type Control, useWatch } from 'react-hook-form';
+import { useNavigate } from 'react-router-dom';
+
+import SingleSignOnContext from '@/Providers/SingleSignOnContextProvider/SingleSignOnContext';
+import { getSingleSignOnConnectors } from '@/apis/single-sign-on';
+import { singleSignOnPath } from '@/constants/env';
+import useApi from '@/hooks/use-api';
+import { validateEmail } from '@/utils/form';
+
+import type { FormState } from './index';
+
+const useSingleSignOnWatch = (control: Control) => {
+ const navigate = useNavigate();
+ const { setEmail, setSsoConnectors, availableSsoConnectorsMap } = useContext(SingleSignOnContext);
+ const [showSingleSignOn, setShowSingleSignOn] = useState(false);
+ const request = useApi(getSingleSignOnConnectors);
+
+ const isSsoEnabled = availableSsoConnectorsMap.size > 0;
+
+ const identifierInput = useWatch({
+ control,
+ name: 'identifier',
+ });
+
+ /**
+ * Silently check if the email is registered with any SSO connectors
+ */
+ const fetchSsoConnectors = useCallback(
+ async (email: string) => {
+ const [, result] = await request(email);
+
+ if (!result) {
+ return false;
+ }
+
+ const connectors = result
+ .map((connectorId) => availableSsoConnectorsMap.get(connectorId))
+ // eslint-disable-next-line unicorn/prefer-native-coercion-functions -- make the type more specific
+ .filter((connector): connector is SsoConnectorMetadata => Boolean(connector));
+
+ if (connectors.length === 0) {
+ return false;
+ }
+
+ setSsoConnectors(connectors);
+ setEmail(email);
+ return true;
+ },
+ [availableSsoConnectorsMap, request, setEmail, setSsoConnectors]
+ );
+
+ const navigateToSingleSignOn = useCallback(() => {
+ navigate(`/${singleSignOnPath}/connectors`);
+ }, [navigate]);
+
+ useEffect(() => {
+ if (!isSsoEnabled) {
+ return;
+ }
+
+ const { type, value } = identifierInput;
+
+ if (type !== SignInIdentifier.Email) {
+ setShowSingleSignOn(false);
+ return;
+ }
+
+ // Will throw an error if the value is not a valid email
+ if (validateEmail(value)) {
+ setShowSingleSignOn(false);
+ return;
+ }
+
+ // Add a debouncing delay to avoid unnecessary API calls
+ const handler = setTimeout(async () => {
+ setShowSingleSignOn(await fetchSsoConnectors(value));
+ }, 300);
+
+ return () => {
+ clearTimeout(handler);
+ };
+ }, [fetchSsoConnectors, identifierInput, isSsoEnabled]);
+
+ return {
+ showSingleSignOn,
+ navigateToSingleSignOn,
+ };
+};
+
+export default useSingleSignOnWatch;
diff --git a/packages/phrases-experience/src/locales/de/description.ts b/packages/phrases-experience/src/locales/de/description.ts
index 4b363b266..e8a9d6789 100644
--- a/packages/phrases-experience/src/locales/de/description.ts
+++ b/packages/phrases-experience/src/locales/de/description.ts
@@ -75,6 +75,7 @@ const description = {
single_sign_on_email_form: 'Gib deine Unternehmens-E-Mail-Adresse ein.',
single_sign_on_connectors_list:
'Ihr Unternehmen hat Single Sign-On für das E-Mail-Konto {{email}} aktiviert. Sie können sich weiterhin mit den folgenden SSO-Anbietern anmelden.',
+ single_sign_on_enabled: 'Single Sign-On ist für dieses Konto aktiviert',
};
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 b4fd64ee1..1fd729863 100644
--- a/packages/phrases-experience/src/locales/en/description.ts
+++ b/packages/phrases-experience/src/locales/en/description.ts
@@ -72,6 +72,7 @@ const description = {
single_sign_on_email_form: 'Enter your enterprise email address',
single_sign_on_connectors_list:
'Your enterprise has enabled Single Sign-On for the email account {{email}}. You can continue to sign in with the following SSO providers.',
+ single_sign_on_enabled: 'Single Sign-On is enabled for this account',
};
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 810a17e57..0d6a54730 100644
--- a/packages/phrases-experience/src/locales/es/description.ts
+++ b/packages/phrases-experience/src/locales/es/description.ts
@@ -73,6 +73,8 @@ const description = {
single_sign_on_email_form: 'Ingrese su dirección de correo electrónico corporativo',
single_sign_on_connectors_list:
'Su empresa ha habilitado el inicio de sesión único (Single Sign-On) para la cuenta de correo electrónico {{email}}. Puede continuar iniciando sesión con los siguientes proveedores de SSO.',
+ single_sign_on_enabled:
+ 'El inicio de sesión único (Single Sign-On) está habilitado para esta cuenta',
};
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 cda45bdff..c83d280be 100644
--- a/packages/phrases-experience/src/locales/fr/description.ts
+++ b/packages/phrases-experience/src/locales/fr/description.ts
@@ -75,6 +75,7 @@ const description = {
single_sign_on_email_form: "Entrez votre adresse e-mail d'entreprise",
single_sign_on_connectors_list:
'Votre entreprise a activé la connexion unique (Single Sign-On) pour le compte email {{email}}. Vous pouvez continuer à vous connecter avec les fournisseurs SSO suivants.',
+ single_sign_on_enabled: 'La connexion unique (Single Sign-On) est activée pour ce compte',
};
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 83f1141f2..2f6aff72f 100644
--- a/packages/phrases-experience/src/locales/it/description.ts
+++ b/packages/phrases-experience/src/locales/it/description.ts
@@ -71,6 +71,7 @@ const description = {
single_sign_on_email_form: 'Inserisci il tuo indirizzo email aziendale',
single_sign_on_connectors_list:
"La tua azienda ha abilitato il Single Sign-On per l'account email {{email}}. Puoi continuare ad accedere con i seguenti fornitori di SSO.",
+ single_sign_on_enabled: 'Il Single Sign-On è abilitato per questo account',
};
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 a820cd3c7..5113d9dab 100644
--- a/packages/phrases-experience/src/locales/ja/description.ts
+++ b/packages/phrases-experience/src/locales/ja/description.ts
@@ -71,6 +71,7 @@ const description = {
single_sign_on_email_form: '企業のメールアドレスを入力してください',
single_sign_on_connectors_list:
'あなたの企業は、メールアカウント{{email}}に対してシングルサインオンを有効にしました。以下のSSOプロバイダーを使用してサインインを続けることができます。',
+ single_sign_on_enabled: 'このアカウントではシングル サインオンが有効になっています',
};
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 16c747e3f..fc4935c82 100644
--- a/packages/phrases-experience/src/locales/ko/description.ts
+++ b/packages/phrases-experience/src/locales/ko/description.ts
@@ -66,6 +66,7 @@ const description = {
single_sign_on_email_form: '기업 이메일 주소를 입력하세요',
single_sign_on_connectors_list:
'귀하의 기업은 {{email}} 이메일 계정에 대해 Single Sign-On을 활성화했습니다. 다음 SSO 제공업체를 사용하여 로그인을 계속할 수 있습니다.',
+ single_sign_on_enabled: '이 계정에는 Single Sign-On이 활성화되어 있습니다.',
};
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 fd3be873e..355d4fb64 100644
--- a/packages/phrases-experience/src/locales/pl-pl/description.ts
+++ b/packages/phrases-experience/src/locales/pl-pl/description.ts
@@ -71,6 +71,7 @@ const description = {
single_sign_on_email_form: 'Wpisz swój służbowy adres email',
single_sign_on_connectors_list:
'Twoja firma włączyła jednokrotne logowanie dla konta e-mail {{email}}. Możesz kontynuować logowanie za pomocą następujących dostawców SSO.',
+ single_sign_on_enabled: 'To konto ma włączone jednokrotne logowanie.',
};
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 0e5f63801..a90d99cea 100644
--- a/packages/phrases-experience/src/locales/pt-br/description.ts
+++ b/packages/phrases-experience/src/locales/pt-br/description.ts
@@ -70,6 +70,7 @@ const description = {
single_sign_on_email_form: 'Insira o endereço de e-mail corporativo',
single_sign_on_connectors_list:
'Sua empresa ativou o Single Sign-On para a conta de email {{email}}. Você pode continuar a fazer login com os seguintes provedores de SSO.',
+ single_sign_on_enabled: 'Esta conta tem Single Sign-On ativado.',
};
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 d94a85677..703896a8b 100644
--- a/packages/phrases-experience/src/locales/pt-pt/description.ts
+++ b/packages/phrases-experience/src/locales/pt-pt/description.ts
@@ -70,6 +70,7 @@ const description = {
single_sign_on_email_form: 'Insira o endereço de email corporativo',
single_sign_on_connectors_list:
'A sua empresa ativou o Single Sign-On para a conta de email {{email}}. Pode continuar a iniciar sessão com os seguintes fornecedores de SSO.',
+ single_sign_on_enabled: 'Esta conta tem o Single Sign-On ativado.',
};
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 83e355048..f305e53fa 100644
--- a/packages/phrases-experience/src/locales/ru/description.ts
+++ b/packages/phrases-experience/src/locales/ru/description.ts
@@ -74,6 +74,7 @@ const description = {
single_sign_on_email_form: 'Введите корпоративный адрес электронной почты',
single_sign_on_connectors_list:
'Ваше предприятие включило функцию единого входа для электронной почты {{email}}. Вы можете продолжить вход в систему с помощью следующих провайдеров SSO.',
+ single_sign_on_enabled: 'Единый вход в систему включен для этой учетной записи',
};
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 a71a11b3f..0361d9cf0 100644
--- a/packages/phrases-experience/src/locales/tr-tr/description.ts
+++ b/packages/phrases-experience/src/locales/tr-tr/description.ts
@@ -70,6 +70,7 @@ const description = {
single_sign_on_email_form: 'Kurumsal e-posta adresinizi girin',
single_sign_on_connectors_list:
'Şirketiniz, {{email}} e-posta hesabı için Tekli Oturum Açmayı (Single Sign-On) etkinleştirdi. Aşağıdaki SSO sağlayıcıları ile oturum açmaya devam edebilirsiniz.',
+ single_sign_on_enabled: 'Bu hesapta Tekli Oturum Açma etkinleştirildi.',
};
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 e8ed5710c..91fa97f24 100644
--- a/packages/phrases-experience/src/locales/zh-cn/description.ts
+++ b/packages/phrases-experience/src/locales/zh-cn/description.ts
@@ -62,6 +62,7 @@ const description = {
single_sign_on_email_form: '输入你的企业电子邮件地址',
single_sign_on_connectors_list:
'你的企业已为电子邮件账户{{email}}启用了单点登录。你可以继续使用以下SSO提供商进行登录。',
+ single_sign_on_enabled: '该帐户已启用单点登录',
};
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 fd8bbb6ee..7775e1a61 100644
--- a/packages/phrases-experience/src/locales/zh-hk/description.ts
+++ b/packages/phrases-experience/src/locales/zh-hk/description.ts
@@ -62,6 +62,7 @@ const description = {
single_sign_on_email_form: '輸入你的企業電子郵件地址',
single_sign_on_connectors_list:
'您的企業已為電郵賬戶{{email}}啟用單一登入。您可以繼續使用以下的SSO供應商登入。',
+ single_sign_on_enabled: '該帳戶已啟用單一登入',
};
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 a1eb6abfa..fed3aa331 100644
--- a/packages/phrases-experience/src/locales/zh-tw/description.ts
+++ b/packages/phrases-experience/src/locales/zh-tw/description.ts
@@ -62,6 +62,7 @@ const description = {
single_sign_on_email_form: '輸入你的企業電子郵件地址',
single_sign_on_connectors_list:
'您的企業已為電子郵件帳戶{{email}}啟用單一登入。您可以繼續使用以下的SSO供應商登入。',
+ single_sign_on_enabled: '該帳戶已啟用單一登入',
};
export default Object.freeze(description);