Skip to content

Commit 8b5645b

Browse files
authored
fix(clerk-js): Correctly show alternative re-verification methods (clerk#5164)
1 parent a83f614 commit 8b5645b

11 files changed

+261
-13
lines changed

.changeset/sharp-sheep-reflect.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@clerk/clerk-js": patch
3+
---
4+
5+
fix(clerk-js): Correctly show alternative methods for user re-verification card

packages/clerk-js/src/ui/components/UserVerification/AlternativeMethods.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import type { LocalizationKey } from '../../customizables';
55
import { Col, descriptors, Flex, Flow, localizationKeys } from '../../customizables';
66
import { ArrowBlockButton, BackLink, Card, Header } from '../../elements';
77
import { useCardState } from '../../elements/contexts';
8-
import { useAlternativeStrategies } from '../../hooks/useAlternativeStrategies';
98
import { ChatAltIcon, Email, LockClosedIcon } from '../../icons';
109
import { formatSafeIdentifier } from '../../utils';
10+
import { useReverificationAlternativeStrategies } from './useReverificationAlternativeStrategies';
1111
import { useUserVerificationSession } from './useUserVerificationSession';
1212
import { withHavingTrouble } from './withHavingTrouble';
1313

@@ -29,7 +29,7 @@ const AlternativeMethodsList = (props: AlternativeMethodListProps) => {
2929
const { onBackLinkClick, onHavingTroubleClick, onFactorSelected } = props;
3030
const card = useCardState();
3131
const { data } = useUserVerificationSession();
32-
const { firstPartyFactors, hasAnyStrategy } = useAlternativeStrategies<SessionVerificationFirstFactor>({
32+
const { firstPartyFactors, hasAnyStrategy } = useReverificationAlternativeStrategies<SessionVerificationFirstFactor>({
3333
filterOutFactor: props?.currentFactor,
3434
supportedFirstFactors: data?.supportedFirstFactors,
3535
});

packages/clerk-js/src/ui/components/UserVerification/UVFactorOnePhoneCodeCard.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export const UVFactorOnePhoneCodeCard = (props: UVFactorOnePhoneCodeCardProps) =
1515
cardSubtitle={localizationKeys('reverification.phoneCode.subtitle')}
1616
inputLabel={localizationKeys('reverification.phoneCode.formTitle')}
1717
resendButton={localizationKeys('reverification.phoneCode.resendButton')}
18+
showAlternativeMethods={props.showAlternativeMethods}
1819
/>
1920
</Flow.Part>
2021
);

packages/clerk-js/src/ui/components/UserVerification/UVFactorTwoCodeForm.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export type UVFactorTwoCodeCard = Pick<VerificationCodeCardProps, 'onShowAlterna
1313
factorAlreadyPrepared: boolean;
1414
onFactorPrepare: () => void;
1515
prepare?: () => Promise<SessionVerificationResource>;
16+
showAlternativeMethods?: boolean;
1617
};
1718

1819
type SignInFactorTwoCodeFormProps = UVFactorTwoCodeCard & {
@@ -64,6 +65,7 @@ export const UVFactorTwoCodeForm = (props: SignInFactorTwoCodeFormProps) => {
6465
safeIdentifier={'safeIdentifier' in props.factor ? props.factor.safeIdentifier : undefined}
6566
profileImageUrl={session?.user?.imageUrl}
6667
onShowAlternativeMethodsClicked={props.onShowAlternativeMethodsClicked}
68+
showAlternativeMethods={props.showAlternativeMethods}
6769
/>
6870
);
6971
};

packages/clerk-js/src/ui/components/UserVerification/UVFactorTwoPhoneCodeCard.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import type { UVFactorTwoCodeCard } from './UVFactorTwoCodeForm';
66
import { UVFactorTwoCodeForm } from './UVFactorTwoCodeForm';
77

88
type UVFactorTwoPhoneCodeCardProps = UVFactorTwoCodeCard & { factor: PhoneCodeFactor };
9-
109
export const UVFactorTwoPhoneCodeCard = (props: UVFactorTwoPhoneCodeCardProps) => {
1110
const { session } = useSession();
1211

packages/clerk-js/src/ui/components/UserVerification/UserVerificationFactorOne.tsx

+4-2
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ import React, { useEffect } from 'react';
33

44
import { useEnvironment } from '../../contexts';
55
import { ErrorCard, LoadingCard, useCardState, withCardStateProvider } from '../../elements';
6-
import { useAlternativeStrategies } from '../../hooks/useAlternativeStrategies';
76
import { localizationKeys } from '../../localization';
87
import { useRouter } from '../../router';
98
import { determineStartingSignInFactor, factorHasLocalStrategy } from '../SignIn/utils';
109
import { AlternativeMethods } from './AlternativeMethods';
10+
import { useReverificationAlternativeStrategies } from './useReverificationAlternativeStrategies';
1111
import { UserVerificationFactorOnePasswordCard } from './UserVerificationFactorOnePassword';
1212
import { useUserVerificationSession, withUserVerificationSessionGuard } from './useUserVerificationSession';
1313
import { UVFactorOneEmailCodeCard } from './UVFactorOneEmailCodeCard';
@@ -46,7 +46,7 @@ export function _UserVerificationFactorOne(): JSX.Element | null {
4646
prevCurrentFactor: undefined,
4747
}));
4848

49-
const { hasAnyStrategy } = useAlternativeStrategies({
49+
const { hasAnyStrategy, hasFirstParty } = useReverificationAlternativeStrategies({
5050
filterOutFactor: currentFactor,
5151
supportedFirstFactors: availableFactors,
5252
});
@@ -116,6 +116,7 @@ export function _UserVerificationFactorOne(): JSX.Element | null {
116116
onFactorPrepare={handleFactorPrepare}
117117
onShowAlternativeMethodsClicked={toggleAllStrategies}
118118
factor={currentFactor}
119+
showAlternativeMethods={hasFirstParty}
119120
/>
120121
);
121122
case 'phone_code':
@@ -125,6 +126,7 @@ export function _UserVerificationFactorOne(): JSX.Element | null {
125126
onFactorPrepare={handleFactorPrepare}
126127
onShowAlternativeMethodsClicked={toggleAllStrategies}
127128
factor={currentFactor}
129+
showAlternativeMethods={hasFirstParty}
128130
/>
129131
);
130132
default:

packages/clerk-js/src/ui/components/UserVerification/UserVerificationFactorTwo.tsx

+19-5
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import type { SessionVerificationResource, SessionVerificationSecondFactor, SignInFactor } from '@clerk/types';
2-
import React, { useEffect } from 'react';
2+
import React, { useEffect, useMemo } from 'react';
33

44
import { LoadingCard, withCardStateProvider } from '../../elements';
55
import { useRouter } from '../../router';
66
import { determineStartingSignInSecondFactor } from '../SignIn/utils';
7+
import { secondFactorsAreEqual } from './useReverificationAlternativeStrategies';
78
import { UserVerificationFactorTwoTOTP } from './UserVerificationFactorTwoTOTP';
89
import { useUserVerificationSession, withUserVerificationSessionGuard } from './useUserVerificationSession';
910
import { UVFactorTwoAlternativeMethods } from './UVFactorTwoAlternativeMethods';
@@ -21,7 +22,7 @@ const factorKey = (factor: SignInFactor | null | undefined) => {
2122
return key;
2223
};
2324

24-
export function _UserVerificationFactorTwo(): JSX.Element {
25+
export function UserVerificationFactorTwoComponent(): JSX.Element {
2526
const { navigate } = useRouter();
2627
const { data } = useUserVerificationSession();
2728
const sessionVerification = data as SessionVerificationResource;
@@ -35,6 +36,11 @@ export function _UserVerificationFactorTwo(): JSX.Element {
3536
const [showAllStrategies, setShowAllStrategies] = React.useState<boolean>(!currentFactor);
3637
const toggleAllStrategies = () => setShowAllStrategies(s => !s);
3738

39+
const secondFactorsExcludingCurrent = useMemo(
40+
() => availableFactors?.filter(factor => !secondFactorsAreEqual(factor, currentFactor)),
41+
[availableFactors, currentFactor],
42+
);
43+
3844
const handleFactorPrepare = () => {
3945
lastPreparedFactorKeyRef.current = factorKey(currentFactor);
4046
};
@@ -44,20 +50,26 @@ export function _UserVerificationFactorTwo(): JSX.Element {
4450
toggleAllStrategies();
4551
};
4652

53+
const hasAlternativeStrategies = useMemo(
54+
() => (secondFactorsExcludingCurrent && secondFactorsExcludingCurrent.length > 0) || false,
55+
[secondFactorsExcludingCurrent],
56+
);
57+
4758
useEffect(() => {
4859
if (sessionVerification.status === 'needs_first_factor') {
4960
void navigate('../');
5061
}
62+
// eslint-disable-next-line react-hooks/exhaustive-deps
5163
}, []);
5264

5365
if (!currentFactor) {
5466
return <LoadingCard />;
5567
}
5668

57-
if (showAllStrategies) {
69+
if (showAllStrategies && hasAlternativeStrategies) {
5870
return (
5971
<UVFactorTwoAlternativeMethods
60-
supportedSecondFactors={sessionVerification.supportedSecondFactors}
72+
supportedSecondFactors={secondFactorsExcludingCurrent || null}
6173
onBackLinkClick={toggleAllStrategies}
6274
onFactorSelected={selectFactor}
6375
/>
@@ -72,6 +84,7 @@ export function _UserVerificationFactorTwo(): JSX.Element {
7284
onFactorPrepare={handleFactorPrepare}
7385
factor={currentFactor}
7486
onShowAlternativeMethodsClicked={toggleAllStrategies}
87+
showAlternativeMethods={hasAlternativeStrategies}
7588
/>
7689
);
7790
case 'totp':
@@ -81,6 +94,7 @@ export function _UserVerificationFactorTwo(): JSX.Element {
8194
onFactorPrepare={handleFactorPrepare}
8295
factor={currentFactor}
8396
onShowAlternativeMethodsClicked={toggleAllStrategies}
97+
showAlternativeMethods={hasAlternativeStrategies}
8498
/>
8599
);
86100
case 'backup_code':
@@ -91,5 +105,5 @@ export function _UserVerificationFactorTwo(): JSX.Element {
91105
}
92106

93107
export const UserVerificationFactorTwo = withUserVerificationSessionGuard(
94-
withCardStateProvider(_UserVerificationFactorTwo),
108+
withCardStateProvider(UserVerificationFactorTwoComponent),
95109
);

packages/clerk-js/src/ui/components/UserVerification/UserVerificationFactorTwoTOTP.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export function UserVerificationFactorTwoTOTP(props: UVFactorTwoTOTPCardProps):
1414
cardTitle={localizationKeys('reverification.totpMfa.title')}
1515
cardSubtitle={localizationKeys('reverification.totpMfa.subtitle')}
1616
inputLabel={localizationKeys('reverification.totpMfa.formTitle')}
17+
showAlternativeMethods={props.showAlternativeMethods}
1718
/>
1819
</Flow.Part>
1920
);

packages/clerk-js/src/ui/components/UserVerification/__tests__/UVFactorOne.test.tsx

+75-1
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,81 @@ describe('UserVerificationFactorOne', () => {
135135
});
136136

137137
describe('Use another method', () => {
138-
it.todo('should list enabled first factor methods without the current one');
138+
it('should list enabled first factor methods without the current one', async () => {
139+
const { wrapper, fixtures } = await createFixtures(f => {
140+
f.withUser({ username: 'clerkuser' });
141+
});
142+
fixtures.session?.startVerification.mockResolvedValue({
143+
status: 'needs_first_factor',
144+
supportedFirstFactors: [
145+
{
146+
strategy: 'email_code',
147+
emailAddressId: 'email_1',
148+
safeIdentifier: 'xxx@hello.com',
149+
},
150+
{
151+
strategy: 'email_code',
152+
emailAddressId: 'email_2',
153+
safeIdentifier: 'xxx+1@hello.com',
154+
},
155+
],
156+
});
157+
fixtures.session?.prepareFirstFactorVerification.mockResolvedValue({});
158+
159+
const { getByText, getByRole } = render(<UserVerificationFactorOne />, { wrapper });
160+
161+
await waitFor(() => {
162+
getByText('Verification required');
163+
getByText('Use another method');
164+
});
165+
166+
await waitFor(() => {
167+
getByText('Use another method').click();
168+
expect(getByRole('button')).toHaveTextContent('Email code to xxx+1@hello.com');
169+
expect(getByRole('button')).not.toHaveTextContent('Email code to xxx@hello.com');
170+
});
171+
});
172+
173+
it('can select another method', async () => {
174+
const { wrapper, fixtures } = await createFixtures(f => {
175+
f.withUser({ username: 'clerkuser' });
176+
});
177+
fixtures.session?.startVerification.mockResolvedValue({
178+
status: 'needs_first_factor',
179+
supportedFirstFactors: [
180+
{
181+
strategy: 'email_code',
182+
emailAddressId: 'email_1',
183+
safeIdentifier: 'xxx@hello.com',
184+
},
185+
{
186+
strategy: 'email_code',
187+
emailAddressId: 'email_2',
188+
safeIdentifier: 'xxx+1@hello.com',
189+
},
190+
],
191+
});
192+
fixtures.session?.prepareFirstFactorVerification.mockResolvedValue({});
193+
194+
const { getByText, container } = render(<UserVerificationFactorOne />, { wrapper });
195+
196+
await waitFor(() => {
197+
getByText('Verification required');
198+
expect(container).toHaveTextContent('xxx@hello.com');
199+
expect(container).not.toHaveTextContent('xxx+1@hello.com');
200+
getByText('Use another method');
201+
});
202+
203+
await waitFor(() => {
204+
getByText('Use another method').click();
205+
getByText('Email code to xxx+1@hello.com').click();
206+
});
207+
208+
await waitFor(() => {
209+
getByText('Verification required');
210+
expect(container).toHaveTextContent('xxx+1@hello.com');
211+
});
212+
});
139213
});
140214

141215
describe('Get Help', () => {

packages/clerk-js/src/ui/components/UserVerification/__tests__/UVFactorTwo.test.tsx

+82-2
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,88 @@ describe('UserVerificationFactorTwo', () => {
134134
});
135135
});
136136

137-
describe('Use another method', () => {
138-
it.todo('should list enabled second factor methods without the current one');
137+
describe('Use another second factor method', () => {
138+
it('should list enabled second factor methods without the current one', async () => {
139+
const { wrapper, fixtures } = await createFixtures(f => {
140+
f.withUser({ username: 'clerkuser' });
141+
});
142+
fixtures.session?.startVerification.mockResolvedValue({
143+
status: 'needs_second_factor',
144+
supportedSecondFactors: [
145+
{
146+
strategy: 'phone_code',
147+
phoneNumberId: 'phone_1',
148+
safeIdentifier: '+3069XXXXXXX1',
149+
},
150+
{
151+
strategy: 'phone_code',
152+
phoneNumberId: 'phone_2',
153+
safeIdentifier: '+3069XXXXXXX2',
154+
},
155+
],
156+
});
157+
fixtures.session?.prepareSecondFactorVerification.mockResolvedValue({});
158+
159+
const { getByText, getByRole } = render(<UserVerificationFactorTwo />, { wrapper });
160+
161+
await waitFor(() => {
162+
getByText('Verification required');
163+
getByText('Use another method');
164+
});
165+
166+
await waitFor(() => {
167+
getByText('Use another method').click();
168+
expect(getByRole('button')).toHaveTextContent('Send SMS code to +3069XXXXXXX2');
169+
expect(getByRole('button')).not.toHaveTextContent('Send SMS code to +3069XXXXXXX1');
170+
});
171+
});
172+
173+
it('can select another method', async () => {
174+
const { wrapper, fixtures } = await createFixtures(f => {
175+
f.withUser({ username: 'clerkuser' });
176+
});
177+
fixtures.session?.startVerification.mockResolvedValue({
178+
status: 'needs_second_factor',
179+
supportedSecondFactors: [
180+
{
181+
strategy: 'phone_code',
182+
phoneNumberId: 'phone_1',
183+
safeIdentifier: '+3069XXXXXXX1',
184+
},
185+
{
186+
strategy: 'phone_code',
187+
phoneNumberId: 'phone_2',
188+
safeIdentifier: '+3069XXXXXXX2',
189+
},
190+
],
191+
});
192+
fixtures.session?.prepareSecondFactorVerification.mockResolvedValue({});
193+
194+
const { getByText, container } = render(<UserVerificationFactorTwo />, { wrapper });
195+
196+
await waitFor(() => {
197+
getByText('Verification required');
198+
expect(container).toHaveTextContent('+3069XXXXXXX1');
199+
expect(container).not.toHaveTextContent('+3069XXXXXXX2');
200+
getByText('Use another method');
201+
});
202+
203+
await waitFor(() => {
204+
getByText('Use another method').click();
205+
getByText('Send SMS code to +3069XXXXXXX2').click();
206+
});
207+
208+
await waitFor(() => {
209+
getByText('Verification required');
210+
expect(container).toHaveTextContent('+3069XXXXXXX2');
211+
getByText('Use another method');
212+
});
213+
214+
await waitFor(() => {
215+
getByText('Use another method').click();
216+
expect(container).toHaveTextContent('+3069XXXXXXX1');
217+
});
218+
});
139219
});
140220

141221
describe('Get Help', () => {

0 commit comments

Comments
 (0)