From d07a673a99ed6f0ad204809ddb99936c13e71921 Mon Sep 17 00:00:00 2001 From: simeng-li Date: Fri, 10 Nov 2023 14:41:59 +0800 Subject: [PATCH] feat(core,experience): add sso link and email form (#4850) * feat(core,experience): add sso link and email form add sso link and email form * chore(phrases): update phrases phdate phrases * fix(core): fix ut fix ut * test(experience): add single sign on link test add single sign on link test Please enter the commit message for your changes. Lines starting --- .../sign-in-experience/index.test.ts | 2 + .../src/libraries/sign-in-experience/index.ts | 3 +- .../src/middleware/koa-spa-session-guard.ts | 1 + .../src/routes/interaction/single-sign-on.ts | 9 +- packages/experience/src/App.tsx | 14 ++- .../src/Layout/AppLayout/CustomContent.tsx | 6 +- .../SingleSignOnContext.tsx | 21 +++++ .../SingleSignOnContextProvider/index.tsx | 38 ++++++++ packages/experience/src/__mocks__/logto.tsx | 12 ++- .../experience/src/apis/single-sign-on.ts | 12 +++ packages/experience/src/constants/env.ts | 2 + packages/experience/src/hooks/use-sie.ts | 8 +- .../src/pages/Register/index.module.scss | 4 +- .../src/pages/Register/index.test.tsx | 10 ++- .../experience/src/pages/Register/index.tsx | 13 ++- .../src/pages/SignIn/index.module.scss | 5 +- .../src/pages/SignIn/index.test.tsx | 14 ++- .../experience/src/pages/SignIn/index.tsx | 13 ++- .../pages/SingleSignOnEmail/index.module.scss | 19 ++++ .../src/pages/SingleSignOnEmail/index.tsx | 89 +++++++++++++++++++ .../pages/SingleSignOnEmail/use-on-submit.ts | 62 +++++++++++++ packages/experience/src/types/index.ts | 2 + .../src/api/interaction-sso.ts | 2 +- .../single-sign-on/happy-path.test.ts | 7 +- .../src/tests/api/well-known.test.ts | 1 + .../src/locales/de/action.ts | 1 + .../src/locales/de/description.ts | 4 + .../src/locales/de/error/index.ts | 1 + .../src/locales/en/action.ts | 1 + .../src/locales/en/description.ts | 4 + .../src/locales/en/error/index.ts | 1 + .../src/locales/es/action.ts | 1 + .../src/locales/es/description.ts | 4 + .../src/locales/es/error/index.ts | 2 + .../src/locales/fr/action.ts | 1 + .../src/locales/fr/description.ts | 4 + .../src/locales/fr/error/index.ts | 1 + .../src/locales/it/action.ts | 1 + .../src/locales/it/description.ts | 4 + .../src/locales/it/error/index.ts | 1 + .../src/locales/ja/action.ts | 1 + .../src/locales/ja/description.ts | 4 + .../src/locales/ja/error/index.ts | 1 + .../src/locales/ko/action.ts | 1 + .../src/locales/ko/description.ts | 4 + .../src/locales/ko/error/index.ts | 1 + .../src/locales/pl-pl/action.ts | 1 + .../src/locales/pl-pl/description.ts | 4 + .../src/locales/pl-pl/error/index.ts | 1 + .../src/locales/pt-br/action.ts | 1 + .../src/locales/pt-br/description.ts | 4 + .../src/locales/pt-br/error/index.ts | 1 + .../src/locales/pt-pt/action.ts | 1 + .../src/locales/pt-pt/description.ts | 4 + .../src/locales/pt-pt/error/index.ts | 1 + .../src/locales/ru/action.ts | 1 + .../src/locales/ru/description.ts | 4 + .../src/locales/ru/error/index.ts | 1 + .../src/locales/tr-tr/action.ts | 1 + .../src/locales/tr-tr/description.ts | 4 + .../src/locales/tr-tr/error/index.ts | 1 + .../src/locales/zh-cn/action.ts | 1 + .../src/locales/zh-cn/description.ts | 4 + .../src/locales/zh-cn/error/index.ts | 1 + .../src/locales/zh-hk/action.ts | 1 + .../src/locales/zh-hk/description.ts | 4 + .../src/locales/zh-hk/error/index.ts | 1 + .../src/locales/zh-tw/action.ts | 1 + .../src/locales/zh-tw/description.ts | 4 + .../src/locales/zh-tw/error/index.ts | 1 + packages/schemas/src/types/sso-connector.ts | 1 + 71 files changed, 429 insertions(+), 32 deletions(-) create mode 100644 packages/experience/src/Providers/SingleSignOnContextProvider/SingleSignOnContext.tsx create mode 100644 packages/experience/src/Providers/SingleSignOnContextProvider/index.tsx create mode 100644 packages/experience/src/apis/single-sign-on.ts create mode 100644 packages/experience/src/pages/SingleSignOnEmail/index.module.scss create mode 100644 packages/experience/src/pages/SingleSignOnEmail/index.tsx create mode 100644 packages/experience/src/pages/SingleSignOnEmail/use-on-submit.ts diff --git a/packages/core/src/libraries/sign-in-experience/index.test.ts b/packages/core/src/libraries/sign-in-experience/index.test.ts index 388bfa154..917b67720 100644 --- a/packages/core/src/libraries/sign-in-experience/index.test.ts +++ b/packages/core/src/libraries/sign-in-experience/index.test.ts @@ -160,6 +160,7 @@ describe('getFullSignInExperience()', () => { { id: wellConfiguredSsoConnector.id, connectorName: wellConfiguredSsoConnector.connectorName, + ssoOnly: wellConfiguredSsoConnector.ssoOnly, logo: ssoConnectorFactories[wellConfiguredSsoConnector.providerName].logo, darkLogo: undefined, }, @@ -184,6 +185,7 @@ describe('get sso connectors', () => { { id: wellConfiguredSsoConnector.id, connectorName: wellConfiguredSsoConnector.connectorName, + ssoOnly: wellConfiguredSsoConnector.ssoOnly, logo: ssoConnectorFactories[wellConfiguredSsoConnector.providerName].logo, darkLogo: undefined, }, diff --git a/packages/core/src/libraries/sign-in-experience/index.ts b/packages/core/src/libraries/sign-in-experience/index.ts index 3fa0e2921..37f603b9f 100644 --- a/packages/core/src/libraries/sign-in-experience/index.ts +++ b/packages/core/src/libraries/sign-in-experience/index.ts @@ -72,12 +72,13 @@ export const createSignInExperienceLibrary = ( const ssoConnectors = await getAvailableSsoConnectors(); return ssoConnectors.map( - ({ providerName, connectorName, id, branding }): SsoConnectorMetadata => { + ({ providerName, connectorName, id, branding, ssoOnly }): SsoConnectorMetadata => { const factory = ssoConnectorFactories[providerName]; return { id, connectorName, + ssoOnly, logo: branding.logo ?? factory.logo, darkLogo: branding.darkLogo, }; diff --git a/packages/core/src/middleware/koa-spa-session-guard.ts b/packages/core/src/middleware/koa-spa-session-guard.ts index 103cb0a7d..f9eb1f3b2 100644 --- a/packages/core/src/middleware/koa-spa-session-guard.ts +++ b/packages/core/src/middleware/koa-spa-session-guard.ts @@ -14,6 +14,7 @@ export const sessionNotFoundPath = '/unknown-session'; export const guardedPath = [ '/sign-in', '/register', + '/single-sign-on', '/social/register', '/reset-password', '/forgot-password', diff --git a/packages/core/src/routes/interaction/single-sign-on.ts b/packages/core/src/routes/interaction/single-sign-on.ts index 175b48087..508d65d07 100644 --- a/packages/core/src/routes/interaction/single-sign-on.ts +++ b/packages/core/src/routes/interaction/single-sign-on.ts @@ -124,12 +124,7 @@ export default function singleSignOnRoutes( email: z.string().email(), }), status: [200, 400], - response: z.array( - z.object({ - id: z.string(), - ssoOnly: z.boolean(), - }) - ), + response: z.string().array(), }), async (ctx, next) => { const { email } = ctx.guard.query; @@ -141,7 +136,7 @@ export default function singleSignOnRoutes( const availableConnectors = connectors.filter(({ domains }) => domains.includes(domain)); - ctx.body = availableConnectors.map(({ id, ssoOnly }) => ({ id, ssoOnly })); + ctx.body = availableConnectors.map(({ id }) => id); return next(); } diff --git a/packages/experience/src/App.tsx b/packages/experience/src/App.tsx index 092a6ead5..d1f06201f 100644 --- a/packages/experience/src/App.tsx +++ b/packages/experience/src/App.tsx @@ -7,7 +7,11 @@ import AppBoundary from './Providers/AppBoundary'; import LoadingLayerProvider from './Providers/LoadingLayerProvider'; import PageContextProvider from './Providers/PageContextProvider'; import SettingsProvider from './Providers/SettingsProvider'; -import { isDevFeaturesEnabled as isDevelopmentFeaturesEnabled } from './constants/env'; +import SingleSignOnContextProvider from './Providers/SingleSignOnContextProvider'; +import { + isDevFeaturesEnabled as isDevelopmentFeaturesEnabled, + singleSignOnPath, +} from './constants/env'; import Callback from './pages/Callback'; import Consent from './pages/Consent'; import Continue from './pages/Continue'; @@ -26,6 +30,7 @@ import RegisterPassword from './pages/RegisterPassword'; import ResetPassword from './pages/ResetPassword'; import SignIn from './pages/SignIn'; import SignInPassword from './pages/SignInPassword'; +import SingleSignOnEmail from './pages/SingleSignOnEmail'; import SocialLanding from './pages/SocialLanding'; import SocialLinkAccount from './pages/SocialLinkAccount'; import SocialSignIn from './pages/SocialSignInCallback'; @@ -110,6 +115,13 @@ const App = () => { } /> + {/* Single sign on */} + {isDevelopmentFeaturesEnabled && ( + }> + } /> + + )} + } /> diff --git a/packages/experience/src/Layout/AppLayout/CustomContent.tsx b/packages/experience/src/Layout/AppLayout/CustomContent.tsx index d5f217f55..8565165a9 100644 --- a/packages/experience/src/Layout/AppLayout/CustomContent.tsx +++ b/packages/experience/src/Layout/AppLayout/CustomContent.tsx @@ -1,16 +1,16 @@ import { useLocation } from 'react-router-dom'; -import { useSignInExperience } from '@/hooks/use-sie'; +import { useSieMethods } from '@/hooks/use-sie'; type Props = { className?: string; }; const CustomContent = ({ className }: Props) => { - const signInExperience = useSignInExperience(); + const { customContent } = useSieMethods(); const { pathname } = useLocation(); - const customHtml = signInExperience?.customContent[pathname]; + const customHtml = customContent?.[pathname]; if (!customHtml) { return null; diff --git a/packages/experience/src/Providers/SingleSignOnContextProvider/SingleSignOnContext.tsx b/packages/experience/src/Providers/SingleSignOnContextProvider/SingleSignOnContext.tsx new file mode 100644 index 000000000..3295c46e9 --- /dev/null +++ b/packages/experience/src/Providers/SingleSignOnContextProvider/SingleSignOnContext.tsx @@ -0,0 +1,21 @@ +import { type SsoConnectorMetadata } from '@logto/schemas'; +import { noop } from '@silverhand/essentials'; +import { createContext } from 'react'; + +export type SingleSignOnContextType = { + // All the enabled sso connectors + availableSsoConnectorsMap: Map; + email?: string; + setEmail: React.Dispatch>; + // The sso connectors that are enabled for the current domain + ssoConnectors: SsoConnectorMetadata[]; + setSsoConnectors: React.Dispatch>; +}; + +export default createContext({ + email: undefined, + availableSsoConnectorsMap: new Map(), + ssoConnectors: [], + setEmail: noop, + setSsoConnectors: noop, +}); diff --git a/packages/experience/src/Providers/SingleSignOnContextProvider/index.tsx b/packages/experience/src/Providers/SingleSignOnContextProvider/index.tsx new file mode 100644 index 000000000..b16071032 --- /dev/null +++ b/packages/experience/src/Providers/SingleSignOnContextProvider/index.tsx @@ -0,0 +1,38 @@ +import { type SsoConnectorMetadata } from '@logto/schemas'; +import { useMemo, useState } from 'react'; +import { Outlet } from 'react-router-dom'; + +import { useSieMethods } from '@/hooks/use-sie'; + +import SingleSignOnContext, { type SingleSignOnContextType } from './SingleSignOnContext'; + +const SingleSignOnContextProvider = () => { + const { ssoConnectors } = useSieMethods(); + const [email, setEmail] = useState(); + const [domainFilteredConnectors, setDomainFilteredConnectors] = useState( + [] + ); + const ssoConnectorsMap = useMemo( + () => new Map(ssoConnectors.map((connector) => [connector.id, connector])), + [ssoConnectors] + ); + + const singleSignOnContext = useMemo( + () => ({ + email, + setEmail, + availableSsoConnectorsMap: ssoConnectorsMap, + ssoConnectors: domainFilteredConnectors, + setSsoConnectors: setDomainFilteredConnectors, + }), + [domainFilteredConnectors, email, ssoConnectorsMap] + ); + + return ( + + + + ); +}; + +export default SingleSignOnContextProvider; diff --git a/packages/experience/src/__mocks__/logto.tsx b/packages/experience/src/__mocks__/logto.tsx index 0f280b188..9815658be 100644 --- a/packages/experience/src/__mocks__/logto.tsx +++ b/packages/experience/src/__mocks__/logto.tsx @@ -1,4 +1,4 @@ -import type { SignInExperience, SignIn } from '@logto/schemas'; +import type { SignInExperience, SignIn, SsoConnectorMetadata } from '@logto/schemas'; import { ConnectorPlatform, ConnectorType, @@ -41,6 +41,15 @@ export const mockSocialConnectorData = { configTemplate: '', }; +export const mockSsoConnectors: SsoConnectorMetadata[] = [ + { + id: 'arbitrary-sso-connector', + connectorName: 'AzureAD', + ssoOnly: true, + logo: 'http://logto.dev/logto.png', + }, +]; + export const emailSignInMethod = { identifier: SignInIdentifier.Email, password: true, @@ -113,6 +122,7 @@ export const mockSignInExperienceSettings: SignInExperienceResponse = { verify: true, }, socialConnectors, + ssoConnectors: [], signInMode: SignInMode.SignInAndRegister, forgotPassword: { email: true, diff --git a/packages/experience/src/apis/single-sign-on.ts b/packages/experience/src/apis/single-sign-on.ts new file mode 100644 index 000000000..a2492b96c --- /dev/null +++ b/packages/experience/src/apis/single-sign-on.ts @@ -0,0 +1,12 @@ +import api from './api'; + +const ssoPrefix = '/api/interaction/single-sign-on'; + +export const getSingleSignOnConnectors = async (email: string) => + api + .get(`${ssoPrefix}/connectors`, { + searchParams: { + email, + }, + }) + .json(); diff --git a/packages/experience/src/constants/env.ts b/packages/experience/src/constants/env.ts index a53651b16..b1bd9d001 100644 --- a/packages/experience/src/constants/env.ts +++ b/packages/experience/src/constants/env.ts @@ -5,3 +5,5 @@ export const isDevFeaturesEnabled = process.env.NODE_ENV !== 'production' || yes(process.env.DEV_FEATURES_ENABLED) || yes(process.env.INTEGRATION_TEST); + +export const singleSignOnPath = 'single-sign-on'; diff --git a/packages/experience/src/hooks/use-sie.ts b/packages/experience/src/hooks/use-sie.ts index 4af0ab3d0..9a990921a 100644 --- a/packages/experience/src/hooks/use-sie.ts +++ b/packages/experience/src/hooks/use-sie.ts @@ -20,17 +20,13 @@ export const useSieMethods = () => { ({ password, verificationCode }) => password || verificationCode ) ?? [], socialConnectors: experienceSettings?.socialConnectors ?? [], + ssoConnectors: experienceSettings?.ssoConnectors ?? [], signInMode: experienceSettings?.signInMode, forgotPassword: experienceSettings?.forgotPassword, + customContent: experienceSettings?.customContent, }; }; -export const useSignInExperience = () => { - const { experienceSettings } = useContext(PageContext); - - return experienceSettings; -}; - export const usePasswordPolicy = () => { const { t } = useTranslation(); const { experienceSettings } = useContext(PageContext); diff --git a/packages/experience/src/pages/Register/index.module.scss b/packages/experience/src/pages/Register/index.module.scss index db93b48a0..82f53078d 100644 --- a/packages/experience/src/pages/Register/index.module.scss +++ b/packages/experience/src/pages/Register/index.module.scss @@ -10,8 +10,8 @@ margin-bottom: _.unit(4); } -.createAccount { - margin-top: _.unit(2); +.createAccount, +.singleSignOn { text-align: center; margin-bottom: _.unit(4); } diff --git a/packages/experience/src/pages/Register/index.test.tsx b/packages/experience/src/pages/Register/index.test.tsx index 275bee3b0..98f28b02f 100644 --- a/packages/experience/src/pages/Register/index.test.tsx +++ b/packages/experience/src/pages/Register/index.test.tsx @@ -4,7 +4,7 @@ import { Route, Routes } from 'react-router-dom'; import renderWithPageContext from '@/__mocks__/RenderWithPageContext'; import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider'; -import { mockSignInExperienceSettings } from '@/__mocks__/logto'; +import { mockSignInExperienceSettings, mockSsoConnectors } from '@/__mocks__/logto'; import Register from '@/pages/Register'; import type { SignInExperienceResponse } from '@/types'; @@ -73,4 +73,12 @@ describe('', () => { ); expect(queryByText('sign-in')).not.toBeNull(); }); + + test('render single sign on link', () => { + const { queryByText } = renderRegisterPage({ + ssoConnectors: mockSsoConnectors, + }); + + expect(queryByText('action.single_sign_on')).not.toBeNull(); + }); }); diff --git a/packages/experience/src/pages/Register/index.tsx b/packages/experience/src/pages/Register/index.tsx index 6ccfd1235..bdb44c4ee 100644 --- a/packages/experience/src/pages/Register/index.tsx +++ b/packages/experience/src/pages/Register/index.tsx @@ -5,6 +5,7 @@ import { Navigate } from 'react-router-dom'; import LandingPageLayout from '@/Layout/LandingPageLayout'; import Divider from '@/components/Divider'; import TextLink from '@/components/TextLink'; +import { isDevFeaturesEnabled } from '@/constants/env'; import SocialSignInList from '@/containers/SocialSignInList'; import TermsAndPrivacy from '@/containers/TermsAndPrivacy'; import { useSieMethods } from '@/hooks/use-sie'; @@ -15,7 +16,8 @@ import IdentifierRegisterForm from './IdentifierRegisterForm'; import * as styles from './index.module.scss'; const Register = () => { - const { signUpMethods, socialConnectors, signInMode, signInMethods } = useSieMethods(); + const { signUpMethods, socialConnectors, signInMode, signInMethods, ssoConnectors } = + useSieMethods(); const { t } = useTranslation(); if (!signInMode) { @@ -37,6 +39,15 @@ const Register = () => { )} + { + // Single Sign On footer TODO: remove the dev feature check once SSO is ready + isDevFeaturesEnabled && ssoConnectors.length > 0 && ( +
+ {t('description.use')}{' '} + +
+ ) + } { // SignIn footer signInMode === SignInMode.SignInAndRegister && signInMethods.length > 0 && ( diff --git a/packages/experience/src/pages/SignIn/index.module.scss b/packages/experience/src/pages/SignIn/index.module.scss index 28dc9e7be..a66efb4c8 100644 --- a/packages/experience/src/pages/SignIn/index.module.scss +++ b/packages/experience/src/pages/SignIn/index.module.scss @@ -12,8 +12,9 @@ font: var(--font-body-3); } -.createAccount { - margin-top: _.unit(2); + +.createAccount, +.singleSignOn { text-align: center; margin-bottom: _.unit(4); } diff --git a/packages/experience/src/pages/SignIn/index.test.tsx b/packages/experience/src/pages/SignIn/index.test.tsx index 72148b302..a4c1d5bf8 100644 --- a/packages/experience/src/pages/SignIn/index.test.tsx +++ b/packages/experience/src/pages/SignIn/index.test.tsx @@ -3,7 +3,11 @@ import { Route, Routes } from 'react-router-dom'; import renderWithPageContext from '@/__mocks__/RenderWithPageContext'; import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider'; -import { mockSignInExperienceSettings, mockSignInMethodSettingsTestCases } from '@/__mocks__/logto'; +import { + mockSignInExperienceSettings, + mockSignInMethodSettingsTestCases, + mockSsoConnectors, +} from '@/__mocks__/logto'; import SignIn from '@/pages/SignIn'; jest.mock('i18next', () => ({ @@ -108,4 +112,12 @@ describe('', () => { expect(queryByText('Register')).not.toBeNull(); }); + + test('render single sign on link', () => { + const { queryByText } = renderSignIn({ + ssoConnectors: mockSsoConnectors, + }); + + expect(queryByText('action.single_sign_on')).not.toBeNull(); + }); }); diff --git a/packages/experience/src/pages/SignIn/index.tsx b/packages/experience/src/pages/SignIn/index.tsx index 83b1ce764..fcc4ac148 100644 --- a/packages/experience/src/pages/SignIn/index.tsx +++ b/packages/experience/src/pages/SignIn/index.tsx @@ -5,6 +5,7 @@ import { Navigate } from 'react-router-dom'; import LandingPageLayout from '@/Layout/LandingPageLayout'; import Divider from '@/components/Divider'; import TextLink from '@/components/TextLink'; +import { isDevFeaturesEnabled } from '@/constants/env'; import SocialSignInList from '@/containers/SocialSignInList'; import TermsAndPrivacyLinks from '@/containers/TermsAndPrivacyLinks'; import { useSieMethods } from '@/hooks/use-sie'; @@ -15,7 +16,8 @@ import Main from './Main'; import * as styles from './index.module.scss'; const SignIn = () => { - const { signInMethods, signUpMethods, socialConnectors, signInMode } = useSieMethods(); + const { signInMethods, signUpMethods, socialConnectors, signInMode, ssoConnectors } = + useSieMethods(); const { t } = useTranslation(); if (!signInMode) { @@ -29,6 +31,15 @@ const SignIn = () => { return (
+ { + // Single Sign On footer TODO: remove the dev feature check once SSO is ready + isDevFeaturesEnabled && ssoConnectors.length > 0 && ( +
+ {t('description.use')}{' '} + +
+ ) + } { // Create Account footer signInMode === SignInMode.SignInAndRegister && signUpMethods.length > 0 && ( diff --git a/packages/experience/src/pages/SingleSignOnEmail/index.module.scss b/packages/experience/src/pages/SingleSignOnEmail/index.module.scss new file mode 100644 index 000000000..74d3215ec --- /dev/null +++ b/packages/experience/src/pages/SingleSignOnEmail/index.module.scss @@ -0,0 +1,19 @@ +@use '@/scss/underscore' as _; + +.form { + @include _.flex-column; + + > * { + width: 100%; + } + + .inputField, + .formErrors { + margin-bottom: _.unit(4); + } + + .formErrors { + margin-left: _.unit(0.5); + margin-top: _.unit(-3); + } +} diff --git a/packages/experience/src/pages/SingleSignOnEmail/index.tsx b/packages/experience/src/pages/SingleSignOnEmail/index.tsx new file mode 100644 index 000000000..6ad84ce50 --- /dev/null +++ b/packages/experience/src/pages/SingleSignOnEmail/index.tsx @@ -0,0 +1,89 @@ +import { SignInIdentifier } from '@logto/schemas'; +import { useCallback, useEffect } from 'react'; +import { Controller, useForm } from 'react-hook-form'; + +import SecondaryPageLayout from '@/Layout/SecondaryPageLayout'; +import Button from '@/components/Button'; +import ErrorMessage from '@/components/ErrorMessage'; +import SmartInputField, { + type IdentifierInputValue, +} from '@/components/InputFields/SmartInputField'; +import { getGeneralIdentifierErrorMessage, validateIdentifierField } from '@/utils/form'; + +import * as styles from './index.module.scss'; +import useOnSubmit from './use-on-submit'; + +type FormState = { + identifier: IdentifierInputValue; +}; + +const SingleSignOnEmail = () => { + const { errorMessage, clearErrorMessage, onSubmit } = useOnSubmit(); + + const { + handleSubmit, + control, + formState: { errors, isValid }, + } = useForm({ + reValidateMode: 'onBlur', + }); + + useEffect(() => { + if (!isValid) { + clearErrorMessage(); + } + }, [clearErrorMessage, isValid]); + + const onSubmitHandler = useCallback( + async (event?: React.FormEvent) => { + clearErrorMessage(); + await handleSubmit(async ({ identifier: { value } }) => onSubmit(value))(event); + }, + [clearErrorMessage, handleSubmit, onSubmit] + ); + + return ( + +
+ { + if (!value) { + return getGeneralIdentifierErrorMessage([SignInIdentifier.Email], 'required'); + } + + const errorMessage = validateIdentifierField(SignInIdentifier.Email, value); + + return errorMessage + ? getGeneralIdentifierErrorMessage([SignInIdentifier.Email], 'invalid') + : true; + }, + }} + render={({ field }) => ( + + )} + /> + + {errorMessage && {errorMessage}} + +