first commit

This commit is contained in:
2026-03-10 16:18:05 +00:00
commit 11f9c069b5
31635 changed files with 3187747 additions and 0 deletions

225
node_modules/@expo/dom-webview/ios/DomWebView.swift generated vendored Normal file
View File

@@ -0,0 +1,225 @@
// Copyright 2015-present 650 Industries. All rights reserved.
import ExpoModulesCore
import WebKit
internal final class DomWebView: ExpoView, UIScrollViewDelegate, WKUIDelegate, WKScriptMessageHandler {
// swiftlint:disable implicitly_unwrapped_optional
private(set) var webView: WKWebView!
private(set) var id: WebViewId!
// swiftlint:enable implicitly_unwrapped_optional
private var source: DomWebViewSource?
private var injectedJSBeforeContentLoaded: WKUserScript?
var webviewDebuggingEnabled = false
var decelerationRate: UIScrollView.DecelerationRate = .normal
internal typealias SyncCompletionHandler = (String?) -> Void
private var needsResetupScripts = false
private static let EVAL_PROMPT_HEADER = "__EXPO_DOM_WEBVIEW_JS_EVAL__"
private static let POST_MESSAGE_HANDLER_NAME = "ReactNativeWebView"
private let onMessage = EventDispatcher()
required init(appContext: AppContext? = nil) {
super.init(appContext: appContext)
super.backgroundColor = .clear
self.id = DomWebViewRegistry.shared.add(webView: self)
webView = createWebView()
resetupScripts()
addSubview(webView)
}
override func layoutSubviews() {
super.layoutSubviews()
webView.frame = bounds
}
override var backgroundColor: UIColor? {
get { webView.backgroundColor }
set {
let isOpaque = (newValue ?? UIColor.clear).cgColor.alpha == 1.0
self.isOpaque = isOpaque
webView.isOpaque = isOpaque
webView.scrollView.backgroundColor = newValue
webView.backgroundColor = newValue
}
}
override func removeFromSuperview() {
webView.removeFromSuperview()
webView = nil
DomWebViewRegistry.shared.remove(webViewId: self.id)
super.removeFromSuperview()
}
// MARK: - Public methods
func reload() {
if #available(iOS 16.4, *) {
webView.isInspectable = webviewDebuggingEnabled
}
if needsResetupScripts {
resetupScripts()
needsResetupScripts = false
}
if let source,
let request = RCTConvert.nsurlRequest(source.toDictionary(appContext: appContext)),
webView.url?.absoluteURL != request.url {
webView.load(request)
}
}
func scrollTo(offset: CGPoint, animated: Bool) {
webView.scrollView.setContentOffset(offset, animated: animated)
}
func injectJavaScript(_ script: String) {
DispatchQueue.main.async { [weak self] in
self?.webView.evaluateJavaScript(script)
}
}
func setSource(_ source: DomWebViewSource) {
self.source = source
}
func setInjectedJSBeforeContentLoaded(_ script: String?) {
if let script, !script.isEmpty {
injectedJSBeforeContentLoaded = WKUserScript(source: script, injectionTime: .atDocumentStart, forMainFrameOnly: false)
} else {
injectedJSBeforeContentLoaded = nil
}
needsResetupScripts = true
}
// MARK: - UIScrollViewDelegate implementations
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
scrollView.decelerationRate = decelerationRate
}
// MARK: - WKUIDelegate implementations
func webView(
_ webView: WKWebView,
runJavaScriptTextInputPanelWithPrompt prompt: String,
defaultText: String?,
initiatedByFrame frame: WKFrameInfo,
completionHandler: @escaping SyncCompletionHandler
) {
if !prompt.hasPrefix(Self.EVAL_PROMPT_HEADER) {
completionHandler(nil)
return
}
let script = String(prompt.dropFirst(Self.EVAL_PROMPT_HEADER.count))
if let data = script.data(using: .utf8),
let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
let deferredId = json["deferredId"] as? Int,
let source = json["source"] as? String {
nativeJsiEvalSync(deferredId: deferredId, source: source, completionHandler: completionHandler)
} else {
completionHandler("Invalid parameters for nativeJsiEvalSync")
}
}
// MARK: - WKScriptMessageHandler implementations
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if message.name == Self.POST_MESSAGE_HANDLER_NAME {
var payload = createBaseEventPayload()
payload["data"] = message.body
onMessage(payload)
return
}
}
// MARK: - Internals
private func createWebView() -> WKWebView {
let config = WKWebViewConfiguration()
config.userContentController = WKUserContentController()
let webView = WKWebView(frame: .zero, configuration: config)
webView.uiDelegate = self
webView.backgroundColor = .clear
webView.scrollView.delegate = self
return webView
}
private func createBaseEventPayload() -> [String: Any] {
return [
"url": webView.url?.absoluteString ?? "",
"title": webView.title ?? ""
]
}
private func resetupScripts() {
let userContentController = webView.configuration.userContentController
userContentController.removeAllUserScripts()
userContentController.removeAllScriptMessageHandlers()
userContentController.add(self, name: Self.POST_MESSAGE_HANDLER_NAME)
if let injectedJSBeforeContentLoaded {
userContentController.addUserScript(injectedJSBeforeContentLoaded)
}
let addDomWebViewBridgeScript = """
window.ExpoDomWebViewBridge = {
eval: function eval(params) {
return window.prompt('\(Self.EVAL_PROMPT_HEADER)' + params);
},
};
true;
"""
userContentController.addUserScript(WKUserScript(source: addDomWebViewBridgeScript, injectionTime: .atDocumentStart, forMainFrameOnly: false))
let addRNWObjectScript = """
window.ReactNativeWebView ||= {};
window.ReactNativeWebView.postMessage = function postMessage(data) {
window.webkit.messageHandlers.\(Self.POST_MESSAGE_HANDLER_NAME).postMessage(String(data));
};
true;
"""
userContentController.addUserScript(WKUserScript(source: addRNWObjectScript, injectionTime: .atDocumentStart, forMainFrameOnly: false))
guard let webViewId = self.id else {
return
}
let addExpoDomWebViewObjectScript = "\(INSTALL_GLOBALS_SCRIPT);true;"
.replacingOccurrences(of: "\"%%WEBVIEW_ID%%\"", with: String(webViewId))
userContentController.addUserScript(WKUserScript(source: addExpoDomWebViewObjectScript, injectionTime: .atDocumentStart, forMainFrameOnly: false))
}
private func nativeJsiEvalSync(deferredId: Int, source: String, completionHandler: @escaping SyncCompletionHandler) {
guard let appContext else {
completionHandler("Missing AppContext")
return
}
guard let webViewId = self.id else {
completionHandler("Missing webViewId")
return
}
guard let runtime = try? appContext.runtime else {
completionHandler("Missing JS Runtime")
return
}
try? appContext.runtime.schedule {
let wrappedSource = NATIVE_EVAL_WRAPPER_SCRIPT
.replacingOccurrences(of: "\"%%DEFERRED_ID%%\"", with: String(deferredId))
.replacingOccurrences(of: "\"%%WEBVIEW_ID%%\"", with: String(webViewId))
.replacingOccurrences(of: "\"%%SOURCE%%\"", with: source)
do {
let result = try runtime.eval(wrappedSource)
completionHandler(result.getString())
} catch {
completionHandler("\(error)")
}
}
}
}

