mirror of
https://github.com/logto-io/logto.git
synced 2025-01-13 21:30:30 -05:00
refactor(experience): cache input identifier for reset password first screen (#6516)
This commit is contained in:
parent
ef78823948
commit
41ee881bb7
6 changed files with 103 additions and 89 deletions
|
@ -2,10 +2,7 @@ import { type SsoConnectorMetadata } from '@logto/schemas';
|
|||
import { noop } from '@silverhand/essentials';
|
||||
import { createContext } from 'react';
|
||||
|
||||
import {
|
||||
type IdentifierInputType,
|
||||
type IdentifierInputValue,
|
||||
} from '@/components/InputFields/SmartInputField';
|
||||
import { type IdentifierInputValue } from '@/components/InputFields/SmartInputField';
|
||||
|
||||
export type UserInteractionContextType = {
|
||||
// All the enabled sso connectors
|
||||
|
@ -19,26 +16,6 @@ export type UserInteractionContextType = {
|
|||
* The cached identifier input value that the user has inputted.
|
||||
*/
|
||||
identifierInputValue?: IdentifierInputValue;
|
||||
/**
|
||||
* Retrieves the cached identifier input value that the user has inputted based on enabled types.
|
||||
* The value will be used to pre-fill the identifier input field in experience pages.
|
||||
*
|
||||
* @param {IdentifierInputType[]} enabledTypes - Array of enabled identifier types
|
||||
* @returns {IdentifierInputValue | undefined} The identifier input value object or undefined
|
||||
*
|
||||
* The function checks if the type of identifierInputValue is in the `enabledTypes` array,
|
||||
* if the type matches, it returns `identifierInputValue`; otherwise, it returns `undefined`
|
||||
*
|
||||
* Example:
|
||||
* ```ts
|
||||
* const value = getIdentifierInputValueByTypes(['email', 'phone']);
|
||||
* // Returns `identifierInputValue` if its type is 'email' or 'phone'
|
||||
* // Returns `undefined` otherwise
|
||||
* ```
|
||||
*/
|
||||
getIdentifierInputValueByTypes: (
|
||||
enabledTypes: IdentifierInputType[]
|
||||
) => IdentifierInputValue | undefined;
|
||||
/**
|
||||
* This method is used to cache the identifier input value.
|
||||
*/
|
||||
|
@ -74,8 +51,6 @@ export default createContext<UserInteractionContextType>({
|
|||
setSsoEmail: noop,
|
||||
setSsoConnectors: noop,
|
||||
identifierInputValue: undefined,
|
||||
// eslint-disable-next-line unicorn/no-useless-undefined
|
||||
getIdentifierInputValueByTypes: () => undefined,
|
||||
setIdentifierInputValue: noop,
|
||||
forgotPasswordIdentifierInputValue: undefined,
|
||||
setForgotPasswordIdentifierInputValue: noop,
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
import { type SsoConnectorMetadata } from '@logto/schemas';
|
||||
import { type ReactNode, useEffect, useMemo, useState, useCallback } from 'react';
|
||||
|
||||
import {
|
||||
type IdentifierInputType,
|
||||
type IdentifierInputValue,
|
||||
} from '@/components/InputFields/SmartInputField';
|
||||
import { type IdentifierInputValue } from '@/components/InputFields/SmartInputField';
|
||||
import useSessionStorage, { StorageKeys } from '@/hooks/use-session-storages';
|
||||
import { useSieMethods } from '@/hooks/use-sie';
|
||||
|
||||
|
@ -79,18 +76,6 @@ const UserInteractionContextProvider = ({ children }: Props) => {
|
|||
[ssoConnectors]
|
||||
);
|
||||
|
||||
const getIdentifierInputValueByTypes = useCallback(
|
||||
(enabledTypes: IdentifierInputType[]) => {
|
||||
const { type } = identifierInputValue ?? {};
|
||||
/**
|
||||
* Check if the type is included in the enabledTypes array
|
||||
* If it is, return identifierInputValue; otherwise, return undefined
|
||||
*/
|
||||
return type && enabledTypes.includes(type) ? identifierInputValue : undefined;
|
||||
},
|
||||
[identifierInputValue]
|
||||
);
|
||||
|
||||
const clearInteractionContextSessionStorage = useCallback(() => {
|
||||
remove(StorageKeys.IdentifierInputValue);
|
||||
remove(StorageKeys.ForgotPasswordIdentifierInputValue);
|
||||
|
@ -104,7 +89,6 @@ const UserInteractionContextProvider = ({ children }: Props) => {
|
|||
ssoConnectors: domainFilteredConnectors,
|
||||
setSsoConnectors: setDomainFilteredConnectors,
|
||||
identifierInputValue,
|
||||
getIdentifierInputValueByTypes,
|
||||
setIdentifierInputValue,
|
||||
forgotPasswordIdentifierInputValue,
|
||||
setForgotPasswordIdentifierInputValue,
|
||||
|
@ -115,7 +99,6 @@ const UserInteractionContextProvider = ({ children }: Props) => {
|
|||
ssoConnectorsMap,
|
||||
domainFilteredConnectors,
|
||||
identifierInputValue,
|
||||
getIdentifierInputValueByTypes,
|
||||
forgotPasswordIdentifierInputValue,
|
||||
clearInteractionContextSessionStorage,
|
||||
]
|
||||
|
|
|
@ -1,26 +1,80 @@
|
|||
import { type SignInIdentifier } from '@logto/schemas';
|
||||
import { type Optional } from '@silverhand/essentials';
|
||||
import { useContext, useMemo } from 'react';
|
||||
|
||||
import UserInteractionContext from '@/Providers/UserInteractionContextProvider/UserInteractionContext';
|
||||
import { type IdentifierInputValue } from '@/components/InputFields/SmartInputField';
|
||||
import {
|
||||
type IdentifierInputType,
|
||||
type IdentifierInputValue,
|
||||
} from '@/components/InputFields/SmartInputField';
|
||||
|
||||
import useLoginHint from './use-login-hint';
|
||||
|
||||
type Options = {
|
||||
enabledIdentifiers?: SignInIdentifier[];
|
||||
/**
|
||||
* Retrieves the cached identifier input value that the user has inputted based on enabled types.
|
||||
* The value will be used to pre-fill the identifier input field in experience pages.
|
||||
*
|
||||
* @param {IdentifierInputValue} identifierInputValue - The identifier input value to be checked
|
||||
* @param {IdentifierInputType[]} enabledTypes - Array of enabled identifier types
|
||||
* @returns {IdentifierInputValue | undefined} The identifier input value object or undefined
|
||||
*
|
||||
* The function checks if the type of identifierInputValue is in the `enabledTypes` array,
|
||||
* if the type matches, it returns `identifierInputValue`; otherwise, it returns `undefined`
|
||||
*
|
||||
* Example:
|
||||
* ```ts
|
||||
* const value = getIdentifierInputValueByTypes(['email', 'phone']);
|
||||
* // Returns `identifierInputValue` if its type is 'email' or 'phone'
|
||||
* // Returns `undefined` otherwise
|
||||
* ```
|
||||
*/
|
||||
const getIdentifierInputValueByTypes = (
|
||||
identifierInputValue: IdentifierInputValue,
|
||||
enabledTypes: IdentifierInputType[]
|
||||
): Optional<IdentifierInputValue> => {
|
||||
const { type } = identifierInputValue;
|
||||
/**
|
||||
* Check if the type is included in the enabledTypes array
|
||||
* If it is, return identifierInputValue; otherwise, return undefined
|
||||
*/
|
||||
return type && enabledTypes.includes(type) ? identifierInputValue : undefined;
|
||||
};
|
||||
|
||||
const usePrefilledIdentifier = ({ enabledIdentifiers }: Options = {}) => {
|
||||
const { identifierInputValue, getIdentifierInputValueByTypes } =
|
||||
type Options = {
|
||||
enabledIdentifiers?: SignInIdentifier[];
|
||||
/**
|
||||
* Whether the current page is the forgot password page
|
||||
*
|
||||
* Note: since a user may not use the same identifier to sign in and reset password,
|
||||
* we need to distinguish between the two scenarios.
|
||||
* E.g. the user may only use username to sign in, but only email or phone number can be used to reset password.
|
||||
*/
|
||||
isForgotPassword?: boolean;
|
||||
};
|
||||
|
||||
const usePrefilledIdentifier = ({ enabledIdentifiers, isForgotPassword = false }: Options = {}) => {
|
||||
const { identifierInputValue, forgotPasswordIdentifierInputValue } =
|
||||
useContext(UserInteractionContext);
|
||||
|
||||
const loginHint = useLoginHint();
|
||||
|
||||
const cachedInputIdentifier = useMemo(() => {
|
||||
return enabledIdentifiers
|
||||
? getIdentifierInputValueByTypes(enabledIdentifiers)
|
||||
: identifierInputValue;
|
||||
}, [enabledIdentifiers, getIdentifierInputValueByTypes, identifierInputValue]);
|
||||
const identifier = isForgotPassword ? forgotPasswordIdentifierInputValue : identifierInputValue;
|
||||
/**
|
||||
* If there's no identifier input value or no limitations for enabled identifiers,
|
||||
* return the identifier input value as is (which might be undefined)
|
||||
*/
|
||||
if (!identifier || !enabledIdentifiers) {
|
||||
return identifier;
|
||||
}
|
||||
|
||||
return getIdentifierInputValueByTypes(identifier, enabledIdentifiers);
|
||||
}, [
|
||||
enabledIdentifiers,
|
||||
forgotPasswordIdentifierInputValue,
|
||||
identifierInputValue,
|
||||
isForgotPassword,
|
||||
]);
|
||||
|
||||
return useMemo<IdentifierInputValue>(() => {
|
||||
/**
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import { SignInIdentifier } from '@logto/schemas';
|
||||
import { useCallback, useContext } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import SecondaryPageLayout from '@/Layout/SecondaryPageLayout';
|
||||
import UserInteractionContext from '@/Providers/UserInteractionContextProvider/UserInteractionContext';
|
||||
import usePrefilledIdentifier from '@/hooks/use-prefilled-identifier';
|
||||
import { useForgotPasswordSettings } from '@/hooks/use-sie';
|
||||
import { identifierInputDescriptionMap } from '@/utils/form';
|
||||
|
||||
|
@ -15,37 +13,15 @@ const ForgotPassword = () => {
|
|||
const { isForgotPasswordEnabled, enabledMethodSet } = useForgotPasswordSettings();
|
||||
const { t } = useTranslation();
|
||||
const enabledMethods = [...enabledMethodSet];
|
||||
const { forgotPasswordIdentifierInputValue } = useContext(UserInteractionContext);
|
||||
|
||||
const getDefaultIdentifierType = useCallback(
|
||||
(identifier?: SignInIdentifier) => {
|
||||
if (
|
||||
identifier === SignInIdentifier.Username ||
|
||||
identifier === SignInIdentifier.Email ||
|
||||
!identifier
|
||||
) {
|
||||
return enabledMethodSet.has(SignInIdentifier.Email)
|
||||
? SignInIdentifier.Email
|
||||
: SignInIdentifier.Phone;
|
||||
}
|
||||
|
||||
return enabledMethodSet.has(SignInIdentifier.Phone)
|
||||
? SignInIdentifier.Phone
|
||||
: SignInIdentifier.Email;
|
||||
},
|
||||
[enabledMethodSet]
|
||||
);
|
||||
const { value: prefilledValue } = usePrefilledIdentifier({
|
||||
enabledIdentifiers: enabledMethods,
|
||||
isForgotPassword: true,
|
||||
});
|
||||
|
||||
if (!isForgotPasswordEnabled) {
|
||||
return <ErrorPage />;
|
||||
}
|
||||
|
||||
const defaultType = getDefaultIdentifierType(forgotPasswordIdentifierInputValue?.type);
|
||||
const defaultValue =
|
||||
(forgotPasswordIdentifierInputValue?.type === defaultType &&
|
||||
forgotPasswordIdentifierInputValue.value) ||
|
||||
'';
|
||||
|
||||
return (
|
||||
<SecondaryPageLayout
|
||||
title="description.reset_password"
|
||||
|
@ -54,7 +30,7 @@ const ForgotPassword = () => {
|
|||
types: enabledMethods.map((method) => t(identifierInputDescriptionMap[method])),
|
||||
}}
|
||||
>
|
||||
<ForgotPasswordForm autoFocus defaultValue={defaultValue} enabledTypes={enabledMethods} />
|
||||
<ForgotPasswordForm autoFocus defaultValue={prefilledValue} enabledTypes={enabledMethods} />
|
||||
</SecondaryPageLayout>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -21,7 +21,13 @@ const ResetPassword = () => {
|
|||
const { setToast } = useToast();
|
||||
const navigate = useNavigate();
|
||||
const { show } = usePromiseConfirmModal();
|
||||
const { setForgotPasswordIdentifierInputValue } = useContext(UserInteractionContext);
|
||||
const {
|
||||
identifierInputValue,
|
||||
setIdentifierInputValue,
|
||||
forgotPasswordIdentifierInputValue,
|
||||
setForgotPasswordIdentifierInputValue,
|
||||
} = useContext(UserInteractionContext);
|
||||
|
||||
const errorHandlers: ErrorHandlers = useMemo(
|
||||
() => ({
|
||||
'session.verification_session_not_found': async (error) => {
|
||||
|
@ -37,14 +43,31 @@ const ResetPassword = () => {
|
|||
const successHandler: SuccessHandler<typeof setUserPassword> = useCallback(
|
||||
(result) => {
|
||||
if (result) {
|
||||
// Clear the forgot password identifier input value
|
||||
/**
|
||||
* Improve user experience by caching the identifier input value for sign-in page
|
||||
* when the user is first redirected to the reset password page.
|
||||
* This allows user to continue the sign flow without having to re-enter the identifier.
|
||||
*/
|
||||
if (!identifierInputValue) {
|
||||
setIdentifierInputValue(forgotPasswordIdentifierInputValue);
|
||||
}
|
||||
|
||||
// Clear the forgot password identifier input value after the password is set
|
||||
setForgotPasswordIdentifierInputValue(undefined);
|
||||
|
||||
setToast(t('description.password_changed'));
|
||||
navigate('/sign-in', { replace: true });
|
||||
}
|
||||
},
|
||||
[navigate, setForgotPasswordIdentifierInputValue, setToast, t]
|
||||
[
|
||||
forgotPasswordIdentifierInputValue,
|
||||
identifierInputValue,
|
||||
navigate,
|
||||
setForgotPasswordIdentifierInputValue,
|
||||
setIdentifierInputValue,
|
||||
setToast,
|
||||
t,
|
||||
]
|
||||
);
|
||||
|
||||
const [action] = usePasswordAction({
|
||||
|
|
|
@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
|
|||
import { Navigate } from 'react-router-dom';
|
||||
|
||||
import FocusedAuthPageLayout from '@/Layout/FocusedAuthPageLayout';
|
||||
import useLoginHint from '@/hooks/use-login-hint';
|
||||
import usePrefilledIdentifier from '@/hooks/use-prefilled-identifier';
|
||||
import { identifierInputDescriptionMap } from '@/utils/form';
|
||||
|
||||
import ForgotPasswordForm from '../ForgotPassword/ForgotPasswordForm';
|
||||
|
@ -33,7 +33,10 @@ import { useResetPasswordMethods } from './use-reset-password-methods';
|
|||
const ResetPasswordLanding = () => {
|
||||
const { t } = useTranslation();
|
||||
const enabledMethods = useResetPasswordMethods();
|
||||
const loginHint = useLoginHint();
|
||||
const { value: prefilledValue } = usePrefilledIdentifier({
|
||||
enabledIdentifiers: enabledMethods,
|
||||
isForgotPassword: true,
|
||||
});
|
||||
|
||||
// Fallback to sign-in page
|
||||
if (enabledMethods.length === 0) {
|
||||
|
@ -54,7 +57,7 @@ const ResetPasswordLanding = () => {
|
|||
text: 'description.back_to_sign_in',
|
||||
}}
|
||||
>
|
||||
<ForgotPasswordForm autoFocus defaultValue={loginHint} enabledTypes={enabledMethods} />
|
||||
<ForgotPasswordForm autoFocus defaultValue={prefilledValue} enabledTypes={enabledMethods} />
|
||||
</FocusedAuthPageLayout>
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Add table
Reference in a new issue