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..cc931a882f 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 = "androidInjectedJavaScriptForOrigins") + public void setInjectedJavaScriptForOrigins(RNCWebViewContainer view, @Nullable ReadableMap config) { + view.ifHasRNCWebView(webView -> webView.setInjectedJavaScriptForOrigins(config)); + } + @ReactProp(name = "messagingEnabled") public void setMessagingEnabled(RNCWebViewContainer view, boolean enabled) { view.ifHasRNCWebView(webView -> webView.setMessagingEnabled(enabled)); @@ -1749,6 +1764,11 @@ protected static class RNCWebView extends WebView implements LifecycleEventListe protected ReadableMap source; protected @Nullable ReadableMap assetLoaderConfig; + 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 injectedJavaScriptForOriginsHandler; protected boolean loadScheduled; private static int idCounter = 0; @@ -1845,6 +1865,47 @@ public boolean isNewAssetLoaderConfig(@Nullable ReadableMap newConfig) { return !assetLoaderConfig.toHashMap().equals(newConfig.toHashMap()); } + @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 (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, "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, "androidInjectedJavaScriptForOrigins 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 { + injectedJavaScriptForOriginsHandler = WebViewCompat.addDocumentStartJavaScript(this, script, allowedOriginRules); + } catch (IllegalArgumentException e) { + // Thrown for malformed origin rules. + FLog.e(TAG, "androidInjectedJavaScriptForOrigins ignored: invalid allowedOriginRules " + allowedOriginRules, e); + } + } + @Override public void onHostResume() { // do nothing diff --git a/docs/Reference.md b/docs/Reference.md index b3157ed238..d80e071f8e 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) +- [`androidInjectedJavaScriptForOrigins`](Reference.md#androidInjectedJavaScriptForOrigins) - [`mixedContentMode`](Reference.md#mixedcontentmode) - [`thirdPartyCookiesEnabled`](Reference.md#thirdpartycookiesenabled) - [`userAgent`](Reference.md#useragent) @@ -878,6 +879,28 @@ Avoid setting both `androidLayerType` and `androidHardwareAccelerationDisabled` --- +### `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. + +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..e2721c544e 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 AndroidInjectedJavaScriptForOrigins { + /** + * 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; + androidInjectedJavaScriptForOrigins?: AndroidInjectedJavaScriptForOrigins; 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 + */ + androidInjectedJavaScriptForOrigins?: AndroidInjectedJavaScriptForOrigins; /** * 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..756f2f4cf2 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 AndroidInjectedJavaScriptForOrigins { + /** + * 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 + androidInjectedJavaScriptForOrigins?: AndroidInjectedJavaScriptForOrigins; 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 + */ + androidInjectedJavaScriptForOrigins?: AndroidInjectedJavaScriptForOrigins; + /** * Boolean value to enable third party cookies in the `WebView`. Used on * Android Lollipop and above only as third party cookies are enabled by