From 61a9ca8a54ab203903efc347cf0a0c730d5a2da6 Mon Sep 17 00:00:00 2001 From: Tom Milewski Date: Tue, 10 Nov 2026 14:55:01 -0500 Subject: [PATCH 1/6] feat(clerk-js,shared,react): expose clientTrustState on SignInFuture --- .../sign-in-future-client-trust-state.md | 7 ++++ .../clerk-js/src/core/resources/SignIn.ts | 4 +++ .../core/resources/__tests__/SignIn.test.ts | 35 +++++++++++++++++++ .../react/src/__tests__/stateProxy.test.ts | 17 +++++++++ packages/react/src/stateProxy.ts | 3 ++ packages/shared/src/types/signInFuture.ts | 12 +++++++ 6 files changed, 78 insertions(+) create mode 100644 .changeset/sign-in-future-client-trust-state.md diff --git a/.changeset/sign-in-future-client-trust-state.md b/.changeset/sign-in-future-client-trust-state.md new file mode 100644 index 00000000000..3e1a5bedf8a --- /dev/null +++ b/.changeset/sign-in-future-client-trust-state.md @@ -0,0 +1,7 @@ +--- +'@clerk/clerk-js': minor +'@clerk/shared': minor +'@clerk/react': minor +--- + +Add `clientTrustState` to `SignInFuture`, so custom sign-in flows can read whether Clerk recognizes the user's device (`'new'`, `'known'`, or `'pending'`). It's `undefined` when the instance reports Device Trust through the `needs_client_trust` status instead. diff --git a/packages/clerk-js/src/core/resources/SignIn.ts b/packages/clerk-js/src/core/resources/SignIn.ts index 83caee52941..8d19fc64595 100644 --- a/packages/clerk-js/src/core/resources/SignIn.ts +++ b/packages/clerk-js/src/core/resources/SignIn.ts @@ -1004,6 +1004,10 @@ class SignInFuture implements SignInFutureResource { return this.#resource.secondFactorVerification; } + get clientTrustState() { + return this.#resource.clientTrustState; + } + get protectCheck() { return this.#resource.protectCheck; } diff --git a/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts b/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts index 53a50f0baa0..94187ba7e01 100644 --- a/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts @@ -1427,6 +1427,41 @@ describe('SignIn', () => { }); }); + describe('clientTrustState', () => { + afterEach(() => { + vi.clearAllMocks(); + vi.unstubAllGlobals(); + }); + + it.each([ + { reported: 'new', status: 'needs_second_factor' }, + { reported: 'known', status: 'needs_first_factor' }, + { reported: 'pending', status: 'needs_first_factor' }, + ])('exposes $reported when FAPI reports it', async ({ reported, status }) => { + BaseResource._fetch = vi.fn().mockResolvedValue({ + client: null, + response: { id: 'signin_123', status, client_trust_state: reported }, + }); + + const signIn = new SignIn(); + await signIn.__internal_future.password({ identifier: 'user@example.com', password: 'password123' }); + + expect(signIn.__internal_future.clientTrustState).toBe(reported); + }); + + it('is undefined when FAPI does not report it', async () => { + BaseResource._fetch = vi.fn().mockResolvedValue({ + client: null, + response: { id: 'signin_123', status: 'needs_first_factor' }, + }); + + const signIn = new SignIn(); + await signIn.__internal_future.password({ identifier: 'user@example.com', password: 'password123' }); + + expect(signIn.__internal_future.clientTrustState).toBeUndefined(); + }); + }); + describe('password', () => { afterEach(() => { vi.clearAllMocks(); diff --git a/packages/react/src/__tests__/stateProxy.test.ts b/packages/react/src/__tests__/stateProxy.test.ts index e1a8586a0d9..6aea8e546ee 100644 --- a/packages/react/src/__tests__/stateProxy.test.ts +++ b/packages/react/src/__tests__/stateProxy.test.ts @@ -25,6 +25,23 @@ describe('StateProxy', () => { }, ); + it('exposes signIn clientTrustState after loading and follows the active attempt', () => { + const clientAttempt = { clientTrustState: 'known' }; + let stateAttempt: { clientTrustState: string } | null = { clientTrustState: 'new' }; + const isomorphicClerk = { + loaded: false, + client: { signIn: { __internal_future: clientAttempt } }, + __internal_state: { signInSignal: () => ({ signIn: stateAttempt }) }, + }; + const signIn = new StateProxy(isomorphicClerk as any).signInSignal().signIn as SignInFutureResource; + + expect(signIn.clientTrustState).toBeUndefined(); + isomorphicClerk.loaded = true; + expect(signIn.clientTrustState).toBe('new'); + stateAttempt = null; + expect(signIn.clientTrustState).toBe('known'); + }); + it('preserves a completed sign-in across chained calls when the client clears its sign-in attempt', async () => { const emptySignIn = { status: 'needs_identifier', diff --git a/packages/react/src/stateProxy.ts b/packages/react/src/stateProxy.ts index e9447eb3d75..abaf02b3e47 100644 --- a/packages/react/src/stateProxy.ts +++ b/packages/react/src/stateProxy.ts @@ -183,6 +183,9 @@ export class StateProxy implements State { }, }); }, + get clientTrustState() { + return gateProperty(target, 'clientTrustState', undefined); + }, get identifier() { return gateProperty(target, 'identifier', null); }, diff --git a/packages/shared/src/types/signInFuture.ts b/packages/shared/src/types/signInFuture.ts index 079e414e242..45c45b75c3b 100644 --- a/packages/shared/src/types/signInFuture.ts +++ b/packages/shared/src/types/signInFuture.ts @@ -1,5 +1,6 @@ import type { ClerkError } from '../errors/clerkError'; import type { SetActiveNavigate } from './clerk'; +import type { ClientTrustState } from './json'; import type { PhoneCodeChannel } from './phoneCodeChannel'; import type { SignInFirstFactor, SignInSecondFactor, SignInStatus, UserData } from './signInCommon'; import type { ProtectCheckResource } from './signUpCommon'; @@ -402,6 +403,17 @@ export interface SignInFutureResource { */ readonly secondFactorVerification: VerificationResource; + /** + * The [Device Trust](https://clerk.com/docs/guides/protect/device-trust) state of the device the user is signing in from. + * + * `undefined` when Device Trust is off, or when the instance reports Device Trust through the `'needs_client_trust'` status instead. + */ + readonly clientTrustState?: ClientTrustState; + /** * The authentication identifier value for the current sign-in. `null` if the `strategy` is `'oauth_'` or `'enterprise_sso'`. */ From 4d67c50cb949fa9432d2fda546246aba9f31e375 Mon Sep 17 00:00:00 2001 From: Tom Milewski Date: Tue, 10 Nov 2026 15:03:54 -0500 Subject: [PATCH 2/6] fix(clerk-js): keep clientTrustState in SignIn snapshots Expo restores the client from this snapshot when FAPI is unreachable at startup, so the field was lost on restore. --- packages/clerk-js/src/core/resources/SignIn.ts | 1 + .../src/core/resources/__tests__/SignIn.test.ts | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/packages/clerk-js/src/core/resources/SignIn.ts b/packages/clerk-js/src/core/resources/SignIn.ts index 8d19fc64595..19882fdbf4b 100644 --- a/packages/clerk-js/src/core/resources/SignIn.ts +++ b/packages/clerk-js/src/core/resources/SignIn.ts @@ -857,6 +857,7 @@ export class SignIn extends BaseResource implements SignInResource { created_session_id: this.createdSessionId, user_data: this.userData.__internal_toSnapshot(), timezone: this.timezone, + client_trust_state: this.clientTrustState, protect_check: this.protectCheck ? { status: this.protectCheck.status, diff --git a/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts b/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts index 94187ba7e01..69477a5f42e 100644 --- a/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts @@ -1460,6 +1460,18 @@ describe('SignIn', () => { expect(signIn.__internal_future.clientTrustState).toBeUndefined(); }); + + it('survives a snapshot round trip', () => { + const signIn = new SignIn({ + id: 'signin_123', + status: 'needs_second_factor', + client_trust_state: 'new', + } as any); + + const restored = new SignIn(signIn.__internal_toSnapshot()); + + expect(restored.__internal_future.clientTrustState).toBe('new'); + }); }); describe('password', () => { From 486b197bd0c25b5f443ed4aa14559bfbad75621f Mon Sep 17 00:00:00 2001 From: Tom Milewski Date: Tue, 10 Nov 2026 15:49:49 -0500 Subject: [PATCH 3/6] feat(clerk-js,shared): report a new device while SignInFuture status is needs_client_trust --- .../sign-in-future-client-trust-state.md | 2 +- .../clerk-js/src/core/resources/SignIn.ts | 4 +-- .../core/resources/__tests__/SignIn.test.ts | 25 +++++++++++++++++++ packages/shared/src/types/signInFuture.ts | 4 +-- 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/.changeset/sign-in-future-client-trust-state.md b/.changeset/sign-in-future-client-trust-state.md index 3e1a5bedf8a..3c566b0f5d1 100644 --- a/.changeset/sign-in-future-client-trust-state.md +++ b/.changeset/sign-in-future-client-trust-state.md @@ -4,4 +4,4 @@ '@clerk/react': minor --- -Add `clientTrustState` to `SignInFuture`, so custom sign-in flows can read whether Clerk recognizes the user's device (`'new'`, `'known'`, or `'pending'`). It's `undefined` when the instance reports Device Trust through the `needs_client_trust` status instead. +Add `clientTrustState` to `SignInFuture`, so custom sign-in flows can read whether Clerk recognizes the user's device (`'new'`, `'known'`, or `'pending'`). It's `'new'` whenever the status is `needs_client_trust`. On instances that report Device Trust through that status, it's `undefined` at every other status. diff --git a/packages/clerk-js/src/core/resources/SignIn.ts b/packages/clerk-js/src/core/resources/SignIn.ts index 19882fdbf4b..b333101d779 100644 --- a/packages/clerk-js/src/core/resources/SignIn.ts +++ b/packages/clerk-js/src/core/resources/SignIn.ts @@ -1005,8 +1005,8 @@ class SignInFuture implements SignInFutureResource { return this.#resource.secondFactorVerification; } - get clientTrustState() { - return this.#resource.clientTrustState; + get clientTrustState(): ClientTrustState | undefined { + return this.#resource.clientTrustState ?? (this.#resource.status === 'needs_client_trust' ? 'new' : undefined); } get protectCheck() { diff --git a/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts b/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts index 69477a5f42e..067b12d7919 100644 --- a/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts @@ -1461,6 +1461,31 @@ describe('SignIn', () => { expect(signIn.__internal_future.clientTrustState).toBeUndefined(); }); + it('is new while the status is needs_client_trust and FAPI omits the field', async () => { + BaseResource._fetch = vi.fn().mockResolvedValue({ + client: null, + response: { id: 'signin_123', status: 'needs_client_trust' }, + }); + + const signIn = new SignIn(); + await signIn.__internal_future.password({ identifier: 'user@example.com', password: 'password123' }); + + expect(signIn.__internal_future.clientTrustState).toBe('new'); + expect(signIn.clientTrustState).toBeUndefined(); + }); + + it('prefers the value FAPI reports over the needs_client_trust status', async () => { + BaseResource._fetch = vi.fn().mockResolvedValue({ + client: null, + response: { id: 'signin_123', status: 'needs_client_trust', client_trust_state: 'pending' }, + }); + + const signIn = new SignIn(); + await signIn.__internal_future.password({ identifier: 'user@example.com', password: 'password123' }); + + expect(signIn.__internal_future.clientTrustState).toBe('pending'); + }); + it('survives a snapshot round trip', () => { const signIn = new SignIn({ id: 'signin_123', diff --git a/packages/shared/src/types/signInFuture.ts b/packages/shared/src/types/signInFuture.ts index 45c45b75c3b..91bb2eb65e8 100644 --- a/packages/shared/src/types/signInFuture.ts +++ b/packages/shared/src/types/signInFuture.ts @@ -406,11 +406,11 @@ export interface SignInFutureResource { /** * The [Device Trust](https://clerk.com/docs/guides/protect/device-trust) state of the device the user is signing in from. *
    - *
  • `'new'` - The user hasn't signed in on this device before, or Clerk can't confirm that they have.
  • + *
  • `'new'` - The user hasn't signed in on this device before, or Clerk can't confirm that they have. This is always the value while `status` is `'needs_client_trust'`.
  • *
  • `'known'` - The user has signed in on this device before.
  • *
  • `'pending'` - Clerk can't tell yet, for example because the sign-in has no identifier.
  • *
- * `undefined` when Device Trust is off, or when the instance reports Device Trust through the `'needs_client_trust'` status instead. + * `undefined` when Device Trust is off. When the instance reports Device Trust through the `'needs_client_trust'` status, it's also `undefined` at every other status. */ readonly clientTrustState?: ClientTrustState; From b5e105ca83055d9803a9b06337af40a3f4e2bb90 Mon Sep 17 00:00:00 2001 From: Tom Milewski Date: Tue, 10 Nov 2026 16:39:35 -0500 Subject: [PATCH 4/6] test(ui): pin sign-in rendering when FAPI returns clientTrustState with needs_client_trust --- .../__tests__/SignInClientTrust.test.tsx | 56 ++++++++++++++++++- .../SignIn/__tests__/SignInFactorTwo.test.tsx | 18 ++++++ packages/ui/src/test/fixture-helpers.ts | 8 +++ 3 files changed, 81 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/components/SignIn/__tests__/SignInClientTrust.test.tsx b/packages/ui/src/components/SignIn/__tests__/SignInClientTrust.test.tsx index 029f51e9619..049f07d8d9b 100644 --- a/packages/ui/src/components/SignIn/__tests__/SignInClientTrust.test.tsx +++ b/packages/ui/src/components/SignIn/__tests__/SignInClientTrust.test.tsx @@ -1,5 +1,5 @@ import type { SignInResource } from '@clerk/shared/types'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { bindCreateFixtures } from '@/test/create-fixtures'; import { render, screen } from '@/test/utils'; @@ -32,4 +32,58 @@ describe('SignInClientTrust', () => { expect(screen.getByText('Use another method')).toBeInTheDocument(); }); }); + + describe('New device notice', () => { + const NOTICE = /You're signing in from a new device/i; + const cases = [ + ['with client_trust_state', 'new'], + ['without client_trust_state', undefined], + ] as const; + + it.each(cases)('renders the notice once on the email_code factor %s', async (_, clientTrustState) => { + const { wrapper, fixtures } = await createFixtures(f => { + f.startSignInClientTrust({ supportPhoneCode: false, supportEmailCode: true, clientTrustState }); + }); + + fixtures.signIn.prepareSecondFactor.mockResolvedValueOnce({} as SignInResource); + render(, { wrapper }); + + expect(screen.getAllByText(NOTICE)).toHaveLength(1); + expect(screen.getAllByText('Check your email').length).toBeGreaterThan(0); + expect(screen.getAllByTestId('otp-input-segment')).toHaveLength(6); + }); + + it.each(cases)('renders the notice once on the phone_code factor %s', async (_, clientTrustState) => { + const { wrapper, fixtures } = await createFixtures(f => { + f.startSignInClientTrust({ supportPhoneCode: true, clientTrustState }); + }); + + fixtures.signIn.prepareSecondFactor.mockResolvedValueOnce({} as SignInResource); + render(, { wrapper }); + + expect(screen.getAllByText(NOTICE)).toHaveLength(1); + expect(screen.getAllByText('Check your phone').length).toBeGreaterThan(0); + expect(screen.getAllByTestId('otp-input-segment')).toHaveLength(6); + }); + + it.each(cases)('renders the notice once on the email_link factor %s', async (_, clientTrustState) => { + const { wrapper, fixtures } = await createFixtures(f => { + f.startSignInClientTrust({ supportPhoneCode: false, supportEmailLink: true, clientTrustState }); + }); + + fixtures.signIn.prepareSecondFactor.mockResolvedValueOnce({} as SignInResource); + fixtures.signIn.createEmailLinkFlow.mockImplementation( + () => + ({ + startEmailLinkFlow: vi.fn(() => new Promise(() => {})), + cancelEmailLinkFlow: vi.fn(), + }) as any, + ); + render(, { wrapper }); + + expect(screen.getAllByText(NOTICE)).toHaveLength(1); + expect(screen.getAllByText('Check your email').length).toBeGreaterThan(0); + expect(screen.getByText('Use the verification link sent to your email')).toBeInTheDocument(); + }); + }); }); diff --git a/packages/ui/src/components/SignIn/__tests__/SignInFactorTwo.test.tsx b/packages/ui/src/components/SignIn/__tests__/SignInFactorTwo.test.tsx index f5b04763a77..e0678247e9d 100644 --- a/packages/ui/src/components/SignIn/__tests__/SignInFactorTwo.test.tsx +++ b/packages/ui/src/components/SignIn/__tests__/SignInFactorTwo.test.tsx @@ -94,6 +94,24 @@ describe('SignInFactorTwo', () => { }); }); + describe('New device notice with a second factor attribute', () => { + it.each([ + ['with client_trust_state', 'new'], + ['without client_trust_state', undefined], + ] as const)('does not render the notice %s', async (_, clientTrustState) => { + const { wrapper, fixtures } = await createFixtures(f => { + f.withPhoneNumber({ used_for_second_factor: true, second_factors: ['phone_code'] }); + f.startSignInFactorTwo({ supportPhoneCode: true, clientTrustState }); + }); + + fixtures.signIn.prepareSecondFactor.mockResolvedValueOnce({} as SignInResource); + render(, { wrapper }); + + expect(screen.getAllByTestId('otp-input-segment')).toHaveLength(6); + expect(screen.queryByText(/You're signing in from a new device/i)).not.toBeInTheDocument(); + }); + }); + describe('Selected Second Factor Method', () => { describe('Phone Code', () => { it('renders the correct screen with the text "Check your phone"', async () => { diff --git a/packages/ui/src/test/fixture-helpers.ts b/packages/ui/src/test/fixture-helpers.ts index d00ec09826c..1275834b4a8 100644 --- a/packages/ui/src/test/fixture-helpers.ts +++ b/packages/ui/src/test/fixture-helpers.ts @@ -131,6 +131,8 @@ const createSignInFixtureHelpers = (baseClient: ClientJSON) => { supportPasskey?: boolean; supportResetPasswordEmail?: boolean; supportResetPasswordPhone?: boolean; + supportEmailLink?: boolean; + clientTrustState?: SignInJSON['client_trust_state']; }; const startSignInWithEmailAddress = (params?: SignInWithEmailAddressParams) => { @@ -225,15 +227,18 @@ const createSignInFixtureHelpers = (baseClient: ClientJSON) => { identifier = '+30 691 1111111', supportPhoneCode = true, supportEmailCode, + supportEmailLink, supportTotp, supportBackupCode, supportPasskey, supportResetPasswordEmail, supportResetPasswordPhone, + clientTrustState, } = params || {}; baseClient.sign_in = { status, identifier, + ...(clientTrustState ? { client_trust_state: clientTrustState } : {}), ...(supportResetPasswordEmail ? { first_factor_verification: { @@ -254,6 +259,9 @@ const createSignInFixtureHelpers = (baseClient: ClientJSON) => { supported_second_factors: [ ...(supportPhoneCode ? [{ strategy: 'phone_code', safe_identifier: identifier || 'n*****@clerk.com' }] : []), ...(supportEmailCode ? [{ strategy: 'email_code', safe_identifier: 'n*****@clerk.com' }] : []), + ...(supportEmailLink + ? [{ strategy: 'email_link', safe_identifier: 'n*****@clerk.com', email_address_id: 'idn_123' }] + : []), ...(supportTotp ? [{ strategy: 'totp', safe_identifier: identifier || 'n*****@clerk.com' }] : []), ...(supportBackupCode ? [{ strategy: 'backup_code', safe_identifier: identifier || 'n*****@clerk.com' }] : []), ...(supportPasskey ? [{ strategy: 'passkey' }] : []), From 7d2dc32433569d82663cd472cdc9ad90736578c2 Mon Sep 17 00:00:00 2001 From: Tom Milewski Date: Tue, 10 Nov 2026 17:40:06 -0500 Subject: [PATCH 5/6] docs(shared): describe clientTrustState once FAPI returns it alongside needs_client_trust --- .changeset/sign-in-future-client-trust-state.md | 2 +- packages/shared/src/types/signInFuture.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.changeset/sign-in-future-client-trust-state.md b/.changeset/sign-in-future-client-trust-state.md index 3c566b0f5d1..98ba522b377 100644 --- a/.changeset/sign-in-future-client-trust-state.md +++ b/.changeset/sign-in-future-client-trust-state.md @@ -4,4 +4,4 @@ '@clerk/react': minor --- -Add `clientTrustState` to `SignInFuture`, so custom sign-in flows can read whether Clerk recognizes the user's device (`'new'`, `'known'`, or `'pending'`). It's `'new'` whenever the status is `needs_client_trust`. On instances that report Device Trust through that status, it's `undefined` at every other status. +Add `clientTrustState` to `SignInFuture`, so custom sign-in flows can read whether Clerk trusts the user's device (`'new'`, `'known'`, or `'pending'`). A completed sign-in usually reads `'known'`, so read the value before the sign-in completes to learn whether the device was trusted beforehand. It's `undefined` when Device Trust is off, or before a sign-in attempt exists. diff --git a/packages/shared/src/types/signInFuture.ts b/packages/shared/src/types/signInFuture.ts index 91bb2eb65e8..eb0ad593296 100644 --- a/packages/shared/src/types/signInFuture.ts +++ b/packages/shared/src/types/signInFuture.ts @@ -404,13 +404,13 @@ export interface SignInFutureResource { readonly secondFactorVerification: VerificationResource; /** - * The [Device Trust](https://clerk.com/docs/guides/protect/device-trust) state of the device the user is signing in from. + * The [Device Trust](https://clerk.com/docs/guides/protect/device-trust) state of the device the user is signing in from, as of the latest response. *
    - *
  • `'new'` - The user hasn't signed in on this device before, or Clerk can't confirm that they have. This is always the value while `status` is `'needs_client_trust'`.
  • - *
  • `'known'` - The user has signed in on this device before.
  • + *
  • `'new'` - Clerk doesn't trust this device for the user yet, or can't confirm that it does. While `status` is `'needs_client_trust'`, this is `'new'` unless Clerk reports otherwise.
  • + *
  • `'known'` - Clerk trusts this device for the user. Completing a sign-in makes the device trusted in most cases, so a `'complete'` sign-in usually reads `'known'`.
  • *
  • `'pending'` - Clerk can't tell yet, for example because the sign-in has no identifier.
  • *
- * `undefined` when Device Trust is off. When the instance reports Device Trust through the `'needs_client_trust'` status, it's also `undefined` at every other status. + * To record whether the device was trusted before this sign-in, read the value before the sign-in completes, for example after `create({ identifier })` and before `password()`. `undefined` when Device Trust is off, or before a sign-in attempt exists. */ readonly clientTrustState?: ClientTrustState; From 48d45e52198ae21f5dc07ba36456af5f8b8b0b44 Mon Sep 17 00:00:00 2001 From: Tom Milewski Date: Wed, 11 Nov 2026 16:34:19 -0500 Subject: [PATCH 6/6] docs(shared): clientTrustState is returned once the first factor is verified --- .changeset/sign-in-future-client-trust-state.md | 2 +- packages/shared/src/types/signInFuture.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.changeset/sign-in-future-client-trust-state.md b/.changeset/sign-in-future-client-trust-state.md index 98ba522b377..c51d62c4264 100644 --- a/.changeset/sign-in-future-client-trust-state.md +++ b/.changeset/sign-in-future-client-trust-state.md @@ -4,4 +4,4 @@ '@clerk/react': minor --- -Add `clientTrustState` to `SignInFuture`, so custom sign-in flows can read whether Clerk trusts the user's device (`'new'`, `'known'`, or `'pending'`). A completed sign-in usually reads `'known'`, so read the value before the sign-in completes to learn whether the device was trusted beforehand. It's `undefined` when Device Trust is off, or before a sign-in attempt exists. +Add `clientTrustState` to `SignInFuture`, so custom sign-in flows can read whether Clerk trusts the user's device (`'new'`, `'known'`, or `'pending'`). Clerk returns it once the first factor, such as the password, is verified. It's `undefined` before then, and when Device Trust is off. diff --git a/packages/shared/src/types/signInFuture.ts b/packages/shared/src/types/signInFuture.ts index eb0ad593296..bea06126263 100644 --- a/packages/shared/src/types/signInFuture.ts +++ b/packages/shared/src/types/signInFuture.ts @@ -404,13 +404,13 @@ export interface SignInFutureResource { readonly secondFactorVerification: VerificationResource; /** - * The [Device Trust](https://clerk.com/docs/guides/protect/device-trust) state of the device the user is signing in from, as of the latest response. + * The [Device Trust](https://clerk.com/docs/guides/protect/device-trust) state of the device the user is signing in from, as of the latest response. Clerk returns it once the first factor, such as the password, is verified, so entering an identifier doesn't reveal whether that account has signed in on this device. *
    - *
  • `'new'` - Clerk doesn't trust this device for the user yet, or can't confirm that it does. While `status` is `'needs_client_trust'`, this is `'new'` unless Clerk reports otherwise.
  • + *
  • `'new'` - Clerk doesn't trust this device for the user yet, or can't confirm that it does. While `status` is `'needs_client_trust'`, this is `'new'`.
  • *
  • `'known'` - Clerk trusts this device for the user. Completing a sign-in makes the device trusted in most cases, so a `'complete'` sign-in usually reads `'known'`.
  • - *
  • `'pending'` - Clerk can't tell yet, for example because the sign-in has no identifier.
  • + *
  • `'pending'` - Clerk can't tell yet.
  • *
- * To record whether the device was trusted before this sign-in, read the value before the sign-in completes, for example after `create({ identifier })` and before `password()`. `undefined` when Device Trust is off, or before a sign-in attempt exists. + * `undefined` when Device Trust is off, or before the first factor is verified. */ readonly clientTrustState?: ClientTrustState;