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

15
node_modules/@expo/dom-webview/android/build.gradle generated vendored Normal file
View File

@@ -0,0 +1,15 @@
plugins {
id 'com.android.library'
id 'expo-module-gradle-plugin'
}
group = 'expo.modules.webview'
version = '55.0.3'
android {
namespace "expo.modules.webview"
defaultConfig {
versionCode 1
versionName "55.0.3"
}
}

View File

@@ -0,0 +1,2 @@
<manifest>
</manifest>

View File

@@ -0,0 +1,176 @@
// Copyright 2015-present 650 Industries. All rights reserved.
package expo.modules.webview
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Color
import android.view.MotionEvent
import android.view.View
import android.view.View.OnTouchListener
import android.view.ViewGroup
import android.webkit.WebView
import android.webkit.WebViewClient
import expo.modules.kotlin.AppContext
import expo.modules.kotlin.viewevent.EventDispatcher
import expo.modules.kotlin.views.ExpoView
import kotlinx.coroutines.runBlocking
import org.json.JSONObject
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
@SuppressLint("ViewConstructor")
internal class DomWebView(context: Context, appContext: AppContext) : ExpoView(context, appContext), OnTouchListener {
val webView: WebView
val webViewId = DomWebViewRegistry.add(this)
private var source: DomWebViewSource? = null
private var injectedJSBeforeContentLoaded: String? = null
var webviewDebuggingEnabled = false
var nestedScrollEnabled = true
private var needsResetupScripts = false
private val onMessage by EventDispatcher<OnMessageEvent>()
init {
this.webView = createWebView()
addView(
webView,
ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
)
}
// region Public methods
fun reload() {
WebView.setWebContentsDebuggingEnabled(webviewDebuggingEnabled)
source?.uri?.let {
if (it != webView.url) {
webView.loadUrl(it)
}
return
}
if (needsResetupScripts) {
needsResetupScripts = false
webView.reload()
}
}
fun setSource(source: DomWebViewSource) {
this.source = source
}
fun setInjectedJSBeforeContentLoaded(script: String?) {
injectedJSBeforeContentLoaded = if (!script.isNullOrEmpty()) {
"(function() { $script; })();true;"
} else {
null
}
needsResetupScripts = true
}
fun injectJavaScript(script: String) {
webView.post {
webView.evaluateJavascript(script, null)
}
}
fun dispatchMessageEvent(message: String) {
webView.post {
val messageEvent = OnMessageEvent(
title = webView.title ?: "",
url = webView.url ?: "",
data = message
)
onMessage.invoke(messageEvent)
}
}
fun evalSync(data: String): String {
val json = JSONObject(data)
val deferredId = json.getInt("deferredId")
val source = json.getString("source")
return runBlocking {
nativeJsiEvalSync(deferredId, source)
}
}
fun scrollTo(param: ScrollToParam) {
webView.post {
if (!param.animated) {
webView.scrollTo(param.x.toInt(), param.y.toInt())
return@post
}
val duration = 250L
val animatorX = ObjectAnimator.ofInt(webView, "scrollX", webView.scrollX, param.x.toInt())
animatorX.setDuration(duration)
val animatorY = ObjectAnimator.ofInt(webView, "scrollY", webView.scrollY, param.y.toInt())
animatorY.setDuration(duration)
animatorX.start()
animatorY.start()
}
}
// endregion Public methods
// region Override methods
override fun onTouch(view: View?, event: MotionEvent?): Boolean {
if (nestedScrollEnabled) {
requestDisallowInterceptTouchEvent(true)
}
return false
}
// region Override methods
// region Internals
@SuppressLint("SetJavaScriptEnabled")
private fun createWebView(): WebView {
return WebView(context).apply {
setBackgroundColor(Color.TRANSPARENT)
settings.javaScriptEnabled = true
webViewClient = createWebViewClient()
addJavascriptInterface(RNCWebViewBridge(this@DomWebView), "ReactNativeWebView")
addJavascriptInterface(DomWebViewBridge(this@DomWebView), "ExpoDomWebViewBridge")
setOnTouchListener(this@DomWebView)
}
}
private fun createWebViewClient() = object : WebViewClient() {
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
injectJavaScript(INSTALL_GLOBALS_SCRIPT.replace("\"%%WEBVIEW_ID%%\"", webViewId.toString()))
this@DomWebView.injectedJSBeforeContentLoaded?.let {
injectJavaScript(it)
}
}
}
private suspend fun nativeJsiEvalSync(deferredId: Int, source: String): String {
return suspendCoroutine { continuation ->
appContext.executeOnJavaScriptThread {
val wrappedSource = NATIVE_EVAL_WRAPPER_SCRIPT
.replace("\"%%DEFERRED_ID%%\"", deferredId.toString())
.replace("\"%%WEBVIEW_ID%%\"", webViewId.toString())
.replace("\"%%SOURCE%%\"", source)
try {
val result = appContext.runtime.eval(wrappedSource)
continuation.resume(result.getString())
} catch (e: Exception) {
continuation.resumeWithException(e)
}
}
}
}
// endregion Internals
}

