@@ -27,20 +27,6 @@ const RNEventEmitter = new NativeEventEmitter(RNIterableAPI);
2727
2828const defaultConfig = new IterableConfig ( ) ;
2929
30- /**
31- * Fallback safety-net timeout for the auth callback latch when no native
32- * auth success/failure event arrives. Overridable via
33- * {@link IterableConfig.authCallbackTimeoutMs}.
34- */
35- const AUTH_CALLBACK_TIMEOUT_DEFAULT_MS = 1000 ;
36-
37- /**
38- * Default delay (ms) the SDK waits on Android before invoking the URL handler
39- * so the host Activity can wake from the background. Overridable via
40- * {@link IterableConfig.androidWakeDelayMs}.
41- */
42- const ANDROID_WAKE_DELAY_DEFAULT_MS = 1000 ;
43-
4430/**
4531 * Checks if the response is an IterableAuthResponse
4632 */
@@ -1027,9 +1013,7 @@ export class Iterable {
10271013 // dispatching the URL to the handler. Without this delay the
10281014 // handler can race the activity lifecycle and drop the link on
10291015 // cold start. Tunable via IterableConfig.androidWakeDelayMs.
1030- const wakeDelayMs =
1031- Iterable . savedConfig . androidWakeDelayMs ??
1032- ANDROID_WAKE_DELAY_DEFAULT_MS ;
1016+ const wakeDelayMs = Iterable . savedConfig . androidWakeDelayMs ;
10331017 if ( wakeDelayMs > 0 ) {
10341018 setTimeout ( ( ) => {
10351019 callUrlHandler ( Iterable . savedConfig , url , context ) ;
@@ -1074,26 +1058,42 @@ export class Iterable {
10741058 | IterableAuthResponseResult
10751059 | typeof AUTH_RESULT_NO_CALLBACK ;
10761060
1077- // Event-driven auth latch. The native success/failure listeners
1078- // resolve the latch when their event arrives ; the safety-net timer
1079- // resolves it only if no native event arrives within the configured
1080- // window. The timer is a fallback — the latch resolves immediately
1081- // when the native event fires.
1061+ // Per-invocation auth latch state . The native success/failure events
1062+ // resolve the latch when they arrive ; the safety-net timer resolves it
1063+ // only if no native event arrives within the configured window. The
1064+ // timer is a fallback — the latch resolves immediately when the native
1065+ // event fires.
10821066 //
1083- // `pendingAuthResult ` buffers a native result that arrives before the
1067+ // `pendingResult ` buffers a native result that arrives before the
10841068 // latch is created (e.g. the authHandler promise hasn't settled yet).
10851069 // It is consumed when the latch is created, so out-of-order events
10861070 // are not lost.
1087- let authLatchResolver :
1088- | ( ( result : IterableAuthResponseResult ) => void )
1089- | null = null ;
1090- let pendingAuthResult : IterableAuthResponseResult | null = null ;
1071+ //
1072+ // State is scoped per `handleAuthCalled` invocation so overlapping
1073+ // invocations cannot wipe each other's resolver or buffered result
1074+ // (the bug with the original shared `authLatchResolver` /
1075+ // `pendingAuthResult` declarations). The bridge events carry no
1076+ // correlation id, so native success/failure events are routed to the
1077+ // oldest still-pending invocation in FIFO order — matching the
1078+ // original single-flight assumption that native processes auth
1079+ // requests in the order the SDK issues them.
1080+ type AuthInvocation = {
1081+ resolver :
1082+ | ( ( result : IterableAuthResponseResult ) => void )
1083+ | null ;
1084+ pendingResult : IterableAuthResponseResult | null ;
1085+ } ;
1086+ const pendingAuthInvocations : AuthInvocation [ ] = [ ] ;
10911087
10921088 RNEventEmitter . addListener ( IterableEventName . handleAuthCalled , ( ) => {
1093- // Reset per-invocation state so a stale buffered result from a
1094- // previous invocation cannot bleed into this one.
1095- authLatchResolver = null ;
1096- pendingAuthResult = null ;
1089+ // Start a fresh per-invocation state object so a stale buffered
1090+ // result or resolver from a previous invocation cannot bleed into
1091+ // this one.
1092+ const invocation : AuthInvocation = {
1093+ resolver : null ,
1094+ pendingResult : null ,
1095+ } ;
1096+ pendingAuthInvocations . push ( invocation ) ;
10971097
10981098 // MOB-10423: Check if we can use chain operator (?.) here instead
10991099 // Asks frontend of the client/app to pass authToken
@@ -1109,23 +1109,27 @@ export class Iterable {
11091109 if ( isIterableAuthResponse ( promiseResult ) ) {
11101110 Iterable . authManager . passAlongAuthToken ( promiseResult . authToken ) ;
11111111
1112+ // If this invocation was already removed (e.g. a buffered
1113+ // native result already resolved it via another path), do not
1114+ // wire a latch for it.
1115+ if ( ! pendingAuthInvocations . includes ( invocation ) ) {
1116+ return ;
1117+ }
1118+
11121119 const nativeLatch = new Promise < IterableAuthResponseResult > (
11131120 ( resolve ) => {
1114- if ( pendingAuthResult !== null ) {
1121+ if ( invocation . pendingResult !== null ) {
11151122 // A native event arrived before the latch was created;
11161123 // resolve immediately with the buffered result.
1117- const buffered = pendingAuthResult ;
1118- pendingAuthResult = null ;
1119- resolve ( buffered ) ;
1124+ resolve ( invocation . pendingResult ) ;
1125+ invocation . pendingResult = null ;
11201126 } else {
1121- authLatchResolver = resolve ;
1127+ invocation . resolver = resolve ;
11221128 }
11231129 }
11241130 ) ;
11251131
1126- const timeoutMs =
1127- Iterable . savedConfig . authCallbackTimeoutMs ??
1128- AUTH_CALLBACK_TIMEOUT_DEFAULT_MS ;
1132+ const timeoutMs = Iterable . savedConfig . authCallbackTimeoutMs ;
11291133 const timeoutLatch = new Promise < AuthLatchResult > ( ( resolve ) => {
11301134 setTimeout (
11311135 ( ) => resolve ( AUTH_RESULT_NO_CALLBACK ) ,
@@ -1135,10 +1139,19 @@ export class Iterable {
11351139
11361140 Promise . race < AuthLatchResult > ( [ nativeLatch , timeoutLatch ] ) . then (
11371141 ( result ) => {
1138- // Clear the resolver so a late native event after the timeout
1139- // is a no-op.
1140- authLatchResolver = null ;
1141- pendingAuthResult = null ;
1142+ // Clear this invocation's resolver so a late native event
1143+ // after the timeout is a no-op. Only touch this
1144+ // invocation's state — other invocations own their own.
1145+ invocation . resolver = null ;
1146+ invocation . pendingResult = null ;
1147+ // Defensive cleanup: if the native event did not remove
1148+ // this invocation from the queue (e.g. the safety-net
1149+ // timer won the race), remove it now so future native
1150+ // events route to the next pending invocation.
1151+ const index = pendingAuthInvocations . indexOf ( invocation ) ;
1152+ if ( index !== - 1 ) {
1153+ pendingAuthInvocations . splice ( index , 1 ) ;
1154+ }
11421155 if ( result === IterableAuthResponseResult . SUCCESS ) {
11431156 promiseResult . successCallback ?.( ) ;
11441157 } else if ( result === IterableAuthResponseResult . FAILURE ) {
@@ -1170,32 +1183,46 @@ export class Iterable {
11701183 RNEventEmitter . addListener (
11711184 IterableEventName . handleAuthSuccessCalled ,
11721185 ( ) => {
1186+ // Route to the oldest still-pending invocation (FIFO). The bridge
1187+ // events carry no correlation id; this matches the assumption
1188+ // that native processes auth requests in issuance order.
1189+ const invocation = pendingAuthInvocations [ 0 ] ;
1190+ if ( ! invocation ) {
1191+ return ;
1192+ }
11731193 // Resolve the pending auth latch immediately; the timer becomes a
11741194 // no-op for this invocation.
1175- if ( authLatchResolver ) {
1176- const resolve = authLatchResolver ;
1177- authLatchResolver = null ;
1178- pendingAuthResult = null ;
1195+ if ( invocation . resolver ) {
1196+ const resolve = invocation . resolver ;
1197+ invocation . resolver = null ;
1198+ invocation . pendingResult = null ;
1199+ // Remove synchronously so the next native event routes to the
1200+ // following pending invocation, not back to this one.
1201+ pendingAuthInvocations . shift ( ) ;
11791202 resolve ( IterableAuthResponseResult . SUCCESS ) ;
11801203 } else {
11811204 // Latch not created yet — buffer the result for the latch.
1182- pendingAuthResult = IterableAuthResponseResult . SUCCESS ;
1205+ invocation . pendingResult = IterableAuthResponseResult . SUCCESS ;
11831206 }
11841207 }
11851208 ) ;
11861209 RNEventEmitter . addListener (
11871210 IterableEventName . handleAuthFailureCalled ,
11881211 ( authFailureResponse : IterableAuthFailure ) => {
1189- // Resolve the pending auth latch immediately; the timer becomes a
1190- // no-op for this invocation.
1191- if ( authLatchResolver ) {
1192- const resolve = authLatchResolver ;
1193- authLatchResolver = null ;
1194- pendingAuthResult = null ;
1195- resolve ( IterableAuthResponseResult . FAILURE ) ;
1196- } else {
1197- // Latch not created yet — buffer the result for the latch.
1198- pendingAuthResult = IterableAuthResponseResult . FAILURE ;
1212+ const invocation = pendingAuthInvocations [ 0 ] ;
1213+ if ( invocation ) {
1214+ // Resolve the pending auth latch immediately; the timer becomes a
1215+ // no-op for this invocation.
1216+ if ( invocation . resolver ) {
1217+ const resolve = invocation . resolver ;
1218+ invocation . resolver = null ;
1219+ invocation . pendingResult = null ;
1220+ pendingAuthInvocations . shift ( ) ;
1221+ resolve ( IterableAuthResponseResult . FAILURE ) ;
1222+ } else {
1223+ // Latch not created yet — buffer the result for the latch.
1224+ invocation . pendingResult = IterableAuthResponseResult . FAILURE ;
1225+ }
11991226 }
12001227
12011228 // Call the actual JWT error with `authFailure` object.
0 commit comments