From 08a72ed2d682d9d06fa8632d2aaeb549c00862af Mon Sep 17 00:00:00 2001 From: Tom Milewski Date: Thu, 6 Aug 2026 14:47:55 -0400 Subject: [PATCH 1/2] fix(ui): hide unavailable alternative verification methods --- .changeset/tidy-device-trust-methods.md | 5 +++ integration/tests/client-trust.test.ts | 1 + .../components/SignIn/SignInClientTrust.tsx | 8 +++-- .../src/components/SignIn/SignInFactorTwo.tsx | 12 ++++--- .../SignIn/SignInFactorTwoBackupCodeCard.tsx | 2 +- .../__tests__/SignInClientTrust.test.tsx | 35 +++++++++++++++++++ .../SignIn/__tests__/SignInFactorTwo.test.tsx | 11 ++++++ packages/ui/src/test/fixture-helpers.ts | 24 +++++++++++-- 8 files changed, 86 insertions(+), 12 deletions(-) create mode 100644 .changeset/tidy-device-trust-methods.md create mode 100644 packages/ui/src/components/SignIn/__tests__/SignInClientTrust.test.tsx diff --git a/.changeset/tidy-device-trust-methods.md b/.changeset/tidy-device-trust-methods.md new file mode 100644 index 00000000000..0b3908e7a77 --- /dev/null +++ b/.changeset/tidy-device-trust-methods.md @@ -0,0 +1,5 @@ +--- +'@clerk/ui': patch +--- + +Hide the “Use another method” action during Device Trust and second-factor verification when no alternative verification method is available. diff --git a/integration/tests/client-trust.test.ts b/integration/tests/client-trust.test.ts index 7cf5e377f21..9817b5b0135 100644 --- a/integration/tests/client-trust.test.ts +++ b/integration/tests/client-trust.test.ts @@ -38,6 +38,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withNeedsClientTrust] })( // Should contain the new device verification notice await expect(u.page.getByText("You're signing in from a new device.")).toBeVisible(); + await expect(u.page.getByRole('link', { name: 'Use another method' })).toBeHidden(); // User should not be signed in yet since client trust step is required await u.po.expect.toBeSignedOut(); diff --git a/packages/ui/src/components/SignIn/SignInClientTrust.tsx b/packages/ui/src/components/SignIn/SignInClientTrust.tsx index ca3273fc2ff..aa14fd77fb1 100644 --- a/packages/ui/src/components/SignIn/SignInClientTrust.tsx +++ b/packages/ui/src/components/SignIn/SignInClientTrust.tsx @@ -19,6 +19,8 @@ function SignInClientTrustInternal(): JSX.Element { showAllStrategies, toggleAllStrategies, } = useSecondFactorSelection(signIn.supportedSecondFactors); + const onShowAlternativeMethodsClicked = + signIn.supportedSecondFactors && signIn.supportedSecondFactors.length > 1 ? toggleAllStrategies : undefined; if (!currentFactor) { return ; @@ -41,7 +43,7 @@ function SignInClientTrustInternal(): JSX.Element { factorAlreadyPrepared={factorAlreadyPrepared} onFactorPrepare={handleFactorPrepare} factor={currentFactor} - onShowAlternativeMethodsClicked={toggleAllStrategies} + onShowAlternativeMethodsClicked={onShowAlternativeMethodsClicked} /> ); case 'email_code': @@ -51,7 +53,7 @@ function SignInClientTrustInternal(): JSX.Element { factorAlreadyPrepared={factorAlreadyPrepared} onFactorPrepare={handleFactorPrepare} factor={currentFactor} - onShowAlternativeMethodsClicked={toggleAllStrategies} + onShowAlternativeMethodsClicked={onShowAlternativeMethodsClicked} /> ); case 'email_link': @@ -61,7 +63,7 @@ function SignInClientTrustInternal(): JSX.Element { factorAlreadyPrepared={factorAlreadyPrepared} onFactorPrepare={handleFactorPrepare} factor={currentFactor} - onShowAlternativeMethodsClicked={toggleAllStrategies} + onShowAlternativeMethodsClicked={onShowAlternativeMethodsClicked} /> ); default: diff --git a/packages/ui/src/components/SignIn/SignInFactorTwo.tsx b/packages/ui/src/components/SignIn/SignInFactorTwo.tsx index 90bfddd6df7..1af218d771c 100644 --- a/packages/ui/src/components/SignIn/SignInFactorTwo.tsx +++ b/packages/ui/src/components/SignIn/SignInFactorTwo.tsx @@ -28,6 +28,8 @@ function SignInFactorTwoInternal(): JSX.Element { showAllStrategies, toggleAllStrategies, } = useSecondFactorSelection(signIn.supportedSecondFactors); + const onShowAlternativeMethodsClicked = + signIn.supportedSecondFactors && signIn.supportedSecondFactors.length > 1 ? toggleAllStrategies : undefined; React.useEffect(() => { if (clerk.__internal_setActiveInProgress) { @@ -69,7 +71,7 @@ function SignInFactorTwoInternal(): JSX.Element { factorAlreadyPrepared={factorAlreadyPrepared} onFactorPrepare={handleFactorPrepare} factor={currentFactor} - onShowAlternativeMethodsClicked={toggleAllStrategies} + onShowAlternativeMethodsClicked={onShowAlternativeMethodsClicked} /> ); case 'totp': @@ -78,18 +80,18 @@ function SignInFactorTwoInternal(): JSX.Element { factorAlreadyPrepared={factorAlreadyPrepared} onFactorPrepare={handleFactorPrepare} factor={currentFactor} - onShowAlternativeMethodsClicked={toggleAllStrategies} + onShowAlternativeMethodsClicked={onShowAlternativeMethodsClicked} /> ); case 'backup_code': - return ; + return ; case 'email_code': return ( ); case 'email_link': @@ -98,7 +100,7 @@ function SignInFactorTwoInternal(): JSX.Element { factorAlreadyPrepared={factorAlreadyPrepared} onFactorPrepare={handleFactorPrepare} factor={currentFactor} - onShowAlternativeMethodsClicked={toggleAllStrategies} + onShowAlternativeMethodsClicked={onShowAlternativeMethodsClicked} /> ); default: diff --git a/packages/ui/src/components/SignIn/SignInFactorTwoBackupCodeCard.tsx b/packages/ui/src/components/SignIn/SignInFactorTwoBackupCodeCard.tsx index ddd37690b03..3ffe439aa36 100644 --- a/packages/ui/src/components/SignIn/SignInFactorTwoBackupCodeCard.tsx +++ b/packages/ui/src/components/SignIn/SignInFactorTwoBackupCodeCard.tsx @@ -19,7 +19,7 @@ import { navigateOnSignInProtectGate } from './handleProtectCheck'; import { isResetPasswordStrategy } from './utils'; type SignInFactorTwoBackupCodeCardProps = { - onShowAlternativeMethodsClicked: React.MouseEventHandler; + onShowAlternativeMethodsClicked?: React.MouseEventHandler; }; export const SignInFactorTwoBackupCodeCard = (props: SignInFactorTwoBackupCodeCardProps) => { diff --git a/packages/ui/src/components/SignIn/__tests__/SignInClientTrust.test.tsx b/packages/ui/src/components/SignIn/__tests__/SignInClientTrust.test.tsx new file mode 100644 index 00000000000..029f51e9619 --- /dev/null +++ b/packages/ui/src/components/SignIn/__tests__/SignInClientTrust.test.tsx @@ -0,0 +1,35 @@ +import type { SignInResource } from '@clerk/shared/types'; +import { describe, expect, it } from 'vitest'; + +import { bindCreateFixtures } from '@/test/create-fixtures'; +import { render, screen } from '@/test/utils'; + +import { SignInClientTrust } from '../SignInClientTrust'; + +const { createFixtures } = bindCreateFixtures('SignIn'); + +describe('SignInClientTrust', () => { + describe('Use another method', () => { + it('does not render when only one verification factor is available', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.startSignInClientTrust({ supportPhoneCode: true }); + }); + + fixtures.signIn.prepareSecondFactor.mockResolvedValueOnce({} as SignInResource); + render(, { wrapper }); + + expect(screen.queryByText('Use another method')).not.toBeInTheDocument(); + }); + + it('renders when multiple verification factors are available', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.startSignInClientTrust({ supportPhoneCode: true, supportEmailCode: true }); + }); + + fixtures.signIn.prepareSecondFactor.mockResolvedValueOnce({} as SignInResource); + render(, { wrapper }); + + expect(screen.getByText('Use another method')).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/ui/src/components/SignIn/__tests__/SignInFactorTwo.test.tsx b/packages/ui/src/components/SignIn/__tests__/SignInFactorTwo.test.tsx index 3d944954df9..08e66a82705 100644 --- a/packages/ui/src/components/SignIn/__tests__/SignInFactorTwo.test.tsx +++ b/packages/ui/src/components/SignIn/__tests__/SignInFactorTwo.test.tsx @@ -407,6 +407,17 @@ describe('SignInFactorTwo', () => { }); describe('Use another method', () => { + it('does not render when only one verification factor is available', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.startSignInFactorTwo({ supportPhoneCode: true }); + }); + + fixtures.signIn.prepareSecondFactor.mockResolvedValueOnce({} as SignInResource); + render(, { wrapper }); + + expect(screen.queryByText('Use another method')).not.toBeInTheDocument(); + }); + it('renders the other authentication methods list component when clicking on "Use another method"', async () => { const { wrapper, fixtures } = await createFixtures(f => { f.withEmailAddress(); diff --git a/packages/ui/src/test/fixture-helpers.ts b/packages/ui/src/test/fixture-helpers.ts index 9508c2ffe75..51320d1077f 100644 --- a/packages/ui/src/test/fixture-helpers.ts +++ b/packages/ui/src/test/fixture-helpers.ts @@ -119,6 +119,7 @@ const createSignInFixtureHelpers = (baseClient: ClientJSON) => { type SignInFactorTwoParams = { identifier?: string; supportPhoneCode?: boolean; + supportEmailCode?: boolean; supportTotp?: boolean; supportBackupCode?: boolean; supportResetPasswordEmail?: boolean; @@ -185,17 +186,21 @@ const createSignInFixtureHelpers = (baseClient: ClientJSON) => { } as SignInJSON; }; - const startSignInFactorTwo = (params?: SignInFactorTwoParams) => { + const startSignInVerification = ( + status: Extract, + params?: SignInFactorTwoParams, + ) => { const { identifier = '+30 691 1111111', supportPhoneCode = true, + supportEmailCode, supportTotp, supportBackupCode, supportResetPasswordEmail, supportResetPasswordPhone, } = params || {}; baseClient.sign_in = { - status: 'needs_second_factor', + status, identifier, ...(supportResetPasswordEmail ? { @@ -216,6 +221,7 @@ const createSignInFixtureHelpers = (baseClient: ClientJSON) => { supported_identifiers: ['email_address', 'phone_number'], supported_second_factors: [ ...(supportPhoneCode ? [{ strategy: 'phone_code', safe_identifier: identifier || 'n*****@clerk.com' }] : []), + ...(supportEmailCode ? [{ strategy: 'email_code', safe_identifier: 'n*****@clerk.com' }] : []), ...(supportTotp ? [{ strategy: 'totp', safe_identifier: identifier || 'n*****@clerk.com' }] : []), ...(supportBackupCode ? [{ strategy: 'backup_code', safe_identifier: identifier || 'n*****@clerk.com' }] : []), ], @@ -223,6 +229,12 @@ const createSignInFixtureHelpers = (baseClient: ClientJSON) => { } as SignInJSON; }; + const startSignInFactorTwo = (params?: SignInFactorTwoParams) => + startSignInVerification('needs_second_factor', params); + + const startSignInClientTrust = (params?: SignInFactorTwoParams) => + startSignInVerification('needs_client_trust', params); + const startSignInWithProtectCheck = (params?: { expiresAt?: number; uiHints?: Record; @@ -249,7 +261,13 @@ const createSignInFixtureHelpers = (baseClient: ClientJSON) => { } as SignInJSON; }; - return { startSignInWithEmailAddress, startSignInWithPhoneNumber, startSignInFactorTwo, startSignInWithProtectCheck }; + return { + startSignInWithEmailAddress, + startSignInWithPhoneNumber, + startSignInFactorTwo, + startSignInClientTrust, + startSignInWithProtectCheck, + }; }; const createSignUpFixtureHelpers = (baseClient: ClientJSON) => { From c2d5f0344825568743e86ff2ee166ee95667b8ae Mon Sep 17 00:00:00 2001 From: Tom Milewski Date: Thu, 6 Aug 2026 15:05:26 -0400 Subject: [PATCH 2/2] test(ui): cover multiple verification factors --- .../SignIn/__tests__/SignInFactorTwo.test.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/ui/src/components/SignIn/__tests__/SignInFactorTwo.test.tsx b/packages/ui/src/components/SignIn/__tests__/SignInFactorTwo.test.tsx index 08e66a82705..287e934288c 100644 --- a/packages/ui/src/components/SignIn/__tests__/SignInFactorTwo.test.tsx +++ b/packages/ui/src/components/SignIn/__tests__/SignInFactorTwo.test.tsx @@ -418,6 +418,17 @@ describe('SignInFactorTwo', () => { expect(screen.queryByText('Use another method')).not.toBeInTheDocument(); }); + it('renders when multiple verification factors are available', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.startSignInFactorTwo({ supportPhoneCode: true, supportEmailCode: true }); + }); + + fixtures.signIn.prepareSecondFactor.mockResolvedValueOnce({} as SignInResource); + render(, { wrapper }); + + expect(screen.getByText('Use another method')).toBeInTheDocument(); + }); + it('renders the other authentication methods list component when clicking on "Use another method"', async () => { const { wrapper, fixtures } = await createFixtures(f => { f.withEmailAddress();