@dynamic-labs/react-native-extension 4.91.4 → 4.91.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -66,6 +66,12 @@ object EmbeddedWebViewController {
66
66
  // prompting JS again — avoiding a potential approval loop.
67
67
  private var bypassUrl: String? = null
68
68
 
69
+ // Origin pre-approved by `setUrl`. When `shouldOverrideUrlLoading` sees a
70
+ // top-frame navigation whose origin matches, it allows it immediately — no
71
+ // JS round-trip required. Cleared after first use so subsequent navigations
72
+ // still go through the JS allowlist.
73
+ private var preApprovedOrigin: String? = null
74
+
69
75
  private var emitterToken: java.util.UUID? = null
70
76
 
71
77
  // Module-side hook: assign the emitter under a token. The token lets
@@ -108,6 +114,11 @@ object EmbeddedWebViewController {
108
114
  return@runOnMain
109
115
  }
110
116
  val webView = ensureWebView() ?: return@runOnMain
117
+ // Pre-approve the origin so the first `shouldOverrideUrlLoading` call
118
+ // can skip the JS round-trip. On cold boot the JS thread may be too
119
+ // congested to respond within the navigation-decision timeout, silently
120
+ // cancelling the load.
121
+ preApprovedOrigin = originString(android.net.Uri.parse(url))
111
122
  webView.loadUrl(url)
112
123
  }
113
124
  }
@@ -280,6 +291,7 @@ object EmbeddedWebViewController {
280
291
  pendingTimeouts.values.forEach { mainHandler.removeCallbacks(it) }
281
292
  pendingTimeouts.clear()
282
293
  bypassUrl = null
294
+ preApprovedOrigin = null
283
295
  }
284
296
 
