0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

feat: support login_hint params for sign-in url (#6400)

This commit is contained in:
Xiao Yijun 2024-08-09 13:39:02 +08:00 committed by GitHub
parent cec08acb52
commit 25187ef63b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 56 additions and 6 deletions

View file

@ -0,0 +1,20 @@
---
"@logto/experience": minor
"@logto/schemas": minor
"@logto/core": minor
---
add support for `login_hint` parameter in sign-in method
This feature allows you to provide a suggested identifier (email, phone, or username) for the user, improving the sign-in experience especially in scenarios where the user's identifier is known or can be inferred.
Example:
```javascript
// Example usage (React project using React SDK)
void signIn({
redirectUri,
loginHint: 'user@example.com',
firstScreen: 'signIn', // or 'register'
});
```

View file

@ -147,6 +147,10 @@ describe('buildLoginPromptUrl', () => {
expect(buildLoginPromptUrl({ first_screen: FirstScreen.SignIn }, demoAppApplicationId)).toBe(
'sign-in?app_id=demo-app'
);
expect(
buildLoginPromptUrl({ first_screen: FirstScreen.SignIn, login_hint: 'user@mail.com' })
).toBe('sign-in?login_hint=user%40mail.com');
// Legacy interactionMode support
expect(buildLoginPromptUrl({ interaction_mode: InteractionMode.SignUp })).toBe('register');
});
@ -169,7 +173,10 @@ describe('buildLoginPromptUrl', () => {
it('should return the correct url for mixed parameters', () => {
expect(
buildLoginPromptUrl({ first_screen: FirstScreen.Register, direct_sign_in: 'method:target' })
buildLoginPromptUrl({
first_screen: FirstScreen.Register,
direct_sign_in: 'method:target',
})
).toBe('direct/method/target?fallback=register');
expect(
buildLoginPromptUrl(

View file

@ -105,6 +105,10 @@ export const buildLoginPromptUrl = (params: ExtraParamsObject, appId?: unknown):
searchParams.append(ExtraParamsKey.OrganizationId, params[ExtraParamsKey.OrganizationId]);
}
if (params[ExtraParamsKey.LoginHint]) {
searchParams.append(ExtraParamsKey.LoginHint, params[ExtraParamsKey.LoginHint]);
}
if (directSignIn) {
searchParams.append('fallback', firstScreen);
const [method, target] = directSignIn.split(':');

View file

@ -1,8 +1,9 @@
import { AgreeToTermsPolicy, type SignInIdentifier } from '@logto/schemas';
import { AgreeToTermsPolicy, ExtraParamsKey, type SignInIdentifier } from '@logto/schemas';
import classNames from 'classnames';
import { useCallback, useContext, useEffect } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { useSearchParams } from 'react-router-dom';
import UserInteractionContext from '@/Providers/UserInteractionContextProvider/UserInteractionContext';
import LockIcon from '@/assets/icons/lock.svg?react';
@ -37,6 +38,8 @@ const IdentifierRegisterForm = ({ className, autoFocus, signUpMethods }: Props)
const { identifierInputValue, setIdentifierInputValue } = useContext(UserInteractionContext);
const [searchParams] = useSearchParams();
const {
watch,
handleSubmit,
@ -117,7 +120,9 @@ const IdentifierRegisterForm = ({ className, autoFocus, signUpMethods }: Props)
autoFocus={autoFocus}
className={styles.inputField}
{...field}
defaultValue={identifierInputValue?.value}
defaultValue={
identifierInputValue?.value ?? searchParams.get(ExtraParamsKey.LoginHint) ?? undefined
}
defaultType={identifierInputValue?.type}
isDanger={!!errors.id || !!errorMessage}
errorMessage={errors.id?.message}

View file

@ -1,8 +1,9 @@
import { AgreeToTermsPolicy, type SignIn } from '@logto/schemas';
import { AgreeToTermsPolicy, ExtraParamsKey, type SignIn } from '@logto/schemas';
import classNames from 'classnames';
import { useCallback, useContext, useEffect, useMemo } from 'react';
import { useForm, Controller } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { useSearchParams } from 'react-router-dom';
import UserInteractionContext from '@/Providers/UserInteractionContextProvider/UserInteractionContext';
import LockIcon from '@/assets/icons/lock.svg?react';
@ -34,6 +35,7 @@ const IdentifierSignInForm = ({ className, autoFocus, signInMethods }: Props) =>
const { errorMessage, clearErrorMessage, onSubmit } = useOnSubmit(signInMethods);
const { termsValidation, agreeToTermsPolicy } = useTerms();
const { identifierInputValue, setIdentifierInputValue } = useContext(UserInteractionContext);
const [searchParams] = useSearchParams();
const enabledSignInMethods = useMemo(
() => signInMethods.map(({ identifier }) => identifier),
@ -123,7 +125,9 @@ const IdentifierSignInForm = ({ className, autoFocus, signInMethods }: Props) =>
errorMessage={errors.identifier?.message}
enabledTypes={enabledSignInMethods}
defaultType={identifierInputValue?.type}
defaultValue={identifierInputValue?.value}
defaultValue={
identifierInputValue?.value ?? searchParams.get(ExtraParamsKey.LoginHint) ?? undefined
}
/>
)}
/>

View file

@ -1,8 +1,9 @@
import { AgreeToTermsPolicy, type SignInIdentifier } from '@logto/schemas';
import { AgreeToTermsPolicy, ExtraParamsKey, type SignInIdentifier } from '@logto/schemas';
import classNames from 'classnames';
import { useCallback, useContext, useEffect } from 'react';
import { useForm, Controller } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { useSearchParams } from 'react-router-dom';
import UserInteractionContext from '@/Providers/UserInteractionContextProvider/UserInteractionContext';
import LockIcon from '@/assets/icons/lock.svg?react';
@ -39,6 +40,7 @@ const PasswordSignInForm = ({ className, autoFocus, signInMethods }: Props) => {
const { isForgotPasswordEnabled } = useForgotPasswordSettings();
const { termsValidation, agreeToTermsPolicy } = useTerms();
const { setIdentifierInputValue } = useContext(UserInteractionContext);
const [searchParams] = useSearchParams();
const {
watch,
@ -127,6 +129,7 @@ const PasswordSignInForm = ({ className, autoFocus, signInMethods }: Props) => {
isDanger={!!errors.identifier}
errorMessage={errors.identifier?.message}
enabledTypes={signInMethods}
defaultValue={searchParams.get(ExtraParamsKey.LoginHint) ?? undefined}
/>
)}
/>

View file

@ -41,6 +41,11 @@ export enum ExtraParamsKey {
* organization ID.
*/
OrganizationId = 'organization_id',
/**
* Provides a hint about the login identifier the user might use.
* This can be used to pre-fill the identifier field **only on the first screen** of the sign-in/sign-up flow.
*/
LoginHint = 'login_hint',
}
/** @deprecated Use {@link FirstScreen} instead. */
@ -60,6 +65,7 @@ export const extraParamsObjectGuard = z
[ExtraParamsKey.FirstScreen]: z.nativeEnum(FirstScreen),
[ExtraParamsKey.DirectSignIn]: z.string(),
[ExtraParamsKey.OrganizationId]: z.string(),
[ExtraParamsKey.LoginHint]: z.string(),
})
.partial() satisfies ToZodObject<ExtraParamsObject>;
@ -68,4 +74,5 @@ export type ExtraParamsObject = Partial<{
[ExtraParamsKey.FirstScreen]: FirstScreen;
[ExtraParamsKey.DirectSignIn]: string;
[ExtraParamsKey.OrganizationId]: string;
[ExtraParamsKey.LoginHint]: string;
}>;