View File

@@ -0,0 +1,266 @@
// Copyright 2015-present 650 Industries. All rights reserved.
// swiftlint:disable line_length
/**
* This file contains the browser scripts that are injected into the WebView.
* @generated by buildBrowserScripts.ts
*/
internal let INSTALL_GLOBALS_SCRIPT: String = """
// browserScripts/InstallGlobals/Deferred.ts
class Deferred {
promise;
resolveCallback;
rejectCallback;
constructor() {
this.promise = new Promise((resolve, reject) => {
this.resolveCallback = resolve;
this.rejectCallback = reject;
});
}
resolve(value) {
this.resolveCallback(value);
}
reject(reason) {
this.rejectCallback(reason);
}
getPromise() {
return this.promise;
}
}
// browserScripts/InstallGlobals/EventEmitterProxy.ts
class EventEmitterProxy {
moduleName;
listeners;
constructor(moduleName) {
this.moduleName = moduleName;
}
addListener = (eventName, listener) => {
if (!this.listeners) {
this.listeners = new Map;
}
if (!this.listeners.has(eventName)) {
this.listeners.set(eventName, new Set);
}
this.listeners.get(eventName)?.add(listener);
const nativeListenerId = window.ExpoDomWebView.nextEventListenerId++;
listener.$$nativeListenerId = nativeListenerId;
const source = `
globalThis.expo.$$DomWebViewEventListenerMap ||= {};
globalThis.expo.$$DomWebViewEventListenerMap['${eventName}'] ||= new Map();
const listener = (...args) => {
const serializeArgs = args.map((arg) => JSON.stringify(arg)).join(',');
const script = 'window.ExpoDomWebView.eventEmitterProxy.${this.moduleName}.emit("${eventName}", ' + serializeArgs + ')';
globalThis.expo.modules.ExpoDomWebViewModule.evalJsForWebViewAsync("%%WEBVIEW_ID%%", script);
};
globalThis.expo.$$DomWebViewEventListenerMap['${eventName}'].set(${nativeListenerId}, listener);
globalThis.expo.modules.${this.moduleName}.addListener('${eventName}', listener);
`;
window.ExpoDomWebView.eval(source);
return {
remove: () => {
this.removeListener(eventName, listener);
}
};
};
removeListener = (eventName, listener) => {
const nativeListenerId = listener.$$nativeListenerId;
if (nativeListenerId != null) {
const source = `(function() {
const nativeListener = globalThis.expo.$$DomWebViewEventListenerMap['${eventName}'].get(${nativeListenerId});
if (nativeListener != null) {
globalThis.expo.modules.${this.moduleName}.removeListener('${eventName}', nativeListener);
globalThis.expo.$$DomWebViewEventListenerMap['${eventName}'].delete(${nativeListenerId});
}
})();
true;
`;
window.ExpoDomWebView.eval(source);
}
this.listeners?.get(eventName)?.delete(listener);
};
removeAllListeners = (eventName) => {
const source = `
globalThis.expo.$$DomWebViewEventListenerMap['${eventName}'].clear();
globalThis.expo.modules.${this.moduleName}.removeAllListeners('${eventName}');
`;
window.ExpoDomWebView.eval(source);
this.listeners?.get(eventName)?.clear();
};
emit = (eventName, ...args) => {
const listeners = new Set(this.listeners?.get(eventName));
listeners.forEach((listener) => {
try {
listener(...args);
} catch (error) {
console.error(error);
}
});
};
}
// browserScripts/InstallGlobals/utils.ts
function serializeArgs(args) {
return args.map((arg) => {
if (typeof arg === "object" && arg.sharedObjectId != null) {
return `globalThis.expo.sharedObjectRegistry.get(${arg.sharedObjectId})`;
}
return JSON.stringify(arg);
}).join(",");
}
// browserScripts/InstallGlobals/proxies.ts
function createSharedObjectProxy(sharedObjectId) {
return new Proxy({}, {
get: (target, prop) => {
const name = String(prop);
if (name === "sharedObjectId") {
return sharedObjectId;
}
return function(...args) {
const serializedArgs = serializeArgs(args);
const source = `globalThis.expo.sharedObjectRegistry.get(${sharedObjectId})?.${name}?.call(globalThis.expo.sharedObjectRegistry.get(${sharedObjectId}),${serializedArgs})`;
return window.ExpoDomWebView.eval(source);
};
}
});
}
function createConstructorProxy(moduleName, property, propertyName) {
return new Proxy(function() {
}, {
construct(target, args) {
const serializedArgs = serializeArgs(args);
const sharedObjectId = window.ExpoDomWebView.nextSharedObjectId++;
const sharedObjectProxy = createSharedObjectProxy(sharedObjectId);
window.ExpoDomWebView.sharedObjectFinalizationRegistry.register(sharedObjectProxy, sharedObjectId);
const source = `globalThis.expo.sharedObjectRegistry ||= new Map(); globalThis.expo.sharedObjectRegistry.set(${sharedObjectId}, new ${property}(${serializedArgs}));`;
window.ExpoDomWebView.eval(source);
return sharedObjectProxy;
}
});
}
function createPropertyProxy(propertyTypeCache, moduleName, propertyName) {
const property = `globalThis.expo.modules.${moduleName}.${propertyName}`;
let propertyType = propertyTypeCache[propertyName];
if (!propertyType) {
const typeCheck = `${property}?.prototype?.__expo_shared_object_id__ != null ? 'sharedObject' : typeof ${property}`;
propertyType = window.ExpoDomWebView.eval(typeCheck);
propertyTypeCache[propertyName] = propertyType;
}
if (propertyType === "sharedObject") {
return createConstructorProxy(moduleName, property, propertyName);
}
if (propertyType === "function") {
return function(...args) {
const serializedArgs = serializeArgs(args);
const source = `${property}(${serializedArgs})`;
return window.ExpoDomWebView.eval(source);
};
}
return window.ExpoDomWebView.eval(property);
}
function createExpoModuleProxy(moduleName) {
const propertyTypeCache = {};
return new Proxy({}, {
get: (target, prop) => {
const name = String(prop);
if (["addListener", "removeListener", "removeAllListeners"].includes(name)) {
return window.ExpoDomWebView.eventEmitterProxy[moduleName][name];
}
return createPropertyProxy(propertyTypeCache, moduleName, name);
}
});
}
// browserScripts/InstallGlobals/ExpoDomWebView.ts
class ExpoDomWebView {
nextDeferredId;
nextSharedObjectId;
nextEventListenerId;
deferredMap;
sharedObjectFinalizationRegistry;
expoModulesProxy;
eventEmitterProxy;
constructor() {
this.nextDeferredId = 0;
this.nextSharedObjectId = 0;
this.nextEventListenerId = 0;
this.deferredMap = new Map;
this.sharedObjectFinalizationRegistry = new FinalizationRegistry((sharedObjectId) => {
this.eval(`globalThis.expo.sharedObjectRegistry.delete(${sharedObjectId})`);
});
const expoModules = {};
const eventEmitterProxy = {};
this.eval("Object.keys(globalThis.expo.modules)").forEach((name) => {
expoModules[name] = createExpoModuleProxy(name);
eventEmitterProxy[name] = new EventEmitterProxy(name);
});
this.expoModulesProxy = expoModules;
this.eventEmitterProxy = eventEmitterProxy;
}
eval(source) {
const { deferredId, deferred } = this.createDeferred();
const args = JSON.stringify({ source, deferredId });
const result = JSON.parse(window.ExpoDomWebViewBridge.eval(args));
if (result.isPromise) {
return deferred.getPromise();
}
this.removeDeferred(deferredId);
return result.value;
}
createDeferred() {
const deferred = new Deferred;
const deferredId = this.nextDeferredId;
this.deferredMap.set(deferredId, deferred);
this.nextDeferredId += 1;
return { deferredId, deferred };
}
resolveDeferred(deferredId, value) {
const deferred = this.deferredMap.get(deferredId);
if (deferred) {
deferred.resolve(value);
this.deferredMap.delete(deferredId);
}
}
rejectDeferred(deferredId, reason) {
const deferred = this.deferredMap.get(deferredId);
if (deferred) {
deferred.reject(reason);
this.deferredMap.delete(deferredId);
}
}
removeDeferred(deferredId) {
this.deferredMap.delete(deferredId);
}
}
// browserScripts/InstallGlobals/index.ts
window.ExpoDomWebView = new ExpoDomWebView;
"""
internal let NATIVE_EVAL_WRAPPER_SCRIPT: String = """
// browserScripts/NativeEvalWrapper/index.ts
(function() {
const result = "%%SOURCE%%";
if (result instanceof Promise) {
result.then((resolved) => {
const resolvedString = JSON.stringify(resolved);
const script = 'window.ExpoDomWebView.resolveDeferred("%%DEFERRED_ID%%", ' + resolvedString + ")";
globalThis.expo.modules.ExpoDomWebViewModule.evalJsForWebViewAsync("%%WEBVIEW_ID%%", script);
}).catch((error) => {
const errorString = JSON.stringify(error);
const script = 'window.ExpoDomWebView.rejectDeferred("%%DEFERRED_ID%%", ' + errorString + ")";
globalThis.expo.modules.ExpoDomWebViewModule.evalJsForWebViewAsync("%%WEBVIEW_ID%%", script);
});
return JSON.stringify({ isPromise: true, value: null });
} else {
return JSON.stringify({ isPromise: false, value: result });
}
})();
"""
// swiftlint:enable line_length

