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:
parent
cec08acb52
commit
25187ef63b
7 changed files with 56 additions and 6 deletions
20
.changeset/popular-monkeys-complain.md
Normal file
20
.changeset/popular-monkeys-complain.md
Normal 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'
|
||||
});
|
||||
```
|
|
@ -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(
|
||||
|
|
|
@ -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(':');
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
|
|
@ -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;
|
||||
}>;
|
||||
|
|
Loading…
Reference in a new issue