@dynamic-labs/react-native-extension 4.81.0 → 4.83.1

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.
Files changed (35) hide show
  1. package/android/EmbeddedWebViewController.kt +476 -0
  2. package/android/EmbeddedWebViewModule.kt +55 -0
  3. package/android/KeyStoreKeyManager.kt +7 -1
  4. package/android/dynamic/embeddedwebview/EmbeddedWebViewController.kt +476 -0
  5. package/android/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +55 -0
  6. package/android/dynamic/keychain/KeyStoreKeyManager.kt +7 -1
  7. package/android/embeddedwebview/EmbeddedWebViewController.kt +476 -0
  8. package/android/embeddedwebview/EmbeddedWebViewModule.kt +55 -0
  9. package/android/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +476 -0
  10. package/android/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +55 -0
  11. package/android/java/xyz/dynamic/keychain/KeyStoreKeyManager.kt +7 -1
  12. package/android/keychain/KeyStoreKeyManager.kt +7 -1
  13. package/android/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +476 -0
  14. package/android/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +55 -0
  15. package/android/main/java/xyz/dynamic/keychain/KeyStoreKeyManager.kt +7 -1
  16. package/android/src/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +476 -0
  17. package/android/src/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +55 -0
  18. package/android/src/main/java/xyz/dynamic/keychain/KeyStoreKeyManager.kt +7 -1
  19. package/android/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +476 -0
  20. package/android/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +55 -0
  21. package/android/xyz/dynamic/keychain/KeyStoreKeyManager.kt +7 -1
  22. package/expo-module.config.json +5 -2
  23. package/index.cjs +178 -42
  24. package/index.js +178 -42
  25. package/ios/EmbeddedWebViewController.swift +426 -0
  26. package/ios/EmbeddedWebViewModule.swift +62 -0
  27. package/ios/Keychain.podspec +2 -2
  28. package/package.json +6 -6
  29. package/src/ReactNativeExtension/ReactNativeExtension.d.ts +24 -1
  30. package/src/components/WebView/EmbeddedWebView/EmbeddedWebView.d.ts +7 -0
  31. package/src/components/WebView/EmbeddedWebView/index.d.ts +1 -0
  32. package/src/components/WebView/utils/shouldAllowNavigation/index.d.ts +1 -0
  33. package/src/components/WebView/utils/shouldAllowNavigation/shouldAllowNavigation.d.ts +5 -0
  34. package/src/errors/WebViewFailedToLoadError.d.ts +84 -1
  35. package/src/nativeModules/EmbeddedWebView.d.ts +29 -0
