0
Fork 0
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:
Xiao Yijun 2024-08-24 08:41:47 +08:00 committed by GitHub
parent 61fb856ccd
commit 3aba357ba6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 73 additions and 56 deletions

View file

@ -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}

View file

@ -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
}
/> />
)} )}
/> />

View file

@ -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,

View file

@ -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}
/> />
)} )}
/> />

View 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;

View 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;

View file

@ -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;

View file

@ -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) {