Skip to content

Commit 10b5bea

Browse files
authored
feat(ui): Hide "Remove" action for last second factor strategy when required (#7729)
1 parent 85d213d commit 10b5bea

File tree

7 files changed

+267
-49
lines changed

7 files changed

+267
-49
lines changed

.changeset/every-friends-sort.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@clerk/clerk-js': minor
3+
'@clerk/shared': minor
4+
'@clerk/ui': minor
5+
---
6+
7+
Hide the "Remove" action from the last available 2nd factor strategy when MFA is required

packages/clerk-js/src/core/resources/UserSettings.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@ export class UserSettings extends BaseResource implements UserSettingsResource {
123123
legal_consent_enabled: false,
124124
mode: 'public',
125125
progressive: true,
126+
mfa: {
127+
required: false,
128+
},
126129
};
127130
social: OAuthProviders = {} as OAuthProviders;
128131
usernameSettings: UsernameSettingsData = {} as UsernameSettingsData;

packages/shared/src/types/userSettings.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ export type SignUpData = {
5858
captcha_enabled: boolean;
5959
mode: SignUpModes;
6060
legal_consent_enabled: boolean;
61+
mfa?: {
62+
required: boolean;
63+
};
6164
};
6265

6366
export type PasswordSettingsData = {

packages/ui/src/components/UserProfile/MfaSection.tsx

Lines changed: 55 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { MfaBackupCodeCreateScreen, MfaScreen, RemoveMfaPhoneCodeScreen, RemoveM
2020

2121
export const MfaSection = () => {
2222
const {
23-
userSettings: { attributes },
23+
userSettings: { attributes, signUp },
2424
} = useEnvironment();
2525
const { user } = useUser();
2626
const [actionValue, setActionValue] = useState<string | null>(null);
@@ -34,12 +34,16 @@ export const MfaSection = () => {
3434

3535
const showTOTP = secondFactors.includes('totp') && user.totpEnabled;
3636
const showBackupCode = secondFactors.includes('backup_code') && user.backupCodeEnabled;
37+
const showPhoneCode = secondFactors.includes('phone_code');
3738

3839
const mfaPhones = user.phoneNumbers
3940
.filter(ph => ph.verification.status === 'verified')
4041
.filter(ph => ph.reservedForSecondFactor)
4142
.sort(defaultFirst);
4243

44+
const hideTOTPDeleteAction = Boolean(signUp.mfa?.required && mfaPhones.length === 0);
45+
const hidePhoneCodeDeleteAction = Boolean(signUp.mfa?.required && !showTOTP && mfaPhones.length === 1);
46+
4347
return (
4448
<ProfileSection.Root
4549
title={localizationKeys('userProfile.start.mfaSection.title')}
@@ -68,7 +72,7 @@ export const MfaSection = () => {
6872
<Badge localizationKey={localizationKeys('badge__default')} />
6973
</Flex>
7074

71-
<MfaTOTPMenu />
75+
{!hideTOTPDeleteAction && <MfaTOTPMenu />}
7276
</ProfileSection.Item>
7377

7478
<Action.Open value='remove-totp'>
@@ -79,7 +83,7 @@ export const MfaSection = () => {
7983
</>
8084
)}
8185

82-
{secondFactors.includes('phone_code') &&
86+
{showPhoneCode &&
8387
mfaPhones.map(phone => {
8488
const isDefault = !showTOTP && phone.defaultSecondFactor;
8589
const phoneId = phone.id;
@@ -102,7 +106,8 @@ export const MfaSection = () => {
102106

103107
<MfaPhoneCodeMenu
104108
phone={phone}
105-
showTOTP={showTOTP}
109+
isDefault={isDefault}
110+
hidePhoneCodeDeleteAction={hidePhoneCodeDeleteAction}
106111
/>
107112
</ProfileSection.Item>
108113

@@ -153,30 +158,37 @@ export const MfaSection = () => {
153158

154159
type MfaPhoneCodeMenuProps = {
155160
phone: PhoneNumberResource;
156-
showTOTP: boolean;
161+
isDefault: boolean;
162+
hidePhoneCodeDeleteAction: boolean;
157163
};
158164

159-
const MfaPhoneCodeMenu = ({ phone, showTOTP }: MfaPhoneCodeMenuProps) => {
165+
const MfaPhoneCodeMenu = ({ phone, isDefault, hidePhoneCodeDeleteAction }: MfaPhoneCodeMenuProps) => {
160166
const { open } = useActionContext();
161167
const card = useCardState();
162168
const phoneId = phone.id;
163169

164170
const actions = (
165171
[
166-
!showTOTP && !phone.defaultSecondFactor
172+
!isDefault
167173
? {
168174
label: localizationKeys('userProfile.start.mfaSection.phoneCode.actionLabel__setDefault'),
169175
onClick: () => phone.makeDefaultSecondFactor().catch(err => handleError(err, [], card.setError)),
170176
}
171177
: null,
172-
{
173-
label: localizationKeys('userProfile.start.mfaSection.phoneCode.destructiveActionLabel'),
174-
isDestructive: true,
175-
onClick: () => open(`remove-${phoneId}`),
176-
},
178+
!hidePhoneCodeDeleteAction
179+
? {
180+
label: localizationKeys('userProfile.start.mfaSection.phoneCode.destructiveActionLabel'),
181+
isDestructive: true,
182+
onClick: () => open(`remove-${phoneId}`),
183+
}
184+
: null,
177185
] satisfies (PropsOfComponent<typeof ThreeDotsMenu>['actions'][0] | null)[]
178186
).filter(a => a !== null) as PropsOfComponent<typeof ThreeDotsMenu>['actions'];
179187

188+
if (actions.length === 0) {
189+
return null;
190+
}
191+
180192
return <ThreeDotsMenu actions={actions} />;
181193
};
182194

@@ -216,6 +228,24 @@ type MfaAddMenuProps = ProfileSectionActionMenuItemProps & {
216228
onClick?: () => void;
217229
};
218230

231+
const strategiesMap = {
232+
phone_code: {
233+
icon: Mobile,
234+
text: 'SMS code',
235+
key: 'phone_code',
236+
},
237+
totp: {
238+
icon: AuthApp,
239+
text: 'Authenticator application',
240+
key: 'totp',
241+
},
242+
backup_code: {
243+
icon: DotCircle,
244+
text: 'Backup code',
245+
key: 'backup_code',
246+
},
247+
} as const;
248+
219249
const MfaAddMenu = (props: MfaAddMenuProps) => {
220250
const { open } = useActionContext();
221251
const { secondFactorsAvailableToAdd, onClick } = props;
@@ -225,27 +255,7 @@ const MfaAddMenu = (props: MfaAddMenuProps) => {
225255
() =>
226256
secondFactorsAvailableToAdd
227257
.map(key => {
228-
if (key === 'phone_code') {
229-
return {
230-
icon: Mobile,
231-
text: 'SMS code',
232-
key: 'phone_code',
233-
} as const;
234-
} else if (key === 'totp') {
235-
return {
236-
icon: AuthApp,
237-
text: 'Authenticator application',
238-
key: 'totp',
239-
} as const;
240-
} else if (key === 'backup_code') {
241-
return {
242-
icon: DotCircle,
243-
text: 'Backup code',
244-
key: 'backup_code',
245-
} as const;
246-
}
247-
248-
return null;
258+
return strategiesMap[key as keyof typeof strategiesMap] || null;
249259
})
250260
.filter(element => element !== null),
251261
[secondFactorsAvailableToAdd],
@@ -260,21 +270,18 @@ const MfaAddMenu = (props: MfaAddMenuProps) => {
260270
triggerLocalizationKey={localizationKeys('userProfile.start.mfaSection.primaryButton')}
261271
onClick={onClick}
262272
>
263-
{strategies.map(
264-
method =>
265-
method && (
266-
<ProfileSection.ActionMenuItem
267-
key={method.key}
268-
id={method.key}
269-
localizationKey={method.text}
270-
leftIcon={method.icon}
271-
onClick={() => {
272-
setSelectedStrategy(method.key);
273-
open('multi-factor');
274-
}}
275-
/>
276-
),
277-
)}
273+
{strategies.map(method => (
274+
<ProfileSection.ActionMenuItem
275+
key={method.key}
276+
id={method.key}
277+
localizationKey={method.text}
278+
leftIcon={method.icon}
279+
onClick={() => {
280+
setSelectedStrategy(method.key);
281+
open('multi-factor');
282+
}}
283+
/>
284+
))}
278285
</ProfileSection.ActionMenu>
279286
</Action.Closed>
280287
)}

0 commit comments

Comments
 (0)