mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
refactor(experience): cache user input identifier value
This commit is contained in:
parent
978817ec0c
commit
46fb70c4bf
24 changed files with 247 additions and 135 deletions
|
@ -6,7 +6,7 @@ import AppBoundary from './Providers/AppBoundary';
|
||||||
import LoadingLayerProvider from './Providers/LoadingLayerProvider';
|
import LoadingLayerProvider from './Providers/LoadingLayerProvider';
|
||||||
import PageContextProvider from './Providers/PageContextProvider';
|
import PageContextProvider from './Providers/PageContextProvider';
|
||||||
import SettingsProvider from './Providers/SettingsProvider';
|
import SettingsProvider from './Providers/SettingsProvider';
|
||||||
import SingleSignOnContextProvider from './Providers/SingleSignOnContextProvider';
|
import UserInteractionContextProvider from './Providers/UserInteractionContextProvider';
|
||||||
import Callback from './pages/Callback';
|
import Callback from './pages/Callback';
|
||||||
import Consent from './pages/Consent';
|
import Consent from './pages/Consent';
|
||||||
import Continue from './pages/Continue';
|
import Continue from './pages/Continue';
|
||||||
|
@ -45,7 +45,7 @@ const App = () => {
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<PageContextProvider>
|
<PageContextProvider>
|
||||||
<SettingsProvider>
|
<SettingsProvider>
|
||||||
<SingleSignOnContextProvider>
|
<UserInteractionContextProvider>
|
||||||
<AppBoundary>
|
<AppBoundary>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route element={<LoadingLayerProvider />}>
|
<Route element={<LoadingLayerProvider />}>
|
||||||
|
@ -125,7 +125,7 @@ const App = () => {
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</AppBoundary>
|
</AppBoundary>
|
||||||
</SingleSignOnContextProvider>
|
</UserInteractionContextProvider>
|
||||||
</SettingsProvider>
|
</SettingsProvider>
|
||||||
</PageContextProvider>
|
</PageContextProvider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
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<string, SsoConnectorMetadata>;
|
|
||||||
email?: string;
|
|
||||||
setEmail: React.Dispatch<React.SetStateAction<string | undefined>>;
|
|
||||||
// The sso connectors that are enabled for the current domain
|
|
||||||
ssoConnectors: SsoConnectorMetadata[];
|
|
||||||
setSsoConnectors: React.Dispatch<React.SetStateAction<SsoConnectorMetadata[]>>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default createContext<SingleSignOnContextType>({
|
|
||||||
email: undefined,
|
|
||||||
availableSsoConnectorsMap: new Map(),
|
|
||||||
ssoConnectors: [],
|
|
||||||
setEmail: noop,
|
|
||||||
setSsoConnectors: noop,
|
|
||||||
});
|
|
|
@ -1,62 +0,0 @@
|
||||||
import { type SsoConnectorMetadata } from '@logto/schemas';
|
|
||||||
import { type ReactNode, useEffect, useMemo, useState } from 'react';
|
|
||||||
|
|
||||||
import useSessionStorage, { StorageKeys } from '@/hooks/use-session-storages';
|
|
||||||
import { useSieMethods } from '@/hooks/use-sie';
|
|
||||||
|
|
||||||
import SingleSignOnContext, { type SingleSignOnContextType } from './SingleSignOnContext';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
readonly children: ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
const SingleSignOnContextProvider = ({ children }: Props) => {
|
|
||||||
const { ssoConnectors } = useSieMethods();
|
|
||||||
const { get, set, remove } = useSessionStorage();
|
|
||||||
const [email, setEmail] = useState<string | undefined>(get(StorageKeys.SsoEmail));
|
|
||||||
const [domainFilteredConnectors, setDomainFilteredConnectors] = useState<SsoConnectorMetadata[]>(
|
|
||||||
get(StorageKeys.SsoConnectors) ?? []
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!email) {
|
|
||||||
remove(StorageKeys.SsoEmail);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
set(StorageKeys.SsoEmail, email);
|
|
||||||
}, [email, remove, set]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (domainFilteredConnectors.length === 0) {
|
|
||||||
remove(StorageKeys.SsoConnectors);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
set(StorageKeys.SsoConnectors, domainFilteredConnectors);
|
|
||||||
}, [domainFilteredConnectors, remove, set]);
|
|
||||||
|
|
||||||
const ssoConnectorsMap = useMemo(
|
|
||||||
() => new Map(ssoConnectors.map((connector) => [connector.id, connector])),
|
|
||||||
[ssoConnectors]
|
|
||||||
);
|
|
||||||
|
|
||||||
const singleSignOnContext = useMemo<SingleSignOnContextType>(
|
|
||||||
() => ({
|
|
||||||
email,
|
|
||||||
setEmail,
|
|
||||||
availableSsoConnectorsMap: ssoConnectorsMap,
|
|
||||||
ssoConnectors: domainFilteredConnectors,
|
|
||||||
setSsoConnectors: setDomainFilteredConnectors,
|
|
||||||
}),
|
|
||||||
[domainFilteredConnectors, email, ssoConnectorsMap]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SingleSignOnContext.Provider value={singleSignOnContext}>
|
|
||||||
{children}
|
|
||||||
</SingleSignOnContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SingleSignOnContextProvider;
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { type SsoConnectorMetadata } from '@logto/schemas';
|
||||||
|
import { noop } from '@silverhand/essentials';
|
||||||
|
import { createContext } from 'react';
|
||||||
|
|
||||||
|
import { type CurrentIdentifierSession } from '@/types/guard';
|
||||||
|
|
||||||
|
export type UserInteractionContextType = {
|
||||||
|
// All the enabled sso connectors
|
||||||
|
availableSsoConnectorsMap: Map<string, SsoConnectorMetadata>;
|
||||||
|
ssoEmail?: string;
|
||||||
|
setSsoEmail: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||||
|
// The sso connectors that are enabled for the current domain
|
||||||
|
ssoConnectors: SsoConnectorMetadata[];
|
||||||
|
setSsoConnectors: React.Dispatch<React.SetStateAction<SsoConnectorMetadata[]>>;
|
||||||
|
currentIdentifier?: CurrentIdentifierSession;
|
||||||
|
setCurrentIdentifier: React.Dispatch<React.SetStateAction<CurrentIdentifierSession | undefined>>;
|
||||||
|
clearUserInteractionSession: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createContext<UserInteractionContextType>({
|
||||||
|
ssoEmail: undefined,
|
||||||
|
availableSsoConnectorsMap: new Map(),
|
||||||
|
ssoConnectors: [],
|
||||||
|
setSsoEmail: noop,
|
||||||
|
setSsoConnectors: noop,
|
||||||
|
currentIdentifier: undefined,
|
||||||
|
setCurrentIdentifier: noop,
|
||||||
|
clearUserInteractionSession: noop,
|
||||||
|
});
|
|
@ -0,0 +1,90 @@
|
||||||
|
import { type SsoConnectorMetadata } from '@logto/schemas';
|
||||||
|
import { type ReactNode, useEffect, useMemo, useState, useCallback } from 'react';
|
||||||
|
|
||||||
|
import useSessionStorage, { StorageKeys } from '@/hooks/use-session-storages';
|
||||||
|
import { useSieMethods } from '@/hooks/use-sie';
|
||||||
|
import { type CurrentIdentifierSession } from '@/types/guard';
|
||||||
|
|
||||||
|
import UserInteractionContext, { type UserInteractionContextType } from './UserInteractionContext';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
readonly children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const UserInteractionContextProvider = ({ children }: Props) => {
|
||||||
|
const { ssoConnectors } = useSieMethods();
|
||||||
|
const { get, set, remove } = useSessionStorage();
|
||||||
|
const [ssoEmail, setSsoEmail] = useState<string | undefined>(get(StorageKeys.SsoEmail));
|
||||||
|
const [domainFilteredConnectors, setDomainFilteredConnectors] = useState<SsoConnectorMetadata[]>(
|
||||||
|
get(StorageKeys.SsoConnectors) ?? []
|
||||||
|
);
|
||||||
|
const [currentIdentifier, setCurrentIdentifier] = useState<CurrentIdentifierSession | undefined>(
|
||||||
|
get(StorageKeys.CurrentIdentifier)
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ssoEmail) {
|
||||||
|
remove(StorageKeys.SsoEmail);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(StorageKeys.SsoEmail, ssoEmail);
|
||||||
|
}, [ssoEmail, remove, set]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (domainFilteredConnectors.length === 0) {
|
||||||
|
remove(StorageKeys.SsoConnectors);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(StorageKeys.SsoConnectors, domainFilteredConnectors);
|
||||||
|
}, [domainFilteredConnectors, remove, set]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!currentIdentifier) {
|
||||||
|
remove(StorageKeys.CurrentIdentifier);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(StorageKeys.CurrentIdentifier, currentIdentifier);
|
||||||
|
}, [currentIdentifier, remove, set]);
|
||||||
|
|
||||||
|
const ssoConnectorsMap = useMemo(
|
||||||
|
() => new Map(ssoConnectors.map((connector) => [connector.id, connector])),
|
||||||
|
[ssoConnectors]
|
||||||
|
);
|
||||||
|
|
||||||
|
const clearUserInteractionSession = useCallback(() => {
|
||||||
|
setSsoEmail(undefined);
|
||||||
|
setDomainFilteredConnectors([]);
|
||||||
|
setCurrentIdentifier(undefined);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const singleSignOnContext = useMemo<UserInteractionContextType>(
|
||||||
|
() => ({
|
||||||
|
ssoEmail,
|
||||||
|
setSsoEmail,
|
||||||
|
availableSsoConnectorsMap: ssoConnectorsMap,
|
||||||
|
ssoConnectors: domainFilteredConnectors,
|
||||||
|
setSsoConnectors: setDomainFilteredConnectors,
|
||||||
|
currentIdentifier,
|
||||||
|
setCurrentIdentifier,
|
||||||
|
clearUserInteractionSession,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
ssoEmail,
|
||||||
|
ssoConnectorsMap,
|
||||||
|
domainFilteredConnectors,
|
||||||
|
currentIdentifier,
|
||||||
|
clearUserInteractionSession,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UserInteractionContext.Provider value={singleSignOnContext}>
|
||||||
|
{children}
|
||||||
|
</UserInteractionContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserInteractionContextProvider;
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { useContext } from 'react';
|
||||||
|
|
||||||
|
import UserInteractionContext from '@/Providers/UserInteractionContextProvider/UserInteractionContext';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component renders a hidden input field that stores the user's identifier.
|
||||||
|
* Its primary purpose is to assist password managers in associating the correct
|
||||||
|
* identifier with the password being set or changed.
|
||||||
|
*
|
||||||
|
* By including this hidden field, we enable password managers to correctly save
|
||||||
|
* or update the user's credentials, enhancing the user experience and security.
|
||||||
|
*/
|
||||||
|
const HiddenIdentifierInput = () => {
|
||||||
|
const { currentIdentifier } = useContext(UserInteractionContext);
|
||||||
|
|
||||||
|
if (!currentIdentifier) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <input readOnly hidden type={currentIdentifier.type} value={currentIdentifier.value} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HiddenIdentifierInput;
|
|
@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import ErrorMessage from '@/components/ErrorMessage';
|
import ErrorMessage from '@/components/ErrorMessage';
|
||||||
|
import HiddenIdentifierInput from '@/components/HiddenIdentifierInput';
|
||||||
import { PasswordInputField } from '@/components/InputFields';
|
import { PasswordInputField } from '@/components/InputFields';
|
||||||
|
|
||||||
import * as styles from './index.module.scss';
|
import * as styles from './index.module.scss';
|
||||||
|
@ -53,6 +54,7 @@ const Lite = ({ className, autoFocus, onSubmit, errorMessage, clearErrorMessage
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className={classNames(styles.form, className)} onSubmit={onSubmitHandler}>
|
<form className={classNames(styles.form, className)} onSubmit={onSubmitHandler}>
|
||||||
|
<HiddenIdentifierInput />
|
||||||
<PasswordInputField
|
<PasswordInputField
|
||||||
className={styles.inputField}
|
className={styles.inputField}
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
|
|
|
@ -7,6 +7,7 @@ import ClearIcon from '@/assets/icons/clear-icon.svg';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import IconButton from '@/components/Button/IconButton';
|
import IconButton from '@/components/Button/IconButton';
|
||||||
import ErrorMessage from '@/components/ErrorMessage';
|
import ErrorMessage from '@/components/ErrorMessage';
|
||||||
|
import HiddenIdentifierInput from '@/components/HiddenIdentifierInput';
|
||||||
import { InputField } from '@/components/InputFields';
|
import { InputField } from '@/components/InputFields';
|
||||||
|
|
||||||
import TogglePassword from './TogglePassword';
|
import TogglePassword from './TogglePassword';
|
||||||
|
@ -67,6 +68,7 @@ const SetPassword = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className={classNames(styles.form, className)} onSubmit={onSubmitHandler}>
|
<form className={classNames(styles.form, className)} onSubmit={onSubmitHandler}>
|
||||||
|
<HiddenIdentifierInput />
|
||||||
<InputField
|
<InputField
|
||||||
className={styles.inputField}
|
className={styles.inputField}
|
||||||
type={showPassword ? 'text' : 'password'}
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { useCallback, useState, useContext } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import SingleSignOnContext from '@/Providers/SingleSignOnContextProvider/SingleSignOnContext';
|
import UserInteractionContext from '@/Providers/UserInteractionContextProvider/UserInteractionContext';
|
||||||
import { getSingleSignOnConnectors } from '@/apis/single-sign-on';
|
import { getSingleSignOnConnectors } from '@/apis/single-sign-on';
|
||||||
import useApi from '@/hooks/use-api';
|
import useApi from '@/hooks/use-api';
|
||||||
import useErrorHandler from '@/hooks/use-error-handler';
|
import useErrorHandler from '@/hooks/use-error-handler';
|
||||||
|
@ -15,7 +15,8 @@ const useCheckSingleSignOn = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const request = useApi(getSingleSignOnConnectors);
|
const request = useApi(getSingleSignOnConnectors);
|
||||||
const [errorMessage, setErrorMessage] = useState<string | undefined>();
|
const [errorMessage, setErrorMessage] = useState<string | undefined>();
|
||||||
const { setEmail, setSsoConnectors, availableSsoConnectorsMap } = useContext(SingleSignOnContext);
|
const { setSsoEmail, setSsoConnectors, availableSsoConnectorsMap } =
|
||||||
|
useContext(UserInteractionContext);
|
||||||
const singleSignOn = useSingleSignOn();
|
const singleSignOn = useSingleSignOn();
|
||||||
|
|
||||||
const handleError = useErrorHandler();
|
const handleError = useErrorHandler();
|
||||||
|
@ -26,9 +27,9 @@ const useCheckSingleSignOn = () => {
|
||||||
|
|
||||||
// Should clear the context and storage if the user trying to resubmit the form
|
// Should clear the context and storage if the user trying to resubmit the form
|
||||||
const clearContext = useCallback(() => {
|
const clearContext = useCallback(() => {
|
||||||
setEmail(undefined);
|
setSsoEmail(undefined);
|
||||||
setSsoConnectors([]);
|
setSsoConnectors([]);
|
||||||
}, [setEmail, setSsoConnectors]);
|
}, [setSsoEmail, setSsoConnectors]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the email is registered with any SSO connectors
|
* Check if the email is registered with any SSO connectors
|
||||||
|
@ -66,7 +67,7 @@ const useCheckSingleSignOn = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
setSsoConnectors(connectors);
|
setSsoConnectors(connectors);
|
||||||
setEmail(email);
|
setSsoEmail(email);
|
||||||
|
|
||||||
if (!continueSignIn) {
|
if (!continueSignIn) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -87,7 +88,7 @@ const useCheckSingleSignOn = () => {
|
||||||
handleError,
|
handleError,
|
||||||
navigate,
|
navigate,
|
||||||
request,
|
request,
|
||||||
setEmail,
|
setSsoEmail,
|
||||||
setSsoConnectors,
|
setSsoConnectors,
|
||||||
singleSignOn,
|
singleSignOn,
|
||||||
t,
|
t,
|
||||||
|
|
|
@ -1,23 +1,26 @@
|
||||||
import { useCallback, useContext } from 'react';
|
import { useCallback, useContext } from 'react';
|
||||||
|
|
||||||
import PageContext from '@/Providers/PageContextProvider/PageContext';
|
import PageContext from '@/Providers/PageContextProvider/PageContext';
|
||||||
|
import UserInteractionContext from '@/Providers/UserInteractionContextProvider/UserInteractionContext';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This hook provides a function that process the app redirection after user successfully signs in.
|
* This hook provides a function that process the app redirection after user successfully signs in.
|
||||||
* Use window.location.replace to handle the redirection.
|
* Use window.location.replace to handle the redirection.
|
||||||
* Set the global loading state to true before redirecting.
|
* Set the global loading state to true before redirecting.
|
||||||
|
* Clear the user interaction session before redirecting.
|
||||||
* This is to prevent the user from interacting with the app while the redirection is in progress.
|
* This is to prevent the user from interacting with the app while the redirection is in progress.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function useGlobalRedirectTo() {
|
function useGlobalRedirectTo() {
|
||||||
const { setLoading } = useContext(PageContext);
|
const { setLoading } = useContext(PageContext);
|
||||||
|
const { clearUserInteractionSession } = useContext(UserInteractionContext);
|
||||||
|
|
||||||
const redirectTo = useCallback(
|
const redirectTo = useCallback(
|
||||||
(url: string | URL) => {
|
(url: string | URL) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
clearUserInteractionSession();
|
||||||
window.location.replace(url);
|
window.location.replace(url);
|
||||||
},
|
},
|
||||||
[setLoading]
|
[clearUserInteractionSession, setLoading]
|
||||||
);
|
);
|
||||||
|
|
||||||
return redirectTo;
|
return redirectTo;
|
||||||
|
|
|
@ -4,18 +4,20 @@
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import * as s from 'superstruct';
|
import * as s from 'superstruct';
|
||||||
|
|
||||||
import { ssoConnectorMetadataGuard } from '@/types/guard';
|
import { currentIdentifierSessionGuard, ssoConnectorMetadataGuard } from '@/types/guard';
|
||||||
|
|
||||||
const logtoStorageKeyPrefix = `logto:${window.location.origin}`;
|
const logtoStorageKeyPrefix = `logto:${window.location.origin}`;
|
||||||
|
|
||||||
export enum StorageKeys {
|
export enum StorageKeys {
|
||||||
SsoEmail = 'sso-email',
|
SsoEmail = 'sso-email',
|
||||||
SsoConnectors = 'sso-connectors',
|
SsoConnectors = 'sso-connectors',
|
||||||
|
CurrentIdentifier = 'current-identifier',
|
||||||
}
|
}
|
||||||
|
|
||||||
const valueGuard = Object.freeze({
|
const valueGuard = Object.freeze({
|
||||||
[StorageKeys.SsoEmail]: s.string(),
|
[StorageKeys.SsoEmail]: s.string(),
|
||||||
[StorageKeys.SsoConnectors]: s.array(ssoConnectorMetadataGuard),
|
[StorageKeys.SsoConnectors]: s.array(ssoConnectorMetadataGuard),
|
||||||
|
[StorageKeys.CurrentIdentifier]: currentIdentifierSessionGuard,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- we don't care about the superstruct details
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- we don't care about the superstruct details
|
||||||
} satisfies { [key in StorageKeys]: s.Struct<any> });
|
} satisfies { [key in StorageKeys]: s.Struct<any> });
|
||||||
|
|
||||||
|
|
|
@ -7,8 +7,8 @@ import {
|
||||||
import { useEffect, useCallback, useContext } from 'react';
|
import { useEffect, useCallback, useContext } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import SingleSignOnContext from '@/Providers/SingleSignOnContextProvider/SingleSignOnContext';
|
|
||||||
import SingleSignOnFormModeContext from '@/Providers/SingleSignOnFormModeContextProvider/SingleSignOnFormModeContext';
|
import SingleSignOnFormModeContext from '@/Providers/SingleSignOnFormModeContextProvider/SingleSignOnFormModeContext';
|
||||||
|
import UserInteractionContext from '@/Providers/UserInteractionContextProvider/UserInteractionContext';
|
||||||
import { getSingleSignOnConnectors } from '@/apis/single-sign-on';
|
import { getSingleSignOnConnectors } from '@/apis/single-sign-on';
|
||||||
import type { IdentifierInputValue } from '@/components/InputFields/SmartInputField';
|
import type { IdentifierInputValue } from '@/components/InputFields/SmartInputField';
|
||||||
import useApi from '@/hooks/use-api';
|
import useApi from '@/hooks/use-api';
|
||||||
|
@ -23,8 +23,8 @@ const useSingleSignOnWatch = (identifierInput?: IdentifierInputValue) => {
|
||||||
|
|
||||||
const { singleSignOnEnabled } = useSieMethods();
|
const { singleSignOnEnabled } = useSieMethods();
|
||||||
|
|
||||||
const { setEmail, setSsoConnectors, ssoConnectors, availableSsoConnectorsMap } =
|
const { setSsoEmail, setSsoConnectors, ssoConnectors, availableSsoConnectorsMap } =
|
||||||
useContext(SingleSignOnContext);
|
useContext(UserInteractionContext);
|
||||||
|
|
||||||
const { showSingleSignOnForm, setShowSingleSignOnForm } = useContext(SingleSignOnFormModeContext);
|
const { showSingleSignOnForm, setShowSingleSignOnForm } = useContext(SingleSignOnFormModeContext);
|
||||||
|
|
||||||
|
@ -53,10 +53,10 @@ const useSingleSignOnWatch = (identifierInput?: IdentifierInputValue) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
setSsoConnectors(connectors);
|
setSsoConnectors(connectors);
|
||||||
setEmail(email);
|
setSsoEmail(email);
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
[availableSsoConnectorsMap, request, setEmail, setSsoConnectors]
|
[availableSsoConnectorsMap, request, setSsoEmail, setSsoConnectors]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Reset the ssoContext
|
// Reset the ssoContext
|
||||||
|
@ -64,9 +64,9 @@ const useSingleSignOnWatch = (identifierInput?: IdentifierInputValue) => {
|
||||||
if (!showSingleSignOnForm) {
|
if (!showSingleSignOnForm) {
|
||||||
setSsoConnectors([]);
|
setSsoConnectors([]);
|
||||||
|
|
||||||
setEmail(undefined);
|
setSsoEmail(undefined);
|
||||||
}
|
}
|
||||||
}, [setEmail, setSsoConnectors, showSingleSignOnForm]);
|
}, [setSsoEmail, setSsoConnectors, showSingleSignOnForm]);
|
||||||
|
|
||||||
const navigateToSingleSignOn = useCallback(async () => {
|
const navigateToSingleSignOn = useCallback(async () => {
|
||||||
if (!showSingleSignOnForm) {
|
if (!showSingleSignOnForm) {
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useCallback, useEffect } from 'react';
|
import { useCallback, useContext, useEffect } from 'react';
|
||||||
import { useForm, Controller } from 'react-hook-form';
|
import { useForm, Controller } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import UserInteractionContext from '@/Providers/UserInteractionContextProvider/UserInteractionContext';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import ErrorMessage from '@/components/ErrorMessage';
|
import ErrorMessage from '@/components/ErrorMessage';
|
||||||
import { SmartInputField } from '@/components/InputFields';
|
import { SmartInputField } from '@/components/InputFields';
|
||||||
|
@ -41,6 +42,8 @@ const ForgotPasswordForm = ({
|
||||||
UserFlow.ForgotPassword
|
UserFlow.ForgotPassword
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { setCurrentIdentifier } = useContext(UserInteractionContext);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
control,
|
control,
|
||||||
|
@ -70,10 +73,13 @@ const ForgotPasswordForm = ({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update user interaction identifier session
|
||||||
|
setCurrentIdentifier({ type, value });
|
||||||
|
|
||||||
await onSubmit({ identifier: type, value });
|
await onSubmit({ identifier: type, value });
|
||||||
})(event);
|
})(event);
|
||||||
},
|
},
|
||||||
[clearErrorMessage, handleSubmit, onSubmit]
|
[clearErrorMessage, handleSubmit, onSubmit, setCurrentIdentifier]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
import { SignInIdentifier } from '@logto/schemas';
|
import { SignInIdentifier } from '@logto/schemas';
|
||||||
import { useCallback } from 'react';
|
import { useCallback, useContext } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useLocation } from 'react-router-dom';
|
|
||||||
import { validate } from 'superstruct';
|
|
||||||
|
|
||||||
import SecondaryPageLayout from '@/Layout/SecondaryPageLayout';
|
import SecondaryPageLayout from '@/Layout/SecondaryPageLayout';
|
||||||
|
import UserInteractionContext from '@/Providers/UserInteractionContextProvider/UserInteractionContext';
|
||||||
import { useForgotPasswordSettings } from '@/hooks/use-sie';
|
import { useForgotPasswordSettings } from '@/hooks/use-sie';
|
||||||
import { passwordIdentifierStateGuard } from '@/types/guard';
|
|
||||||
import { identifierInputDescriptionMap } from '@/utils/form';
|
import { identifierInputDescriptionMap } from '@/utils/form';
|
||||||
|
|
||||||
import ErrorPage from '../ErrorPage';
|
import ErrorPage from '../ErrorPage';
|
||||||
|
@ -15,9 +13,9 @@ import ForgotPasswordForm from './ForgotPasswordForm';
|
||||||
|
|
||||||
const ForgotPassword = () => {
|
const ForgotPassword = () => {
|
||||||
const { isForgotPasswordEnabled, enabledMethodSet } = useForgotPasswordSettings();
|
const { isForgotPasswordEnabled, enabledMethodSet } = useForgotPasswordSettings();
|
||||||
const { state } = useLocation();
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const enabledMethods = [...enabledMethodSet];
|
const enabledMethods = [...enabledMethodSet];
|
||||||
|
const { currentIdentifier } = useContext(UserInteractionContext);
|
||||||
|
|
||||||
const getDefaultIdentifierType = useCallback(
|
const getDefaultIdentifierType = useCallback(
|
||||||
(identifier?: SignInIdentifier) => {
|
(identifier?: SignInIdentifier) => {
|
||||||
|
@ -42,10 +40,8 @@ const ForgotPassword = () => {
|
||||||
return <ErrorPage />;
|
return <ErrorPage />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [_, identifierState] = validate(state, passwordIdentifierStateGuard);
|
const defaultType = getDefaultIdentifierType(currentIdentifier?.type);
|
||||||
|
const defaultValue = (currentIdentifier?.type === defaultType && currentIdentifier.value) || '';
|
||||||
const defaultType = getDefaultIdentifierType(identifierState?.identifier);
|
|
||||||
const defaultValue = (identifierState?.identifier === defaultType && identifierState.value) || '';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SecondaryPageLayout
|
<SecondaryPageLayout
|
||||||
|
|
|
@ -3,8 +3,8 @@ import { assert } from '@silverhand/essentials';
|
||||||
import { fireEvent, act, waitFor } from '@testing-library/react';
|
import { fireEvent, act, waitFor } from '@testing-library/react';
|
||||||
|
|
||||||
import ConfirmModalProvider from '@/Providers/ConfirmModalProvider';
|
import ConfirmModalProvider from '@/Providers/ConfirmModalProvider';
|
||||||
import SingleSignOnContextProvider from '@/Providers/SingleSignOnContextProvider';
|
|
||||||
import SingleSignOnFormModeContextProvider from '@/Providers/SingleSignOnFormModeContextProvider';
|
import SingleSignOnFormModeContextProvider from '@/Providers/SingleSignOnFormModeContextProvider';
|
||||||
|
import UserInteractionContextProvider from '@/Providers/UserInteractionContextProvider';
|
||||||
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
|
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
|
||||||
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
|
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
|
||||||
import { mockSignInExperienceSettings, mockSsoConnectors } from '@/__mocks__/logto';
|
import { mockSignInExperienceSettings, mockSsoConnectors } from '@/__mocks__/logto';
|
||||||
|
@ -53,11 +53,11 @@ const renderForm = (
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ConfirmModalProvider>
|
<ConfirmModalProvider>
|
||||||
<SingleSignOnContextProvider>
|
<UserInteractionContextProvider>
|
||||||
<SingleSignOnFormModeContextProvider>
|
<SingleSignOnFormModeContextProvider>
|
||||||
<IdentifierRegisterForm signUpMethods={signUpMethods} />
|
<IdentifierRegisterForm signUpMethods={signUpMethods} />
|
||||||
</SingleSignOnFormModeContextProvider>
|
</SingleSignOnFormModeContextProvider>
|
||||||
</SingleSignOnContextProvider>
|
</UserInteractionContextProvider>
|
||||||
</ConfirmModalProvider>
|
</ConfirmModalProvider>
|
||||||
</SettingsProvider>
|
</SettingsProvider>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { AgreeToTermsPolicy, type SignInIdentifier } from '@logto/schemas';
|
import { AgreeToTermsPolicy, type SignInIdentifier } from '@logto/schemas';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useCallback, useEffect } from 'react';
|
import { useCallback, useContext, useEffect } from 'react';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import UserInteractionContext from '@/Providers/UserInteractionContextProvider/UserInteractionContext';
|
||||||
import LockIcon from '@/assets/icons/lock.svg';
|
import LockIcon from '@/assets/icons/lock.svg';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import ErrorMessage from '@/components/ErrorMessage';
|
import ErrorMessage from '@/components/ErrorMessage';
|
||||||
|
@ -34,6 +35,8 @@ const IdentifierRegisterForm = ({ className, autoFocus, signUpMethods }: Props)
|
||||||
|
|
||||||
const { errorMessage, clearErrorMessage, onSubmit } = useOnSubmit();
|
const { errorMessage, clearErrorMessage, onSubmit } = useOnSubmit();
|
||||||
|
|
||||||
|
const { currentIdentifier } = useContext(UserInteractionContext);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
watch,
|
watch,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
@ -114,6 +117,8 @@ const IdentifierRegisterForm = ({ className, autoFocus, signUpMethods }: Props)
|
||||||
isDanger={!!errors.id || !!errorMessage}
|
isDanger={!!errors.id || !!errorMessage}
|
||||||
errorMessage={errors.id?.message}
|
errorMessage={errors.id?.message}
|
||||||
enabledTypes={signUpMethods}
|
enabledTypes={signUpMethods}
|
||||||
|
defaultType={currentIdentifier?.type}
|
||||||
|
defaultValue={currentIdentifier?.value}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { SignInIdentifier } from '@logto/schemas';
|
import { SignInIdentifier } from '@logto/schemas';
|
||||||
import { useCallback } from 'react';
|
import { useCallback, useContext } from 'react';
|
||||||
|
|
||||||
|
import UserInteractionContext from '@/Providers/UserInteractionContextProvider/UserInteractionContext';
|
||||||
import useCheckSingleSignOn from '@/hooks/use-check-single-sign-on';
|
import useCheckSingleSignOn from '@/hooks/use-check-single-sign-on';
|
||||||
import useSendVerificationCode from '@/hooks/use-send-verification-code';
|
import useSendVerificationCode from '@/hooks/use-send-verification-code';
|
||||||
import { useSieMethods } from '@/hooks/use-sie';
|
import { useSieMethods } from '@/hooks/use-sie';
|
||||||
|
@ -11,6 +12,7 @@ import useRegisterWithUsername from './use-register-with-username';
|
||||||
const useOnSubmit = () => {
|
const useOnSubmit = () => {
|
||||||
const { ssoConnectors } = useSieMethods();
|
const { ssoConnectors } = useSieMethods();
|
||||||
const { onSubmit: checkSingleSignOn } = useCheckSingleSignOn();
|
const { onSubmit: checkSingleSignOn } = useCheckSingleSignOn();
|
||||||
|
const { setCurrentIdentifier } = useContext(UserInteractionContext);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
errorMessage: usernameRegisterErrorMessage,
|
errorMessage: usernameRegisterErrorMessage,
|
||||||
|
@ -31,6 +33,8 @@ const useOnSubmit = () => {
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
async (identifier: SignInIdentifier, value: string) => {
|
async (identifier: SignInIdentifier, value: string) => {
|
||||||
|
setCurrentIdentifier({ type: identifier, value });
|
||||||
|
|
||||||
if (identifier === SignInIdentifier.Username) {
|
if (identifier === SignInIdentifier.Username) {
|
||||||
await registerWithUsername(value);
|
await registerWithUsername(value);
|
||||||
|
|
||||||
|
@ -48,7 +52,13 @@ const useOnSubmit = () => {
|
||||||
|
|
||||||
await sendVerificationCode({ identifier, value });
|
await sendVerificationCode({ identifier, value });
|
||||||
},
|
},
|
||||||
[checkSingleSignOn, registerWithUsername, sendVerificationCode, ssoConnectors.length]
|
[
|
||||||
|
checkSingleSignOn,
|
||||||
|
registerWithUsername,
|
||||||
|
sendVerificationCode,
|
||||||
|
setCurrentIdentifier,
|
||||||
|
ssoConnectors.length,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -3,8 +3,8 @@ import { SignInIdentifier, experience } from '@logto/schemas';
|
||||||
import { assert } from '@silverhand/essentials';
|
import { assert } from '@silverhand/essentials';
|
||||||
import { fireEvent, act, waitFor } from '@testing-library/react';
|
import { fireEvent, act, waitFor } from '@testing-library/react';
|
||||||
|
|
||||||
import SingleSignOnContextProvider from '@/Providers/SingleSignOnContextProvider';
|
|
||||||
import SingleSignOnFormModeContextProvider from '@/Providers/SingleSignOnFormModeContextProvider';
|
import SingleSignOnFormModeContextProvider from '@/Providers/SingleSignOnFormModeContextProvider';
|
||||||
|
import UserInteractionContextProvider from '@/Providers/UserInteractionContextProvider';
|
||||||
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
|
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
|
||||||
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
|
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
|
||||||
import {
|
import {
|
||||||
|
@ -52,11 +52,11 @@ const renderForm = (signInMethods: SignIn['methods'], ssoConnectors: SsoConnecto
|
||||||
ssoConnectors,
|
ssoConnectors,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SingleSignOnContextProvider>
|
<UserInteractionContextProvider>
|
||||||
<SingleSignOnFormModeContextProvider>
|
<SingleSignOnFormModeContextProvider>
|
||||||
<IdentifierSignInForm signInMethods={signInMethods} />
|
<IdentifierSignInForm signInMethods={signInMethods} />
|
||||||
</SingleSignOnFormModeContextProvider>
|
</SingleSignOnFormModeContextProvider>
|
||||||
</SingleSignOnContextProvider>
|
</UserInteractionContextProvider>
|
||||||
</SettingsProvider>
|
</SettingsProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { AgreeToTermsPolicy, type SignIn } from '@logto/schemas';
|
import { AgreeToTermsPolicy, type SignIn } from '@logto/schemas';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useCallback, useEffect, useMemo } from 'react';
|
import { useCallback, useContext, useEffect, useMemo } from 'react';
|
||||||
import { useForm, Controller } from 'react-hook-form';
|
import { useForm, Controller } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import UserInteractionContext from '@/Providers/UserInteractionContextProvider/UserInteractionContext';
|
||||||
import LockIcon from '@/assets/icons/lock.svg';
|
import LockIcon from '@/assets/icons/lock.svg';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import ErrorMessage from '@/components/ErrorMessage';
|
import ErrorMessage from '@/components/ErrorMessage';
|
||||||
|
@ -32,6 +33,7 @@ const IdentifierSignInForm = ({ className, autoFocus, signInMethods }: Props) =>
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { errorMessage, clearErrorMessage, onSubmit } = useOnSubmit(signInMethods);
|
const { errorMessage, clearErrorMessage, onSubmit } = useOnSubmit(signInMethods);
|
||||||
const { termsValidation, agreeToTermsPolicy } = useTerms();
|
const { termsValidation, agreeToTermsPolicy } = useTerms();
|
||||||
|
const { currentIdentifier } = useContext(UserInteractionContext);
|
||||||
|
|
||||||
const enabledSignInMethods = useMemo(
|
const enabledSignInMethods = useMemo(
|
||||||
() => signInMethods.map(({ identifier }) => identifier),
|
() => signInMethods.map(({ identifier }) => identifier),
|
||||||
|
@ -117,6 +119,8 @@ const IdentifierSignInForm = ({ className, autoFocus, signInMethods }: Props) =>
|
||||||
isDanger={!!errors.identifier || !!errorMessage}
|
isDanger={!!errors.identifier || !!errorMessage}
|
||||||
errorMessage={errors.identifier?.message}
|
errorMessage={errors.identifier?.message}
|
||||||
enabledTypes={enabledSignInMethods}
|
enabledTypes={enabledSignInMethods}
|
||||||
|
defaultType={currentIdentifier?.type}
|
||||||
|
defaultValue={currentIdentifier?.value}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import type { SignIn } from '@logto/schemas';
|
import type { SignIn } from '@logto/schemas';
|
||||||
import { SignInIdentifier } from '@logto/schemas';
|
import { SignInIdentifier } from '@logto/schemas';
|
||||||
import { useCallback } from 'react';
|
import { useCallback, useContext } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import UserInteractionContext from '@/Providers/UserInteractionContextProvider/UserInteractionContext';
|
||||||
import useCheckSingleSignOn from '@/hooks/use-check-single-sign-on';
|
import useCheckSingleSignOn from '@/hooks/use-check-single-sign-on';
|
||||||
import useSendVerificationCode from '@/hooks/use-send-verification-code';
|
import useSendVerificationCode from '@/hooks/use-send-verification-code';
|
||||||
import { useSieMethods } from '@/hooks/use-sie';
|
import { useSieMethods } from '@/hooks/use-sie';
|
||||||
|
@ -12,6 +13,7 @@ const useOnSubmit = (signInMethods: SignIn['methods']) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { ssoConnectors } = useSieMethods();
|
const { ssoConnectors } = useSieMethods();
|
||||||
const { onSubmit: checkSingleSignOn } = useCheckSingleSignOn();
|
const { onSubmit: checkSingleSignOn } = useCheckSingleSignOn();
|
||||||
|
const { setCurrentIdentifier } = useContext(UserInteractionContext);
|
||||||
|
|
||||||
const signInWithPassword = useCallback(
|
const signInWithPassword = useCallback(
|
||||||
(identifier: SignInIdentifier, value: string) => {
|
(identifier: SignInIdentifier, value: string) => {
|
||||||
|
@ -39,6 +41,8 @@ const useOnSubmit = (signInMethods: SignIn['methods']) => {
|
||||||
throw new Error(`Cannot find method with identifier type ${identifier}`);
|
throw new Error(`Cannot find method with identifier type ${identifier}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setCurrentIdentifier({ type: identifier, value });
|
||||||
|
|
||||||
const { password, isPasswordPrimary, verificationCode } = method;
|
const { password, isPasswordPrimary, verificationCode } = method;
|
||||||
|
|
||||||
if (identifier === SignInIdentifier.Username) {
|
if (identifier === SignInIdentifier.Username) {
|
||||||
|
@ -69,6 +73,7 @@ const useOnSubmit = (signInMethods: SignIn['methods']) => {
|
||||||
[
|
[
|
||||||
checkSingleSignOn,
|
checkSingleSignOn,
|
||||||
sendVerificationCode,
|
sendVerificationCode,
|
||||||
|
setCurrentIdentifier,
|
||||||
signInMethods,
|
signInMethods,
|
||||||
signInWithPassword,
|
signInWithPassword,
|
||||||
ssoConnectors.length,
|
ssoConnectors.length,
|
||||||
|
|
|
@ -3,8 +3,8 @@ import { assert } from '@silverhand/essentials';
|
||||||
import { fireEvent, waitFor } from '@testing-library/react';
|
import { fireEvent, waitFor } from '@testing-library/react';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
|
|
||||||
import SingleSignOnContextProvider from '@/Providers/SingleSignOnContextProvider';
|
|
||||||
import SingleSignOnFormModeContextProvider from '@/Providers/SingleSignOnFormModeContextProvider';
|
import SingleSignOnFormModeContextProvider from '@/Providers/SingleSignOnFormModeContextProvider';
|
||||||
|
import UserInteractionContextProvider from '@/Providers/UserInteractionContextProvider';
|
||||||
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
|
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
|
||||||
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
|
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
|
||||||
import { mockSignInExperienceSettings, mockSsoConnectors } from '@/__mocks__/logto';
|
import { mockSignInExperienceSettings, mockSsoConnectors } from '@/__mocks__/logto';
|
||||||
|
@ -50,11 +50,11 @@ describe('UsernamePasswordSignInForm', () => {
|
||||||
) =>
|
) =>
|
||||||
renderWithPageContext(
|
renderWithPageContext(
|
||||||
<SettingsProvider settings={{ ...mockSignInExperienceSettings, ...settings }}>
|
<SettingsProvider settings={{ ...mockSignInExperienceSettings, ...settings }}>
|
||||||
<SingleSignOnContextProvider>
|
<UserInteractionContextProvider>
|
||||||
<SingleSignOnFormModeContextProvider>
|
<SingleSignOnFormModeContextProvider>
|
||||||
<PasswordSignInForm signInMethods={signInMethods} />
|
<PasswordSignInForm signInMethods={signInMethods} />
|
||||||
</SingleSignOnFormModeContextProvider>
|
</SingleSignOnFormModeContextProvider>
|
||||||
</SingleSignOnContextProvider>
|
</UserInteractionContextProvider>
|
||||||
</SettingsProvider>
|
</SettingsProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import SecondaryPageLayout from '@/Layout/SecondaryPageLayout';
|
import SecondaryPageLayout from '@/Layout/SecondaryPageLayout';
|
||||||
import PageContext from '@/Providers/PageContextProvider/PageContext';
|
import PageContext from '@/Providers/PageContextProvider/PageContext';
|
||||||
import SingleSignOnContext from '@/Providers/SingleSignOnContextProvider/SingleSignOnContext';
|
import UserInteractionContext from '@/Providers/UserInteractionContextProvider/UserInteractionContext';
|
||||||
import SocialLinkButton from '@/components/Button/SocialLinkButton';
|
import SocialLinkButton from '@/components/Button/SocialLinkButton';
|
||||||
import useNativeMessageListener from '@/hooks/use-native-message-listener';
|
import useNativeMessageListener from '@/hooks/use-native-message-listener';
|
||||||
import useSingleSignOn from '@/hooks/use-single-sign-on';
|
import useSingleSignOn from '@/hooks/use-single-sign-on';
|
||||||
|
@ -13,7 +13,7 @@ import * as styles from './index.module.scss';
|
||||||
|
|
||||||
const SingleSignOnConnectors = () => {
|
const SingleSignOnConnectors = () => {
|
||||||
const { theme } = useContext(PageContext);
|
const { theme } = useContext(PageContext);
|
||||||
const { email, ssoConnectors } = useContext(SingleSignOnContext);
|
const { ssoEmail, ssoConnectors } = useContext(UserInteractionContext);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const onSubmit = useSingleSignOn();
|
const onSubmit = useSingleSignOn();
|
||||||
|
|
||||||
|
@ -22,18 +22,18 @@ const SingleSignOnConnectors = () => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Return to the previous page if no email and no connectors are available in the context
|
// Return to the previous page if no email and no connectors are available in the context
|
||||||
if (!email || ssoConnectors.length === 0) {
|
if (!ssoEmail || ssoConnectors.length === 0) {
|
||||||
navigate('../email', {
|
navigate('../email', {
|
||||||
replace: true,
|
replace: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [email, navigate, ssoConnectors.length]);
|
}, [ssoEmail, navigate, ssoConnectors.length]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SecondaryPageLayout
|
<SecondaryPageLayout
|
||||||
title="action.single_sign_on"
|
title="action.single_sign_on"
|
||||||
description="description.single_sign_on_connectors_list"
|
description="description.single_sign_on_connectors_list"
|
||||||
descriptionProps={{ email }}
|
descriptionProps={{ email: ssoEmail }}
|
||||||
>
|
>
|
||||||
<div className={styles.ssoLinkList}>
|
<div className={styles.ssoLinkList}>
|
||||||
{ssoConnectors.map((connector) => {
|
{ssoConnectors.map((connector) => {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { useCallback, useContext, useEffect } from 'react';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
|
||||||
import SecondaryPageLayout from '@/Layout/SecondaryPageLayout';
|
import SecondaryPageLayout from '@/Layout/SecondaryPageLayout';
|
||||||
import SingleSignOnContext from '@/Providers/SingleSignOnContextProvider/SingleSignOnContext';
|
import UserInteractionContext from '@/Providers/UserInteractionContextProvider/UserInteractionContext';
|
||||||
import LockIcon from '@/assets/icons/lock.svg';
|
import LockIcon from '@/assets/icons/lock.svg';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import ErrorMessage from '@/components/ErrorMessage';
|
import ErrorMessage from '@/components/ErrorMessage';
|
||||||
|
@ -21,7 +21,7 @@ type FormState = {
|
||||||
|
|
||||||
const SingleSignOnEmail = () => {
|
const SingleSignOnEmail = () => {
|
||||||
const { errorMessage, clearErrorMessage, onSubmit } = useOnSubmit();
|
const { errorMessage, clearErrorMessage, onSubmit } = useOnSubmit();
|
||||||
const { email } = useContext(SingleSignOnContext);
|
const { ssoEmail } = useContext(UserInteractionContext);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
@ -73,7 +73,7 @@ const SingleSignOnEmail = () => {
|
||||||
className={styles.inputField}
|
className={styles.inputField}
|
||||||
{...field}
|
{...field}
|
||||||
isDanger={!!errors.identifier}
|
isDanger={!!errors.identifier}
|
||||||
defaultValue={email}
|
defaultValue={ssoEmail}
|
||||||
errorMessage={errors.identifier?.message}
|
errorMessage={errors.identifier?.message}
|
||||||
enabledTypes={[SignInIdentifier.Email]}
|
enabledTypes={[SignInIdentifier.Email]}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -119,3 +119,20 @@ export const ssoConnectorMetadataGuard: s.Describe<SsoConnectorMetadata> = s.obj
|
||||||
darkLogo: s.optional(s.string()),
|
darkLogo: s.optional(s.string()),
|
||||||
connectorName: s.string(),
|
connectorName: s.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the structure for caching the current user's identifier.
|
||||||
|
*
|
||||||
|
* Purpose:
|
||||||
|
* - Used in conjunction with the HiddenIdentifierInput component to assist
|
||||||
|
* password managers in associating the correct identifier with passwords.
|
||||||
|
*
|
||||||
|
* - Cache the identifier so that when the user returns from the verification
|
||||||
|
* page or the password page, the identifier they entered will not be cleared.
|
||||||
|
*/
|
||||||
|
export const currentIdentifierSessionGuard = s.object({
|
||||||
|
type: s.enums([SignInIdentifier.Email, SignInIdentifier.Phone, SignInIdentifier.Username]),
|
||||||
|
value: s.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CurrentIdentifierSession = s.Infer<typeof currentIdentifierSessionGuard>;
|
||||||
|
|
Loading…
Reference in a new issue