From 612e9fa9505450d748ba60b37fab24532523785f Mon Sep 17 00:00:00 2001 From: Brad Date: Thu, 8 Oct 2026 10:42:52 -0700 Subject: [PATCH 1/3] [android] Add androidDocumentStartScript prop Exposes androidx.webkit WebViewCompat.addDocumentStartJavaScript so a script can run at document start in every frame whose origin matches allowedOriginRules, including cross-origin iframes. The existing injectedJavaScript* props go through evaluateJavascript and only reach the main frame. The ScriptHandler and config live on the RNCWebView, so a keyed WebView reparented into a new container with an equal config keeps its registration. A changed config replaces the previous script; null removes it. Unsupported WebViews and malformed origin rules log and no-op. Bumps the default androidx.webkit to 1.5.0 (first release with the public ScriptHandler API), which requires compileSdk 32. Co-Authored-By: Claude Opus 5.5 (1M context) --- android/gradle.properties | 4 +- .../webview/RNCWebViewManager.java | 57 +++++++++++++++++++ docs/Reference.md | 23 ++++++++ lib/WebViewTypes.d.ts | 25 ++++++++ src/WebViewTypes.ts | 28 +++++++++ 5 files changed, 135 insertions(+), 2 deletions(-) diff --git a/android/gradle.properties b/android/gradle.properties index e6f0f5eb08..c353255f28 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,6 +1,6 @@ ReactNativeWebView_kotlinVersion=1.6.0 -ReactNativeWebView_webkitVersion=1.4.0 -ReactNativeWebView_compileSdkVersion=29 +ReactNativeWebView_webkitVersion=1.5.0 +ReactNativeWebView_compileSdkVersion=32 ReactNativeWebView_buildToolsVersion=29.0.3 ReactNativeWebView_targetSdkVersion=28 ReactNativeWebView_minSdkVersion=21 diff --git a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java index bd1268a430..ed91236a9f 100644 --- a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java +++ b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java @@ -49,7 +49,9 @@ import androidx.core.content.ContextCompat; import androidx.core.util.Pair; import androidx.webkit.WebSettingsCompat; +import androidx.webkit.ScriptHandler; import androidx.webkit.WebViewAssetLoader; +import androidx.webkit.WebViewCompat; import androidx.webkit.WebViewFeature; import com.facebook.common.logging.FLog; @@ -629,6 +631,19 @@ public void setInjectedJavaScriptBeforeContentLoadedForMainFrameOnly(RNCWebViewC view.ifHasRNCWebView(webView -> webView.setInjectedJavaScriptBeforeContentLoadedForMainFrameOnly(enabled)); } + /** + * Registers a script that runs at document start in every frame (main frame and subframes) whose + * origin matches one of allowedOriginRules, before any of the page's own scripts. Unlike + * injectedJavaScriptBeforeContentLoaded, which uses evaluateJavascript and so only ever reaches the + * main frame, this can reach cross-origin iframes. Takes effect on the next navigation of each + * matching frame; documents that are already loaded are not affected. + * See https://developer.android.com/reference/androidx/webkit/WebViewCompat#addDocumentStartJavaScript(android.webkit.WebView,java.lang.String,java.util.Set%3Cjava.lang.String%3E) + */ + @ReactProp(name = "androidDocumentStartScript") + public void setDocumentStartScript(RNCWebViewContainer view, @Nullable ReadableMap config) { + view.ifHasRNCWebView(webView -> webView.setDocumentStartScript(config)); + } + @ReactProp(name = "messagingEnabled") public void setMessagingEnabled(RNCWebViewContainer view, boolean enabled) { view.ifHasRNCWebView(webView -> webView.setMessagingEnabled(enabled)); @@ -1749,6 +1764,8 @@ protected static class RNCWebView extends WebView implements LifecycleEventListe protected ReadableMap source; protected @Nullable ReadableMap assetLoaderConfig; + protected @Nullable ReadableMap documentStartScriptConfig; + protected @Nullable ScriptHandler documentStartScriptHandler; protected boolean loadScheduled; private static int idCounter = 0; @@ -1845,6 +1862,46 @@ public boolean isNewAssetLoaderConfig(@Nullable ReadableMap newConfig) { return !assetLoaderConfig.toHashMap().equals(newConfig.toHashMap()); } + public void setDocumentStartScript(@Nullable ReadableMap config) { + // Stored on the RNCWebView rather than the container so a keyed WebView that is reparented into a + // new container with an equal config keeps its existing registration. + if (documentStartScriptConfig == null && config == null) return; + if (documentStartScriptConfig != null && config != null + && documentStartScriptConfig.toHashMap().equals(config.toHashMap())) return; + documentStartScriptConfig = config; + + if (documentStartScriptHandler != null) { + documentStartScriptHandler.remove(); + documentStartScriptHandler = null; + } + + if (config == null) return; + + if (!WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) { + FLog.w(TAG, "androidDocumentStartScript ignored: DOCUMENT_START_SCRIPT is not supported by this WebView."); + return; + } + + String script = config.hasKey("script") ? config.getString("script") : null; + ReadableArray rules = config.hasKey("allowedOriginRules") ? config.getArray("allowedOriginRules") : null; + if (TextUtils.isEmpty(script) || rules == null || rules.size() == 0) { + FLog.w(TAG, "androidDocumentStartScript ignored: script and a non-empty allowedOriginRules are required."); + return; + } + + Set allowedOriginRules = new HashSet<>(); + for (int i = 0; i < rules.size(); i++) { + allowedOriginRules.add(rules.getString(i)); + } + + try { + documentStartScriptHandler = WebViewCompat.addDocumentStartJavaScript(this, script, allowedOriginRules); + } catch (IllegalArgumentException e) { + // Thrown for malformed origin rules. + FLog.e(TAG, "androidDocumentStartScript ignored: invalid allowedOriginRules " + allowedOriginRules, e); + } + } + @Override public void onHostResume() { // do nothing diff --git a/docs/Reference.md b/docs/Reference.md index b3157ed238..c9ad21e8b6 100644 --- a/docs/Reference.md +++ b/docs/Reference.md @@ -38,6 +38,7 @@ This document lays out the current public properties and methods for the React N - [`javaScriptCanOpenWindowsAutomatically`](Reference.md#javascriptcanopenwindowsautomatically) - [`androidHardwareAccelerationDisabled`](Reference.md#androidHardwareAccelerationDisabled) - [`androidLayerType`](Reference.md#androidLayerType) +- [`androidDocumentStartScript`](Reference.md#androidDocumentStartScript) - [`mixedContentMode`](Reference.md#mixedcontentmode) - [`thirdPartyCookiesEnabled`](Reference.md#thirdpartycookiesenabled) - [`userAgent`](Reference.md#useragent) @@ -878,6 +879,28 @@ Avoid setting both `androidLayerType` and `androidHardwareAccelerationDisabled` --- +### `androidDocumentStartScript`[⬆](#props-index) + +Registers a script with [`WebViewCompat.addDocumentStartJavaScript`](https://developer.android.com/reference/androidx/webkit/WebViewCompat#addDocumentStartJavaScript(android.webkit.WebView,java.lang.String,java.util.Set%3Cjava.lang.String%3E)). The script runs at document start, before any of the page's own scripts, in every frame whose origin matches one of `allowedOriginRules`, including cross-origin iframes. This differs from `injectedJavaScriptBeforeContentLoaded`, which on Android only ever reaches the main frame. + +The script is applied on the next navigation of each matching frame; documents that are already loaded are not affected. Changing the prop replaces the previous registration, and passing `undefined` removes it. If the installed WebView does not support `WebViewFeature.DOCUMENT_START_SCRIPT`, or the origin rules are malformed, the prop is ignored and a native warning is logged. + +```jsx + +``` + +| Type | Required | Platform | +| ------------------------------------------------------ | -------- | -------- | +| `{ script: string, allowedOriginRules: string[] }` | No | Android | + +--- + ### `mixedContentMode`[⬆](#props-index) Specifies the mixed content mode. i.e WebView will allow a secure origin to load content from any other origin. diff --git a/lib/WebViewTypes.d.ts b/lib/WebViewTypes.d.ts index aa9be613b3..5387568b3e 100644 --- a/lib/WebViewTypes.d.ts +++ b/lib/WebViewTypes.d.ts @@ -150,6 +150,21 @@ export interface AndroidAssetLoaderConfig { */ pathHandlers: AndroidAssetLoaderPathHandler[]; } +/** + * A script registered via androidx.webkit.WebViewCompat.addDocumentStartJavaScript. + */ +export interface AndroidDocumentStartScript { + /** + * JavaScript source to run at document start, before any of the page's own scripts. + */ + script: string; + /** + * Origin rules a frame must match for the script to run in it, e.g. `https://*.example.com`. + * Matching applies to every frame, including cross-origin iframes. + * See https://developer.android.com/reference/androidx/webkit/WebViewCompat#addDocumentStartJavaScript(android.webkit.WebView,java.lang.String,java.util.Set%3Cjava.lang.String%3E) + */ + allowedOriginRules: string[]; +} export interface WebViewSourceUri { /** * The URI to load in the `WebView`. Can be a local or remote file. @@ -261,6 +276,7 @@ export interface AndroidNativeWebViewProps extends CommonNativeWebViewProps { allowsFullscreenVideo?: boolean; allowUniversalAccessFromFileURLs?: boolean; androidAssetLoaderConfig?: AndroidAssetLoaderConfig; + androidDocumentStartScript?: AndroidDocumentStartScript; androidHardwareAccelerationDisabled?: boolean; androidLayerType?: AndroidLayerType; domStorageEnabled?: boolean; @@ -906,6 +922,15 @@ export interface AndroidWebViewProps extends WebViewSharedProps { * assets and resources using http(s):// URLS inside a WebView. */ androidAssetLoaderConfig?: AndroidAssetLoaderConfig; + /** + * Injects `script` at document start into every frame (main frame and iframes) whose origin + * matches `allowedOriginRules`. Unlike `injectedJavaScriptBeforeContentLoaded`, this reaches + * cross-origin iframes. Changes take effect on the next navigation of each matching frame. + * No-op (with a native warning) when the installed WebView does not support + * `WebViewFeature.DOCUMENT_START_SCRIPT`. + * @platform android + */ + androidDocumentStartScript?: AndroidDocumentStartScript; /** * Boolean value to enable third party cookies in the `WebView`. Used on * Android Lollipop and above only as third party cookies are enabled by diff --git a/src/WebViewTypes.ts b/src/WebViewTypes.ts index 28208a75cb..518d4c2c45 100644 --- a/src/WebViewTypes.ts +++ b/src/WebViewTypes.ts @@ -240,6 +240,23 @@ export interface AndroidAssetLoaderConfig { pathHandlers: AndroidAssetLoaderPathHandler[] } +/** + * A script registered via androidx.webkit.WebViewCompat.addDocumentStartJavaScript. + */ +export interface AndroidDocumentStartScript { + /** + * JavaScript source to run at document start, before any of the page's own scripts. + */ + script: string; + + /** + * Origin rules a frame must match for the script to run in it, e.g. `https://*.example.com`. + * Matching applies to every frame, including cross-origin iframes. + * See https://developer.android.com/reference/androidx/webkit/WebViewCompat#addDocumentStartJavaScript(android.webkit.WebView,java.lang.String,java.util.Set%3Cjava.lang.String%3E) + */ + allowedOriginRules: string[]; +} + export interface WebViewSourceUri { /** * The URI to load in the `WebView`. Can be a local or remote file. @@ -371,6 +388,7 @@ export interface AndroidNativeWebViewProps extends CommonNativeWebViewProps { allowsFullscreenVideo?: boolean; allowUniversalAccessFromFileURLs?: boolean; androidAssetLoaderConfig?: AndroidAssetLoaderConfig + androidDocumentStartScript?: AndroidDocumentStartScript; androidHardwareAccelerationDisabled?: boolean; androidLayerType?: AndroidLayerType; domStorageEnabled?: boolean; @@ -1100,6 +1118,16 @@ export interface AndroidWebViewProps extends WebViewSharedProps { */ androidAssetLoaderConfig?: AndroidAssetLoaderConfig; + /** + * Injects `script` at document start into every frame (main frame and iframes) whose origin + * matches `allowedOriginRules`. Unlike `injectedJavaScriptBeforeContentLoaded`, this reaches + * cross-origin iframes. Changes take effect on the next navigation of each matching frame. + * No-op (with a native warning) when the installed WebView does not support + * `WebViewFeature.DOCUMENT_START_SCRIPT`. + * @platform android + */ + androidDocumentStartScript?: AndroidDocumentStartScript; + /** * Boolean value to enable third party cookies in the `WebView`. Used on * Android Lollipop and above only as third party cookies are enabled by From 2a5d9daa449cc155e39a8063833ecb5ce30c7c19 Mon Sep 17 00:00:00 2001 From: Brad Date: Thu, 8 Oct 2026 11:05:23 -0700 Subject: [PATCH 2/3] [android] Suppress RestrictedApi lint for ScriptHandler ScriptHandler is @RestrictTo(LIBRARY_GROUP) in androidx.webkit before 1.9.0. Its signatures are unchanged from 1.5.0 through 1.13.0, and 1.9.0 requires compileSdk 34, so suppress rather than bump. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../com/reactnativecommunity/webview/RNCWebViewManager.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java index ed91236a9f..9a89025320 100644 --- a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java +++ b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java @@ -1765,6 +1765,9 @@ protected static class RNCWebView extends WebView implements LifecycleEventListe protected ReadableMap source; protected @Nullable ReadableMap assetLoaderConfig; protected @Nullable ReadableMap documentStartScriptConfig; + // ScriptHandler is @RestrictTo(LIBRARY_GROUP) until androidx.webkit 1.9.0, which needs compileSdk 34. The + // signatures we use are unchanged from 1.5.0 through 1.13.0, so the restriction is lint-only for us. + @SuppressLint("RestrictedApi") protected @Nullable ScriptHandler documentStartScriptHandler; protected boolean loadScheduled; private static int idCounter = 0; @@ -1862,6 +1865,7 @@ public boolean isNewAssetLoaderConfig(@Nullable ReadableMap newConfig) { return !assetLoaderConfig.toHashMap().equals(newConfig.toHashMap()); } + @SuppressLint("RestrictedApi") // See documentStartScriptHandler. public void setDocumentStartScript(@Nullable ReadableMap config) { // Stored on the RNCWebView rather than the container so a keyed WebView that is reparented into a // new container with an equal config keeps its existing registration. From 3c00699234f67db597bdfeb463ea6c38c15019b0 Mon Sep 17 00:00:00 2001 From: Brad Date: Thu, 8 Oct 2026 13:44:59 -0700 Subject: [PATCH 3/3] [android] Rename androidDocumentStartScript to androidInjectedJavaScriptForOrigins Co-Authored-By: Claude Opus 5.5 (1M context) --- .../webview/RNCWebViewManager.java | 38 +++++++++---------- docs/Reference.md | 6 +-- lib/WebViewTypes.d.ts | 6 +-- src/WebViewTypes.ts | 6 +-- 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java index 9a89025320..cc931a882f 100644 --- a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java +++ b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java @@ -639,9 +639,9 @@ public void setInjectedJavaScriptBeforeContentLoadedForMainFrameOnly(RNCWebViewC * matching frame; documents that are already loaded are not affected. * See https://developer.android.com/reference/androidx/webkit/WebViewCompat#addDocumentStartJavaScript(android.webkit.WebView,java.lang.String,java.util.Set%3Cjava.lang.String%3E) */ - @ReactProp(name = "androidDocumentStartScript") - public void setDocumentStartScript(RNCWebViewContainer view, @Nullable ReadableMap config) { - view.ifHasRNCWebView(webView -> webView.setDocumentStartScript(config)); + @ReactProp(name = "androidInjectedJavaScriptForOrigins") + public void setInjectedJavaScriptForOrigins(RNCWebViewContainer view, @Nullable ReadableMap config) { + view.ifHasRNCWebView(webView -> webView.setInjectedJavaScriptForOrigins(config)); } @ReactProp(name = "messagingEnabled") @@ -1764,11 +1764,11 @@ protected static class RNCWebView extends WebView implements LifecycleEventListe protected ReadableMap source; protected @Nullable ReadableMap assetLoaderConfig; - protected @Nullable ReadableMap documentStartScriptConfig; + protected @Nullable ReadableMap injectedJavaScriptForOriginsConfig; // ScriptHandler is @RestrictTo(LIBRARY_GROUP) until androidx.webkit 1.9.0, which needs compileSdk 34. The // signatures we use are unchanged from 1.5.0 through 1.13.0, so the restriction is lint-only for us. @SuppressLint("RestrictedApi") - protected @Nullable ScriptHandler documentStartScriptHandler; + protected @Nullable ScriptHandler injectedJavaScriptForOriginsHandler; protected boolean loadScheduled; private static int idCounter = 0; @@ -1865,31 +1865,31 @@ public boolean isNewAssetLoaderConfig(@Nullable ReadableMap newConfig) { return !assetLoaderConfig.toHashMap().equals(newConfig.toHashMap()); } - @SuppressLint("RestrictedApi") // See documentStartScriptHandler. - public void setDocumentStartScript(@Nullable ReadableMap config) { + @SuppressLint("RestrictedApi") // See injectedJavaScriptForOriginsHandler. + public void setInjectedJavaScriptForOrigins(@Nullable ReadableMap config) { // Stored on the RNCWebView rather than the container so a keyed WebView that is reparented into a // new container with an equal config keeps its existing registration. - if (documentStartScriptConfig == null && config == null) return; - if (documentStartScriptConfig != null && config != null - && documentStartScriptConfig.toHashMap().equals(config.toHashMap())) return; - documentStartScriptConfig = config; - - if (documentStartScriptHandler != null) { - documentStartScriptHandler.remove(); - documentStartScriptHandler = null; + if (injectedJavaScriptForOriginsConfig == null && config == null) return; + if (injectedJavaScriptForOriginsConfig != null && config != null + && injectedJavaScriptForOriginsConfig.toHashMap().equals(config.toHashMap())) return; + injectedJavaScriptForOriginsConfig = config; + + if (injectedJavaScriptForOriginsHandler != null) { + injectedJavaScriptForOriginsHandler.remove(); + injectedJavaScriptForOriginsHandler = null; } if (config == null) return; if (!WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) { - FLog.w(TAG, "androidDocumentStartScript ignored: DOCUMENT_START_SCRIPT is not supported by this WebView."); + FLog.w(TAG, "androidInjectedJavaScriptForOrigins ignored: DOCUMENT_START_SCRIPT is not supported by this WebView."); return; } String script = config.hasKey("script") ? config.getString("script") : null; ReadableArray rules = config.hasKey("allowedOriginRules") ? config.getArray("allowedOriginRules") : null; if (TextUtils.isEmpty(script) || rules == null || rules.size() == 0) { - FLog.w(TAG, "androidDocumentStartScript ignored: script and a non-empty allowedOriginRules are required."); + FLog.w(TAG, "androidInjectedJavaScriptForOrigins ignored: script and a non-empty allowedOriginRules are required."); return; } @@ -1899,10 +1899,10 @@ public void setDocumentStartScript(@Nullable ReadableMap config) { } try { - documentStartScriptHandler = WebViewCompat.addDocumentStartJavaScript(this, script, allowedOriginRules); + injectedJavaScriptForOriginsHandler = WebViewCompat.addDocumentStartJavaScript(this, script, allowedOriginRules); } catch (IllegalArgumentException e) { // Thrown for malformed origin rules. - FLog.e(TAG, "androidDocumentStartScript ignored: invalid allowedOriginRules " + allowedOriginRules, e); + FLog.e(TAG, "androidInjectedJavaScriptForOrigins ignored: invalid allowedOriginRules " + allowedOriginRules, e); } } diff --git a/docs/Reference.md b/docs/Reference.md index c9ad21e8b6..d80e071f8e 100644 --- a/docs/Reference.md +++ b/docs/Reference.md @@ -38,7 +38,7 @@ This document lays out the current public properties and methods for the React N - [`javaScriptCanOpenWindowsAutomatically`](Reference.md#javascriptcanopenwindowsautomatically) - [`androidHardwareAccelerationDisabled`](Reference.md#androidHardwareAccelerationDisabled) - [`androidLayerType`](Reference.md#androidLayerType) -- [`androidDocumentStartScript`](Reference.md#androidDocumentStartScript) +- [`androidInjectedJavaScriptForOrigins`](Reference.md#androidInjectedJavaScriptForOrigins) - [`mixedContentMode`](Reference.md#mixedcontentmode) - [`thirdPartyCookiesEnabled`](Reference.md#thirdpartycookiesenabled) - [`userAgent`](Reference.md#useragent) @@ -879,7 +879,7 @@ Avoid setting both `androidLayerType` and `androidHardwareAccelerationDisabled` --- -### `androidDocumentStartScript`[⬆](#props-index) +### `androidInjectedJavaScriptForOrigins`[⬆](#props-index) Registers a script with [`WebViewCompat.addDocumentStartJavaScript`](https://developer.android.com/reference/androidx/webkit/WebViewCompat#addDocumentStartJavaScript(android.webkit.WebView,java.lang.String,java.util.Set%3Cjava.lang.String%3E)). The script runs at document start, before any of the page's own scripts, in every frame whose origin matches one of `allowedOriginRules`, including cross-origin iframes. This differs from `injectedJavaScriptBeforeContentLoaded`, which on Android only ever reaches the main frame. @@ -888,7 +888,7 @@ The script is applied on the next navigation of each matching frame; documents t ```jsx