Skip to content

Commit

Permalink
feat: support login_hint params for sign-in url
Browse files Browse the repository at this point in the history
  • Loading branch information
xiaoyijun committed Aug 8, 2024
1 parent a731b09 commit 167053a
Show file tree
Hide file tree
Showing 7 changed files with 56 additions and 6 deletions.
20 changes: 20 additions & 0 deletions .changeset/popular-monkeys-complain.md
Original file line number Diff line number Diff line change
@@ -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'
});
```
9 changes: 8 additions & 1 deletion packages/core/src/oidc/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
Expand All @@ -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(
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/oidc/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(':');
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -37,6 +38,8 @@ const IdentifierRegisterForm = ({ className, autoFocus, signUpMethods }: Props)

const { identifierInputValue, setIdentifierInputValue } = useContext(UserInteractionContext);

const [searchParams] = useSearchParams();

const {
watch,
handleSubmit,
Expand Down Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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
}
/>
)}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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}
/>
)}
/>
Expand Down
7 changes: 7 additions & 0 deletions packages/schemas/src/consts/oidc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 **on the first screen** of the sign-in/sign-up flow.
*/
LoginHint = 'login_hint',
}

/** @deprecated Use {@link FirstScreen} instead. */
Expand All @@ -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>;

Expand All @@ -68,4 +74,5 @@ export type ExtraParamsObject = Partial<{
[ExtraParamsKey.FirstScreen]: FirstScreen;
[ExtraParamsKey.DirectSignIn]: string;
[ExtraParamsKey.OrganizationId]: string;
[ExtraParamsKey.LoginHint]: string;
}>;

0 comments on commit 167053a

Please sign in to comment.