@@ -0,0 +1,426 @@
1
+ import UIKit
2
+ import WebKit
3
+
4
+ private let scriptHandlerName = "dynamicEmbeddedWebView"
5
+ private let navigationDecisionTimeout: TimeInterval = 2.0
6
+
7
+ // Cleartext http is only permitted in debug builds (e.g. Metro at
8
+ // http://localhost:4202). Release builds reject http top-frame and sub-frame
9
+ // navigations regardless of what the JS allowlist says — so a JS compromise
10
+ // cannot trick production into loading http content.
11
+ #if DEBUG
12
+ private let allowsHttpScheme = true
13
+ #else
14
+ private let allowsHttpScheme = false
15
+ #endif
16
+
17
+ public typealias EmbeddedWebViewEventEmitter = (String, [String: Any]) -> Void
18
+
19
+ public final class EmbeddedWebViewController: NSObject {
20
+ public static let shared = EmbeddedWebViewController()
21
+
22
+ public var eventEmitter: EmbeddedWebViewEventEmitter?
23
+
24
+ private var window: UIWindow?
25
+ private var webView: WKWebView?
26
+ private var debuggingEnabled = false
27
+ private var hasLoaded = false
28
+ private var pendingNavigationDecisions: [String: (WKNavigationActionPolicy) -> Void] = [:]
29
+ private var navigationTimers: [String: Timer] = [:]
30
+ private var emitterToken: UUID?
31
+
32
+ private override init() {
33
+ super.init()
34
+ }
35
+
36
+ // Module-side hook: assign the emitter under a token. The token lets
37
+ // OnDestroy avoid clobbering an emitter that a newer OnCreate has already
38
+ // installed — relevant during dev hot reloads where module instances briefly
39
+ // overlap.
40
+ @discardableResult
41
+ public func setEmitter(_ emitter: @escaping EmbeddedWebViewEventEmitter) -> UUID {
42
+ let token = UUID()
43
+ emitterToken = token
44
+ eventEmitter = emitter
45
+ return token
46
+ }
47
+
48
+ public func clearEmitter(token: UUID) {
49
+ if emitterToken == token {
50
+ eventEmitter = nil
51
+ emitterToken = nil
52
+ }
53
+ }
54
+
55
+ // MARK: - Public API (must be called on main thread)
56
+
57
+ public func setUrl(_ url: String) {
58
+ // Match react-native-webview parity: any invalid URL surfaces as a load
59
+ // error instead of silently no-op'ing. WKWebView accepts URLs without a
60
+ // scheme but never emits a navigation event for them — fail fast here so
61
+ // the JS side throws WebViewFailedToLoadError.
62
+ guard let parsed = URL(string: url), parsed.scheme != nil else {
63
+ emitInvalidUrlError(url: url)
64
+ return
65
+ }
66
+ ensureWebView()
67
+ hasLoaded = true
68
+ webView?.load(URLRequest(url: parsed))
69
+ }
70
+
71
+ private func emitInvalidUrlError(url: String) {
72
+ eventEmitter?("onLoadError", [
73
+ "url": url,
74
+ "code": -1,
75
+ "domain": "EmbeddedWebViewInvalidUrl",
76
+ "description": "Invalid URL: \(url)",
77
+ "isProvisional": true,
78
+ ])
79
+ }
80
+
81
+ public func setVisible(_ visible: Bool) {
82
+ ensureWebView()
83
+ // We deliberately do NOT toggle `isHidden`. WKWebView attached to a hidden
84
+ // UIWindow gets throttled by iOS — JS timers slow, fetch callbacks stall,
85
+ // message events stop dispatching — so the page effectively pauses until
86
+ // the window becomes visible again. By keeping the window in the visible
87
+ // window stack and toggling `alpha` + interaction instead, the WKWebView
88
+ // stays "foreground" from the OS's POV and continues running normally.
89
+ if visible {
90
+ window?.alpha = 1
91
+ window?.isUserInteractionEnabled = true
92
+ window?.makeKeyAndVisible()
93
+ } else {
94
+ window?.alpha = 0
95
+ window?.isUserInteractionEnabled = false
96
+ }
97
+ }
98
+
99
+ public func setDebuggingEnabled(_ enabled: Bool) {
100
+ debuggingEnabled = enabled
101
+ if let webView = webView, #available(iOS 16.4, *) {
102
+ webView.isInspectable = enabled
103
+ }
104
+ }
105
+
106
+ public func destroy() {
107
+ webView?.stopLoading()
108
+ if let userContentController = webView?.configuration.userContentController {
109
+ userContentController.removeAllUserScripts()
110
+ userContentController.removeScriptMessageHandler(forName: scriptHandlerName)
111
+ }
112
+ webView?.navigationDelegate = nil
113
+ webView?.removeFromSuperview()
114
+ webView = nil
115
+
116
+ window?.isHidden = true
117
+ window?.rootViewController = nil
118
+ window = nil
119
+
120
+ // WKWebView contract: every policy decision handler must be invoked
121
+ // exactly once. Cancel any in-flight decisions before clearing.
122
+ pendingNavigationDecisions.values.forEach { $0(.cancel) }
123
+ pendingNavigationDecisions.removeAll()
124
+ navigationTimers.values.forEach { $0.invalidate() }
125
+ navigationTimers.removeAll()
126
+ hasLoaded = false
127
+ }
128
+
129
+ public func postMessage(_ message: String) {
130
+ guard let webView = webView else { return }
131
+ let escaped = jsStringLiteral(message)
132
+ let script = "window.dispatchEvent(new MessageEvent('message', { data: \(escaped) }));"
133
+ webView.evaluateJavaScript(script, completionHandler: nil)
134
+ }
135
+
136
+ public func respondToShouldStartLoad(id: String, allow: Bool) {
137
+ DispatchQueue.main.async { [weak self] in
138
+ guard let self = self else { return }
139
+ guard let handler = self.pendingNavigationDecisions.removeValue(forKey: id) else { return }
140
+ self.navigationTimers.removeValue(forKey: id)?.invalidate()
141
+ handler(allow ? .allow : .cancel)
142
+ }
143
+ }
144
+
145
+ // MARK: - Lazy creation
146
+
147
+ private func ensureWebView() {
148
+ if webView != nil { return }
149
+
150
+ let configuration = WKWebViewConfiguration()
151
+ configuration.websiteDataStore = .default()
152
+
153
+ let userContentController = WKUserContentController()
154
+ let polyfill = """
155
+ (function() {
156
+ if (window.ReactNativeWebView) return;
157
+ window.ReactNativeWebView = {
158
+ postMessage: function(message) {
159
+ window.webkit.messageHandlers.\(scriptHandlerName).postMessage(message);
160
+ }
161
+ };
162
+ })();
163
+ """
164
+ let userScript = WKUserScript(
165
+ source: polyfill,
166
+ injectionTime: .atDocumentStart,
167
+ forMainFrameOnly: false
168
+ )
169
+ userContentController.addUserScript(userScript)
170
+ userContentController.add(self, name: scriptHandlerName)
171
+ configuration.userContentController = userContentController
172
+
173
+ let frame = UIScreen.main.bounds
174
+ let webView = WKWebView(frame: frame, configuration: configuration)
175
+ webView.navigationDelegate = self
176
+ webView.scrollView.contentInsetAdjustmentBehavior = .never
177
+ webView.translatesAutoresizingMaskIntoConstraints = false
178
+
179
+ // Make the WKWebView transparent so the RN content beneath shows through
180
+ // when the webview-controller renders without an opaque backdrop.
181
+ webView.isOpaque = false
182
+ webView.backgroundColor = .clear
183
+ webView.scrollView.backgroundColor = .clear
184
+
185
+ if #available(iOS 16.4, *) {
186
+ webView.isInspectable = debuggingEnabled
187
+ }
188
+
189
+ let rootVC = UIViewController()
190
+ rootVC.view.backgroundColor = .clear
191
+ rootVC.view.isOpaque = false
192
+ rootVC.view.addSubview(webView)
193
+ NSLayoutConstraint.activate([
194
+ webView.topAnchor.constraint(equalTo: rootVC.view.topAnchor),
195
+ webView.bottomAnchor.constraint(equalTo: rootVC.view.bottomAnchor),
196
+ webView.leadingAnchor.constraint(equalTo: rootVC.view.leadingAnchor),
197
+ webView.trailingAnchor.constraint(equalTo: rootVC.view.trailingAnchor),
198
+ ])
199
+
200
+ let window = UIWindow(frame: frame)
201
+ window.windowLevel = .alert
202
+ window.backgroundColor = .clear
203
+ window.isOpaque = false
204
+ window.rootViewController = rootVC
205
+ // Keep the window in the visible window stack from creation onward so
206
+ // iOS does not throttle the WKWebView's JS execution. We start fully
207
+ // transparent and non-interactive — `setVisible(true)` flips `alpha` and
208
+ // interaction back on without ever hiding the window.
209
+ window.isHidden = false
210
+ window.alpha = 0
211
+ window.isUserInteractionEnabled = false
212
+
213
+ self.webView = webView
214
+ self.window = window
215
+ }
216
+
217
+ // Walk the WKWebView subview tree, find the WKContentView (the first
218
+ // responder for keyboard input), and dynamically subclass it so its
219
+ // `inputAccessoryView` getter returns nil. This is the same technique
220
+ // react-native-webview uses for `hideKeyboardAccessoryView`.
221
+ private func hideInputAccessoryView(on webView: WKWebView) {
222
+ let hiddenClassName = "DynamicEmbeddedWebViewHiddenAccessory"
223
+ guard let contentView = findContentView(in: webView) else { return }
224
+ if NSStringFromClass(type(of: contentView)) == hiddenClassName { return }
225
+ guard let baseClass = object_getClass(contentView) else { return }
226
+ let existing: AnyClass? = NSClassFromString(hiddenClassName)
227
+ let targetClass: AnyClass
228
+ if let existing = existing {
229
+ targetClass = existing
230
+ } else {
231
+ guard let newClass = objc_allocateClassPair(baseClass, hiddenClassName, 0) else { return }
232
+ let block: @convention(block) (Any) -> UIView? = { _ in nil }
233
+ let imp = imp_implementationWithBlock(block)
234
+ let selector = #selector(getter: UIResponder.inputAccessoryView)
235
+ class_addMethod(newClass, selector, imp, "@@:")
236
+ objc_registerClassPair(newClass)
237
+ targetClass = newClass
238
+ }
239
+ object_setClass(contentView, targetClass)
240
+ }
241
+
242
+ private func findContentView(in view: UIView) -> UIView? {
243
+ for subview in view.subviews {
244
+ let className = NSStringFromClass(type(of: subview))
245
+ if className.contains("WKContentView") { return subview }
246
+ if let found = findContentView(in: subview) { return found }
247
+ }
248
+ return nil
249
+ }
250
+
251
+ // Use JSONSerialization to safely encode an arbitrary string as a JS string literal.
252
+ private func jsStringLiteral(_ raw: String) -> String {
253
+ guard
254
+ let data = try? JSONSerialization.data(withJSONObject: [raw], options: []),
255
+ let arrayString = String(data: data, encoding: .utf8),
256
+ arrayString.count >= 2
257
+ else {
258
+ return "\"\""
259
+ }
260
+ let start = arrayString.index(after: arrayString.startIndex)
261
+ let end = arrayString.index(before: arrayString.endIndex)
262
+ return String(arrayString[start..<end])
263
+ }
264
+ }
265
+
266
+ extension EmbeddedWebViewController: WKScriptMessageHandler {
267
+ public func userContentController(
268
+ _: WKUserContentController,
269
+ didReceive message: WKScriptMessage
270
+ ) {
271
+ guard message.name == scriptHandlerName else { return }
272
+ let payload: String
273
+ if let asString = message.body as? String {
274
+ payload = asString
275
+ } else {
276
+ payload = String(describing: message.body)
277
+ }
278
+ eventEmitter?("onMessage", ["message": payload])
279
+ }
280
+ }
281
+
282
+ extension EmbeddedWebViewController: WKNavigationDelegate {
283
+ public func webView(
284
+ _: WKWebView,
285
+ decidePolicyFor navigationAction: WKNavigationAction,
286
+ decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
287
+ ) {
288
+ let url = navigationAction.request.url?.absoluteString ?? ""
289
+ let scheme = navigationAction.request.url?.scheme?.lowercased()
290
+ let isTopFrame = navigationAction.targetFrame?.isMainFrame ?? true
291
+
292
+ // Sub-frame requests are auto-allowed (matches Android + JS allowlist).
293
+ // The trusted top-frame controls iframe content; iframes routinely start
294
+ // as `about:blank` and use `blob:` / `data:` / `about:srcdoc` URLs for
295
+ // legitimate functionality (WaaS MPC iframes, web workers, sandboxed
296
+ // inline content), so we defer iframe trust to the top-frame.
297
+ if !isTopFrame {
298
+ decisionHandler(.allow)
299
+ return
300
+ }
301
+
302
+ // Defense-in-depth: reject non-https top-frame schemes at the native layer
303
+ // before prompting JS. The JS allowlist would also reject these, but a JS
304
+ // bug approving a `javascript:` / `file:` / `data:` URL must not be enough
305
+ // to load it. http is permitted only in debug builds.
306
+ let isAllowedTopFrameScheme =
307
+ scheme == "https" || (allowsHttpScheme && scheme == "http")
308
+ if !isAllowedTopFrameScheme {
309
+ decisionHandler(.cancel)
310
+ eventEmitter?("onLoadError", [
311
+ "url": url,
312
+ "code": -1,
313
+ "domain": "EmbeddedWebViewBlockedScheme",
314
+ "description": "Blocked navigation to disallowed scheme: \(url)",
315
+ "isProvisional": true,
316
+ ])
317
+ return
318
+ }
319
+
320
+ let id = UUID().uuidString
321
+ pendingNavigationDecisions[id] = decisionHandler
322
+
323
+ let timer = Timer.scheduledTimer(withTimeInterval: navigationDecisionTimeout, repeats: false) { [weak self] _ in
324
+ guard let self = self else { return }
325
+ DispatchQueue.main.async {
326
+ if let handler = self.pendingNavigationDecisions.removeValue(forKey: id) {
327
+ self.navigationTimers.removeValue(forKey: id)?.invalidate()
328
+ handler(.cancel)
329
+ }
330
+ }
331
+ }
332
+ navigationTimers[id] = timer
333
+
334
+ eventEmitter?("onShouldStartLoad", [
335
+ "id": id,
336
+ "url": url,
337
+ "isTopFrame": isTopFrame,
338
+ ])
339
+ }
340
+
341
+ public func webView(
342
+ _: WKWebView,
343
+ decidePolicyFor navigationResponse: WKNavigationResponse,
344
+ decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void
345
+ ) {
346
+ // WKWebView does not fire didFail / didFailProvisionalNavigation for HTTP
347
+ // error status codes — a 404 or 500 just renders an empty page. Surface
348
+ // main-frame HTTP errors as onLoadError so the SDK's load-error path runs.
349
+ if let httpResponse = navigationResponse.response as? HTTPURLResponse,
350
+ navigationResponse.isForMainFrame,
351
+ httpResponse.statusCode >= 400 {
352
+ let url = httpResponse.url?.absoluteString ?? ""
353
+ let description = HTTPURLResponse.localizedString(forStatusCode: httpResponse.statusCode)
354
+ decisionHandler(.cancel)
355
+ eventEmitter?("onLoadError", [
356
+ "url": url,
357
+ "code": httpResponse.statusCode,
358
+ "domain": "EmbeddedWebViewHttpError",
359
+ "description": "HTTP \(httpResponse.statusCode): \(description)",
360
+ "isProvisional": false,
361
+ ])
362
+ return
363
+ }
364
+ decisionHandler(.allow)
365
+ }
366
+
367
+ public func webView(
368
+ _ webView: WKWebView,
369
+ didFailProvisionalNavigation _: WKNavigation!,
370
+ withError error: Error
371
+ ) {
372
+ emitLoadError(error: error as NSError, isProvisional: true, url: webView.url?.absoluteString)
373
+ }
374
+
375
+ public func webView(
376
+ _ webView: WKWebView,
377
+ didFail _: WKNavigation!,
378
+ withError error: Error
379
+ ) {
380
+ emitLoadError(error: error as NSError, isProvisional: false, url: webView.url?.absoluteString)
381
+ }
382
+
383
+ public func webView(_ webView: WKWebView, didCommit _: WKNavigation!) {
384
+ // WKContentView is created lazily once content starts being committed —
385
+ // suppress its input accessory bar here so focusing inputs in the page
386
+ // doesn't show the system "< > Done" toolbar. Matches the
387
+ // `hideKeyboardAccessoryView` setting used by `react-native-webview`.
388
+ hideInputAccessoryView(on: webView)
389
+ }
390
+
391
+ public func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
392
+ eventEmitter?("onLoadError", [
393
+ "url": webView.url?.absoluteString ?? "",
394
+ "code": -1,
395
+ "domain": "EmbeddedWebViewProcessTerminated",
396
+ "description": "WebContent process terminated",
397
+ "isProvisional": false,
398
+ ])
399
+ }
400
+
401
+ // Errors from cancelling our own navigation (e.g. when the JS allowlist
402
+ // returns false or the 2 s decision timeout fires) are not real load
403
+ // failures and should not be instrumented as such.
404
+ private func isCancellationError(_ error: NSError) -> Bool {
405
+ if error.domain == NSURLErrorDomain && error.code == NSURLErrorCancelled {
406
+ return true
407
+ }
408
+ // WebKit "frame load interrupted" — fired when our policy decision cancels
409
+ // the navigation.
410
+ if error.domain == "WebKitErrorDomain" && error.code == 102 {
411
+ return true
412
+ }
413
+ return false
414
+ }
415
+
416
+ private func emitLoadError(error: NSError, isProvisional: Bool, url: String?) {
417
+ if isCancellationError(error) { return }
418
+ eventEmitter?("onLoadError", [
419
+ "url": url ?? "",
420
+ "code": error.code,
421
+ "domain": "EmbeddedWebViewLoadError",
422
+ "description": error.localizedDescription,
423
+ "isProvisional": isProvisional,
424
+ ])
425
+ }
426
+ }
@@ -0,0 +1,62 @@
1
+ import ExpoModulesCore
2
+
3
+ public class EmbeddedWebViewModule: Module {
4
+ private var emitterToken: UUID?
5
+
6
+ public func definition() -> ModuleDefinition {
7
+ Name("EmbeddedWebView")
8
+
9
+ Events("onMessage", "onShouldStartLoad", "onLoadError")
10
+
11
+ OnCreate {
12
+ self.emitterToken = EmbeddedWebViewController.shared.setEmitter { [weak self] name, payload in
13
+ guard let self = self else { return }
14
+ self.sendEvent(name, payload)
15
+ }
16
+ }
17
+
18
+ OnDestroy {
19
+ // Only clear if our emitter is still the live one. Prevents a stale
20
+ // OnDestroy (e.g. during dev hot reload after a newer OnCreate already
21
+ // ran) from nulling the active emitter.
22
+ if let token = self.emitterToken {
23
+ EmbeddedWebViewController.shared.clearEmitter(token: token)
24
+ self.emitterToken = nil
25
+ }
26
+ }
27
+
28
+ AsyncFunction("setUrl") { (url: String) in
29
+ DispatchQueue.main.async {
30
+ EmbeddedWebViewController.shared.setUrl(url)
31
+ }
32
+ }
33
+
34
+ AsyncFunction("setVisible") { (visible: Bool) in
35
+ DispatchQueue.main.async {
36
+ EmbeddedWebViewController.shared.setVisible(visible)
37
+ }
38
+ }
39
+
40
+ AsyncFunction("setDebuggingEnabled") { (enabled: Bool) in
41
+ DispatchQueue.main.async {
42
+ EmbeddedWebViewController.shared.setDebuggingEnabled(enabled)
43
+ }
44
+ }
45
+
46
+ AsyncFunction("destroy") {
47
+ DispatchQueue.main.async {
48
+ EmbeddedWebViewController.shared.destroy()
49
+ }
50
+ }
51
+
52
+ AsyncFunction("postMessage") { (message: String) in
53
+ DispatchQueue.main.async {
54
+ EmbeddedWebViewController.shared.postMessage(message)
55
+ }
56
+ }
57
+
58
+ AsyncFunction("respondToShouldStartLoad") { (id: String, allow: Bool) in
59
+ EmbeddedWebViewController.shared.respondToShouldStartLoad(id: id, allow: allow)
60
+ }
61
+ }
62
+ }
@@ -1,8 +1,8 @@
1
1
  Pod::Spec.new do |s|
