207 lines
7.3 KiB
Swift
207 lines
7.3 KiB
Swift
// Keep in sync with webview-wrapper.tsx
|
|
// https://github.com/expo/expo/blob/main/packages/expo/src/dom/webview-wrapper.tsx
|
|
#if canImport(UIKit) && EXPO_UNSTABLE_LOG_BOX
|
|
|
|
import UIKit
|
|
import WebKit
|
|
import React
|
|
|
|
protocol ExpoLogBoxNativeActionsProtocol {
|
|
func onReload() -> Void
|
|
func fetchTextAsync(url: String, method: String?, body: String?) async -> String
|
|
}
|
|
|
|
private class ExpoLogBoxNativeActions: ExpoLogBoxNativeActionsProtocol {
|
|
func onReload() -> Void {
|
|
fatalError()
|
|
}
|
|
func fetchTextAsync(url: String, method: String?, body: String?) async -> String {
|
|
fatalError()
|
|
}
|
|
static let onReloadName = "onReload"
|
|
static let fetchTextAsyncName = "fetchTextAsync"
|
|
static let names = [
|
|
onReloadName,
|
|
fetchTextAsyncName,
|
|
]
|
|
}
|
|
|
|
private struct Constants {
|
|
static let DOM_EVENT = "$$dom_event"
|
|
static let NATIVE_ACTION_RESULT = "$$native_action_result"
|
|
static let NATIVE_ACTION = "$$native_action"
|
|
}
|
|
|
|
class ExpoLogBoxWebViewWrapper: NSObject, WKScriptMessageHandler {
|
|
private let nativeMessageHandlerName = "nativeHandler"
|
|
private let nativeActions: ExpoLogBoxNativeActionsProtocol
|
|
private let props: [String: Any]
|
|
private let webView: WKWebView
|
|
|
|
init(nativeActions: ExpoLogBoxNativeActionsProtocol, props: [String: Any]) {
|
|
self.nativeActions = nativeActions
|
|
self.props = props
|
|
self.webView = WKWebView(frame: .zero)
|
|
}
|
|
|
|
func prepareWebView() -> WKWebView {
|
|
let initProps: [String: Any] = [
|
|
"names": ExpoLogBoxNativeActions.names,
|
|
"props": props
|
|
]
|
|
|
|
guard let initPropsObject = try? JSONSerialization.data(withJSONObject: initProps, options: []),
|
|
let initPropsStringified = String(data: initPropsObject, encoding: .utf8) else {
|
|
fatalError("Failed to serialize initProps. This is an issue in ExpoLogBox. Please report it.")
|
|
}
|
|
|
|
let devServerOrigin: String? = {
|
|
guard let bundleUrl = RCTBundleURLProvider.sharedSettings()
|
|
.jsBundleURL(forBundleRoot: "unused.name"),
|
|
let scheme = bundleUrl.scheme,
|
|
let host = bundleUrl.host,
|
|
let port = bundleUrl.port
|
|
else {
|
|
return nil
|
|
}
|
|
return "\(scheme)://\(host):\(port)"
|
|
}()
|
|
let devServerOriginJsValue: String = devServerOrigin.map { "'\($0)'" } ?? "undefined"
|
|
|
|
let injectJavascript = """
|
|
var process = globalThis.process || {};
|
|
process.env = process.env || {};
|
|
process.env.EXPO_DEV_SERVER_ORIGIN = \(devServerOriginJsValue);
|
|
window.$$EXPO_DOM_HOST_OS = 'ios';
|
|
window.$$EXPO_INITIAL_PROPS = \(initPropsStringified);
|
|
window.ReactNativeWebView = {};
|
|
window.ReactNativeWebView.postMessage = (message) => {
|
|
window.webkit.messageHandlers.\(nativeMessageHandlerName).postMessage(
|
|
JSON.parse(message)
|
|
);
|
|
};
|
|
"""
|
|
|
|
webView.configuration.userContentController.addUserScript(WKUserScript(
|
|
source: injectJavascript,
|
|
injectionTime: .atDocumentStart,
|
|
forMainFrameOnly: true
|
|
))
|
|
if #available(iOS 16.4, *) {
|
|
#if EXPO_DEBUG_LOG_BOX || EXPO_DEVELOP_LOG_BOX
|
|
webView.isInspectable = true
|
|
#else
|
|
webView.isInspectable = false
|
|
#endif
|
|
}
|
|
webView.configuration.userContentController.add(self, name: nativeMessageHandlerName)
|
|
webView.translatesAutoresizingMaskIntoConstraints = false
|
|
webView.isOpaque = false
|
|
return webView
|
|
}
|
|
|
|
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
|
Task.detached {
|
|
await self.handleWebViewMessageAsync(message: message)
|
|
}
|
|
}
|
|
|
|
func handleWebViewMessageAsync(message: WKScriptMessage) async {
|
|
guard message.name == nativeMessageHandlerName,
|
|
let messageBody = message.body as? [String: Any],
|
|
let messageType = messageBody["type"] as? String else {
|
|
return
|
|
}
|
|
|
|
let data = messageBody["data"] as? [String: Any] ?? [:]
|
|
|
|
if (messageType == Constants.NATIVE_ACTION) {
|
|
guard let actionId = data["actionId"] as? String,
|
|
let uid = data["uid"] as? String,
|
|
let args = data["args"] as? [Any] else {
|
|
print("ExpoLogBoxDomRuntimeError native actions is missing actionId or uid.")
|
|
return
|
|
}
|
|
|
|
do {
|
|
switch actionId {
|
|
case ExpoLogBoxNativeActions.onReloadName:
|
|
nativeActions.onReload()
|
|
case ExpoLogBoxNativeActions.fetchTextAsyncName:
|
|
guard let url = args[0] as? String,
|
|
let options = args[1] as? [String: Any] else {
|
|
print("ExpoLogBox fetchTextAsync action is missing url or options.")
|
|
return
|
|
}
|
|
|
|
let method = options["method"] as? String
|
|
let body = options["body"] as? String
|
|
let result = await nativeActions.fetchTextAsync(url: url, method: method, body: body)
|
|
sendReturn(result: result, uid: uid, actionId: actionId)
|
|
default:
|
|
print("Unknown native action: \(actionId)")
|
|
}
|
|
} catch {
|
|
sendReturn(error: error, uid: uid, actionId: actionId)
|
|
}
|
|
} else {
|
|
print("Unknown message type: \(messageType)")
|
|
}
|
|
}
|
|
|
|
func sendReturn(result: Any, uid: String, actionId: String) {
|
|
sendReturn(data: [
|
|
"type": Constants.NATIVE_ACTION_RESULT,
|
|
"data": [
|
|
"uid": uid,
|
|
"actionId": actionId,
|
|
"result": result,
|
|
],
|
|
])
|
|
}
|
|
|
|
func sendReturn(error: Any, uid: String, actionId: String) {
|
|
sendReturn(data: [
|
|
"type": Constants.NATIVE_ACTION_RESULT,
|
|
"data": [
|
|
"uid": uid,
|
|
"actionId": actionId,
|
|
"error": [
|
|
"message": "\(error)",
|
|
],
|
|
],
|
|
])
|
|
}
|
|
|
|
func sendReturn(data: [String: Any]) {
|
|
guard let jsonData = try? JSONSerialization.data(
|
|
withJSONObject: [ "detail": data ],
|
|
options: []
|
|
), let jsonDataStringified = String(data: jsonData, encoding: .utf8) else {
|
|
print("ExpoLogBox failed to stringify native action result.")
|
|
return
|
|
}
|
|
sendReturn(value: jsonDataStringified)
|
|
}
|
|
|
|
func sendReturn(value: String) {
|
|
let injectedJavascript = """
|
|
;
|
|
(function() {
|
|
try {
|
|
console.log("received", \(value));
|
|
window.dispatchEvent(new CustomEvent("\(Constants.DOM_EVENT)", \(value)));
|
|
} catch (e) {}
|
|
})();
|
|
true;
|
|
"""
|
|
webView.evaluateJavaScript(injectedJavascript) { _, error in
|
|
if let error = error {
|
|
print("ExpoLogBox NativeActions return value injection error: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#endif
|