285
297
  private fun runOnMain(action: () -> Unit) {
@@ -332,6 +344,7 @@ object EmbeddedWebViewController {
332
344
  val isAllowedTopFrameScheme =
333
345
  scheme == "https" || (httpAllowed && scheme == "http")
334
346
  if (!isAllowedTopFrameScheme) {
347
+ preApprovedOrigin = null
335
348
  emitLoadError(
336
349
  url = url,
337
350
  code = -1,
@@ -342,6 +355,17 @@ object EmbeddedWebViewController {
342
355
  return true
343
356
  }
344
357
 
358
+ // Fast-path: if the URL's origin matches the pre-approved origin (set
359
+ // by `setUrl`), allow immediately. This eliminates the cold-boot race
360
+ // where the JS thread is too congested to respond to
361
+ // `onShouldStartLoad` within the navigation-decision timeout.
362
+ val approved = preApprovedOrigin
363
+ if (approved != null && approved == originString(request.url)) {
364
+ preApprovedOrigin = null
365
+ bypassUrl = url
366
+ return false
367
+ }
368
+
345
369
  val id = UUID.randomUUID().toString()
346
370
  pendingNavigationUrls[id] = url
347
371
 
@@ -486,6 +510,22 @@ object EmbeddedWebViewController {
486
510
  }
487
511
  }
488
512
 
513
+ // Extract the scheme + host + port origin from a URI (e.g.
514
+ // "https://webview.dynamicauth.com"). Returns null for URIs without a host.
515
+ // Default ports (443 for HTTPS, 80 for HTTP) are omitted so that
516
+ // "https://example.com" and "https://example.com:443" produce the same
517
+ // origin string.
518
+ private fun originString(uri: android.net.Uri?): String? {
519
+ val scheme = uri?.scheme ?: return null
520
+ val host = uri.host ?: return null
521
+ val port = uri.port
522
+
523
+ val shouldIncludePort = port != -1 &&
524
+ !((scheme == "https" && port == 443) || (scheme == "http" && port == 80))
525
+
526
+ return if (shouldIncludePort) "$scheme://$host:$port" else "$scheme://$host"
527
+ }
528
+
489
529
  // Refuse target=_blank / window.open popups (matches react-native-webview's
490
530
  // `setSupportMultipleWindows={false}` semantics: there's nowhere reasonable
491
531
  // to open the new window inside the overlay).
@@ -66,6 +66,12 @@ object EmbeddedWebViewController {
66
66
  // prompting JS again — avoiding a potential approval loop.
67
67
  private var bypassUrl: String? = null
68
68
 
69
+ // Origin pre-approved by `setUrl`. When `shouldOverrideUrlLoading` sees a
70
+ // top-frame navigation whose origin matches, it allows it immediately — no
71
+ // JS round-trip required. Cleared after first use so subsequent navigations
72
+ // still go through the JS allowlist.
73
+ private var preApprovedOrigin: String? = null
74
+
69
75
  private var emitterToken: java.util.UUID? = null
70
76
 
71
77
  // Module-side hook: assign the emitter under a token. The token lets
@@ -108,6 +114,11 @@ object EmbeddedWebViewController {
108
114
  return@runOnMain
109
115
  }
110
116
  val webView = ensureWebView() ?: return@runOnMain
117
+ // Pre-approve the origin so the first `shouldOverrideUrlLoading` call
118
+ // can skip the JS round-trip. On cold boot the JS thread may be too
119
+ // congested to respond within the navigation-decision timeout, silently
120
+ // cancelling the load.
121
+ preApprovedOrigin = originString(android.net.Uri.parse(url))
111
122
  webView.loadUrl(url)
112
123
  }
113
124
  }
@@ -280,6 +291,7 @@ object EmbeddedWebViewController {
280
291
  pendingTimeouts.values.forEach { mainHandler.removeCallbacks(it) }
281
292
  pendingTimeouts.clear()
282
293
  bypassUrl = null
294
+ preApprovedOrigin = null
283
295
  }
284
296
 
285
297
  private fun runOnMain(action: () -> Unit) {
@@ -332,6 +344,7 @@ object EmbeddedWebViewController {
332
344
  val isAllowedTopFrameScheme =
333
345
  scheme == "https" || (httpAllowed && scheme == "http")
334
346
  if (!isAllowedTopFrameScheme) {
347
+ preApprovedOrigin = null
335
348
  emitLoadError(
336
349
  url = url,
337
350
  code = -1,
@@ -342,6 +355,17 @@ object EmbeddedWebViewController {
342
355
  return true
343
356
  }
344
357
 
358
+ // Fast-path: if the URL's origin matches the pre-approved origin (set
359
+ // by `setUrl`), allow immediately. This eliminates the cold-boot race
360
+ // where the JS thread is too congested to respond to
361
+ // `onShouldStartLoad` within the navigation-decision timeout.
362
+ val approved = preApprovedOrigin
363
+ if (approved != null && approved == originString(request.url)) {
364
+ preApprovedOrigin = null
365
+ bypassUrl = url
366
+ return false
367
+ }
368
+
345
369
  val id = UUID.randomUUID().toString()
346
370
  pendingNavigationUrls[id] = url
347
371
 
@@ -486,6 +510,22 @@ object EmbeddedWebViewController {
486
510
  }
487
511
  }
488
512
 
513
+ // Extract the scheme + host + port origin from a URI (e.g.
514
+ // "https://webview.dynamicauth.com"). Returns null for URIs without a host.
515
+ // Default ports (443 for HTTPS, 80 for HTTP) are omitted so that
516
+ // "https://example.com" and "https://example.com:443" produce the same
517
+ // origin string.
518
+ private fun originString(uri: android.net.Uri?): String? {
519
+ val scheme = uri?.scheme ?: return null
520
+ val host = uri.host ?: return null
521
+ val port = uri.port
522
+
523
+ val shouldIncludePort = port != -1 &&
524
+ !((scheme == "https" && port == 443) || (scheme == "http" && port == 80))
525
+
526
+ return if (shouldIncludePort) "$scheme://$host:$port" else "$scheme://$host"
527
+ }
528
+
489
529
  // Refuse target=_blank / window.open popups (matches react-native-webview's
490
530
  // `setSupportMultipleWindows={false}` semantics: there's nowhere reasonable
491
531
  // to open the new window inside the overlay).
@@ -66,6 +66,12 @@ object EmbeddedWebViewController {
66
66
  // prompting JS again — avoiding a potential approval loop.
67
67
  private var bypassUrl: String? = null
68
68
 
69
+ // Origin pre-approved by `setUrl`. When `shouldOverrideUrlLoading` sees a
70
+ // top-frame navigation whose origin matches, it allows it immediately — no
71
+ // JS round-trip required. Cleared after first use so subsequent navigations
72
+ // still go through the JS allowlist.
73
+ private var preApprovedOrigin: String? = null
74
+
69
75
  private var emitterToken: java.util.UUID? = null
70
76
 
71
77
  // Module-side hook: assign the emitter under a token. The token lets
@@ -108,6 +114,11 @@ object EmbeddedWebViewController {
108
114
  return@runOnMain
109
115
  }
110
116
  val webView = ensureWebView() ?: return@runOnMain
117
+ // Pre-approve the origin so the first `shouldOverrideUrlLoading` call
118
+ // can skip the JS round-trip. On cold boot the JS thread may be too
119
+ // congested to respond within the navigation-decision timeout, silently
120
+ // cancelling the load.
121
+ preApprovedOrigin = originString(android.net.Uri.parse(url))
111
122
  webView.loadUrl(url)
112
123
  }
113
124
  }
@@ -280,6 +291,7 @@ object EmbeddedWebViewController {
280
291
  pendingTimeouts.values.forEach { mainHandler.removeCallbacks(it) }
281
292
  pendingTimeouts.clear()
282
293
  bypassUrl = null
294
+ preApprovedOrigin = null
283
295
  }
284
296
 
285
297
  private fun runOnMain(action: () -> Unit) {
@@ -332,6 +344,7 @@ object EmbeddedWebViewController {
332
344
  val isAllowedTopFrameScheme =
333
345
  scheme == "https" || (httpAllowed && scheme == "http")
334
346
  if (!isAllowedTopFrameScheme) {
347
+ preApprovedOrigin = null
335
348
  emitLoadError(
336
349
  url = url,
337
350
  code = -1,
@@ -342,6 +355,17 @@ object EmbeddedWebViewController {
342
355
  return true
343
356
  }
344
357
 
358
+ // Fast-path: if the URL's origin matches the pre-approved origin (set
359
+ // by `setUrl`), allow immediately. This eliminates the cold-boot race
360
+ // where the JS thread is too congested to respond to
361
+ // `onShouldStartLoad` within the navigation-decision timeout.
362
+ val approved = preApprovedOrigin
363
+ if (approved != null && approved == originString(request.url)) {
364
+ preApprovedOrigin = null
365
+ bypassUrl = url
366
+ return false
367
+ }
368
+
345
369
  val id = UUID.randomUUID().toString()
346
370
  pendingNavigationUrls[id] = url
347
371
 
@@ -486,6 +510,22 @@ object EmbeddedWebViewController {
486
510
  }
487
511
  }
488
512
 
513
+ // Extract the scheme + host + port origin from a URI (e.g.
514
+ // "https://webview.dynamicauth.com"). Returns null for URIs without a host.
515
+ // Default ports (443 for HTTPS, 80 for HTTP) are omitted so that
516
+ // "https://example.com" and "https://example.com:443" produce the same
517
+ // origin string.
518
+ private fun originString(uri: android.net.Uri?): String? {
519
+ val scheme = uri?.scheme ?: return null
520
+ val host = uri.host ?: return null
521
+ val port = uri.port
522
+
523
+ val shouldIncludePort = port != -1 &&
524
+ !((scheme == "https" && port == 443) || (scheme == "http" && port == 80))
525
+
526
+ return if (shouldIncludePort) "$scheme://$host:$port" else "$scheme://$host"
527
+ }
528
+
489
529
  // Refuse target=_blank / window.open popups (matches react-native-webview's
490
530
  // `setSupportMultipleWindows={false}` semantics: there's nowhere reasonable
491
531
  // to open the new window inside the overlay).
@@ -66,6 +66,12 @@ object EmbeddedWebViewController {
66
66
  // prompting JS again — avoiding a potential approval loop.
67
67
  private var bypassUrl: String? = null
68
68
 
69
+ // Origin pre-approved by `setUrl`. When `shouldOverrideUrlLoading` sees a
70
+ // top-frame navigation whose origin matches, it allows it immediately — no
71
+ // JS round-trip required. Cleared after first use so subsequent navigations
72
+ // still go through the JS allowlist.
73
+ private var preApprovedOrigin: String? = null
74
+
69
75
  private var emitterToken: java.util.UUID? = null
70
76
 
71
77
  // Module-side hook: assign the emitter under a token. The token lets
@@ -108,6 +114,11 @@ object EmbeddedWebViewController {
108
114
  return@runOnMain
109
115
  }
110
116
  val webView = ensureWebView() ?: return@runOnMain
117
+ // Pre-approve the origin so the first `shouldOverrideUrlLoading` call
118
+ // can skip the JS round-trip. On cold boot the JS thread may be too
119
+ // congested to respond within the navigation-decision timeout, silently
120
+ // cancelling the load.
121
+ preApprovedOrigin = originString(android.net.Uri.parse(url))
111
122
  webView.loadUrl(url)
112
123
  }
113
124
  }
@@ -280,6 +291,7 @@ object EmbeddedWebViewController {
280
291
  pendingTimeouts.values.forEach { mainHandler.removeCallbacks(it) }
281
292
  pendingTimeouts.clear()
282
293
  bypassUrl = null
294
+ preApprovedOrigin = null
283
295
  }
284
296
 
285
297
  private fun runOnMain(action: () -> Unit) {
@@ -332,6 +344,7 @@ object EmbeddedWebViewController {
332
344
  val isAllowedTopFrameScheme =
333
345
  scheme == "https" || (httpAllowed && scheme == "http")
334
346
  if (!isAllowedTopFrameScheme) {
347
+ preApprovedOrigin = null
335
348
  emitLoadError(
336
349
  url = url,
337
350
  code = -1,
@@ -342,6 +355,17 @@ object EmbeddedWebViewController {
342
355
  return true
343
356
  }
344
357
 
358
+ // Fast-path: if the URL's origin matches the pre-approved origin (set
359
+ // by `setUrl`), allow immediately. This eliminates the cold-boot race
360
+ // where the JS thread is too congested to respond to
361
+ // `onShouldStartLoad` within the navigation-decision timeout.
362
+ val approved = preApprovedOrigin
363
+ if (approved != null && approved == originString(request.url)) {
364
+ preApprovedOrigin = null
365
+ bypassUrl = url
366
+ return false
367
+ }
368
+
345
369
  val id = UUID.randomUUID().toString()
346
370
  pendingNavigationUrls[id] = url
347
371
 
@@ -486,6 +510,22 @@ object EmbeddedWebViewController {
486
510
  }
487
511
  }
488
512
 
513
+ // Extract the scheme + host + port origin from a URI (e.g.
514
+ // "https://webview.dynamicauth.com"). Returns null for URIs without a host.
515
+ // Default ports (443 for HTTPS, 80 for HTTP) are omitted so that
516
+ // "https://example.com" and "https://example.com:443" produce the same
517
+ // origin string.
518
+ private fun originString(uri: android.net.Uri?): String? {
519
+ val scheme = uri?.scheme ?: return null
520
+ val host = uri.host ?: return null
521
+ val port = uri.port
522
+
523
+ val shouldIncludePort = port != -1 &&
524
+ !((scheme == "https" && port == 443) || (scheme == "http" && port == 80))
525
+
526
+ return if (shouldIncludePort) "$scheme://$host:$port" else "$scheme://$host"
527
+ }
528
+
489
529
  // Refuse target=_blank / window.open popups (matches react-native-webview's
490
530
  // `setSupportMultipleWindows={false}` semantics: there's nowhere reasonable
491
531
  // to open the new window inside the overlay).
@@ -66,6 +66,12 @@ object EmbeddedWebViewController {
66
66
  // prompting JS again — avoiding a potential approval loop.
67
67
  private var bypassUrl: String? = null
68
68
 
69
+ // Origin pre-approved by `setUrl`. When `shouldOverrideUrlLoading` sees a
70
+ // top-frame navigation whose origin matches, it allows it immediately — no
71
+ // JS round-trip required. Cleared after first use so subsequent navigations
72
+ // still go through the JS allowlist.
73
+ private var preApprovedOrigin: String? = null
74
+
69
75
  private var emitterToken: java.util.UUID? = null
70
76
 
71
77
  // Module-side hook: assign the emitter under a token. The token lets
@@ -108,6 +114,11 @@ object EmbeddedWebViewController {
108
114
  return@runOnMain
109
115
  }
110
116
  val webView = ensureWebView() ?: return@runOnMain
117
+ // Pre-approve the origin so the first `shouldOverrideUrlLoading` call
118
+ // can skip the JS round-trip. On cold boot the JS thread may be too
119
+ // congested to respond within the navigation-decision timeout, silently
120
+ // cancelling the load.
121
+ preApprovedOrigin = originString(android.net.Uri.parse(url))
111
122
  webView.loadUrl(url)
112
123
  }
113
124
  }
@@ -280,6 +291,7 @@ object EmbeddedWebViewController {
280
291
  pendingTimeouts.values.forEach { mainHandler.removeCallbacks(it) }
281
292
  pendingTimeouts.clear()
282
293
  bypassUrl = null
294
+ preApprovedOrigin = null
283
295
  }
284
296
 
285
297
  private fun runOnMain(action: () -> Unit) {
@@ -332,6 +344,7 @@ object EmbeddedWebViewController {
332
344
  val isAllowedTopFrameScheme =
333
345
  scheme == "https" || (httpAllowed && scheme == "http")
334
346
  if (!isAllowedTopFrameScheme) {
347
+ preApprovedOrigin = null
335
348
  emitLoadError(
336
349
  url = url,
337
350
  code = -1,
@@ -342,6 +355,17 @@ object EmbeddedWebViewController {
342
355
  return true
343
356
  }
344
357
 
358
+ // Fast-path: if the URL's origin matches the pre-approved origin (set
359
+ // by `setUrl`), allow immediately. This eliminates the cold-boot race
360
+ // where the JS thread is too congested to respond to
361
+ // `onShouldStartLoad` within the navigation-decision timeout.
362
+ val approved = preApprovedOrigin
363
+ if (approved != null && approved == originString(request.url)) {
364
+ preApprovedOrigin = null
365
+ bypassUrl = url
366
+ return false
367
+ }
368
+
345
369
  val id = UUID.randomUUID().toString()
346
370
  pendingNavigationUrls[id] = url
347
371
 
@@ -486,6 +510,22 @@ object EmbeddedWebViewController {
486
510
  }
487
511
  }
488
512
 
513
+ // Extract the scheme + host + port origin from a URI (e.g.
514
+ // "https://webview.dynamicauth.com"). Returns null for URIs without a host.
515
+ // Default ports (443 for HTTPS, 80 for HTTP) are omitted so that
516
+ // "https://example.com" and "https://example.com:443" produce the same
517
+ // origin string.
518
+ private fun originString(uri: android.net.Uri?): String? {
519
+ val scheme = uri?.scheme ?: return null
520
+ val host = uri.host ?: return null
521
+ val port = uri.port
522
+
523
+ val shouldIncludePort = port != -1 &&
524
+ !((scheme == "https" && port == 443) || (scheme == "http" && port == 80))
525
+
526
+ return if (shouldIncludePort) "$scheme://$host:$port" else "$scheme://$host"
527
+ }
528
+
489
529
  // Refuse target=_blank / window.open popups (matches react-native-webview's
490
530
  // `setSupportMultipleWindows={false}` semantics: there's nowhere reasonable
491
531
  // to open the new window inside the overlay).
@@ -66,6 +66,12 @@ object EmbeddedWebViewController {
66
66
  // prompting JS again — avoiding a potential approval loop.
67
67
  private var bypassUrl: String? = null
68
68
 
69
+ // Origin pre-approved by `setUrl`. When `shouldOverrideUrlLoading` sees a
70
+ // top-frame navigation whose origin matches, it allows it immediately — no
71
+ // JS round-trip required. Cleared after first use so subsequent navigations
72
+ // still go through the JS allowlist.
73
+ private var preApprovedOrigin: String? = null
74
+
69
75
  private var emitterToken: java.util.UUID? = null
70
76
 
71
77
  // Module-side hook: assign the emitter under a token. The token lets
@@ -108,6 +114,11 @@ object EmbeddedWebViewController {
108
114
  return@runOnMain
109
115
  }
110
116
  val webView = ensureWebView() ?: return@runOnMain
117
+ // Pre-approve the origin so the first `shouldOverrideUrlLoading` call
118
+ // can skip the JS round-trip. On cold boot the JS thread may be too
119
+ // congested to respond within the navigation-decision timeout, silently
120
+ // cancelling the load.
121
+ preApprovedOrigin = originString(android.net.Uri.parse(url))
111
122
  webView.loadUrl(url)
112
123
  }
113
124
  }
@@ -280,6 +291,7 @@ object EmbeddedWebViewController {
280
291
  pendingTimeouts.values.forEach { mainHandler.removeCallbacks(it) }
281
292
  pendingTimeouts.clear()
282
293
  bypassUrl = null
294
+ preApprovedOrigin = null
283
295
  }
284
296
 
285
297
  private fun runOnMain(action: () -> Unit) {
@@ -332,6 +344,7 @@ object EmbeddedWebViewController {
332
344
  val isAllowedTopFrameScheme =
333
345
  scheme == "https" || (httpAllowed && scheme == "http")
334
346
  if (!isAllowedTopFrameScheme) {
347
+ preApprovedOrigin = null
335
348
  emitLoadError(
336
349
  url = url,
337
350
  code = -1,
@@ -342,6 +355,17 @@ object EmbeddedWebViewController {
342
355
  return true
343
356
  }
344
357
 
358
+ // Fast-path: if the URL's origin matches the pre-approved origin (set
359
+ // by `setUrl`), allow immediately. This eliminates the cold-boot race
360
+ // where the JS thread is too congested to respond to
361
+ // `onShouldStartLoad` within the navigation-decision timeout.
362
+ val approved = preApprovedOrigin
363
+ if (approved != null && approved == originString(request.url)) {
364
+ preApprovedOrigin = null
365
+ bypassUrl = url
366
+ return false
367
+ }
368
+
345
369
  val id = UUID.randomUUID().toString()
346
370
  pendingNavigationUrls[id] = url
347
371
 
@@ -486,6 +510,22 @@ object EmbeddedWebViewController {
486
510
  }
487
511
  }
488
512
 
513
+ // Extract the scheme + host + port origin from a URI (e.g.
514
+ // "https://webview.dynamicauth.com"). Returns null for URIs without a host.
515
+ // Default ports (443 for HTTPS, 80 for HTTP) are omitted so that
516
+ // "https://example.com" and "https://example.com:443" produce the same
517
+ // origin string.
518
+ private fun originString(uri: android.net.Uri?): String? {
519
+ val scheme = uri?.scheme ?: return null
520
+ val host = uri.host ?: return null
521
+ val port = uri.port
522
+
523
+ val shouldIncludePort = port != -1 &&
524
+ !((scheme == "https" && port == 443) || (scheme == "http" && port == 80))
525
+
526
+ return if (shouldIncludePort) "$scheme://$host:$port" else "$scheme://$host"
527
+ }
528
+
489
529
  // Refuse target=_blank / window.open popups (matches react-native-webview's
490
530
  // `setSupportMultipleWindows={false}` semantics: there's nowhere reasonable
491
531
  // to open the new window inside the overlay).
@@ -66,6 +66,12 @@ object EmbeddedWebViewController {
66
66
  // prompting JS again — avoiding a potential approval loop.
67
67
  private var bypassUrl: String? = null
68
68
 
69
+ // Origin pre-approved by `setUrl`. When `shouldOverrideUrlLoading` sees a
70
+ // top-frame navigation whose origin matches, it allows it immediately — no
71
+ // JS round-trip required. Cleared after first use so subsequent navigations
72
+ // still go through the JS allowlist.
73
+ private var preApprovedOrigin: String? = null
74
+
69
75
  private var emitterToken: java.util.UUID? = null
70
76
 
71
77
  // Module-side hook: assign the emitter under a token. The token lets
@@ -108,6 +114,11 @@ object EmbeddedWebViewController {
108
114
  return@runOnMain
109
115
  }
110
116
  val webView = ensureWebView() ?: return@runOnMain
117
+ // Pre-approve the origin so the first `shouldOverrideUrlLoading` call
118
+ // can skip the JS round-trip. On cold boot the JS thread may be too
119
+ // congested to respond within the navigation-decision timeout, silently
120
+ // cancelling the load.
121
+ preApprovedOrigin = originString(android.net.Uri.parse(url))
111
122
  webView.loadUrl(url)
112
123
  }
113
124
  }
@@ -280,6 +291,7 @@ object EmbeddedWebViewController {
280
291
  pendingTimeouts.values.forEach { mainHandler.removeCallbacks(it) }
281
292
  pendingTimeouts.clear()
282
293
  bypassUrl = null
294
+ preApprovedOrigin = null
283
295
  }
284
296
 
285
297
  private fun runOnMain(action: () -> Unit) {
@@ -332,6 +344,7 @@ object EmbeddedWebViewController {
332
344
  val isAllowedTopFrameScheme =
333
345
  scheme == "https" || (httpAllowed && scheme == "http")
334
346
  if (!isAllowedTopFrameScheme) {
347
+ preApprovedOrigin = null
335
348
  emitLoadError(
336
349
  url = url,
337
350
  code = -1,
@@ -342,6 +355,17 @@ object EmbeddedWebViewController {
342
355
  return true
343
356
  }
344
357
 
358
+ // Fast-path: if the URL's origin matches the pre-approved origin (set
359
+ // by `setUrl`), allow immediately. This eliminates the cold-boot race
360
+ // where the JS thread is too congested to respond to
361
+ // `onShouldStartLoad` within the navigation-decision timeout.
362
+ val approved = preApprovedOrigin
363
+ if (approved != null && approved == originString(request.url)) {
364
+ preApprovedOrigin = null
365
+ bypassUrl = url
366
+ return false
367
+ }
368
+
345
369
  val id = UUID.randomUUID().toString()
346
370
  pendingNavigationUrls[id] = url
347
371
 
@@ -486,6 +510,22 @@ object EmbeddedWebViewController {
486
510
  }
487
511
  }
488
512
 
513
+ // Extract the scheme + host + port origin from a URI (e.g.
514
+ // "https://webview.dynamicauth.com"). Returns null for URIs without a host.
515
+ // Default ports (443 for HTTPS, 80 for HTTP) are omitted so that
516
+ // "https://example.com" and "https://example.com:443" produce the same
517
+ // origin string.
518
+ private fun originString(uri: android.net.Uri?): String? {
519
+ val scheme = uri?.scheme ?: return null
520
+ val host = uri.host ?: return null
521
+ val port = uri.port
522
+
523
+ val shouldIncludePort = port != -1 &&
524
+ !((scheme == "https" && port == 443) || (scheme == "http" && port == 80))
525
+
526
+ return if (shouldIncludePort) "$scheme://$host:$port" else "$scheme://$host"
527
+ }
528
+
489
529
  // Refuse target=_blank / window.open popups (matches react-native-webview's
490
530
  // `setSupportMultipleWindows={false}` semantics: there's nowhere reasonable
491
531
  // to open the new window inside the overlay).
package/index.cjs CHANGED
@@ -35,7 +35,7 @@ function _interopNamespace(e) {
35
35
  return Object.freeze(n);
36
36
  }
37
37
 
38
- var version = "4.91.4";
38
+ var version = "4.91.5";
39
39
 
40
40
  function _extends() {
41
41
  return _extends = Object.assign ? Object.assign.bind() : function (n) {
package/index.js CHANGED
@@ -13,7 +13,7 @@ import { createURL, getInitialURL, addEventListener, openURL } from 'expo-linkin
13
13
  import { openAuthSessionAsync } from 'expo-web-browser';
14
14
  import { createPasskey, PasskeyStamper } from '@turnkey/react-native-passkey-stamper';
15
15
 
16
- var version = "4.91.4";
16
+ var version = "4.91.5";
17
17
 
18
18
  function _extends() {
19
19
  return _extends = Object.assign ? Object.assign.bind() : function (n) {
@@ -28,6 +28,11 @@ public final class EmbeddedWebViewController: NSObject {
28
28
  private var pendingNavigationDecisions: [String: (WKNavigationActionPolicy) -> Void] = [:]
29
29
  private var navigationTimers: [String: Timer] = [:]
30
30
  private var emitterToken: UUID?
31
+ // Origin pre-approved by `setUrl`. When `decidePolicyFor` sees a top-frame
32
+ // navigation whose origin matches, it allows it immediately — no JS
33
+ // round-trip required. Cleared after first use so subsequent navigations
34
+ // still go through the JS allowlist.
35
+ private var preApprovedOrigin: String?
31
36
 
32
37
  private override init() {
33
38
  super.init()
@@ -65,6 +70,11 @@ public final class EmbeddedWebViewController: NSObject {
65
70
  }
66
71
  ensureWebView()
67
72
  hasLoaded = true
73
+ // Pre-approve the origin so the first `decidePolicyFor` call can skip
74
+ // the JS round-trip. On cold boot the JS thread may be too congested to
75
+ // respond within the 2 s navigation-decision timeout, silently
76
+ // cancelling the load.
77
+ preApprovedOrigin = originString(for: parsed)
68
78
  webView?.load(URLRequest(url: parsed))
69
79
  }
70
80
 
@@ -124,6 +134,7 @@ public final class EmbeddedWebViewController: NSObject {
124
134
  navigationTimers.values.forEach { $0.invalidate() }
125
135
  navigationTimers.removeAll()
126
136
  hasLoaded = false
137
+ preApprovedOrigin = nil
127
138
  }
128
139
 
129
140
  public func postMessage(_ message: String) {
@@ -248,6 +259,24 @@ public final class EmbeddedWebViewController: NSObject {
248
259
  return nil
249
260
  }
250
261
 
262
+ // Extract the scheme + host + port origin from a URL (e.g.
263
+ // "https://webview.dynamicauth.com"). Returns nil for URLs without a host.
264
+ // Default ports (443 for HTTPS, 80 for HTTP) are omitted so that
265
+ // "https://example.com" and "https://example.com:443" produce the same
266
+ // origin string.
267
+ private func originString(for url: URL?) -> String? {
268
+ guard let url = url, let scheme = url.scheme, let host = url.host else {
269
+ return nil
270
+ }
271
+ if let port = url.port {
272
+ let isDefaultPort = (scheme == "https" && port == 443) || (scheme == "http" && port == 80)
273
+ if !isDefaultPort {
274
+ return "\(scheme)://\(host):\(port)"
275
+ }
276
+ }
277
+ return "\(scheme)://\(host)"
278
+ }
279
+
251
280
  // Use JSONSerialization to safely encode an arbitrary string as a JS string literal.
252
281
  private func jsStringLiteral(_ raw: String) -> String {
253
282
  guard
@@ -317,6 +346,17 @@ extension EmbeddedWebViewController: WKNavigationDelegate {
317
346
  return
318
347
  }
319
348
 
349
+ // Fast-path: if the URL's origin matches the pre-approved origin (set
350
+ // by `setUrl`), allow immediately. This eliminates the cold-boot race
351
+ // where the JS thread is too congested to respond to
352
+ // `onShouldStartLoad` within the 2 s navigation-decision timeout.
353
+ if let approved = preApprovedOrigin,
354
+ approved == originString(for: navigationAction.request.url) {
355
+ preApprovedOrigin = nil
356
+ decisionHandler(.allow)
357
+ return
358
+ }
359
+
320
360
  let id = UUID().uuidString
321
361
  pendingNavigationDecisions[id] = decisionHandler
322
362
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dynamic-labs/react-native-extension",
3
- "version": "4.91.4",
3
+ "version": "4.91.5",
4
4
  "main": "./index.cjs",
5
5
  "module": "./index.js",
6
6
  "types": "./src/index.d.ts",
@@ -18,11 +18,11 @@
18
18
  "@turnkey/react-native-passkey-stamper": "1.2.7",
19
19
  "@react-native-documents/picker": "^11.0.0",
20
20
  "react-native-fs": ">=2.20.0",
21
- "@dynamic-labs/assert-package-version": "4.91.4",
22
- "@dynamic-labs/client": "4.91.4",
23
- "@dynamic-labs/logger": "4.91.4",
24
- "@dynamic-labs/message-transport": "4.91.4",
25
- "@dynamic-labs/webview-messages": "4.91.4"
21
+ "@dynamic-labs/assert-package-version": "4.91.5",
22
+ "@dynamic-labs/client": "4.91.5",
23
+ "@dynamic-labs/logger": "4.91.5",
24
+ "@dynamic-labs/message-transport": "4.91.5",
25
+ "@dynamic-labs/webview-messages": "4.91.5"
26
26
  },
27
27
  "peerDependencies": {
28
28
  "react": ">=18.0.0 <20.0.0",