@@ -11,23 +11,13 @@ import {
1111} from '../icons/mfa' ;
1212import { StyledButton } from '../styledComponents' ;
1313import { Message } from './Message' ;
14+ import { BackLink } from './BackLink' ;
1415import { AuthorizerTOTPScanner } from './AuthorizerTOTPScanner' ;
1516import { AuthorizerPasskeyRegister } from './AuthorizerPasskeyRegister' ;
1617import { AuthorizerVerifyOtp } from './AuthorizerVerifyOtp' ;
1718import { useAuthorizer } from '../contexts/AuthorizerContext' ;
1819import { TotpEnrollment } from '../utils/mfaTriage' ;
1920
20- const BackLink : FC < { onClick : ( ) => void } > = ( { onClick } ) => (
21- < button
22- type = "button"
23- className = "mfa-icon-button"
24- onClick = { onClick }
25- style = { { border : 'none' , background : 'none' , padding : '4px 0' } }
26- >
27- ← All methods
28- </ button >
29- ) ;
30-
3121export type MfaMethod = 'totp' | 'passkey' | 'email_otp' | 'sms_otp' ;
3222
3323// Which MFA methods the server offers. This shape is designed to map 1:1
@@ -76,12 +66,24 @@ export const AuthorizerMFASetup: FC<{
7666 state ?: string ;
7767 onComplete : ( response : AuthTokenLike ) => void ;
7868 } ;
69+ // When present, a "Back" link is shown on the top-level method list so the
70+ // host can let the user leave MFA setup entirely (distinct from BackLink's
71+ // existing "All methods" links, which only return from a method's
72+ // sub-flow to this same list).
73+ onBack ?: ( ) => void ;
74+ // Whether the signed-in user already has at least one passkey registered
75+ // (the host knows this via webauthnCredentials - there's no per-user
76+ // enrolment signal for TOTP/email-OTP/SMS-OTP available to the client, so
77+ // only passkey can be accurately highlighted as already set up).
78+ passkeyRegistered ?: boolean ;
7979} > = ( {
8080 availableMfaMethods,
8181 totpEnrollment,
8282 onSetupMethod,
8383 heading = 'Add a second step to sign in' ,
8484 loginContext,
85+ onBack,
86+ passkeyRegistered,
8587} ) => {
8688 const [ selected , setSelected ] = useState < MfaMethod | null > ( null ) ;
8789 const [ notice , setNotice ] = useState ( '' ) ;
@@ -110,6 +112,7 @@ export const AuthorizerMFASetup: FC<{
110112 description : string ;
111113 disabled ?: boolean ;
112114 disabledReason ?: string ;
115+ enabled ?: boolean ;
113116 } [ ] = [
114117 {
115118 key : 'totp' ,
@@ -126,6 +129,7 @@ export const AuthorizerMFASetup: FC<{
126129 description : 'Sign in with your fingerprint, face, or device PIN.' ,
127130 disabled : ! passkeySupported ,
128131 disabledReason : 'Not supported on this browser or device.' ,
132+ enabled : ! ! passkeyRegistered ,
129133 } ,
130134 {
131135 key : 'email_otp' ,
@@ -264,12 +268,11 @@ export const AuthorizerMFASetup: FC<{
264268 if ( selected === 'totp' && effectiveTotpEnrollment ) {
265269 return (
266270 < >
267- < BackLink onClick = { backToList } />
271+ < BackLink onClick = { backToList } label = "All methods" />
268272 < AuthorizerTOTPScanner
269273 { ...effectiveTotpEnrollment }
270274 email = { loginContext ?. email }
271275 phone_number = { loginContext ?. phone_number }
272- setView = { backToList }
273276 onLogin = { ( data ) => {
274277 if ( loginContext && data && ( data as AuthTokenLike ) . access_token ) {
275278 loginContext . onComplete ( data as AuthTokenLike ) ;
@@ -285,7 +288,7 @@ export const AuthorizerMFASetup: FC<{
285288 if ( otpMethodPending ) {
286289 return (
287290 < >
288- < BackLink onClick = { ( ) => setOtpMethodPending ( null ) } />
291+ < BackLink onClick = { ( ) => setOtpMethodPending ( null ) } label = "All methods" />
289292 < AuthorizerVerifyOtp
290293 email = { loginContext ?. email }
291294 phone_number = { loginContext ?. phone_number }
@@ -306,7 +309,7 @@ export const AuthorizerMFASetup: FC<{
306309 if ( selected === 'passkey' ) {
307310 return (
308311 < >
309- < BackLink onClick = { backToList } />
312+ < BackLink onClick = { backToList } label = "All methods" />
310313 < p style = { { margin : '10px 0px' , fontWeight : 'bold' } } > Add a passkey</ p >
311314 < AuthorizerPasskeyRegister
312315 // showCredentials calls webauthn_credentials, which requires a
@@ -335,6 +338,7 @@ export const AuthorizerMFASetup: FC<{
335338
336339 return (
337340 < >
341+ { onBack && < BackLink onClick = { onBack } /> }
338342 < p style = { { margin : '10px 0px' , fontWeight : 'bold' } } > { heading } </ p >
339343 { notice && (
340344 < Message
@@ -359,10 +363,18 @@ export const AuthorizerMFASetup: FC<{
359363 ) : (
360364 < ul className = "mfa-list" aria-label = "Available multi-factor methods" >
361365 { visibleMethods . map ( ( m ) => (
362- < li key = { m . key } className = "mfa-method" >
366+ < li
367+ key = { m . key }
368+ className = { `mfa-method${ m . enabled ? ' mfa-method-enabled' : '' } ` }
369+ >
363370 < span className = "mfa-method-icon" > { m . icon } </ span >
364371 < div className = "mfa-method-body" >
365- < p className = "mfa-method-title" > { m . title } </ p >
372+ < p className = "mfa-method-title" >
373+ { m . title }
374+ { m . enabled && (
375+ < span className = "mfa-method-badge" > Enabled</ span >
376+ ) }
377+ </ p >
366378 < p className = "mfa-method-desc" >
367379 { m . disabled && m . disabledReason
368380 ? m . disabledReason
@@ -377,7 +389,7 @@ export const AuthorizerMFASetup: FC<{
377389 onClick = { ( ) => handleSetup ( m . key ) }
378390 style = { { width : 'auto' } }
379391 >
380- Set up
392+ { m . enabled ? 'Manage' : ' Set up' }
381393 </ StyledButton >
382394 </ div >
383395 </ li >
0 commit comments