View File

@@ -0,0 +1,23 @@
// Copyright 2015-present 650 Industries. All rights reserved.
import ExpoModulesCore
internal enum ContentInsetAdjustmentBehavior: String, Enumerable {
case automatic
case scrollableAxes
case never
case always
func toContentInsetAdjustmentBehavior() -> UIScrollView.ContentInsetAdjustmentBehavior {
switch self {
case .automatic:
return .automatic
case .scrollableAxes:
return .scrollableAxes
case .never:
return .never
case .always:
return .always
}
}
}

View File

@@ -0,0 +1,105 @@
// Copyright 2015-present 650 Industries. All rights reserved.
import ExpoModulesCore
public final class DomWebViewModule: Module {
public func definition() -> ModuleDefinition {
Name("ExpoDomWebViewModule")
OnDestroy {
DomWebViewRegistry.shared.reset()
}
AsyncFunction("evalJsForWebViewAsync") { (webViewId: Int, source: String) in
if let webView = DomWebViewRegistry.shared.get(webViewId: webViewId) {
webView.injectJavaScript(source)
}
}
// swiftlint:disable closure_body_length
View(DomWebView.self) {
Events("onMessage")
Prop("source") { (view: DomWebView, source: DomWebViewSource) in
view.setSource(source)
}
Prop("injectedJavaScriptBeforeContentLoaded") { (view: DomWebView, script: String) in
view.setInjectedJSBeforeContentLoaded(script)
}
Prop("webviewDebuggingEnabled") { (view: DomWebView, enabled: Bool) in
view.webviewDebuggingEnabled = enabled
}
// MARK: - IosScrollViewProps
Prop("bounces") { (view: DomWebView, enabled: Bool) in
view.webView.scrollView.bounces = enabled
}
Prop("decelerationRate") { (view: DomWebView, decelerationRate: Either<String, Double>) in
var newDecelerationRate: UIScrollView.DecelerationRate?
if let rateString: String = decelerationRate.get() {
if rateString == "normal" {
newDecelerationRate = .normal
} else if rateString == "fast" {
newDecelerationRate = .fast
}
} else if let rate: Double = decelerationRate.get() {
newDecelerationRate = UIScrollView.DecelerationRate(rawValue: rate)
}
if let newDecelerationRate {
view.decelerationRate = newDecelerationRate
}
}
Prop("scrollEnabled") { (view: DomWebView, enabled: Bool) in
view.webView.scrollView.isScrollEnabled = enabled
}
Prop("pagingEnabled") { (view: DomWebView, enabled: Bool) in
view.webView.scrollView.isPagingEnabled = enabled
}
Prop("automaticallyAdjustsScrollIndicatorInsets") { (view: DomWebView, enabled: Bool) in
view.webView.scrollView.automaticallyAdjustsScrollIndicatorInsets = enabled
}
Prop("contentInset") { (view: DomWebView, inset: ContentInset) in
view.webView.scrollView.contentInset = inset.toEdgeInsets()
}
Prop("contentInsetAdjustmentBehavior") { (view: DomWebView, value: ContentInsetAdjustmentBehavior) in
view.webView.scrollView.contentInsetAdjustmentBehavior = value.toContentInsetAdjustmentBehavior()
}
Prop("directionalLockEnabled") { (view: DomWebView, enabled: Bool) in
view.webView.scrollView.isDirectionalLockEnabled = enabled
}
Prop("showsHorizontalScrollIndicator") { (view: DomWebView, enabled: Bool) in
view.webView.scrollView.showsHorizontalScrollIndicator = enabled
}
Prop("showsVerticalScrollIndicator") { (view: DomWebView, enabled: Bool) in
view.webView.scrollView.showsVerticalScrollIndicator = enabled
}
// MARK: - Imperative methods
AsyncFunction("scrollTo") { (view: DomWebView, param: ScrollToParam) in
view.scrollTo(offset: CGPoint(x: param.x, y: param.y), animated: param.animated)
}
AsyncFunction("injectJavaScript") { (view: DomWebView, script: String) in
view.injectJavaScript(script)
}
OnViewDidUpdateProps { view in
view.reload()
}
}
// swiftlint:enable closure_body_length
}
}

