@dynamic-labs/react-native-extension 4.81.0 → 4.83.0
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 +476 -0
- package/android/EmbeddedWebViewModule.kt +55 -0
- package/android/dynamic/embeddedwebview/EmbeddedWebViewController.kt +476 -0
- package/android/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +55 -0
- package/android/embeddedwebview/EmbeddedWebViewController.kt +476 -0
- package/android/embeddedwebview/EmbeddedWebViewModule.kt +55 -0
- package/android/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +476 -0
- package/android/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +55 -0
- package/android/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +476 -0
- package/android/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +55 -0
- package/android/src/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +476 -0
- package/android/src/main/java/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +55 -0
- package/android/xyz/dynamic/embeddedwebview/EmbeddedWebViewController.kt +476 -0
- package/android/xyz/dynamic/embeddedwebview/EmbeddedWebViewModule.kt +55 -0
- package/expo-module.config.json +5 -2
- package/index.cjs +172 -39
- package/index.js +172 -39
- package/ios/EmbeddedWebViewController.swift +426 -0
- package/ios/EmbeddedWebViewModule.swift +62 -0
- package/ios/Keychain.podspec +2 -2
- package/package.json +6 -6
- package/src/ReactNativeExtension/ReactNativeExtension.d.ts +24 -1
- package/src/components/WebView/EmbeddedWebView/EmbeddedWebView.d.ts +7 -0
- package/src/components/WebView/EmbeddedWebView/index.d.ts +1 -0
- package/src/components/WebView/utils/shouldAllowNavigation/index.d.ts +1 -0
- package/src/components/WebView/utils/shouldAllowNavigation/shouldAllowNavigation.d.ts +5 -0
- 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
|
+
}
|
package/ios/Keychain.podspec
CHANGED
|
@@ -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 = '
|
|
5
|
-
s.description = 'Provides Secure Enclave
|
|
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.
|
|
3
|
+
"version": "4.83.0",
|
|
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.
|
|
22
|
-
"@dynamic-labs/client": "4.
|
|
23
|
-
"@dynamic-labs/logger": "4.
|
|
24
|
-
"@dynamic-labs/message-transport": "4.
|
|
25
|
-
"@dynamic-labs/webview-messages": "4.
|
|
21
|
+
"@dynamic-labs/assert-package-version": "4.83.0",
|
|
22
|
+
"@dynamic-labs/client": "4.83.0",
|
|
23
|
+
"@dynamic-labs/logger": "4.83.0",
|
|
24
|
+
"@dynamic-labs/message-transport": "4.83.0",
|
|
25
|
+
"@dynamic-labs/webview-messages": "4.83.0"
|
|
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,29 @@
|
|
|
1
|
+
export type EmbeddedWebViewSubscription = {
|
|
2
|
+
remove: () => void;
|
|
3
|
+
};
|
|
4
|
+
export type EmbeddedWebViewOnMessageEvent = {
|
|
5
|
+
message: string;
|
|
6
|
+
};
|
|
7
|
+
export type EmbeddedWebViewOnShouldStartLoadEvent = {
|
|
8
|
+
id: string;
|
|
9
|
+
url: string;
|
|
10
|
+
isTopFrame: boolean;
|
|
11
|
+
};
|
|
12
|
+
export type EmbeddedWebViewOnLoadErrorEvent = {
|
|
13
|
+
url: string;
|
|
14
|
+
code: number;
|
|
15
|
+
domain: string;
|
|
16
|
+
description: string;
|
|
17
|
+
isProvisional: boolean;
|
|
18
|
+
};
|
|
19
|
+
export type EmbeddedWebViewEventName = 'onMessage' | 'onShouldStartLoad' | 'onLoadError';
|
|
20
|
+
export type EmbeddedWebViewNativeModule = {
|
|
21
|
+
setUrl: (url: string) => Promise<void>;
|
|
22
|
+
setVisible: (visible: boolean) => Promise<void>;
|
|
23
|
+
setDebuggingEnabled: (enabled: boolean) => Promise<void>;
|
|
24
|
+
destroy: () => Promise<void>;
|
|
25
|
+
postMessage: (message: string) => Promise<void>;
|
|
26
|
+
respondToShouldStartLoad: (id: string, allow: boolean) => Promise<void>;
|
|
27
|
+
addListener: <T extends EmbeddedWebViewOnMessageEvent | EmbeddedWebViewOnShouldStartLoadEvent | EmbeddedWebViewOnLoadErrorEvent>(eventName: EmbeddedWebViewEventName, listener: (event: T) => void) => EmbeddedWebViewSubscription;
|
|
28
|
+
};
|
|
29
|
+
export declare const getEmbeddedWebView: () => EmbeddedWebViewNativeModule;
|