@dynamic-labs/react-native-extension 4.91.4 → 4.91.6
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.
- package/android/EmbeddedWebViewController.kt +41 -1
- package/android/dynamic/embeddedwebview/EmbeddedWebViewController.kt +41 -1
- package/android/embeddedwebview/EmbeddedWebViewController.kt +41 -1
- package/android/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +41 -1
- package/android/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +41 -1
- package/android/src/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +41 -1
- package/android/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +41 -1
- package/index.cjs +27 -4
- package/index.js +27 -4
- package/ios/EmbeddedWebViewController.swift +52 -1
- package/package.json +6 -6
|
@@ -24,7 +24,7 @@ import org.json.JSONObject
|
|
|
24
24
|
import java.util.UUID
|
|
25
25
|
|
|
26
26
|
private const val SCRIPT_HANDLER_NAME = "DynamicEmbeddedWebView"
|
|
27
|
-
private const val NAVIGATION_DECISION_TIMEOUT_MS =
|
|
27
|
+
private const val NAVIGATION_DECISION_TIMEOUT_MS = 5000L
|
|
28
28
|
|
|
29
29
|
// Singleton owning the in-app overlay view that hosts the WebView.
|
|
30
30
|
// Mirrors EmbeddedWebViewController on iOS.
|
|
@@ -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).
|
|
@@ -24,7 +24,7 @@ import org.json.JSONObject
|
|
|
24
24
|
import java.util.UUID
|
|
25
25
|
|
|
26
26
|
private const val SCRIPT_HANDLER_NAME = "DynamicEmbeddedWebView"
|
|
27
|
-
private const val NAVIGATION_DECISION_TIMEOUT_MS =
|
|
27
|
+
private const val NAVIGATION_DECISION_TIMEOUT_MS = 5000L
|
|
28
28
|
|
|
29
29
|
// Singleton owning the in-app overlay view that hosts the WebView.
|
|
30
30
|
// Mirrors EmbeddedWebViewController on iOS.
|
|
@@ -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).
|
|
@@ -24,7 +24,7 @@ import org.json.JSONObject
|
|
|
24
24
|
import java.util.UUID
|
|
25
25
|
|
|
26
26
|
private const val SCRIPT_HANDLER_NAME = "DynamicEmbeddedWebView"
|
|
27
|
-
private const val NAVIGATION_DECISION_TIMEOUT_MS =
|
|
27
|
+
private const val NAVIGATION_DECISION_TIMEOUT_MS = 5000L
|
|
28
28
|
|
|
29
29
|
// Singleton owning the in-app overlay view that hosts the WebView.
|
|
30
30
|
// Mirrors EmbeddedWebViewController on iOS.
|
|
@@ -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).
|
|
@@ -24,7 +24,7 @@ import org.json.JSONObject
|
|
|
24
24
|
import java.util.UUID
|
|
25
25
|
|
|
26
26
|
private const val SCRIPT_HANDLER_NAME = "DynamicEmbeddedWebView"
|
|
27
|
-
private const val NAVIGATION_DECISION_TIMEOUT_MS =
|
|
27
|
+
private const val NAVIGATION_DECISION_TIMEOUT_MS = 5000L
|
|
28
28
|
|
|
29
29
|
// Singleton owning the in-app overlay view that hosts the WebView.
|
|
30
30
|
// Mirrors EmbeddedWebViewController on iOS.
|
|
@@ -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).
|
|
@@ -24,7 +24,7 @@ import org.json.JSONObject
|
|
|
24
24
|
import java.util.UUID
|
|
25
25
|
|
|
26
26
|
private const val SCRIPT_HANDLER_NAME = "DynamicEmbeddedWebView"
|
|
27
|
-
private const val NAVIGATION_DECISION_TIMEOUT_MS =
|
|
27
|
+
private const val NAVIGATION_DECISION_TIMEOUT_MS = 5000L
|
|
28
28
|
|
|
29
29
|
// Singleton owning the in-app overlay view that hosts the WebView.
|
|
30
30
|
// Mirrors EmbeddedWebViewController on iOS.
|
|
@@ -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).
|
|
@@ -24,7 +24,7 @@ import org.json.JSONObject
|
|
|
24
24
|
import java.util.UUID
|
|
25
25
|
|
|
26
26
|
private const val SCRIPT_HANDLER_NAME = "DynamicEmbeddedWebView"
|
|
27
|
-
private const val NAVIGATION_DECISION_TIMEOUT_MS =
|
|
27
|
+
private const val NAVIGATION_DECISION_TIMEOUT_MS = 5000L
|
|
28
28
|
|
|
29
29
|
// Singleton owning the in-app overlay view that hosts the WebView.
|
|
30
30
|
// Mirrors EmbeddedWebViewController on iOS.
|
|
@@ -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).
|
|
@@ -24,7 +24,7 @@ import org.json.JSONObject
|
|
|
24
24
|
import java.util.UUID
|
|
25
25
|
|
|
26
26
|
private const val SCRIPT_HANDLER_NAME = "DynamicEmbeddedWebView"
|
|
27
|
-
private const val NAVIGATION_DECISION_TIMEOUT_MS =
|
|
27
|
+
private const val NAVIGATION_DECISION_TIMEOUT_MS = 5000L
|
|
28
28
|
|
|
29
29
|
// Singleton owning the in-app overlay view that hosts the WebView.
|
|
30
30
|
// Mirrors EmbeddedWebViewController on iOS.
|
|
@@ -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.
|
|
38
|
+
var version = "4.91.6";
|
|
39
39
|
|
|
40
40
|
function _extends() {
|
|
41
41
|
return _extends = Object.assign ? Object.assign.bind() : function (n) {
|
|
@@ -1002,6 +1002,7 @@ const setupEmbeddedWebView = ({
|
|
|
1002
1002
|
let recoveryTimer = null;
|
|
1003
1003
|
let hasFailed = false;
|
|
1004
1004
|
let hasEmittedSuccessLog = false;
|
|
1005
|
+
let tornDown = false;
|
|
1005
1006
|
const clearLoadingTimer = () => {
|
|
1006
1007
|
if (loadingTimer !== null) {
|
|
1007
1008
|
clearTimeout(loadingTimer);
|
|
@@ -1023,6 +1024,7 @@ const setupEmbeddedWebView = ({
|
|
|
1023
1024
|
hasFailed = true;
|
|
1024
1025
|
clearLoadingTimer();
|
|
1025
1026
|
clearRecoveryTimer();
|
|
1027
|
+
logger.debug(`[EmbeddedWebView] raiseFailure phase=${phase}`);
|
|
1026
1028
|
const meta = phaseTimers.getMeta({
|
|
1027
1029
|
phase
|
|
1028
1030
|
});
|
|
@@ -1064,6 +1066,7 @@ const setupEmbeddedWebView = ({
|
|
|
1064
1066
|
const handleSdkReady = message => {
|
|
1065
1067
|
if (message.origin !== 'webview') return;
|
|
1066
1068
|
if (message.type !== webviewMessages.sdkHasLoadedEventName) return;
|
|
1069
|
+
logger.debug('[EmbeddedWebView] SDK ready signal received');
|
|
1067
1070
|
clearRecoveryTimer();
|
|
1068
1071
|
if (hasEmittedSuccessLog || hasFailed) return;
|
|
1069
1072
|
hasEmittedSuccessLog = true;
|
|
@@ -1099,11 +1102,13 @@ const setupEmbeddedWebView = ({
|
|
|
1099
1102
|
isTopFrame: event.isTopFrame,
|
|
1100
1103
|
url: event.url
|
|
1101
1104
|
}, builtUrl);
|
|
1105
|
+
logger.debug(`[EmbeddedWebView] onShouldStartLoad id=${event.id} url=${event.url} isTopFrame=${event.isTopFrame} allow=${allow}`);
|
|
1102
1106
|
native.respondToShouldStartLoad(event.id, allow).catch(err => {
|
|
1103
1107
|
logger.warn('EmbeddedWebView.respondToShouldStartLoad failed', err);
|
|
1104
1108
|
});
|
|
1105
1109
|
});
|
|
1106
1110
|
const onLoadStartSub = native.addListener('onLoadStart', () => {
|
|
1111
|
+
logger.debug('[EmbeddedWebView] onLoadStart');
|
|
1107
1112
|
phaseTimers.recordEvent('load_start');
|
|
1108
1113
|
// Re-arm both timers on every load_start. A retry from the native
|
|
1109
1114
|
// side (e.g. a redirect, or a reload after a transient failure)
|
|
@@ -1116,10 +1121,12 @@ const setupEmbeddedWebView = ({
|
|
|
1116
1121
|
}, _loadingTimeoutMs);
|
|
1117
1122
|
});
|
|
1118
1123
|
const onLoadSub = native.addListener('onLoad', () => {
|
|
1124
|
+
logger.debug('[EmbeddedWebView] onLoad (response committed)');
|
|
1119
1125
|
phaseTimers.recordEvent('load');
|
|
1120
1126
|
clearLoadingTimer();
|
|
1121
1127
|
});
|
|
1122
1128
|
const onLoadEndSub = native.addListener('onLoadEnd', () => {
|
|
1129
|
+
logger.debug('[EmbeddedWebView] onLoadEnd (page finished)');
|
|
1123
1130
|
phaseTimers.recordEvent('load_end');
|
|
1124
1131
|
clearLoadingTimer();
|
|
1125
1132
|
clearRecoveryTimer();
|
|
@@ -1128,6 +1135,7 @@ const setupEmbeddedWebView = ({
|
|
|
1128
1135
|
}, _recoveryTimeoutMs);
|
|
1129
1136
|
});
|
|
1130
1137
|
const onLoadErrorSub = native.addListener('onLoadError', event => {
|
|
1138
|
+
logger.debug(`[EmbeddedWebView] onLoadError domain=${event.domain} code=${event.code} url=${event.url}`);
|
|
1131
1139
|
phaseTimers.recordEvent('native_error');
|
|
1132
1140
|
logger.warn('EmbeddedWebView load error', event);
|
|
1133
1141
|
raiseFailure('embedded_native_error', {
|
|
@@ -1147,13 +1155,28 @@ const setupEmbeddedWebView = ({
|
|
|
1147
1155
|
// without emitting onLoadError — leaving onLoadStart (and therefore the
|
|
1148
1156
|
// timer) never triggered. The onLoadStart handler clears and re-arms this
|
|
1149
1157
|
// timer when it fires, so behaviour after onLoadStart is unchanged.
|
|
1158
|
+
logger.debug(`[EmbeddedWebView] arming loading timer (${_loadingTimeoutMs}ms) before setUrl`);
|
|
1150
1159
|
loadingTimer = setTimeout(() => {
|
|
1160
|
+
logger.debug('[EmbeddedWebView] loading timer fired — raising html_load failure');
|
|
1151
1161
|
raiseFailure('html_load');
|
|
1152
1162
|
}, _loadingTimeoutMs);
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1163
|
+
// Defer setUrl by one event-loop tick so the JS thread has fully settled
|
|
1164
|
+
// after listener registration. On cold boot the main thread is congested
|
|
1165
|
+
// with bundle evaluation — yielding here gives pending microtasks and
|
|
1166
|
+
// native bridge batches a chance to flush before the native side starts
|
|
1167
|
+
// firing navigation events that require a JS round-trip response.
|
|
1168
|
+
const warmupTimer = setTimeout(() => {
|
|
1169
|
+
var _a;
|
|
1170
|
+
if (tornDown) return;
|
|
1171
|
+
logger.debug(`[EmbeddedWebView] calling setUrl: ${builtUrl.toString()}`);
|
|
1172
|
+
(_a = native.setUrl(builtUrl.toString())) === null || _a === void 0 ? void 0 : _a.catch(err => {
|
|
1173
|
+
logger.warn('EmbeddedWebView.setUrl failed', err);
|
|
1174
|
+
});
|
|
1175
|
+
}, 0);
|
|
1156
1176
|
return () => {
|
|
1177
|
+
logger.debug('[EmbeddedWebView] teardown — removing listeners');
|
|
1178
|
+
tornDown = true;
|
|
1179
|
+
clearTimeout(warmupTimer);
|
|
1157
1180
|
clearLoadingTimer();
|
|
1158
1181
|
clearRecoveryTimer();
|
|
1159
1182
|
removeVisibilityHandler();
|
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.
|
|
16
|
+
var version = "4.91.6";
|
|
17
17
|
|
|
18
18
|
function _extends() {
|
|
19
19
|
return _extends = Object.assign ? Object.assign.bind() : function (n) {
|
|
@@ -980,6 +980,7 @@ const setupEmbeddedWebView = ({
|
|
|
980
980
|
let recoveryTimer = null;
|
|
981
981
|
let hasFailed = false;
|
|
982
982
|
let hasEmittedSuccessLog = false;
|
|
983
|
+
let tornDown = false;
|
|
983
984
|
const clearLoadingTimer = () => {
|
|
984
985
|
if (loadingTimer !== null) {
|
|
985
986
|
clearTimeout(loadingTimer);
|
|
@@ -1001,6 +1002,7 @@ const setupEmbeddedWebView = ({
|
|
|
1001
1002
|
hasFailed = true;
|
|
1002
1003
|
clearLoadingTimer();
|
|
1003
1004
|
clearRecoveryTimer();
|
|
1005
|
+
logger.debug(`[EmbeddedWebView] raiseFailure phase=${phase}`);
|
|
1004
1006
|
const meta = phaseTimers.getMeta({
|
|
1005
1007
|
phase
|
|
1006
1008
|
});
|
|
@@ -1042,6 +1044,7 @@ const setupEmbeddedWebView = ({
|
|
|
1042
1044
|
const handleSdkReady = message => {
|
|
1043
1045
|
if (message.origin !== 'webview') return;
|
|
1044
1046
|
if (message.type !== sdkHasLoadedEventName) return;
|
|
1047
|
+
logger.debug('[EmbeddedWebView] SDK ready signal received');
|
|
1045
1048
|
clearRecoveryTimer();
|
|
1046
1049
|
if (hasEmittedSuccessLog || hasFailed) return;
|
|
1047
1050
|
hasEmittedSuccessLog = true;
|
|
@@ -1077,11 +1080,13 @@ const setupEmbeddedWebView = ({
|
|
|
1077
1080
|
isTopFrame: event.isTopFrame,
|
|
1078
1081
|
url: event.url
|
|
1079
1082
|
}, builtUrl);
|
|
1083
|
+
logger.debug(`[EmbeddedWebView] onShouldStartLoad id=${event.id} url=${event.url} isTopFrame=${event.isTopFrame} allow=${allow}`);
|
|
1080
1084
|
native.respondToShouldStartLoad(event.id, allow).catch(err => {
|
|
1081
1085
|
logger.warn('EmbeddedWebView.respondToShouldStartLoad failed', err);
|
|
1082
1086
|
});
|
|
1083
1087
|
});
|
|
1084
1088
|
const onLoadStartSub = native.addListener('onLoadStart', () => {
|
|
1089
|
+
logger.debug('[EmbeddedWebView] onLoadStart');
|
|
1085
1090
|
phaseTimers.recordEvent('load_start');
|
|
1086
1091
|
// Re-arm both timers on every load_start. A retry from the native
|
|
1087
1092
|
// side (e.g. a redirect, or a reload after a transient failure)
|
|
@@ -1094,10 +1099,12 @@ const setupEmbeddedWebView = ({
|
|
|
1094
1099
|
}, _loadingTimeoutMs);
|
|
1095
1100
|
});
|
|
1096
1101
|
const onLoadSub = native.addListener('onLoad', () => {
|
|
1102
|
+
logger.debug('[EmbeddedWebView] onLoad (response committed)');
|
|
1097
1103
|
phaseTimers.recordEvent('load');
|
|
1098
1104
|
clearLoadingTimer();
|
|
1099
1105
|
});
|
|
1100
1106
|
const onLoadEndSub = native.addListener('onLoadEnd', () => {
|
|
1107
|
+
logger.debug('[EmbeddedWebView] onLoadEnd (page finished)');
|
|
1101
1108
|
phaseTimers.recordEvent('load_end');
|
|
1102
1109
|
clearLoadingTimer();
|
|
1103
1110
|
clearRecoveryTimer();
|
|
@@ -1106,6 +1113,7 @@ const setupEmbeddedWebView = ({
|
|
|
1106
1113
|
}, _recoveryTimeoutMs);
|
|
1107
1114
|
});
|
|
1108
1115
|
const onLoadErrorSub = native.addListener('onLoadError', event => {
|
|
1116
|
+
logger.debug(`[EmbeddedWebView] onLoadError domain=${event.domain} code=${event.code} url=${event.url}`);
|
|
1109
1117
|
phaseTimers.recordEvent('native_error');
|
|
1110
1118
|
logger.warn('EmbeddedWebView load error', event);
|
|
1111
1119
|
raiseFailure('embedded_native_error', {
|
|
@@ -1125,13 +1133,28 @@ const setupEmbeddedWebView = ({
|
|
|
1125
1133
|
// without emitting onLoadError — leaving onLoadStart (and therefore the
|
|
1126
1134
|
// timer) never triggered. The onLoadStart handler clears and re-arms this
|
|
1127
1135
|
// timer when it fires, so behaviour after onLoadStart is unchanged.
|
|
1136
|
+
logger.debug(`[EmbeddedWebView] arming loading timer (${_loadingTimeoutMs}ms) before setUrl`);
|
|
1128
1137
|
loadingTimer = setTimeout(() => {
|
|
1138
|
+
logger.debug('[EmbeddedWebView] loading timer fired — raising html_load failure');
|
|
1129
1139
|
raiseFailure('html_load');
|
|
1130
1140
|
}, _loadingTimeoutMs);
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1141
|
+
// Defer setUrl by one event-loop tick so the JS thread has fully settled
|
|
1142
|
+
// after listener registration. On cold boot the main thread is congested
|
|
1143
|
+
// with bundle evaluation — yielding here gives pending microtasks and
|
|
1144
|
+
// native bridge batches a chance to flush before the native side starts
|
|
1145
|
+
// firing navigation events that require a JS round-trip response.
|
|
1146
|
+
const warmupTimer = setTimeout(() => {
|
|
1147
|
+
var _a;
|
|
1148
|
+
if (tornDown) return;
|
|
1149
|
+
logger.debug(`[EmbeddedWebView] calling setUrl: ${builtUrl.toString()}`);
|
|
1150
|
+
(_a = native.setUrl(builtUrl.toString())) === null || _a === void 0 ? void 0 : _a.catch(err => {
|
|
1151
|
+
logger.warn('EmbeddedWebView.setUrl failed', err);
|
|
1152
|
+
});
|
|
1153
|
+
}, 0);
|
|
1134
1154
|
return () => {
|
|
1155
|
+
logger.debug('[EmbeddedWebView] teardown — removing listeners');
|
|
1156
|
+
tornDown = true;
|
|
1157
|
+
clearTimeout(warmupTimer);
|
|
1135
1158
|
clearLoadingTimer();
|
|
1136
1159
|
clearRecoveryTimer();
|
|
1137
1160
|
removeVisibilityHandler();
|
|
@@ -2,7 +2,7 @@ import UIKit
|
|
|
2
2
|
import WebKit
|
|
3
3
|
|
|
4
4
|
private let scriptHandlerName = "dynamicEmbeddedWebView"
|
|
5
|
-
private let navigationDecisionTimeout: TimeInterval =
|
|
5
|
+
private let navigationDecisionTimeout: TimeInterval = 5.0
|
|
6
6
|
|
|
7
7
|
// Cleartext http is only permitted in debug builds (e.g. Metro at
|
|
8
8
|
// http://localhost:4202). Release builds reject http top-frame and sub-frame
|
|
@@ -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
|
|
|
@@ -326,6 +366,17 @@ extension EmbeddedWebViewController: WKNavigationDelegate {
|
|
|
326
366
|
if let handler = self.pendingNavigationDecisions.removeValue(forKey: id) {
|
|
327
367
|
self.navigationTimers.removeValue(forKey: id)?.invalidate()
|
|
328
368
|
handler(.cancel)
|
|
369
|
+
// Surface an explicit load error so the SDK's error path runs
|
|
370
|
+
// immediately instead of waiting for the 20 s html_load timer.
|
|
371
|
+
// Matches Android's EmbeddedWebViewNavigationTimeout behavior.
|
|
372
|
+
let timeoutMs = Int(navigationDecisionTimeout * 1000)
|
|
373
|
+
self.eventEmitter?("onLoadError", [
|
|
374
|
+
"url": url,
|
|
375
|
+
"code": -1,
|
|
376
|
+
"domain": "EmbeddedWebViewNavigationTimeout",
|
|
377
|
+
"description": "Navigation decision timed out after \(timeoutMs)ms",
|
|
378
|
+
"isProvisional": true,
|
|
379
|
+
])
|
|
329
380
|
}
|
|
330
381
|
}
|
|
331
382
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dynamic-labs/react-native-extension",
|
|
3
|
-
"version": "4.91.
|
|
3
|
+
"version": "4.91.6",
|
|
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.
|
|
22
|
-
"@dynamic-labs/client": "4.91.
|
|
23
|
-
"@dynamic-labs/logger": "4.91.
|
|
24
|
-
"@dynamic-labs/message-transport": "4.91.
|
|
25
|
-
"@dynamic-labs/webview-messages": "4.91.
|
|
21
|
+
"@dynamic-labs/assert-package-version": "4.91.6",
|
|
22
|
+
"@dynamic-labs/client": "4.91.6",
|
|
23
|
+
"@dynamic-labs/logger": "4.91.6",
|
|
24
|
+
"@dynamic-labs/message-transport": "4.91.6",
|
|
25
|
+
"@dynamic-labs/webview-messages": "4.91.6"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
28
|
"react": ">=18.0.0 <20.0.0",
|