2
2
  s.name = 'Keychain'
3
3
  s.version = '1.0.0'
4
- s.summary = 'TEE-backed key operations for Dynamic SDK'
5
- s.description = 'Provides Secure Enclave key generation, signing, and management via Expo Modules'
4
+ s.summary = 'Native iOS modules for Dynamic SDK'
5
+ s.description = 'Provides Secure Enclave keychain operations and an embedded WKWebView host for the Dynamic webview-controller, exposed via Expo Modules.'
6
6
  s.homepage = 'https://www.dynamic.xyz'
7
7
  s.license = { type: 'MIT' }
8
8
  s.author = 'Dynamic Labs'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dynamic-labs/react-native-extension",
3
- "version": "4.81.0",
3
+ "version": "4.83.1",
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.81.0",
22
- "@dynamic-labs/client": "4.81.0",
23
- "@dynamic-labs/logger": "4.81.0",
24
- "@dynamic-labs/message-transport": "4.81.0",
25
- "@dynamic-labs/webview-messages": "4.81.0"
21
+ "@dynamic-labs/assert-package-version": "4.83.1",
22
+ "@dynamic-labs/client": "4.83.1",
23
+ "@dynamic-labs/logger": "4.83.1",
24
+ "@dynamic-labs/message-transport": "4.83.1",
25
+ "@dynamic-labs/webview-messages": "4.83.1"
26
26
  },
