mirror of
https://github.com/logto-io/logto.git
synced 2024-12-30 20:33:54 -05:00
refactor(experience): improve identifier prefilling (#6508)
This commit is contained in:
parent
61fb856ccd
commit
3aba357ba6
8 changed files with 73 additions and 56 deletions
|
@ -1,9 +1,8 @@
|
||||||
import { AgreeToTermsPolicy, ExtraParamsKey, type SignInIdentifier } from '@logto/schemas';
|
import { AgreeToTermsPolicy, type SignInIdentifier } from '@logto/schemas';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useCallback, useContext, 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 { useSearchParams } from 'react-router-dom';
|
|
||||||
|
|
||||||
import UserInteractionContext from '@/Providers/UserInteractionContextProvider/UserInteractionContext';
|
import UserInteractionContext from '@/Providers/UserInteractionContextProvider/UserInteractionContext';
|
||||||
import LockIcon from '@/assets/icons/lock.svg?react';
|
import LockIcon from '@/assets/icons/lock.svg?react';
|
||||||
|
@ -12,6 +11,7 @@ import ErrorMessage from '@/components/ErrorMessage';
|
||||||
import { SmartInputField } from '@/components/InputFields';
|
import { SmartInputField } from '@/components/InputFields';
|
||||||
import type { IdentifierInputValue } from '@/components/InputFields/SmartInputField';
|
import type { IdentifierInputValue } from '@/components/InputFields/SmartInputField';
|
||||||
import TermsAndPrivacyCheckbox from '@/containers/TermsAndPrivacyCheckbox';
|
import TermsAndPrivacyCheckbox from '@/containers/TermsAndPrivacyCheckbox';
|
||||||
|
import usePrefilledIdentifier from '@/hooks/use-prefilled-identifier';
|
||||||
import useSingleSignOnWatch from '@/hooks/use-single-sign-on-watch';
|
import useSingleSignOnWatch from '@/hooks/use-single-sign-on-watch';
|
||||||
import useTerms from '@/hooks/use-terms';
|
import useTerms from '@/hooks/use-terms';
|
||||||
import { getGeneralIdentifierErrorMessage, validateIdentifierField } from '@/utils/form';
|
import { getGeneralIdentifierErrorMessage, validateIdentifierField } from '@/utils/form';
|
||||||
|
@ -36,11 +36,8 @@ const IdentifierRegisterForm = ({ className, autoFocus, signUpMethods }: Props)
|
||||||
|
|
||||||
const { errorMessage, clearErrorMessage, onSubmit } = useOnSubmit();
|
const { errorMessage, clearErrorMessage, onSubmit } = useOnSubmit();
|
||||||
|
|
||||||
const { getIdentifierInputValueByTypes, setIdentifierInputValue } =
|
const { setIdentifierInputValue } = useContext(UserInteractionContext);
|
||||||
useContext(UserInteractionContext);
|
const prefilledIdentifier = usePrefilledIdentifier({ enabledIdentifiers: signUpMethods });
|
||||||
const identifierInputValue = getIdentifierInputValueByTypes(signUpMethods);
|
|
||||||
|
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
watch,
|
watch,
|
||||||
|
@ -49,6 +46,7 @@ const IdentifierRegisterForm = ({ className, autoFocus, signUpMethods }: Props)
|
||||||
control,
|
control,
|
||||||
} = useForm<FormState>({
|
} = useForm<FormState>({
|
||||||
reValidateMode: 'onBlur',
|
reValidateMode: 'onBlur',
|
||||||
|
defaultValues: { id: prefilledIdentifier },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Watch identifier field and check single sign on method availability
|
// Watch identifier field and check single sign on method availability
|
||||||
|
@ -127,10 +125,8 @@ const IdentifierRegisterForm = ({ className, autoFocus, signUpMethods }: Props)
|
||||||
autoFocus={autoFocus}
|
autoFocus={autoFocus}
|
||||||
className={styles.inputField}
|
className={styles.inputField}
|
||||||
{...field}
|
{...field}
|
||||||
defaultValue={
|
defaultValue={field.value.value}
|
||||||
identifierInputValue?.value ?? searchParams.get(ExtraParamsKey.LoginHint) ?? undefined
|
defaultType={field.value.type}
|
||||||
}
|
|
||||||
defaultType={identifierInputValue?.type}
|
|
||||||
isDanger={!!errors.id || !!errorMessage}
|
isDanger={!!errors.id || !!errorMessage}
|
||||||
errorMessage={errors.id?.message}
|
errorMessage={errors.id?.message}
|
||||||
enabledTypes={signUpMethods}
|
enabledTypes={signUpMethods}
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { AgreeToTermsPolicy, ExtraParamsKey, type SignIn } from '@logto/schemas';
|
import { AgreeToTermsPolicy, type SignIn } from '@logto/schemas';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useCallback, useContext, 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 { useSearchParams } from 'react-router-dom';
|
|
||||||
|
|
||||||
import UserInteractionContext from '@/Providers/UserInteractionContextProvider/UserInteractionContext';
|
import UserInteractionContext from '@/Providers/UserInteractionContextProvider/UserInteractionContext';
|
||||||
import LockIcon from '@/assets/icons/lock.svg?react';
|
import LockIcon from '@/assets/icons/lock.svg?react';
|
||||||
|
@ -12,6 +11,7 @@ import ErrorMessage from '@/components/ErrorMessage';
|
||||||
import { SmartInputField } from '@/components/InputFields';
|
import { SmartInputField } from '@/components/InputFields';
|
||||||
import type { IdentifierInputValue } from '@/components/InputFields/SmartInputField';
|
import type { IdentifierInputValue } from '@/components/InputFields/SmartInputField';
|
||||||
import TermsAndPrivacyCheckbox from '@/containers/TermsAndPrivacyCheckbox';
|
import TermsAndPrivacyCheckbox from '@/containers/TermsAndPrivacyCheckbox';
|
||||||
|
import usePrefilledIdentifier from '@/hooks/use-prefilled-identifier';
|
||||||
import useSingleSignOnWatch from '@/hooks/use-single-sign-on-watch';
|
import useSingleSignOnWatch from '@/hooks/use-single-sign-on-watch';
|
||||||
import useTerms from '@/hooks/use-terms';
|
import useTerms from '@/hooks/use-terms';
|
||||||
import { getGeneralIdentifierErrorMessage, validateIdentifierField } from '@/utils/form';
|
import { getGeneralIdentifierErrorMessage, validateIdentifierField } from '@/utils/form';
|
||||||
|
@ -34,16 +34,16 @@ 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 { getIdentifierInputValueByTypes, setIdentifierInputValue } =
|
const { setIdentifierInputValue } = useContext(UserInteractionContext);
|
||||||
useContext(UserInteractionContext);
|
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
|
|
||||||
const enabledSignInMethods = useMemo(
|
const enabledSignInMethods = useMemo(
|
||||||
() => signInMethods.map(({ identifier }) => identifier),
|
() => signInMethods.map(({ identifier }) => identifier),
|
||||||
[signInMethods]
|
[signInMethods]
|
||||||
);
|
);
|
||||||
|
|
||||||
const identifierInputValue = getIdentifierInputValueByTypes(enabledSignInMethods);
|
const prefilledIdentifier = usePrefilledIdentifier({
|
||||||
|
enabledIdentifiers: enabledSignInMethods,
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
watch,
|
watch,
|
||||||
|
@ -52,6 +52,9 @@ const IdentifierSignInForm = ({ className, autoFocus, signInMethods }: Props) =>
|
||||||
formState: { errors, isValid, isSubmitting },
|
formState: { errors, isValid, isSubmitting },
|
||||||
} = useForm<FormState>({
|
} = useForm<FormState>({
|
||||||
reValidateMode: 'onBlur',
|
reValidateMode: 'onBlur',
|
||||||
|
defaultValues: {
|
||||||
|
identifier: prefilledIdentifier,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Watch identifier field and check single sign on method availability
|
// Watch identifier field and check single sign on method availability
|
||||||
|
@ -127,10 +130,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={identifierInputValue?.type}
|
defaultType={field.value.type}
|
||||||
defaultValue={
|
defaultValue={field.value.value}
|
||||||
identifierInputValue?.value ?? searchParams.get(ExtraParamsKey.LoginHint) ?? undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { assert } from '@silverhand/essentials';
|
||||||
import { useState, useCallback, useMemo } from 'react';
|
import { useState, useCallback, useMemo } from 'react';
|
||||||
import type { ChangeEventHandler } from 'react';
|
import type { ChangeEventHandler } from 'react';
|
||||||
|
|
||||||
import useUpdateEffect from '@/hooks/use-update-effect';
|
|
||||||
import { getDefaultCountryCallingCode } from '@/utils/country-code';
|
import { getDefaultCountryCallingCode } from '@/utils/country-code';
|
||||||
import { parseIdentifierValue } from '@/utils/form';
|
import { parseIdentifierValue } from '@/utils/form';
|
||||||
|
|
||||||
|
@ -102,12 +101,6 @@ const useSmartInputField = ({ _defaultType, defaultValue, enabledTypes }: Props)
|
||||||
setCurrentType(enabledTypeSet.size === 1 ? defaultType : undefined);
|
setCurrentType(enabledTypeSet.size === 1 ? defaultType : undefined);
|
||||||
}, [defaultType, enabledTypeSet.size]);
|
}, [defaultType, enabledTypeSet.size]);
|
||||||
|
|
||||||
// CAUTION: For preview use only, enabledTypes and defaultType should not be changed after component mounted
|
|
||||||
useUpdateEffect(() => {
|
|
||||||
setInputValue('');
|
|
||||||
setCurrentType(defaultType);
|
|
||||||
}, [defaultType]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
countryCode,
|
countryCode,
|
||||||
onCountryCodeChange,
|
onCountryCodeChange,
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { AgreeToTermsPolicy, ExtraParamsKey, type SignInIdentifier } from '@logto/schemas';
|
import { AgreeToTermsPolicy, type SignInIdentifier } from '@logto/schemas';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useCallback, useContext, 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 { useSearchParams } from 'react-router-dom';
|
|
||||||
|
|
||||||
import UserInteractionContext from '@/Providers/UserInteractionContextProvider/UserInteractionContext';
|
import UserInteractionContext from '@/Providers/UserInteractionContextProvider/UserInteractionContext';
|
||||||
import LockIcon from '@/assets/icons/lock.svg?react';
|
import LockIcon from '@/assets/icons/lock.svg?react';
|
||||||
|
@ -14,6 +13,7 @@ import type { IdentifierInputValue } from '@/components/InputFields/SmartInputFi
|
||||||
import ForgotPasswordLink from '@/containers/ForgotPasswordLink';
|
import ForgotPasswordLink from '@/containers/ForgotPasswordLink';
|
||||||
import TermsAndPrivacyCheckbox from '@/containers/TermsAndPrivacyCheckbox';
|
import TermsAndPrivacyCheckbox from '@/containers/TermsAndPrivacyCheckbox';
|
||||||
import usePasswordSignIn from '@/hooks/use-password-sign-in';
|
import usePasswordSignIn from '@/hooks/use-password-sign-in';
|
||||||
|
import usePrefilledIdentifier from '@/hooks/use-prefilled-identifier';
|
||||||
import { useForgotPasswordSettings } from '@/hooks/use-sie';
|
import { useForgotPasswordSettings } from '@/hooks/use-sie';
|
||||||
import useSingleSignOnWatch from '@/hooks/use-single-sign-on-watch';
|
import useSingleSignOnWatch from '@/hooks/use-single-sign-on-watch';
|
||||||
import useTerms from '@/hooks/use-terms';
|
import useTerms from '@/hooks/use-terms';
|
||||||
|
@ -40,7 +40,7 @@ const PasswordSignInForm = ({ className, autoFocus, signInMethods }: Props) => {
|
||||||
const { isForgotPasswordEnabled } = useForgotPasswordSettings();
|
const { isForgotPasswordEnabled } = useForgotPasswordSettings();
|
||||||
const { termsValidation, agreeToTermsPolicy } = useTerms();
|
const { termsValidation, agreeToTermsPolicy } = useTerms();
|
||||||
const { setIdentifierInputValue } = useContext(UserInteractionContext);
|
const { setIdentifierInputValue } = useContext(UserInteractionContext);
|
||||||
const [searchParams] = useSearchParams();
|
const prefilledIdentifier = usePrefilledIdentifier({ enabledIdentifiers: signInMethods });
|
||||||
|
|
||||||
const {
|
const {
|
||||||
watch,
|
watch,
|
||||||
|
@ -51,7 +51,7 @@ const PasswordSignInForm = ({ className, autoFocus, signInMethods }: Props) => {
|
||||||
} = useForm<FormState>({
|
} = useForm<FormState>({
|
||||||
reValidateMode: 'onBlur',
|
reValidateMode: 'onBlur',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
identifier: {},
|
identifier: prefilledIdentifier,
|
||||||
password: '',
|
password: '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -129,7 +129,8 @@ const PasswordSignInForm = ({ className, autoFocus, signInMethods }: Props) => {
|
||||||
isDanger={!!errors.identifier}
|
isDanger={!!errors.identifier}
|
||||||
errorMessage={errors.identifier?.message}
|
errorMessage={errors.identifier?.message}
|
||||||
enabledTypes={signInMethods}
|
enabledTypes={signInMethods}
|
||||||
defaultValue={searchParams.get(ExtraParamsKey.LoginHint) ?? undefined}
|
defaultValue={field.value.value}
|
||||||
|
defaultType={field.value.type}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
10
packages/experience/src/hooks/use-login-hint.ts
Normal file
10
packages/experience/src/hooks/use-login-hint.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { ExtraParamsKey } from '@logto/schemas';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
const useLoginHint = () => {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
|
return searchParams.get(ExtraParamsKey.LoginHint) ?? undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useLoginHint;
|
35
packages/experience/src/hooks/use-prefilled-identifier.ts
Normal file
35
packages/experience/src/hooks/use-prefilled-identifier.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import { type SignInIdentifier } from '@logto/schemas';
|
||||||
|
import { useContext, useMemo } from 'react';
|
||||||
|
|
||||||
|
import UserInteractionContext from '@/Providers/UserInteractionContextProvider/UserInteractionContext';
|
||||||
|
import { type IdentifierInputValue } from '@/components/InputFields/SmartInputField';
|
||||||
|
|
||||||
|
import useLoginHint from './use-login-hint';
|
||||||
|
|
||||||
|
type Options = {
|
||||||
|
enabledIdentifiers?: SignInIdentifier[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const usePrefilledIdentifier = ({ enabledIdentifiers }: Options = {}) => {
|
||||||
|
const { identifierInputValue, getIdentifierInputValueByTypes } =
|
||||||
|
useContext(UserInteractionContext);
|
||||||
|
|
||||||
|
const loginHint = useLoginHint();
|
||||||
|
|
||||||
|
const cachedInputIdentifier = useMemo(() => {
|
||||||
|
return enabledIdentifiers
|
||||||
|
? getIdentifierInputValueByTypes(enabledIdentifiers)
|
||||||
|
: identifierInputValue;
|
||||||
|
}, [enabledIdentifiers, getIdentifierInputValueByTypes, identifierInputValue]);
|
||||||
|
|
||||||
|
return useMemo<IdentifierInputValue>(() => {
|
||||||
|
/**
|
||||||
|
* First, check if there's a cached input identifier
|
||||||
|
* If there's no cached input identifier, check if there's a valid login hint
|
||||||
|
* If there's neither, return empty
|
||||||
|
*/
|
||||||
|
return cachedInputIdentifier ?? { value: loginHint ?? '' };
|
||||||
|
}, [cachedInputIdentifier, loginHint]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default usePrefilledIdentifier;
|
|
@ -1,19 +0,0 @@
|
||||||
import type { DependencyList, EffectCallback } from 'react';
|
|
||||||
import { useEffect, useRef } from 'react';
|
|
||||||
|
|
||||||
const useUpdateEffect = (effect: EffectCallback, dependencies: DependencyList | undefined = []) => {
|
|
||||||
const isMounted = useRef(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isMounted.current) {
|
|
||||||
// eslint-disable-next-line @silverhand/fp/no-mutation
|
|
||||||
isMounted.current = true;
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return effect();
|
|
||||||
}, [...dependencies]);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useUpdateEffect;
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { experience, ExtraParamsKey } from '@logto/schemas';
|
import { experience } from '@logto/schemas';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Navigate, useSearchParams } from 'react-router-dom';
|
import { Navigate } from 'react-router-dom';
|
||||||
|
|
||||||
import FocusedAuthPageLayout from '@/Layout/FocusedAuthPageLayout';
|
import FocusedAuthPageLayout from '@/Layout/FocusedAuthPageLayout';
|
||||||
|
import useLoginHint from '@/hooks/use-login-hint';
|
||||||
import { identifierInputDescriptionMap } from '@/utils/form';
|
import { identifierInputDescriptionMap } from '@/utils/form';
|
||||||
|
|
||||||
import ForgotPasswordForm from '../ForgotPassword/ForgotPasswordForm';
|
import ForgotPasswordForm from '../ForgotPassword/ForgotPasswordForm';
|
||||||
|
@ -32,8 +33,7 @@ import { useResetPasswordMethods } from './use-reset-password-methods';
|
||||||
const ResetPasswordLanding = () => {
|
const ResetPasswordLanding = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const enabledMethods = useResetPasswordMethods();
|
const enabledMethods = useResetPasswordMethods();
|
||||||
const [searchParams] = useSearchParams();
|
const loginHint = useLoginHint();
|
||||||
const loginHint = searchParams.get(ExtraParamsKey.LoginHint) ?? undefined;
|
|
||||||
|
|
||||||
// Fallback to sign-in page
|
// Fallback to sign-in page
|
||||||
if (enabledMethods.length === 0) {
|
if (enabledMethods.length === 0) {
|
||||||
|
|
Loading…
Reference in a new issue