From ad1058702baa32c7116b1a089d100c6dc993c176 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20H=C3=B6glund?= Date: Tue, 8 Sep 2026 16:07:02 +0200 Subject: [PATCH 1/3] fix(ui): hide use another method for reverification when only one method --- .../hide-reverification-empty-methods.md | 5 +++ .../UVFactorTwoBackupCodeCard.tsx | 2 +- .../UserVerificationFactorTwo.tsx | 6 ++- .../__tests__/UVFactorOne.test.tsx | 39 +++++++++++++++++++ .../__tests__/UVFactorTwo.test.tsx | 35 +++++++++++++++++ .../useReverificationAlternativeStrategies.ts | 13 +++---- 6 files changed, 90 insertions(+), 10 deletions(-) create mode 100644 .changeset/hide-reverification-empty-methods.md diff --git a/.changeset/hide-reverification-empty-methods.md b/.changeset/hide-reverification-empty-methods.md new file mode 100644 index 00000000000..37009c16065 --- /dev/null +++ b/.changeset/hide-reverification-empty-methods.md @@ -0,0 +1,5 @@ +--- +'@clerk/ui': patch +--- + +Hide "Use another method" during reverification when the current factor is the only available method. diff --git a/packages/ui/src/components/UserVerification/UVFactorTwoBackupCodeCard.tsx b/packages/ui/src/components/UserVerification/UVFactorTwoBackupCodeCard.tsx index 2d624d9ff1a..b2f5f84d7a5 100644 --- a/packages/ui/src/components/UserVerification/UVFactorTwoBackupCodeCard.tsx +++ b/packages/ui/src/components/UserVerification/UVFactorTwoBackupCodeCard.tsx @@ -12,7 +12,7 @@ import { Col, descriptors, localizationKeys } from '../../customizables'; import { useAfterVerification } from './use-after-verification'; type UVFactorTwoBackupCodeCardProps = { - onShowAlternativeMethodsClicked: React.MouseEventHandler; + onShowAlternativeMethodsClicked?: React.MouseEventHandler; }; export const UVFactorTwoBackupCodeCard = (props: UVFactorTwoBackupCodeCardProps) => { diff --git a/packages/ui/src/components/UserVerification/UserVerificationFactorTwo.tsx b/packages/ui/src/components/UserVerification/UserVerificationFactorTwo.tsx index 512a4ee00ac..518e2e7ae70 100644 --- a/packages/ui/src/components/UserVerification/UserVerificationFactorTwo.tsx +++ b/packages/ui/src/components/UserVerification/UserVerificationFactorTwo.tsx @@ -95,7 +95,11 @@ export function UserVerificationFactorTwoComponent(): JSX.Element { /> ); case 'backup_code': - return ; + return ( + + ); default: return ; } diff --git a/packages/ui/src/components/UserVerification/__tests__/UVFactorOne.test.tsx b/packages/ui/src/components/UserVerification/__tests__/UVFactorOne.test.tsx index 1db947ebbc8..a1ba3316620 100644 --- a/packages/ui/src/components/UserVerification/__tests__/UVFactorOne.test.tsx +++ b/packages/ui/src/components/UserVerification/__tests__/UVFactorOne.test.tsx @@ -136,6 +136,45 @@ describe('UserVerificationFactorOne', () => { }); describe('Use another method', () => { + it('does not show use another method when password is the only first factor', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.withUser({ username: 'clerkuser' }); + }); + fixtures.session?.startVerification.mockResolvedValue({ + status: 'needs_first_factor', + supportedFirstFactors: [{ strategy: 'password' }], + }); + + const { findByText, queryByText } = render(, { wrapper }); + + await findByText('Verification required'); + expect(queryByText('Use another method')).toBeNull(); + await findByText('Get help'); + }); + + it('does not show use another method when email is the only first factor', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.withUser({ username: 'clerkuser' }); + f.withPreferredSignInStrategy({ strategy: 'otp' }); + }); + fixtures.session?.startVerification.mockResolvedValue({ + status: 'needs_first_factor', + supportedFirstFactors: [ + { + strategy: 'email_code', + emailAddressId: 'email_1', + safeIdentifier: 'xxx@hello.com', + }, + ], + }); + fixtures.session?.prepareFirstFactorVerification.mockResolvedValue({}); + + const { findByText, queryByText } = render(, { wrapper }); + + await findByText('Verification required'); + expect(queryByText('Use another method')).toBeNull(); + }); + it('should list enabled first factor methods without the current one', async () => { const { wrapper, fixtures } = await createFixtures(f => { f.withUser({ username: 'clerkuser' }); diff --git a/packages/ui/src/components/UserVerification/__tests__/UVFactorTwo.test.tsx b/packages/ui/src/components/UserVerification/__tests__/UVFactorTwo.test.tsx index 569073c8c85..f2692566bc6 100644 --- a/packages/ui/src/components/UserVerification/__tests__/UVFactorTwo.test.tsx +++ b/packages/ui/src/components/UserVerification/__tests__/UVFactorTwo.test.tsx @@ -127,6 +127,41 @@ describe('UserVerificationFactorTwo', () => { }); describe('Use another second factor method', () => { + it('does not show use another method when totp is the only second factor', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.withUser({ username: 'clerkuser' }); + }); + vi.spyOn(fixtures.session, 'startVerification').mockResolvedValue({ + status: 'needs_second_factor', + supportedSecondFactors: [{ strategy: 'totp' }], + } as any); + + vi.spyOn(fixtures.session, 'prepareSecondFactorVerification').mockResolvedValue({ + status: 'needs_second_factor', + supportedSecondFactors: [{ strategy: 'totp' }], + } as any); + + const { findByText, queryByText } = render(, { wrapper }); + + await findByText('Verification required'); + expect(queryByText('Use another method')).toBeNull(); + }); + + it('does not show use another method when backup code is the only second factor', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.withUser({ username: 'clerkuser' }); + }); + vi.spyOn(fixtures.session, 'startVerification').mockResolvedValue({ + status: 'needs_second_factor', + supportedSecondFactors: [{ strategy: 'backup_code' }], + } as any); + + const { findByText, queryByText } = render(, { wrapper }); + + await findByText('Enter a backup code'); + expect(queryByText('Use another method')).toBeNull(); + }); + it('should list enabled second factor methods without the current one', async () => { const { wrapper, fixtures } = await createFixtures(f => { f.withUser({ username: 'clerkuser' }); diff --git a/packages/ui/src/components/UserVerification/useReverificationAlternativeStrategies.ts b/packages/ui/src/components/UserVerification/useReverificationAlternativeStrategies.ts index 439cde2a8b3..4330aead6c0 100644 --- a/packages/ui/src/components/UserVerification/useReverificationAlternativeStrategies.ts +++ b/packages/ui/src/components/UserVerification/useReverificationAlternativeStrategies.ts @@ -4,7 +4,7 @@ import { useMemo } from 'react'; import { allStrategiesButtonsComparator } from '@/ui/utils/factorSorting'; -import { factorHasLocalStrategy, isResetPasswordStrategy } from '../SignIn/utils'; +import { factorHasLocalStrategy } from '../SignIn/utils'; const firstFactorsAreEqual = (a: SignInFactor | null | undefined, b: SignInFactor | null | undefined) => { if (!a || !b) { @@ -44,11 +44,6 @@ export function useReverificationAlternativeStrategies({ filterOutFactor: SignInFactor | null | undefined; supportedFirstFactors: SignInFirstFactor[] | null | undefined; }) { - const firstFactors = supportedFirstFactors - ? supportedFirstFactors.filter(f => !isResetPasswordStrategy(f.strategy)) - : []; - const shouldAllowForAlternativeStrategies = firstFactors && firstFactors.length > 0; - const firstPartyFactors = useMemo( () => supportedFirstFactors @@ -64,9 +59,11 @@ export function useReverificationAlternativeStrategies({ [supportedFirstFactors, filterOutFactor], ); + const hasAnyStrategy = firstPartyFactors.length > 0; + return { - hasAnyStrategy: shouldAllowForAlternativeStrategies, - hasFirstParty: firstPartyFactors && firstPartyFactors.length > 0, + hasAnyStrategy, + hasFirstParty: hasAnyStrategy, firstPartyFactors, }; } From 385307612cc7cb67398f5b0c8b27efc6fe40cebf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20H=C3=B6glund?= Date: Tue, 8 Sep 2026 16:13:21 +0200 Subject: [PATCH 2/3] Deduplicate and improve naming --- .../components/UserVerification/AlternativeMethods.tsx | 4 ++-- .../UserVerification/UserVerificationFactorOne.tsx | 8 ++++---- .../useReverificationAlternativeStrategies.ts | 5 +---- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/ui/src/components/UserVerification/AlternativeMethods.tsx b/packages/ui/src/components/UserVerification/AlternativeMethods.tsx index 6f47ecef5ec..fd3275bb8de 100644 --- a/packages/ui/src/components/UserVerification/AlternativeMethods.tsx +++ b/packages/ui/src/components/UserVerification/AlternativeMethods.tsx @@ -33,7 +33,7 @@ const AlternativeMethodsList = (props: AlternativeMethodListProps) => { const { onBackLinkClick, onHavingTroubleClick, onFactorSelected } = props; const card = useCardState(); const { data } = useUserVerificationSession(); - const { firstPartyFactors, hasAnyStrategy } = useReverificationAlternativeStrategies({ + const { firstPartyFactors, hasAlternativeStrategies } = useReverificationAlternativeStrategies({ filterOutFactor: props?.currentFactor, supportedFirstFactors: data?.supportedFirstFactors, }); @@ -54,7 +54,7 @@ const AlternativeMethodsList = (props: AlternativeMethodListProps) => { gap={6} > - {hasAnyStrategy && ( + {hasAlternativeStrategies && ( !currentFactor || !factorHasLocalStrategy(currentFactor), ); - const toggleAllStrategies = hasAnyStrategy + const toggleAllStrategies = hasAlternativeStrategies ? () => { card.setError(undefined); setShowAllStrategies(s => !s); @@ -140,7 +140,7 @@ export function UserVerificationFactorOneInternal(): JSX.Element | null { onFactorPrepare={handleFactorPrepare} onShowAlternativeMethodsClicked={toggleAllStrategies} factor={currentFactor} - showAlternativeMethods={hasFirstParty} + showAlternativeMethods={hasAlternativeStrategies} /> ); case 'phone_code': @@ -150,7 +150,7 @@ export function UserVerificationFactorOneInternal(): JSX.Element | null { onFactorPrepare={handleFactorPrepare} onShowAlternativeMethodsClicked={toggleAllStrategies} factor={currentFactor} - showAlternativeMethods={hasFirstParty} + showAlternativeMethods={hasAlternativeStrategies} /> ); case 'passkey': diff --git a/packages/ui/src/components/UserVerification/useReverificationAlternativeStrategies.ts b/packages/ui/src/components/UserVerification/useReverificationAlternativeStrategies.ts index 4330aead6c0..9b13966c7b0 100644 --- a/packages/ui/src/components/UserVerification/useReverificationAlternativeStrategies.ts +++ b/packages/ui/src/components/UserVerification/useReverificationAlternativeStrategies.ts @@ -59,11 +59,8 @@ export function useReverificationAlternativeStrategies({ [supportedFirstFactors, filterOutFactor], ); - const hasAnyStrategy = firstPartyFactors.length > 0; - return { - hasAnyStrategy, - hasFirstParty: hasAnyStrategy, + hasAlternativeStrategies: firstPartyFactors.length > 0, firstPartyFactors, }; } From 46118ac4b8ee19abb8aaeafa049f7475bec76af1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20H=C3=B6glund?= Date: Tue, 8 Sep 2026 17:06:28 +0200 Subject: [PATCH 3/3] Fix formatting --- .../components/UserVerification/AlternativeMethods.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/ui/src/components/UserVerification/AlternativeMethods.tsx b/packages/ui/src/components/UserVerification/AlternativeMethods.tsx index fd3275bb8de..a77193ea38f 100644 --- a/packages/ui/src/components/UserVerification/AlternativeMethods.tsx +++ b/packages/ui/src/components/UserVerification/AlternativeMethods.tsx @@ -33,10 +33,11 @@ const AlternativeMethodsList = (props: AlternativeMethodListProps) => { const { onBackLinkClick, onHavingTroubleClick, onFactorSelected } = props; const card = useCardState(); const { data } = useUserVerificationSession(); - const { firstPartyFactors, hasAlternativeStrategies } = useReverificationAlternativeStrategies({ - filterOutFactor: props?.currentFactor, - supportedFirstFactors: data?.supportedFirstFactors, - }); + const { firstPartyFactors, hasAlternativeStrategies } = + useReverificationAlternativeStrategies({ + filterOutFactor: props?.currentFactor, + supportedFirstFactors: data?.supportedFirstFactors, + }); return (