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
     ((RNCWebView) view).setInjectedJavaScript(injectedJavaScript);
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
   @ReactProp(name = "messagingEnabled")
409
   @ReactProp(name = "messagingEnabled")
395
   public void setMessagingEnabled(WebView view, boolean enabled) {
410
   public void setMessagingEnabled(WebView view, boolean enabled) {
396
     ((RNCWebView) view).setMessagingEnabled(enabled);
411
     ((RNCWebView) view).setMessagingEnabled(enabled);
732
       super.onPageStarted(webView, url, favicon);
747
       super.onPageStarted(webView, url, favicon);
733
       mLastLoadFailed = false;
748
       mLastLoadFailed = false;
734
 
749
 
750
+      RNCWebView reactWebView = (RNCWebView) webView;
751
+      reactWebView.callInjectedJavaScriptBeforeContentLoaded();       
752
+
735
       dispatchEvent(
753
       dispatchEvent(
736
         webView,
754
         webView,
737
         new TopLoadingStartEvent(
755
         new TopLoadingStartEvent(
970
   protected static class RNCWebView extends WebView implements LifecycleEventListener {
988
   protected static class RNCWebView extends WebView implements LifecycleEventListener {
971
     protected @Nullable
989
     protected @Nullable
972
     String injectedJS;
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
     protected boolean messagingEnabled = false;
1001
     protected boolean messagingEnabled = false;
974
     protected @Nullable
1002
     protected @Nullable
975
     RNCWebViewClient mRNCWebViewClient;
1003
     RNCWebViewClient mRNCWebViewClient;
1043
       injectedJS = js;
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
     protected RNCWebViewBridge createRNCWebViewBridge(RNCWebView webView) {
1086
     protected RNCWebViewBridge createRNCWebViewBridge(RNCWebView webView) {
1047
       return new RNCWebViewBridge(webView);
1087
       return new RNCWebViewBridge(webView);
1048
     }
1088
     }
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
     public void onMessage(String message) {
1135
     public void onMessage(String message) {
1088
       if (mRNCWebViewClient != null) {
1136
       if (mRNCWebViewClient != null) {
1089
         WebView webView = this;
1137
         WebView webView = this;

+ 6
- 0
docs/Guide.md View File

309
 
309
 
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.
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
 ```jsx
318
 ```jsx
313
 import React, { Component } from 'react';
319
 import React, { Component } from 'react';
314
 import { View } from 'react-native';
320
 import { View } from 'react-native';

+ 27
- 1
docs/Reference.md View File

7
 - [`source`](Reference.md#source)
7
 - [`source`](Reference.md#source)
8
 - [`automaticallyAdjustContentInsets`](Reference.md#automaticallyadjustcontentinsets)
8
 - [`automaticallyAdjustContentInsets`](Reference.md#automaticallyadjustcontentinsets)
9
 - [`injectedJavaScript`](Reference.md#injectedjavascript)
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
 - [`mediaPlaybackRequiresUserAction`](Reference.md#mediaplaybackrequiresuseraction)
13
 - [`mediaPlaybackRequiresUserAction`](Reference.md#mediaplaybackrequiresuseraction)
12
 - [`nativeConfig`](Reference.md#nativeconfig)
14
 - [`nativeConfig`](Reference.md#nativeconfig)
13
 - [`onError`](Reference.md#onerror)
15
 - [`onError`](Reference.md#onerror)
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
 ### `mediaPlaybackRequiresUserAction`
203
 ### `mediaPlaybackRequiresUserAction`
178
 
204
 
179
 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).
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
 import Scrolling from './examples/Scrolling';
14
 import Scrolling from './examples/Scrolling';
15
 import Background from './examples/Background';
15
 import Background from './examples/Background';
16
 import Uploads from './examples/Uploads';
16
 import Uploads from './examples/Uploads';
17
+import Injection from './examples/Injection';
17
 
18
 
18
 const TESTS = {
19
 const TESTS = {
19
   Alerts: {
20
   Alerts: {
48
       return <Uploads />;
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
 type Props = {};
62
 type Props = {};
101
             title="Background"
110
             title="Background"
102
             onPress={() => this._changeTest('Background')}
111
             onPress={() => this._changeTest('Background')}
103
           />
112
           />
113
+          <Button
114
+            testID="testType_injection"
115
+            title="Injection"
116
+            onPress={() => this._changeTest('Injection')}
117
+          />
104
           {Platform.OS === 'android' && <Button
118
           {Platform.OS === 'android' && <Button
105
             testID="testType_uploads"
119
             testID="testType_uploads"
106
             title="Uploads"
120
             title="Uploads"

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

2
 
2
 
3
 import android.app.Application;
3
 import android.app.Application;
4
 import android.content.Context;
4
 import android.content.Context;
5
+import android.os.Build;
6
+import android.webkit.WebView;
7
+
5
 import com.facebook.react.PackageList;
8
 import com.facebook.react.PackageList;
6
 import com.facebook.react.ReactApplication;
9
 import com.facebook.react.ReactApplication;
7
 import com.facebook.react.ReactNativeHost;
10
 import com.facebook.react.ReactNativeHost;
44
   @Override
47
   @Override
45
   public void onCreate() {
48
   public void onCreate() {
46
     super.onCreate();
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
     SoLoader.init(this, /* native exopackage */ false);
54
     SoLoader.init(this, /* native exopackage */ false);
48
     initializeFlipper(this); // Remove this line if you don't want Flipper enabled
55
     initializeFlipper(this); // Remove this line if you don't want Flipper enabled
49
   }
56
   }

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

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
   incognito?: boolean;
225
   incognito?: boolean;
226
   injectedJavaScript?: string;
226
   injectedJavaScript?: string;
227
   injectedJavaScriptBeforeContentLoaded?: string;
227
   injectedJavaScriptBeforeContentLoaded?: string;
228
+  injectedJavaScriptForMainFrameOnly?: boolean;
229
+  injectedJavaScriptBeforeContentLoadedForMainFrameOnly?: boolean;
228
   mediaPlaybackRequiresUserAction?: boolean;
230
   mediaPlaybackRequiresUserAction?: boolean;
229
   messagingEnabled: boolean;
231
   messagingEnabled: boolean;
230
   onScroll?: (event: NativeScrollEvent) => void;
232
   onScroll?: (event: NativeScrollEvent) => void;
853
    */
855
    */
854
   injectedJavaScriptBeforeContentLoaded?: string;
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
    * Boolean value that determines whether a horizontal scroll indicator is
871
    * Boolean value that determines whether a horizontal scroll indicator is
858
    * shown in the `WebView`. The default value is `true`.
872
    * shown in the `WebView`. The default value is `true`.