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/AlternativeMethods.tsx b/packages/ui/src/components/UserVerification/AlternativeMethods.tsx index 6f47ecef5ec..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, hasAnyStrategy } = useReverificationAlternativeStrategies({ - filterOutFactor: props?.currentFactor, - supportedFirstFactors: data?.supportedFirstFactors, - }); + const { firstPartyFactors, hasAlternativeStrategies } = + useReverificationAlternativeStrategies({ + filterOutFactor: props?.currentFactor, + supportedFirstFactors: data?.supportedFirstFactors, + }); return ( @@ -54,7 +55,7 @@ const AlternativeMethodsList = (props: AlternativeMethodListProps) => { gap={6} > - {hasAnyStrategy && ( + {hasAlternativeStrategies && ( { diff --git a/packages/ui/src/components/UserVerification/UserVerificationFactorOne.tsx b/packages/ui/src/components/UserVerification/UserVerificationFactorOne.tsx index 14bc9867c0e..56f498bb48f 100644 --- a/packages/ui/src/components/UserVerification/UserVerificationFactorOne.tsx +++ b/packages/ui/src/components/UserVerification/UserVerificationFactorOne.tsx @@ -65,7 +65,7 @@ export function UserVerificationFactorOneInternal(): JSX.Element | null { prevCurrentFactor: undefined, })); - const { hasAnyStrategy, hasFirstParty } = useReverificationAlternativeStrategies({ + const { hasAlternativeStrategies } = useReverificationAlternativeStrategies({ filterOutFactor: currentFactor, supportedFirstFactors: availableFactors, }); @@ -74,7 +74,7 @@ export function UserVerificationFactorOneInternal(): JSX.Element | null { () => !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/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..9b13966c7b0 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 @@ -65,8 +60,7 @@ export function useReverificationAlternativeStrategies({ ); return { - hasAnyStrategy: shouldAllowForAlternativeStrategies, - hasFirstParty: firstPartyFactors && firstPartyFactors.length > 0, + hasAlternativeStrategies: firstPartyFactors.length > 0, firstPartyFactors, }; }