Salvatore Randazzo 4 years ago
parent
commit
d453f9ca41
No account linked to committer's email address

+ 48
- 0
android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java View File

@@ -391,6 +391,21 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
391 391
     ((RNCWebView) view).setInjectedJavaScript(injectedJavaScript);
392 392
   }
393 393
 
394
+  @ReactProp(name = "injectedJavaScriptBeforeContentLoaded")
395
+  public void setInjectedJavaScriptBeforeContentLoaded(WebView view, @Nullable String injectedJavaScriptBeforeContentLoaded) {
396
+    ((RNCWebView) view).setInjectedJavaScriptBeforeContentLoaded(injectedJavaScriptBeforeContentLoaded);
397
+  }
398
+
399
+  @ReactProp(name = "injectedJavaScriptForMainFrameOnly")
400
+  public void setInjectedJavaScriptForMainFrameOnly(WebView view, boolean enabled) {
401
+    ((RNCWebView) view).setInjectedJavaScriptForMainFrameOnly(enabled);
402
+  }
403
+
404
+  @ReactProp(name = "injectedJavaScriptBeforeContentLoadedForMainFrameOnly")
405
+  public void setInjectedJavaScriptBeforeContentLoadedForMainFrameOnly(WebView view, boolean enabled) {
406
+    ((RNCWebView) view).setInjectedJavaScriptBeforeContentLoadedForMainFrameOnly(enabled);
407
+  }
408
+
394 409
   @ReactProp(name = "messagingEnabled")
395 410
   public void setMessagingEnabled(WebView view, boolean enabled) {
396 411
     ((RNCWebView) view).setMessagingEnabled(enabled);
@@ -732,6 +747,9 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
732 747
       super.onPageStarted(webView, url, favicon);
733 748
       mLastLoadFailed = false;
734 749
 
750
+      RNCWebView reactWebView = (RNCWebView) webView;
751
+      reactWebView.callInjectedJavaScriptBeforeContentLoaded();       
752
+
735 753
       dispatchEvent(
736 754
         webView,
737 755
         new TopLoadingStartEvent(
@@ -970,6 +988,16 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
970 988
   protected static class RNCWebView extends WebView implements LifecycleEventListener {
971 989
     protected @Nullable
972 990
     String injectedJS;
991
+    protected @Nullable
992
+    String injectedJSBeforeContentLoaded;
993
+
994
+    /**
995
+     * android.webkit.WebChromeClient fundamentally does not support JS injection into frames other
996
+     * than the main frame, so these two properties are mostly here just for parity with iOS & macOS.
997
+     */
998
+    protected boolean injectedJavaScriptForMainFrameOnly = true;
999
+    protected boolean injectedJavaScriptBeforeContentLoadedForMainFrameOnly = true;
1000
+
973 1001
     protected boolean messagingEnabled = false;
974 1002
     protected @Nullable
975 1003
     RNCWebViewClient mRNCWebViewClient;
@@ -1043,6 +1071,18 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
1043 1071
       injectedJS = js;
1044 1072
     }
1045 1073
 
1074
+    public void setInjectedJavaScriptBeforeContentLoaded(@Nullable String js) {
1075
+      injectedJSBeforeContentLoaded = js;
1076
+    }
1077
+
1078
+    public void setInjectedJavaScriptForMainFrameOnly(boolean enabled) {
1079
+      injectedJavaScriptForMainFrameOnly = enabled;
1080
+    }
1081
+
1082
+    public void setInjectedJavaScriptBeforeContentLoadedForMainFrameOnly(boolean enabled) {
1083
+      injectedJavaScriptBeforeContentLoadedForMainFrameOnly = enabled;
1084
+    }
1085
+
1046 1086
     protected RNCWebViewBridge createRNCWebViewBridge(RNCWebView webView) {
1047 1087
       return new RNCWebViewBridge(webView);
1048 1088
     }
@@ -1084,6 +1124,14 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
1084 1124
       }
1085 1125
     }
1086 1126
 
1127
+    public void callInjectedJavaScriptBeforeContentLoaded() {
1128
+      if (getSettings().getJavaScriptEnabled() &&
1129
+      injectedJSBeforeContentLoaded != null &&
1130
+      !TextUtils.isEmpty(injectedJSBeforeContentLoaded)) {
1131
+        evaluateJavascriptWithFallback("(function() {\n" + injectedJSBeforeContentLoaded + ";\n})();");
1132
+      }
1133
+    }
1134
+
1087 1135
     public void onMessage(String message) {
1088 1136
       if (mRNCWebViewClient != null) {
1089 1137
         WebView webView = this;

+ 6
- 0
docs/Guide.md View File

@@ -309,6 +309,12 @@ _Under the hood_
309 309
 
310 310
 This is a script that runs **before** the web page loads for the first time. It only runs once, even if the page is reloaded or navigated away. This is useful if you want to inject anything into the window, localstorage, or document prior to the web code executing.
311 311
 
312
+> Android Compatibility: 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). 
313
+For example, global variables and functions defined before calling loadUrl(java.lang.String) will not exist in the loaded page. 
314
+Applications should use addJavascriptInterface(Object, String) instead to persist JavaScript objects across navigations.
315
+
316
+
317
+
312 318
 ```jsx
313 319
 import React, { Component } from 'react';
314 320
 import { View } from 'react-native';

+ 27
- 1
docs/Reference.md View File

@@ -7,7 +7,9 @@ This document lays out the current public properties and methods for the React N
7 7
 - [`source`](Reference.md#source)
8 8
 - [`automaticallyAdjustContentInsets`](Reference.md#automaticallyadjustcontentinsets)
9 9
 - [`injectedJavaScript`](Reference.md#injectedjavascript)
10
-- [`injectedJavaScriptBeforeContentLoaded`](Reference.md#injectedJavaScriptBeforeContentLoaded)
10
+- [`injectedJavaScriptBeforeContentLoaded`](Reference.md#injectedjavascriptbeforecontentloaded)
11
+- [`injectedJavaScriptForMainFrameOnly`](Reference.md#injectedjavascriptformainframeonly)
12
+- [`injectedJavaScriptBeforeContentLoadedForMainFrameOnly`](Reference.md#injectedjavascriptbeforecontentloadedformainframeonly)
11 13
 - [`mediaPlaybackRequiresUserAction`](Reference.md#mediaplaybackrequiresuseraction)
12 14
 - [`nativeConfig`](Reference.md#nativeconfig)
13 15
 - [`onError`](Reference.md#onerror)
@@ -174,6 +176,30 @@ const INJECTED_JAVASCRIPT = `(function() {
174 176
 
175 177
 ---
176 178
 
179
+### `injectedJavaScriptForMainFrameOnly`
180
+
181
+If `true` (default; mandatory for Android), loads the `injectedJavaScript` only into the main frame.
182
+
183
+If `false`, (only supported on iOS and macOS), loads it into all frames (e.g. iframes).
184
+
185
+| Type   | Required | Platform |
186
+| ------ | -------- | -------- |
187
+| bool | No       | iOS and macOS (only `true` supported for Android) |
188
+
189
+---
190
+
191
+### `injectedJavaScriptBeforeContentLoadedForMainFrameOnly`
192
+
193
+If `true` (default; mandatory for Android), loads the `injectedJavaScriptBeforeContentLoaded` only into the main frame.
194
+
195
+If `false`, (only supported on iOS and macOS), loads it into all frames (e.g. iframes).
196
+
197
+| Type   | Required | Platform |
198
+| ------ | -------- | -------- |
199
+| bool | No       | iOS and macOS (only `true` supported for Android) |
200
+
201
+---
202
+
177 203
 ### `mediaPlaybackRequiresUserAction`
178 204
 
179 205
 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).

+ 14
- 0
example/App.tsx View File

@@ -14,6 +14,7 @@ import Alerts from './examples/Alerts';
14 14
 import Scrolling from './examples/Scrolling';
15 15
 import Background from './examples/Background';
16 16
 import Uploads from './examples/Uploads';
17
+import Injection from './examples/Injection';
17 18
 
18 19
 const TESTS = {
19 20
   Alerts: {
@@ -48,6 +49,14 @@ const TESTS = {
48 49
       return <Uploads />;
49 50
     },
50 51
   },
52
+  Injection: {
53
+    title: 'Injection',
54
+    testId: 'injection',
55
+    description: 'Injection test',
56
+    render() {
57
+      return <Injection />;
58
+    },
59
+  },
51 60
 };
52 61
 
53 62
 type Props = {};
@@ -101,6 +110,11 @@ export default class App extends Component<Props, State> {
101 110
             title="Background"
102 111
             onPress={() => this._changeTest('Background')}
103 112
           />
113
+          <Button
114
+            testID="testType_injection"
115
+            title="Injection"
116
+            onPress={() => this._changeTest('Injection')}
117
+          />
104 118
           {Platform.OS === 'android' && <Button
105 119
             testID="testType_uploads"
106 120
             title="Uploads"

+ 7
- 0
example/android/app/src/main/java/com/example/MainApplication.java View File

@@ -2,6 +2,9 @@ package com.example;
2 2
 
3 3
 import android.app.Application;
4 4
 import android.content.Context;
5
+import android.os.Build;
6
+import android.webkit.WebView;
7
+
5 8
 import com.facebook.react.PackageList;
6 9
 import com.facebook.react.ReactApplication;
7 10
 import com.facebook.react.ReactNativeHost;
@@ -44,6 +47,10 @@ public class MainApplication extends Application implements ReactApplication {
44 47
   @Override
45 48
   public void onCreate() {
46 49
     super.onCreate();
50
+    /* https://developers.google.com/web/tools/chrome-devtools/remote-debugging/webviews */
51
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
52
+      WebView.setWebContentsDebuggingEnabled(true);
53
+    }
47 54
     SoLoader.init(this, /* native exopackage */ false);
48 55
     initializeFlipper(this); // Remove this line if you don't want Flipper enabled
49 56
   }

+ 114
- 0
example/examples/Injection.tsx View File

@@ -0,0 +1,114 @@
1
+import React, {Component} from 'react';
2
+import {Text, View, ScrollView} from 'react-native';
3
+
4
+import WebView from 'react-native-webview';
5
+
6
+const HTML = `
7
+<!DOCTYPE html>
8
+<html>
9
+	<head>
10
+	    <meta charset="utf-8">
11
+	    <meta name="viewport" content="width=device-width, initial-scale=1">
12
+	    <title>iframe test</title>
13
+	</head>
14
+  <body>
15
+    <p style="">beforeContentLoaded <span id="before_failed" style="display: inline-block;">failed</span><span id="before_succeeded" style="display: none;">succeeded</span>!</p>
16
+    <p style="">afterContentLoaded <span id="after_failed" style="display: inline-block;">failed</span><span id="after_succeeded" style="display: none;">succeeded</span>!</p>
17
+    <iframe src="https://birchlabs.co.uk/linguabrowse/infopages/obsol/iframe.html?v=1" name="iframe_0" style="width: 100%; height: 25px;"></iframe>
18
+		<iframe src="https://birchlabs.co.uk/linguabrowse/infopages/obsol/iframe2.html?v=1" name="iframe_1" style="width: 100%; height: 25px;"></iframe>
19
+		<iframe src="https://www.ebay.co.uk" name="iframe_2" style="width: 100%; height: 25px;"></iframe>
20
+	</body>
21
+</html>
22
+`;
23
+
24
+type Props = {};
25
+type State = {
26
+  backgroundColor: string,
27
+};
28
+
29
+export default class Injection extends Component<Props, State> {
30
+  state = {
31
+    backgroundColor: '#FF00FF00'
32
+  };
33
+
34
+  render() {
35
+    return (
36
+      <ScrollView>
37
+        <View style={{ }}>
38
+          <View style={{ height: 200 }}>
39
+            <WebView
40
+              /**
41
+               * This HTML is a copy of a multi-frame JS injection test that I had lying around.
42
+               * @see https://birchlabs.co.uk/linguabrowse/infopages/obsol/iframeTest.html
43
+               */
44
+              source={{ html: HTML }}
45
+              automaticallyAdjustContentInsets={false}
46
+              style={{backgroundColor:'#00000000'}}
47
+              
48
+              /* Must be populated in order for `messagingEnabled` to be `true` to activate the
49
+               * JS injection user scripts, consistent with current behaviour. This is undesirable,
50
+               * so needs addressing in a follow-up PR. */
51
+              onMessage={() => {}}
52
+
53
+              /* We set this property in each frame */
54
+              injectedJavaScriptBeforeContentLoaded={`
55
+              window.self.colourToUse = "green";
56
+
57
+              function declareSuccessBeforeContentLoaded(head){
58
+                var style = window.self.document.createElement('style');
59
+                style.type = 'text/css';
60
+                style.innerHTML = "#before_failed { display: none !important; }#before_succeeded { display: inline-block !important; }";
61
+                head.appendChild(style);
62
+              }
63
+
64
+              const head = (window.self.document.head || window.self.document.getElementsByTagName('head')[0]);
65
+
66
+              if(head){
67
+                declareSuccessBeforeContentLoaded(head);
68
+              } else {
69
+                window.self.document.addEventListener("DOMContentLoaded", function (event) {
70
+                  const head = (window.self.document.head || window.self.document.getElementsByTagName('head')[0]);
71
+                  declareSuccessBeforeContentLoaded(head);
72
+                });
73
+              }
74
+
75
+              `}
76
+              
77
+              injectedJavaScriptForMainFrameOnly={false}
78
+
79
+              /* We read the colourToUse property in each frame to recolour each frame */
80
+              injectedJavaScript={`
81
+              function declareSuccessAfterContentLoaded(head){
82
+                var style = window.self.document.createElement('style');
83
+                style.type = 'text/css';
84
+                style.innerHTML = "#after_failed { display: none !important; }#after_succeeded { display: inline-block !important; }";
85
+                head.appendChild(style);
86
+              }
87
+
88
+              declareSuccessAfterContentLoaded(window.self.document.head || window.self.document.getElementsByTagName('head')[0]);
89
+
90
+              if(window.self.colourToUse){
91
+                window.self.document.body.style.backgroundColor = window.self.colourToUse;
92
+              } else {
93
+                window.self.document.body.style.backgroundColor = "cyan";
94
+              } 
95
+              `}
96
+            />
97
+          </View>
98
+        </View>
99
+        <Text>This test presents three iframes: iframe_0 (yellow); iframe_1 (pink); and iframe_2 (transparent, because its 'X-Frame-Options' is set to 'SAMEORIGIN').</Text>
100
+        <Text>Before injection, the main frame's background is the browser's default value (transparent or white) and each frame has its natural colour.</Text>
101
+        <Text>1a) At injection time "beforeContentLoaded", a variable will be set in each frame to set 'green' as the "colour to be used".</Text>
102
+        <Text>1b) Also upon "beforeContentLoaded", a style element to change the text "beforeContentLoaded failed" -> "beforeContentLoaded succeeded" will be applied as soon as the head has loaded.</Text>
103
+        <Text>2a) At injection time "afterContentLoaded", that variable will be read – if present, the colour green will be injected into all frames. Otherwise, cyan.</Text>
104
+        <Text>2b) Also upon "afterContentLoaded", a style element to change the text "afterContentLoaded failed" -> "afterContentLoaded succeeded" will be applied as soon as the head has loaded.</Text>
105
+        <Text>✅ If the main frame, iframe_0, and iframe_1 all become green or cyan, then multi-frame injection is supported.</Text>
106
+        <Text>✅ If the two texts say "beforeContentLoaded succeeded!" and "afterContentLoaded succeeded!", then both injection times are supported.</Text>
107
+        <Text>❌ If the text "beforeContentLoaded failed" remains unchanged, then JS injection has failed on the main frame before the content loaded.</Text>
108
+        <Text>❌ If the text "afterContentLoaded failed" remains unchanged, then JS injection has failed on the main frame after the content loaded.</Text>
109
+        <Text>❌ If any frames become coloured cyan, then JS injection has failed before the content loaded (but succeeded after the content loaded).</Text>
110
+        <Text>❌ If only the main frame changes colour (to green or cyan), then multi-frame injection is not supported.</Text>
111
+      </ScrollView>
112
+    );
113
+  }
114
+}

+ 14
- 0
src/WebViewTypes.ts View File

@@ -225,6 +225,8 @@ export interface CommonNativeWebViewProps extends ViewProps {
225 225
   incognito?: boolean;
226 226
   injectedJavaScript?: string;
227 227
   injectedJavaScriptBeforeContentLoaded?: string;
228
+  injectedJavaScriptForMainFrameOnly?: boolean;
229
+  injectedJavaScriptBeforeContentLoadedForMainFrameOnly?: boolean;
228 230
   mediaPlaybackRequiresUserAction?: boolean;
229 231
   messagingEnabled: boolean;
230 232
   onScroll?: (event: NativeScrollEvent) => void;
@@ -853,6 +855,18 @@ export interface WebViewSharedProps extends ViewProps {
853 855
    */
854 856
   injectedJavaScriptBeforeContentLoaded?: string;
855 857
 
858
+  /**
859
+   * If `true` (default; mandatory for Android), loads the `injectedJavaScript` only into the main frame.
860
+   * If `false` (only supported on iOS and macOS), loads it into all frames (e.g. iframes).
861
+   */
862
+  injectedJavaScriptForMainFrameOnly?: boolean;
863
+
864
+  /**
865
+   * If `true` (default; mandatory for Android), loads the `injectedJavaScriptBeforeContentLoaded` only into the main frame.
866
+   * If `false` (only supported on iOS and macOS), loads it into all frames (e.g. iframes).
867
+   */
868
+  injectedJavaScriptBeforeContentLoadedForMainFrameOnly?: boolean;
869
+
856 870
   /**
857 871
    * Boolean value that determines whether a horizontal scroll indicator is
858 872
    * shown in the `WebView`. The default value is `true`.