1+ import { deviceVerificationType } from '@components/MultifactorAuthentication/biometrics/operations' ;
12import { navigate as mfaNavigate , resetMfaNavigation } from '@components/MultifactorAuthentication/mfaNavigation' ;
23
34import { createUnhandledExceptionMFAError , getMFAFailureError } from '@libs/MultifactorAuthentication/shared/MFAResult' ;
45import Navigation from '@libs/Navigation/Navigation' ;
56
7+ import { hasEverAcceptedSoftPrompt , markHasAcceptedSoftPrompt } from '@userActions/MultifactorAuthentication' ;
8+
69import CONST from '@src/CONST' ;
710import SCREENS from '@src/SCREENS' ;
811
@@ -14,15 +17,20 @@ import createActors from './mfaActors';
1417
1518const MFA_STATE = CONST . MULTIFACTOR_AUTHENTICATION . MFA_STATE ;
1619
17- // Absolute target for the outcome branch . The device check runs under `preparing`, so reaching the
18- // sibling `outcome` branch needs an id target rather than a relative one.
20+ // Absolute targets for the screen branches . The device check runs under `preparing`, so reaching a
21+ // sibling branch needs an id target rather than a relative one.
1922const OUTCOME_TARGET = `#${ MFA_STATE . OUTCOME } ` as const ;
23+ const PROMPT_TARGET = `#${ MFA_STATE . PROMPT } ` as const ;
24+
25+ // Which prompt variant the screen renders is a device property, resolved once per platform.
26+ const PROMPT_TYPE = CONST . MULTIFACTOR_AUTHENTICATION . PROMPT_TYPE_MAP [ deviceVerificationType ] ;
2027
2128const DEFAULT_CONTEXT : MfaContext = {
2229 error : undefined ,
2330 scenarioName : undefined ,
2431 scenario : undefined ,
2532 payload : undefined ,
33+ softPromptApproved : false ,
2634 isCancelConfirmVisible : false ,
2735} ;
2836
@@ -69,6 +77,13 @@ const MFAMachine = setup({
6977 navigateToFailureOutcome : ( ) => {
7078 Navigation . runAfterTransition ( ( ) => mfaNavigate ( SCREENS . MULTIFACTOR_AUTHENTICATION . OUTCOME_FAILURE ) ) ;
7179 } ,
80+ navigateToPrompt : ( ) => {
81+ Navigation . runAfterTransition ( ( ) => mfaNavigate ( SCREENS . MULTIFACTOR_AUTHENTICATION . PROMPT , { promptType : PROMPT_TYPE } ) ) ;
82+ } ,
83+ approveSoftPrompt : assign ( { softPromptApproved : true } ) ,
84+ // The durable "asked once" flag lives in Onyx, so future flows on this device can skip the
85+ // prompt. Owned by the machine so every sender of SOFT_PROMPT_APPROVED persists it.
86+ persistSoftPromptAcceptance : ( ) => markHasAcceptedSoftPrompt ( ) ,
7287 // Runs on CLOSE_MODAL: drops the cancel-confirmation modal so it cannot linger over the
7388 // closing navigator (CLOSE_MODAL can fire without the flow completing, e.g. an offline cancel).
7489 hideCancelConfirmModal : assign ( { isCancelConfirmVisible : false } ) ,
@@ -119,11 +134,16 @@ const MFAMachine = setup({
119134 }
120135 return { allowedAuthenticationMethods : context . scenario . allowedAuthenticationMethods } ;
121136 } ,
122- // Every result enters the outcome resolver. A refusal stores its
123- // blocking MFAError first, so the resolver selects failure from that
124- // error, while an eligible device with no error selects success.
137+ // An eligible device moves on to the soft prompt, with two overrides.
138+ // A stored error always wins: the flow goes straight to the outcome
139+ // resolver, which selects failure. A user who already accepted the
140+ // soft prompt on this device (read from Onyx at decision time) skips
141+ // it. A refusal stores its blocking MFAError first and resolves the
142+ // same way as a stored error.
125143 onDone : [
126- { guard : ( { event} ) => event . output . success , target : OUTCOME_TARGET } ,
144+ { guard : ( { context, event} ) => event . output . success && context . error !== undefined , target : OUTCOME_TARGET } ,
145+ { guard : ( { event} ) => event . output . success && hasEverAcceptedSoftPrompt ( ) , target : OUTCOME_TARGET } ,
146+ { guard : ( { event} ) => event . output . success , target : PROMPT_TARGET } ,
127147 { target : OUTCOME_TARGET , actions : assign ( { error : ( { event} ) => getMFAFailureError ( event . output ) } ) } ,
128148 ] ,
129149 // Expected refusals travel as failed results through onDone, so a
@@ -136,6 +156,24 @@ const MFAMachine = setup({
136156 } ,
137157 } ,
138158 } ,
159+ // The soft-prompt screen: asks the user to consent to biometric verification before
160+ // the flow continues. Entered only when the user has never accepted the prompt on
161+ // this device; the registration vs reinstall variants arrive with the registration
162+ // decision.
163+ [ MFA_STATE . PROMPT ] : {
164+ id : MFA_STATE . PROMPT ,
165+ entry : [ 'navigateToPrompt' ] ,
166+ initial : MFA_STATE . AWAITING_SOFT_PROMPT ,
167+ on : {
168+ SOFT_PROMPT_APPROVED : {
169+ target : MFA_STATE . OUTCOME ,
170+ actions : [ 'approveSoftPrompt' , 'persistSoftPromptAcceptance' ] ,
171+ } ,
172+ } ,
173+ states : {
174+ [ MFA_STATE . AWAITING_SOFT_PROMPT ] : { } ,
175+ } ,
176+ } ,
139177 [ MFA_STATE . OUTCOME ] : {
140178 id : MFA_STATE . OUTCOME ,
141179 initial : MFA_STATE . RESOLVING_OUTCOME ,
0 commit comments