@@ -6,6 +6,7 @@ import { error } from '@tauri-apps/plugin-log';
66import { useCallback , useEffect , useMemo , useRef , useState } from 'react' ;
77import { api } from '../../../rust-api/api' ;
88import {
9+ isCancelled ,
910 isConnectFailure ,
1011 isMfaPostureError ,
1112 isServiceUnavailable ,
@@ -14,6 +15,7 @@ import {
1415import { getInstancesQueryOptions } from '../../../rust-api/query' ;
1516import type { LocationInfo , MfaErrorPayload } from '../../../rust-api/types' ;
1617import { MfaMethod , TauriEvent } from '../../../rust-api/types' ;
18+ import { isPresent } from '../../../utils/isPresent' ;
1719
1820type TokenData = {
1921 token : string ;
@@ -27,10 +29,18 @@ type Options = {
2729} ;
2830
2931export const useMfaMobileConnect = ( location : LocationInfo , options ?: Options ) => {
30- const { onConnected, onPostureError, onServiceUnavailable } = options ?? { } ;
31-
3232 const { data : instances } = useQuery ( getInstancesQueryOptions ) ;
3333 const instance = instances ?. find ( ( i ) => i . id === location . instance_id ) ;
34+ const instanceId = instance ?. id ;
35+ const instanceUuid = instance ?. uuid ;
36+ const locationId = location . id ;
37+
38+ // Read through refs so callers passing inline callbacks or a re-fetched
39+ // `location` object don't restart the mobile-approve task on every render.
40+ const optionsRef = useRef ( options ) ;
41+ optionsRef . current = options ;
42+ const locationRef = useRef ( location ) ;
43+ locationRef . current = location ;
3444
3545 const [ isStarting , setIsStarting ] = useState ( false ) ;
3646 const [ startError , setStartError ] = useState < string | null > ( null ) ;
@@ -61,7 +71,7 @@ export const useMfaMobileConnect = (location: LocationInfo, options?: Options) =
6171
6272 // Connect WebSocket via Rust when tokenData is available
6373 useEffect ( ( ) => {
64- if ( ! tokenData || ! instance ) return ;
74+ if ( ! tokenData || ! isPresent ( instanceId ) ) return ;
6575
6676 let cancelled = false ;
6777 cleanupListeners ( ) ;
@@ -71,8 +81,8 @@ export const useMfaMobileConnect = (location: LocationInfo, options?: Options) =
7181 ( async ( ) => {
7282 try {
7383 const taskId = await api . mfaConnectMobileApprove (
74- instance . id ,
75- location . id ,
84+ instanceId ,
85+ locationId ,
7686 tokenData . token ,
7787 ) ;
7888 if ( cancelled ) {
@@ -84,18 +94,20 @@ export const useMfaMobileConnect = (location: LocationInfo, options?: Options) =
8494 // The backend brings up the connection itself; completion means connected.
8595 const completeUnlisten = await listen ( TauriEvent . MfaMobileComplete , ( ) => {
8696 cleanupListeners ( ) ;
97+ taskIdRef . current = null ;
8798 setIsConnecting ( false ) ;
88- onConnected ?.( ) ;
99+ optionsRef . current ?. onConnected ?.( ) ;
89100 } ) ;
90101
91102 const errorUnlisten = await listen < MfaErrorPayload > (
92103 TauriEvent . MfaMobileError ,
93104 ( event ) => {
105+ // Emitted app-wide without a task id, so our own cancels land here.
106+ if ( isCancelled ( event . payload . error ) ) return ;
94107 cleanupListeners ( ) ;
108+ taskIdRef . current = null ;
95109 setIsConnecting ( false ) ;
96- error (
97- `Mobile MFA failed for location ${ location . id } : ${ event . payload . error } ` ,
98- ) ;
110+ error ( `Mobile MFA failed for location ${ locationId } : ${ event . payload . error } ` ) ;
99111 const message = mfaErrorMessage ( event . payload . error ) ;
100112 setConnectionError (
101113 isConnectFailure ( message )
@@ -113,30 +125,35 @@ export const useMfaMobileConnect = (location: LocationInfo, options?: Options) =
113125 if ( ! cancelled ) {
114126 setIsConnecting ( false ) ;
115127 setConnectionError ( 'Failed to start mobile approval. Please try again.' ) ;
116- error ( `Mobile MFA connect failed for location ${ location . id } : ${ e } ` ) ;
128+ error ( `Mobile MFA connect failed for location ${ locationId } : ${ e } ` ) ;
117129 }
118130 }
119131 } ) ( ) ;
120132
121133 return ( ) => {
122134 cancelled = true ;
123135 cleanupListeners ( ) ;
136+ const taskId = taskIdRef . current ;
137+ if ( taskId ) {
138+ taskIdRef . current = null ;
139+ void api . cancelMfa ( taskId ) . catch ( ( ) => { } ) ;
140+ }
124141 setIsConnecting ( false ) ;
125142 } ;
126- } , [ tokenData , instance , location , onConnected , cleanupListeners ] ) ;
143+ } , [ tokenData , instanceId , locationId , cleanupListeners ] ) ;
127144
128145 const qrValue = useMemo ( ( ) => {
129- if ( ! tokenData || ! instance ) return null ;
146+ if ( ! tokenData || ! isPresent ( instanceUuid ) ) return null ;
130147 const json = JSON . stringify ( {
131148 token : tokenData . token ,
132149 challenge : tokenData . challenge ,
133- instance_id : instance . uuid ,
150+ instance_id : instanceUuid ,
134151 } ) ;
135152 return encode ( new TextEncoder ( ) . encode ( json ) ) ;
136- } , [ tokenData , instance ] ) ;
153+ } , [ tokenData , instanceUuid ] ) ;
137154
138155 const start = useCallback ( async ( ) => {
139- if ( ! instance ) {
156+ if ( ! isPresent ( instanceId ) ) {
140157 setStartError ( 'Instance not found' ) ;
141158 return ;
142159 }
@@ -148,28 +165,28 @@ export const useMfaMobileConnect = (location: LocationInfo, options?: Options) =
148165 setTokenData ( null ) ;
149166
150167 try {
151- const info = await api . mfaStart ( instance . id , location . id , MfaMethod . MobileApprove ) ;
168+ const info = await api . mfaStart ( instanceId , locationId , MfaMethod . MobileApprove ) ;
152169 if ( ! info . challenge ) {
153170 setStartError ( 'Unsupported response from proxy' ) ;
154171 return ;
155172 }
156173
157174 setTokenData ( { token : info . token , challenge : info . challenge } ) ;
158175 } catch ( e ) {
159- void error ( `Mobile MFA start failed for location ${ location . id } : ${ e } ` ) ;
160- if ( isMfaPostureError ( e , location ) ) {
161- onPostureError ?.( mfaErrorMessage ( e ) ) ;
176+ void error ( `Mobile MFA start failed for location ${ locationId } : ${ e } ` ) ;
177+ if ( isMfaPostureError ( e , locationRef . current ) ) {
178+ optionsRef . current ?. onPostureError ?.( mfaErrorMessage ( e ) ) ;
162179 return ;
163180 }
164181 if ( isServiceUnavailable ( e ) ) {
165- onServiceUnavailable ?.( ) ;
182+ optionsRef . current ?. onServiceUnavailable ?.( ) ;
166183 return ;
167184 }
168185 setStartError ( mfaErrorMessage ( e ) ) ;
169186 } finally {
170187 setIsStarting ( false ) ;
171188 }
172- } , [ instance , location , onPostureError , onServiceUnavailable ] ) ;
189+ } , [ instanceId , locationId ] ) ;
173190
174191 const reset = useCallback ( ( ) => {
175192 cleanupListeners ( ) ;
0 commit comments