mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
fix(experience): prevent errors from applying unsupported cached identifier types (#6425)
* fix(experience): prevent errors from applying unsupported cached identifier types * test(experience): add integration tests for cached input value * refactor(experience): rename `getIdentifierInputValue` to `getIdentifierInputValueByTypes` * refactor(experience): add `identifierInputValue` back * refactor(experience): update implementation
This commit is contained in:
parent
758d270f7c
commit
ab90f43db2
7 changed files with 122 additions and 9 deletions
7
.changeset/rare-moons-unite.md
Normal file
7
.changeset/rare-moons-unite.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
"@logto/experience": patch
|
||||
---
|
||||
|
||||
fix(experience): prevent errors from applying unsupported cached identifier types
|
||||
|
||||
Previously, cached identifier input values were applied to all pages without type checking, potentially causing errors. Now, the type is verified before application to ensure compatibility with each page's supported types.
|
|
@ -2,7 +2,10 @@ import { type SsoConnectorMetadata } from '@logto/schemas';
|
|||
import { noop } from '@silverhand/essentials';
|
||||
import { createContext } from 'react';
|
||||
|
||||
import { type IdentifierInputValue } from '@/components/InputFields/SmartInputField';
|
||||
import {
|
||||
type IdentifierInputType,
|
||||
type IdentifierInputValue,
|
||||
} from '@/components/InputFields/SmartInputField';
|
||||
|
||||
export type UserInteractionContextType = {
|
||||
// All the enabled sso connectors
|
||||
|
@ -13,12 +16,31 @@ export type UserInteractionContextType = {
|
|||
ssoConnectors: SsoConnectorMetadata[];
|
||||
setSsoConnectors: React.Dispatch<React.SetStateAction<SsoConnectorMetadata[]>>;
|
||||
/**
|
||||
* The cached identifier input value that the user has inputted when signing in.
|
||||
* The value will be used to pre-fill the identifier input field in sign-in pages.
|
||||
* The cached identifier input value that the user has inputted.
|
||||
*/
|
||||
identifierInputValue?: IdentifierInputValue;
|
||||
/**
|
||||
* This method is used to cache the identifier input value when signing in.
|
||||
* 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.
|
||||
*/
|
||||
setIdentifierInputValue: React.Dispatch<React.SetStateAction<IdentifierInputValue | undefined>>;
|
||||
/**
|
||||
|
@ -52,6 +74,8 @@ 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,7 +1,10 @@
|
|||
import { type SsoConnectorMetadata } from '@logto/schemas';
|
||||
import { type ReactNode, useEffect, useMemo, useState, useCallback } from 'react';
|
||||
|
||||
import { type IdentifierInputValue } from '@/components/InputFields/SmartInputField';
|
||||
import {
|
||||
type IdentifierInputType,
|
||||
type IdentifierInputValue,
|
||||
} from '@/components/InputFields/SmartInputField';
|
||||
import useSessionStorage, { StorageKeys } from '@/hooks/use-session-storages';
|
||||
import { useSieMethods } from '@/hooks/use-sie';
|
||||
|
||||
|
@ -76,6 +79,18 @@ 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);
|
||||
|
@ -89,6 +104,7 @@ const UserInteractionContextProvider = ({ children }: Props) => {
|
|||
ssoConnectors: domainFilteredConnectors,
|
||||
setSsoConnectors: setDomainFilteredConnectors,
|
||||
identifierInputValue,
|
||||
getIdentifierInputValueByTypes,
|
||||
setIdentifierInputValue,
|
||||
forgotPasswordIdentifierInputValue,
|
||||
setForgotPasswordIdentifierInputValue,
|
||||
|
@ -99,6 +115,7 @@ const UserInteractionContextProvider = ({ children }: Props) => {
|
|||
ssoConnectorsMap,
|
||||
domainFilteredConnectors,
|
||||
identifierInputValue,
|
||||
getIdentifierInputValueByTypes,
|
||||
forgotPasswordIdentifierInputValue,
|
||||
clearInteractionContextSessionStorage,
|
||||
]
|
||||
|
|
|
@ -42,8 +42,7 @@ const ForgotPasswordForm = ({
|
|||
UserFlow.ForgotPassword
|
||||
);
|
||||
|
||||
const { setForgotPasswordIdentifierInputValue, setIdentifierInputValue } =
|
||||
useContext(UserInteractionContext);
|
||||
const { setForgotPasswordIdentifierInputValue } = useContext(UserInteractionContext);
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
|
|
|
@ -36,7 +36,9 @@ const IdentifierRegisterForm = ({ className, autoFocus, signUpMethods }: Props)
|
|||
|
||||
const { errorMessage, clearErrorMessage, onSubmit } = useOnSubmit();
|
||||
|
||||
const { identifierInputValue, setIdentifierInputValue } = useContext(UserInteractionContext);
|
||||
const { getIdentifierInputValueByTypes, setIdentifierInputValue } =
|
||||
useContext(UserInteractionContext);
|
||||
const identifierInputValue = getIdentifierInputValueByTypes(signUpMethods);
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
|
|
|
@ -34,7 +34,8 @@ const IdentifierSignInForm = ({ className, autoFocus, signInMethods }: Props) =>
|
|||
const { t } = useTranslation();
|
||||
const { errorMessage, clearErrorMessage, onSubmit } = useOnSubmit(signInMethods);
|
||||
const { termsValidation, agreeToTermsPolicy } = useTerms();
|
||||
const { identifierInputValue, setIdentifierInputValue } = useContext(UserInteractionContext);
|
||||
const { getIdentifierInputValueByTypes, setIdentifierInputValue } =
|
||||
useContext(UserInteractionContext);
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const enabledSignInMethods = useMemo(
|
||||
|
@ -42,6 +43,8 @@ const IdentifierSignInForm = ({ className, autoFocus, signInMethods }: Props) =>
|
|||
[signInMethods]
|
||||
);
|
||||
|
||||
const identifierInputValue = getIdentifierInputValueByTypes(enabledSignInMethods);
|
||||
|
||||
const {
|
||||
watch,
|
||||
handleSubmit,
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
import { ConnectorType } from '@logto/connector-kit';
|
||||
import { SignInIdentifier, SignInMode } from '@logto/schemas';
|
||||
import { appendPath } from '@silverhand/essentials';
|
||||
|
||||
import { updateSignInExperience } from '#src/api/sign-in-experience.js';
|
||||
import { demoAppUrl, logtoUrl } from '#src/constants.js';
|
||||
import { clearConnectorsByTypes, setEmailConnector } from '#src/helpers/connector.js';
|
||||
import ExpectExperience from '#src/ui-helpers/expect-experience.js';
|
||||
import { generateEmail } from '#src/utils.js';
|
||||
|
||||
describe('identifier input cache', () => {
|
||||
const testEmail = generateEmail();
|
||||
// eslint-disable-next-line @silverhand/fp/no-let
|
||||
let experience: ExpectExperience;
|
||||
|
||||
beforeAll(async () => {
|
||||
await clearConnectorsByTypes([ConnectorType.Social, ConnectorType.Email, ConnectorType.Sms]);
|
||||
await setEmailConnector();
|
||||
|
||||
await updateSignInExperience({
|
||||
signUp: { identifiers: [SignInIdentifier.Username], password: true, verify: false },
|
||||
signIn: {
|
||||
methods: [
|
||||
{
|
||||
identifier: SignInIdentifier.Username,
|
||||
password: true,
|
||||
verificationCode: false,
|
||||
isPasswordPrimary: true,
|
||||
},
|
||||
{
|
||||
identifier: SignInIdentifier.Email,
|
||||
password: true,
|
||||
verificationCode: true,
|
||||
isPasswordPrimary: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
signInMode: SignInMode.SignInAndRegister,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @silverhand/fp/no-mutation
|
||||
experience = new ExpectExperience(await browser.newPage());
|
||||
});
|
||||
|
||||
it('should be able to cache identifier(email) input on sign-in page', async () => {
|
||||
await experience.startWith(demoAppUrl, 'sign-in');
|
||||
await experience.toFillInput('identifier', testEmail, { submit: true });
|
||||
// The identifier is read from the cache and displayed on the page
|
||||
await experience.toMatchElement('div', { text: new RegExp(testEmail) });
|
||||
// Nav back, the identifier should still be there
|
||||
await experience.toClick('div[role=button][class$=navButton]', /Back/);
|
||||
await experience.toMatchElement(`input[name=identifier][value="${testEmail}"]`);
|
||||
});
|
||||
|
||||
it('cached identifier(email) should not be apply to register form (only username is allowed)', async () => {
|
||||
await experience.toClick('a', 'Create account');
|
||||
experience.toMatchUrl(appendPath(new URL(logtoUrl), 'register').href);
|
||||
// The input should be empty
|
||||
await experience.toMatchElement('input[name="id"][value=""]');
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue