Skip to content

Commit

Permalink
feat(android): Android injectJavaScriptObject (react-native-webview#2960
Browse files Browse the repository at this point in the history
)
  • Loading branch information
kevinvangelder committed Sep 8, 2023
1 parent dd31719 commit 447f68e
Show file tree
Hide file tree
Showing 10 changed files with 157 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import android.os.Build;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;

import android.view.ActionMode;
import android.view.Menu;
Expand All @@ -20,6 +21,7 @@
import android.webkit.WebView;
import android.webkit.WebViewClient;

import com.facebook.common.logging.FLog;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.CatalystInstance;
import com.facebook.react.bridge.LifecycleEventListener;
Expand Down Expand Up @@ -55,6 +57,8 @@ public class RNCWebView extends WebView implements LifecycleEventListener {
protected @Nullable
String injectedJSBeforeContentLoaded;
protected static final String JAVASCRIPT_INTERFACE = "ReactNativeWebView";
protected @Nullable
RNCWebViewBridge bridge;

/**
* android.webkit.WebChromeClient fundamentally does not support JS injection into frames other
Expand Down Expand Up @@ -244,8 +248,16 @@ RNCWebViewClient getRNCWebViewClient() {
return mRNCWebViewClient;
}

public boolean getMessagingEnabled() {
return this.messagingEnabled;
}

protected RNCWebViewBridge createRNCWebViewBridge(RNCWebView webView) {
return new RNCWebViewBridge(webView);
if (bridge == null) {
bridge = new RNCWebViewBridge(webView);
addJavascriptInterface(bridge, JAVASCRIPT_INTERFACE);
}
return bridge;
}

protected void createCatalystInstance() {
Expand All @@ -265,9 +277,7 @@ public void setMessagingEnabled(boolean enabled) {
messagingEnabled = enabled;

if (enabled) {
addJavascriptInterface(createRNCWebViewBridge(this), JAVASCRIPT_INTERFACE);
} else {
removeJavascriptInterface(JAVASCRIPT_INTERFACE);
createRNCWebViewBridge(this);
}
}

Expand All @@ -291,6 +301,13 @@ public void callInjectedJavaScriptBeforeContentLoaded() {
}
}

public void setInjectedJavaScriptObject(String obj) {
if (getSettings().getJavaScriptEnabled()) {
RNCWebViewBridge b = createRNCWebViewBridge(this);
b.setInjectedObjectJson(obj);
}
}

public void onMessage(String message) {
ThemedReactContext reactContext = getThemedReactContext();
RNCWebView mWebView = this;
Expand Down Expand Up @@ -387,20 +404,33 @@ public ThemedReactContext getThemedReactContext() {
}

protected class RNCWebViewBridge {
private String TAG = "RNCWebViewBridge";
RNCWebView mWebView;
String injectedObjectJson;

RNCWebViewBridge(RNCWebView c) {
mWebView = c;
}

public void setInjectedObjectJson(String s) {
injectedObjectJson = s;
}

/**
* This method is called whenever JavaScript running within the web view calls:
* - window[JAVASCRIPT_INTERFACE].postMessage
*/
@JavascriptInterface
public void postMessage(String message) {
mWebView.onMessage(message);
if (mWebView.getMessagingEnabled()) {
mWebView.onMessage(message);
} else {
FLog.w(TAG, "ReactNativeWebView.postMessage method was called but messaging is disabled. Pass an onMessage handler to the WebView.");
}
}

@JavascriptInterface
public String injectedObjectJson() { return injectedObjectJson; }
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,10 @@ class RNCWebViewManagerImpl {
view.injectedJavaScriptBeforeContentLoadedForMainFrameOnly = value
}

fun setInjectedJavaScriptObject(view: RNCWebView, value: String?) {
view.setInjectedJavaScriptObject(value)
}

fun setJavaScriptCanOpenWindowsAutomatically(view: RNCWebView, value: Boolean) {
view.settings.javaScriptCanOpenWindowsAutomatically = value
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,11 @@ public void setInjectedJavaScriptBeforeContentLoadedForMainFrameOnly(RNCWebView

}

@ReactProp(name = "injectedJavaScriptObject")
public void setInjectedJavaScriptObject(RNCWebView view, @Nullable String value) {
mRNCWebViewManagerImpl.setInjectedJavaScriptObject(view, value);
}

@Override
@ReactProp(name = "javaScriptCanOpenWindowsAutomatically")
public void setJavaScriptCanOpenWindowsAutomatically(RNCWebView view, boolean value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@ public void setInjectedJavaScriptBeforeContentLoadedForMainFrameOnly(RNCWebView

}

@ReactProp(name = "injectedJavaScriptObject")
public void setInjectedJavaScriptObject(RNCWebView view, @Nullable String value) {
mRNCWebViewManagerImpl.setInjectedJavaScriptObject(view, value);
}

@ReactProp(name = "javaScriptCanOpenWindowsAutomatically")
public void setJavaScriptCanOpenWindowsAutomatically(RNCWebView view, boolean value) {
mRNCWebViewManagerImpl.setJavaScriptCanOpenWindowsAutomatically(view, value);
Expand Down
47 changes: 46 additions & 1 deletion docs/Guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -366,14 +366,59 @@ export default class App extends Component {
This runs the JavaScript in the `runFirst` string before the page is loaded. In this case, the value of `window.isNativeApp` will be set to true before the web code executes.

> **Warning**
> On Android, this may work, but it is not 100% reliable (see [#1609](https://github.com/react-native-webview/react-native-webview/issues/1609) and [#1099](https://github.com/react-native-webview/react-native-webview/pull/1099)).
> On Android, this may work, but it is not 100% reliable (see [#1609](https://github.com/react-native-webview/react-native-webview/issues/1609) and [#1099](https://github.com/react-native-webview/react-native-webview/pull/1099)). Consider using `injectedJavaScriptObject` instead.
By setting `injectedJavaScriptBeforeContentLoadedForMainFrameOnly: false`, the JavaScript injection will occur on all frames (not just the top frame) if supported for the given platform. However, although support for `injectedJavaScriptBeforeContentLoadedForMainFrameOnly: false` has been implemented for iOS and macOS, [it is not clear](https://github.com/react-native-webview/react-native-webview/pull/1119#issuecomment-600275750) that it is actually possible to inject JS into iframes at this point in the page lifecycle, and so relying on the expected behaviour of this prop when set to `false` is not recommended.

> On iOS, ~~`injectedJavaScriptBeforeContentLoaded` runs a method on WebView called `evaluateJavaScript:completionHandler:`~~ – this is no longer true as of version `8.2.0`. Instead, we use a `WKUserScript` with injection time `WKUserScriptInjectionTimeAtDocumentStart`. As a consequence, `injectedJavaScriptBeforeContentLoaded` no longer returns an evaluation value nor logs a warning to the console. In the unlikely event that your app depended upon this behaviour, please see migration steps [here](https://github.com/react-native-webview/react-native-webview/pull/1119#issuecomment-574919464) to retain equivalent behaviour.
> On Android, `injectedJavaScript` runs a method on the Android WebView called `evaluateJavascriptWithFallback`
> Note on Android Compatibility: For applications targeting `Build.VERSION_CODES.N` or later, JavaScript state from an empty WebView is no longer persisted across navigations like `loadUrl(java.lang.String)`. For example, global variables and functions defined before calling `loadUrl(java.lang.String)` will not exist in the loaded page. Applications should use the Android Native API `addJavascriptInterface(Object, String)` instead to persist JavaScript objects across navigations.

#### The `injectedJavaScriptObject` prop (Android Only)

Due to the Android race condition mentioned above, this more reliable prop was added. While you cannot execute arbitrary JavaScript, you can make an arbitrary JS object available to the JS run in the webview prior to the page load completing.

```html
<html>
<head>
<script>
window.onload = (event) => {
if (window.ReactNativeWebView.injectedObjectJson()) {
document.getElementById('output').innerHTML = JSON.parse(window.ReactNativeWebView.injectedObjectJson()).customValue;
}
}
</script>
</head>
<body>
<p id="output">undefined</p>
</body>
</html>
```

Note: `ReactNativeWebView.injectedObjectJson()` returns the JSON encoded object passed in to `injectedJavaScriptObject`. It must be passed to `JSON.parse` before its properties can be accessed (but it may be `undefined`!).

```jsx
import React, { Component } from 'react';
import { View } from 'react-native';
import { WebView } from 'react-native-webview';

export default class App extends Component {
render() {
return (
<View style={{ flex: 1 }}>
<WebView
source={{
html: HTML
}}
injectedJavaScriptObject={{ customValue: 'myCustomValue' }}
/>
</View>
);
}
}
```

#### The `injectJavaScript` method

While convenient, the downside to the previously mentioned `injectedJavaScript` prop is that it only runs once. That's why we also expose a method on the webview ref called `injectJavaScript` (note the slightly different name!).
Expand Down
40 changes: 39 additions & 1 deletion docs/Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ Make sure the string evaluates to a valid type (`true` works) and doesn't otherw
On iOS, see [`WKUserScriptInjectionTimeAtDocumentStart`](https://developer.apple.com/documentation/webkit/wkuserscriptinjectiontime/wkuserscriptinjectiontimeatdocumentstart?language=objc)

> **Warning**
> On Android, this may work, but it is not 100% reliable (see [#1609](https://github.com/react-native-webview/react-native-webview/issues/1609) and [#1099](https://github.com/react-native-webview/react-native-webview/pull/1099)).
> On Android, this may work, but it is not 100% reliable (see [#1609](https://github.com/react-native-webview/react-native-webview/issues/1609) and [#1099](https://github.com/react-native-webview/react-native-webview/pull/1099)). Consider `injectedJavaScriptObject` instead.
| Type | Required | Platform |
| ------ | -------- | ---------------------------------- |
Expand Down Expand Up @@ -250,6 +250,44 @@ If `false`, (only supported on iOS and macOS), loads it into all frames (e.g. if

---

### `injectedJavaScriptObject`[](#props-index)

Inject any JavaScript object into the webview so it is available to the JS running on the page.

| Type | Required | Platform |
| ---- | -------- | ------------------------------------------------- |
| obj | No | Android only |

Example:

Set a value to be used in JavaScript.

Note: Any value in the object will be accessible to *all* frames of the webpage. If sensitive values are present please ensure that you have a strict Content Security Policy set up to avoid data leaking.

```jsx
<WebView
source={{ uri: 'https://reactnative.dev' }}
injectedJavaScriptObject={{ customValue: 'myCustomValue' }}
/>;
```

```html
<html>
<head>
<script>
window.onload = (event) => {
if (window.ReactNativeWebView.injectedObjectJson()) {
const customValue = JSON.parse(window.ReactNativeWebView.injectedObjectJson()).customValue;
...
}
}
</script>
</head>
</html>
```

---

### `mediaPlaybackRequiresUserAction`[](#props-index)

Boolean that determines whether HTML5 audio and video requires the user to tap them before they start playing. The default value is `true`. (Android API minimum version 17).
Expand Down
15 changes: 15 additions & 0 deletions example/examples/Injection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export default class Injection extends Component<Props, State> {
onMessage={() => {}}
injectedJavaScriptBeforeContentLoadedForMainFrameOnly={false}
injectedJavaScriptForMainFrameOnly={false}
injectedJavaScriptObject={{ hello: "world" }}

/* We set this property in each frame */
injectedJavaScriptBeforeContentLoaded={`
Expand Down Expand Up @@ -101,6 +102,20 @@ export default class Injection extends Component<Props, State> {
window.self.document.body.style.backgroundColor = "cyan";
}
// Example usage of injectedJavaScriptObject({hello: 'world'}), see above
const injectedObjectJson = window.ReactNativeWebView.injectedObjectJson();
// injectedJavaScriptObject is only available on Android
if (injectedObjectJson) {
const injectedObject = JSON.parse(injectedObjectJson);
console.log("injectedJavaScriptObject: ", injectedObject); // injectedJavaScriptObject: { hello: 'world' }
var injectedJavaScriptObjectEle = document.createElement('p');
injectedJavaScriptObjectEle.textContent = "injectedJavaScriptObject: " + injectedObjectJson;
injectedJavaScriptObjectEle.id = "injectedJavaScriptObjectEle";
document.body.appendChild(injectedJavaScriptObjectEle);
}
if(window.self === window.top){
function declareSuccessOfAfterContentLoaded(head){
var style = window.self.document.createElement('style');
Expand Down
1 change: 1 addition & 0 deletions src/RNCWebViewNativeComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ export interface NativeProps extends ViewProps {
thirdPartyCookiesEnabled?: boolean;
// Workaround to watch if listener if defined
hasOnScroll?: boolean;
injectedJavaScriptObject?: string;
// !Android only

// iOS only
Expand Down
2 changes: 2 additions & 0 deletions src/WebView.android.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ const WebViewComponent = forwardRef<{}, AndroidWebViewProps>(({
source,
nativeConfig,
onShouldStartLoadWithRequest: onShouldStartLoadWithRequestProp,
injectedJavaScriptObject,
...otherProps
}, ref) => {
const messagingModuleName = useRef<string>(`WebViewMessageHandler${uniqueRef += 1}`).current;
Expand Down Expand Up @@ -208,6 +209,7 @@ const WebViewComponent = forwardRef<{}, AndroidWebViewProps>(({
setBuiltInZoomControls={setBuiltInZoomControls}
setDisplayZoomControls={setDisplayZoomControls}
nestedScrollEnabled={nestedScrollEnabled}
injectedJavaScriptObject={JSON.stringify(injectedJavaScriptObject)}
{...nativeConfig?.props}
/>

Expand Down
5 changes: 5 additions & 0 deletions src/WebViewTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1275,6 +1275,11 @@ export interface WebViewSharedProps extends ViewProps {
*/
basicAuthCredential?: BasicAuthCredential;

/**
* Inject a JavaScript object to be accessed as a JSON string via JavaScript in the WebView.
*/
injectedJavaScriptObject?: object;

/**
* Enables WebView remote debugging using Chrome (Android) or Safari (iOS).
*/
Expand Down

0 comments on commit 447f68e

Please sign in to comment.