View File

@@ -0,0 +1,42 @@
// Copyright 2015-present 650 Industries. All rights reserved.
import ExpoModulesCore
internal struct DomWebViewSource: Record {
@Field
var uri: String?
}
internal struct ContentInset: Record {
@Field
var top: Double? = 0
@Field
var left: Double? = 0
@Field
var bottom: Double? = 0
@Field
var right: Double? = 0
func toEdgeInsets() -> UIEdgeInsets {
var inset = UIEdgeInsets.zero
inset.top = CGFloat(self.top ?? 0)
inset.left = CGFloat(self.left ?? 0)
inset.bottom = CGFloat(self.bottom ?? 0)
inset.right = CGFloat(self.right ?? 0)
return inset
}
}
internal struct ScrollToParam: Record {
@Field
var x: Double = 0
@Field
var y: Double = 0
@Field
var animated: Bool = true
}

View File

@@ -0,0 +1,41 @@
// Copyright 2015-present 650 Industries. All rights reserved.
private let lockQueue = DispatchQueue(label: "expo.modules.domWebView.RegistryQueue")
internal typealias WebViewId = Int
internal final class DomWebViewRegistry {
static var shared = DomWebViewRegistry()
private var registry: [WebViewId: WeakDomWebViewRef] = [:]
private var nextWebViewId: WebViewId = 0
func get(webViewId: WebViewId) -> DomWebView? {
return lockQueue.sync {
return self.registry[webViewId]?.ref
}
}
func add(webView: DomWebView) -> WebViewId {
return lockQueue.sync {
let webViewId = self.nextWebViewId
self.registry[webViewId] = WeakDomWebViewRef(ref: webView)
self.nextWebViewId += 1
return webViewId
}
}
@discardableResult
func remove(webViewId: WebViewId) -> DomWebView? {
return lockQueue.sync {
return self.registry.removeValue(forKey: webViewId)?.ref
}
}
func reset() {
lockQueue.sync {
self.registry.removeAll()
self.nextWebViewId = 0
}
}
}

View File

@@ -0,0 +1,28 @@
require 'json'
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
Pod::Spec.new do |s|
s.name = 'ExpoDomWebView'
s.version = package['version']
s.summary = package['description']
s.description = package['description']
s.license = package['license']
s.author = package['author']
s.homepage = package['homepage']
s.platforms = {
:ios => '15.1',
}
s.swift_version = '5.9'
s.source = { git: 'https://github.com/expo/expo.git' }
s.static_framework = true
s.dependency 'ExpoModulesCore'
# Swift/Objective-C compatibility
s.pod_target_xcconfig = {
'DEFINES_MODULE' => 'YES',
'SWIFT_COMPILATION_MODE' => 'wholemodule'
}
s.source_files = "**/*.{h,m,swift}"
end

View File

@@ -0,0 +1,5 @@
// Copyright 2015-present 650 Industries. All rights reserved.
internal struct WeakDomWebViewRef {
weak var ref: DomWebView?
}