View File

@@ -0,0 +1,12 @@
// Copyright 2015-present 650 Industries. All rights reserved.
package expo.modules.webview
import android.webkit.JavascriptInterface
internal class DomWebViewBridge(private val webView: DomWebView) {
@JavascriptInterface
fun eval(params: String): String {
return webView.evalSync(params)
}
}

View File

@@ -0,0 +1,264 @@
// Copyright 2015-present 650 Industries. All rights reserved.
package expo.modules.webview
/**
* This file contains the browser scripts that are injected into the WebView.
* @generated by buildBrowserScripts.ts
*/
internal const val 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 const val 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 });
}
})();
"""

View File

@@ -0,0 +1,12 @@
// Copyright 2015-present 650 Industries. All rights reserved.
package expo.modules.webview
import expo.modules.kotlin.records.Field
import expo.modules.kotlin.records.Record
internal data class OnMessageEvent(
@Field val title: String,
@Field val url: String,
@Field val data: String
) : Record

View File

@@ -0,0 +1,65 @@
// Copyright 2015-present 650 Industries. All rights reserved.
package expo.modules.webview
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
@Suppress("unused")
class DomWebViewModule : Module() {
override fun definition() = ModuleDefinition {
Name("ExpoDomWebViewModule")
OnDestroy {
DomWebViewRegistry.reset()
}
AsyncFunction("evalJsForWebViewAsync") { webViewId: Int, source: String ->
DomWebViewRegistry.get(webViewId)?.injectJavaScript(source)
}
View(DomWebView::class) {
Events("onMessage")
Prop("source") { view: DomWebView, source: DomWebViewSource ->
view.setSource(source)
}
Prop("injectedJavaScriptBeforeContentLoaded") { view: DomWebView, script: String ->
view.setInjectedJSBeforeContentLoaded(script)
}
Prop("webviewDebuggingEnabled") { view: DomWebView, enabled: Boolean ->
view.webviewDebuggingEnabled = enabled
}
Prop("showsHorizontalScrollIndicator") { view: DomWebView, enabled: Boolean ->
view.webView.post {
view.webView.isHorizontalScrollBarEnabled = enabled
}
}
Prop("showsVerticalScrollIndicator") { view: DomWebView, enabled: Boolean ->
view.webView.post {
view.webView.isVerticalScrollBarEnabled = enabled
}
}
Prop("nestedScrollEnabled") { view: DomWebView, enabled: Boolean ->
view.nestedScrollEnabled = enabled
}
AsyncFunction("scrollTo") { view: DomWebView, param: ScrollToParam ->
view.scrollTo(param)
}
AsyncFunction("injectJavaScript") { view: DomWebView, script: String ->
view.injectJavaScript(script)
}
OnViewDidUpdateProps { view: DomWebView ->
view.reload()
}
}
}
}

View File

@@ -0,0 +1,16 @@
// Copyright 2015-present 650 Industries. All rights reserved.
package expo.modules.webview
import expo.modules.kotlin.records.Field
import expo.modules.kotlin.records.Record
internal data class DomWebViewSource(
@Field val uri: String?
) : Record
internal data class ScrollToParam(
@Field val x: Double = 0.0,
@Field val y: Double = 0.0,
@Field val animated: Boolean = true
) : Record

View File

@@ -0,0 +1,37 @@
// Copyright 2015-present 650 Industries. All rights reserved.
package expo.modules.webview
import androidx.collection.ArrayMap
import java.lang.ref.WeakReference
internal typealias WebViewId = Int
internal object DomWebViewRegistry {
private val registry = ArrayMap<WebViewId, WeakDomWebViewRef>()
private var nextWebViewId: WebViewId = 0
@Synchronized
fun get(webViewId: WebViewId): DomWebView? {
return registry[webViewId]?.ref?.get()
}
@Synchronized
fun add(webView: DomWebView): WebViewId {
val webViewId = this.nextWebViewId
this.registry[webViewId] = WeakDomWebViewRef(WeakReference(webView))
this.nextWebViewId += 1
return webViewId
}
@Synchronized
fun remove(webViewId: WebViewId): DomWebView? {
return this.registry.remove(webViewId)?.ref?.get()
}
@Synchronized
fun reset() {
this.registry.clear()
this.nextWebViewId = 0
}
}

View File

@@ -0,0 +1,12 @@
// Copyright 2015-present 650 Industries. All rights reserved.
package expo.modules.webview
import android.webkit.JavascriptInterface
internal class RNCWebViewBridge(private val webView: DomWebView) {
@JavascriptInterface
fun postMessage(message: String) {
webView.dispatchMessageEvent(message)
}
}

View File

@@ -0,0 +1,9 @@
// Copyright 2015-present 650 Industries. All rights reserved.
package expo.modules.webview
import java.lang.ref.WeakReference
internal data class WeakDomWebViewRef(
val ref: WeakReference<DomWebView>
)