27
27
  "peerDependencies": {
28
28
  "react": ">=18.0.0 <20.0.0",
@@ -16,10 +16,33 @@ export type ReactNativeExtensionProps = {
16
16
  * Note: Deep link redirects use the auto-configured redirectUrl from expo-linking.
17
17
  */
18
18
  appOrigin?: string;
19
+ /**
20
+ * When true on iOS / Android, the SDK hosts the webview-controller in a
21
+ * native WKWebView (iOS) or android.webkit.WebView (Android) owned by a
22
+ * dedicated overlay window outside the React Native view tree. This
23
+ * isolates the webview from RN re-renders, navigation transitions, and
24
+ * other lifecycle events.
25
+ *
26
+ * When this flag is on, the `WebView` returned under
27
+ * `extension.reactNative.WebView` is a no-op component — do not render it.
28
+ * The native overlay is created lazily and retained for the process
29
+ * lifetime; subsequent extension factory invocations re-bind the JS bridge
30
+ * to the same native overlay (e.g. when the consumer recreates the client
31
+ * with a new `environmentId`).
32
+ *
33
+ * There is no automatic load recovery on this path. Any HTTP error,
34
+ * network failure, blocked navigation, SSL error, or process termination
35
+ * surfaces as `core.initialization.error` with `WebViewFailedToLoadError`
36
+ * — consumers should treat the SDK as un-initialised and react accordingly.
37
+ *
38
+ * On platforms other than iOS / Android (e.g. web), this flag is ignored.
39
+ * Defaults to false.
40
+ */
41
+ embeddedWebView?: boolean;
19
42
  };
20
43
  export type IReactNativeExtension = {
21
44
  reactNative: {
22
45
  WebView: FC<WebViewProps> | (() => null);
23
46
  };
24
47
  };
25
- export declare const ReactNativeExtension: ({ webviewUrl, webviewDebuggingEnabled, appOrigin, }?: ReactNativeExtensionProps) => Extension<IReactNativeExtension>;
48
+ export declare const ReactNativeExtension: ({ webviewUrl, webviewDebuggingEnabled, appOrigin, embeddedWebView, }?: ReactNativeExtensionProps) => Extension<IReactNativeExtension>;
@@ -0,0 +1,7 @@
1
+ import { Core } from '@dynamic-labs/client';
2
+ export type SetupEmbeddedWebViewArgs = {
3
+ webviewUrl: string;
4
+ core: Core;
5
+ webviewDebuggingEnabled?: boolean;
6
+ };
7
+ export declare const setupEmbeddedWebView: ({ webviewUrl, core, webviewDebuggingEnabled, }: SetupEmbeddedWebViewArgs) => (() => void);
@@ -0,0 +1 @@
1
+ export * from './EmbeddedWebView';
@@ -0,0 +1 @@
1
+ export * from './shouldAllowNavigation';
@@ -0,0 +1,5 @@
1
+ export type NavigationRequest = {
2
+ url: string;
3
+ isTopFrame: boolean;
4
+ };
5
+ export declare const shouldAllowNavigation: (request: NavigationRequest, webViewUrl: URL) => boolean;
@@ -1,3 +1,86 @@
1
+ /**
2
+ * Categorical phase the WebView was in when the failure was raised.
3
+ *
4
+ * Use this for high-level grouping; the duration fields on
5
+ * {@link WebViewFailedToLoadErrorMeta} give the granular timing data.
6
+ */
7
+ export type WebViewFailedToLoadErrorPhase = 'html_load' | 'sdk_bootstrap' | 'after_clear_state' | 'native_error' | 'embedded_native_error' | 'unknown';
8
+ /**
9
+ * Structured context attached to {@link WebViewFailedToLoadError} so the
10
+ * resulting error log carries enough information to tell *which* step the
11
+ * WebView was stuck in when the failure was raised, and how long each step
12
+ * took.
13
+ *
14
+ * The setter on `InitializationModule.error` forwards this object directly
15
+ * to `logger.error`, so every field below ends up as a structured field on
16
+ * the same Datadog log as the error message.
17
+ */
18
+ export type WebViewFailedToLoadErrorMeta = {
19
+ phase: WebViewFailedToLoadErrorPhase;
20
+ /**
21
+ * Milliseconds between RN's `onLoadStart` and `onLoad` callbacks (the
22
+ * HTML + asset fetch). `null` if `onLoad` never fired.
23
+ */
24
+ htmlLoadMs: number | null;
25
+ /**
26
+ * Milliseconds between RN's `onLoad` and `onLoadEnd` callbacks. Usually
27
+ * small; mostly there to flag pathological cases. `null` if `onLoadEnd`
28
+ * never fired.
29
+ */
30
+ onLoadToOnLoadEndMs: number | null;
31
+ /**
32
+ * Milliseconds between `onLoadEnd` and the first `manifest` request
33
+ * received from the webview-side SDK. Proxy for "the JS bundle is alive
34
+ * and the message bridge works". `null` if the manifest request never
35
+ * arrived.
36
+ */
37
+ manifestReceivedMs: number | null;
38
+ /**
39
+ * Milliseconds between `onLoadEnd` and the webview-side SDK signalling
40
+ * it has finished initialising (via the `sdkHasLoadedEventName` store
41
+ * change). `null` if the SDK never signalled it was ready.
42
+ */
43
+ sdkReadyMs: number | null;
44
+ /**
45
+ * Number of `retry` increments in the WebView URL at the time of failure
46
+ * (covers OS-kill reloads, load_error reloads, and recovery reloads).
47
+ */
48
+ retryCount: number;
49
+ /**
50
+ * Whether the `clear-state` query parameter was present in the URL when
51
+ * the failure was raised. `true` indicates we already retried with a
52
+ * clean state and still couldn't finish loading.
53
+ */
54
+ hadClearState: boolean;
55
+ /**
56
+ * Number of times RN's `onError` fired across the lifetime of the
57
+ * WebView component instance.
58
+ */
59
+ nativeErrorCount: number;
60
+ /**
61
+ * Number of times the OS killed the WebView process (via
62
+ * `onContentProcessDidTerminate` on iOS or `onRenderProcessGone` on
63
+ * Android) across the lifetime of the WebView component instance.
64
+ */
65
+ osKillCount: number;
66
+ /** The configured value for `loadingTimeout` (ms). */
67
+ loadingTimeoutMs: number;
68
+ /** The configured value for `recoveryTimeout` (ms). */
69
+ recoveryTimeoutMs: number;
70
+ /** The URL the WebView was attempting to load when the failure occurred. */
71
+ webviewUrl?: string;
72
+ /**
73
+ * Native error info, only populated when the failure originated from the
74
+ * embedded native WebView path (iOS WKWebView / Android WebView), where
75
+ * we get a single `onLoadError` event with these fields.
76
+ */
77
+ nativeErrorCode?: number;
78
+ nativeErrorDescription?: string;
79
+ nativeErrorDomain?: string;
80
+ nativeErrorIsProvisional?: boolean;
81
+ nativeErrorFailedUrl?: string;
82
+ };
1
83
  export declare class WebViewFailedToLoadError extends Error {
2
- constructor();
84
+ readonly meta?: WebViewFailedToLoadErrorMeta;
85
+ constructor(meta?: WebViewFailedToLoadErrorMeta);
3
86
  }