Browse Source

feat(iOS): WKUserScripts (e.g. injectedJavaScript) can now update upon props change; and can be configured to inject into all frames. (#1119)

BREAKING CHANGE: 
• Props updates to `injectedJavaScript` are no longer immutable.

• `injectedJavaScript` no longer attaches a `jsEvaluationValue` property to the `onLoadingFinish` event. Check out: https://github.com/react-native-community/react-native-webview/pull/1119#issuecomment-574919464 to migrate with the same behavior.
Jamie Birch 4 years ago
parent
commit
9cb2f6e2f3
No account linked to committer's email address
11 changed files with 435 additions and 129 deletions
  1. 4
    1
      .gitignore
  2. 8
    1
      docs/Guide.md
  3. 42
    7
      docs/Reference.md
  4. 14
    0
      example/App.tsx
  5. 160
    0
      example/examples/Injection.tsx
  6. 2
    2
      example/ios/Podfile.lock
  7. 2
    0
      ios/RNCWebView.h
  8. 179
    118
      ios/RNCWebView.m
  9. 2
    0
      ios/RNCWebViewManager.m
  10. 6
    0
      src/WebView.ios.tsx
  11. 16
    0
      src/WebViewTypes.ts

+ 4
- 1
.gitignore View File

@@ -53,4 +53,7 @@ android/gradle
53 53
 android/gradlew
54 54
 android/gradlew.bat
55 55
 
56
-lib/
56
+lib/
57
+.classpath
58
+.project
59
+.settings/

+ 8
- 1
docs/Guide.md View File

@@ -293,11 +293,13 @@ export default class App extends Component {
293 293
 
294 294
 This runs the JavaScript in the `runFirst` string once the page is loaded. In this case, you can see that both the body style was changed to red and the alert showed up after 2 seconds.
295 295
 
296
+By setting `injectedJavaScriptForMainFrameOnly: false`, the JavaScript injection will occur on all frames (not just the top frame) if supported for the given platform.
297
+
296 298
 <img alt="screenshot of Github repo" width="200" src="https://user-images.githubusercontent.com/1479215/53609254-e5dc9c00-3b7a-11e9-9118-bc4e520ce6ca.png" />
297 299
 
298 300
 _Under the hood_
299 301
 
300
-> On iOS, `injectedJavaScript` runs a method on WebView called `evaluateJavaScript:completionHandler:`
302
+> On iOS, ~~`injectedJavaScript` 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 `WKUserScriptInjectionTimeAtDocumentEnd`. As a consequence, `injectedJavaScript` 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-community/react-native-webview/pull/1119#issuecomment-574919464) to retain equivalent behaviour.
301 303
 > On Android, `injectedJavaScript` runs a method on the Android WebView called `evaluateJavascriptWithFallback`
302 304
 
303 305
 #### The `injectedJavaScriptBeforeContentLoaded` prop
@@ -332,6 +334,11 @@ export default class App extends Component {
332 334
 
333 335
 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.
334 336
 
337
+By setting `injectedJavaScriptBeforeContentLoadedForMainFrameOnly: false`, the JavaScript injection will occur on all frames (not just the top frame) if supported for the given platform. Howver, although support for `injectedJavaScriptBeforeContentLoadedForMainFrameOnly: false` has been implemented for iOS and macOS, [it is not clear](https://github.com/react-native-community/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.
338
+
339
+> 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-community/react-native-webview/pull/1119#issuecomment-574919464) to retain equivalent behaviour.
340
+> On Android, `injectedJavaScript` runs a method on the Android WebView called `evaluateJavascriptWithFallback`
341
+
335 342
 #### The `injectJavaScript` method
336 343
 
337 344
 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!).

+ 42
- 7
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)
@@ -120,11 +122,15 @@ Controls whether to adjust the content inset for web views that are placed behin
120 122
 
121 123
 ### `injectedJavaScript`
122 124
 
123
-Set this to provide JavaScript that will be injected into the web page when the view loads. Make sure the string evaluates to a valid type (`true` works) and doesn't otherwise throw an exception.
125
+Set this to provide JavaScript that will be injected into the web page after the document finishes loading, but before other subresources finish loading.
126
+
127
+Make sure the string evaluates to a valid type (`true` works) and doesn't otherwise throw an exception.
128
+
129
+On iOS, see [`WKUserScriptInjectionTimeAtDocumentEnd`](https://developer.apple.com/documentation/webkit/wkuserscriptinjectiontime/wkuserscriptinjectiontimeatdocumentend?language=objc)
124 130
 
125 131
 | Type   | Required | Platform |
126 132
 | ------ | -------- | -------- |
127
-| string | No       | iOS, Andrdoid, macOS
133
+| string | No       | iOS, Android, macOS
128 134
 
129 135
 To learn more, read the [Communicating between JS and Native](Guide.md#communicating-between-js-and-native) guide.
130 136
 
@@ -148,18 +154,21 @@ const INJECTED_JAVASCRIPT = `(function() {
148 154
 
149 155
 ### `injectedJavaScriptBeforeContentLoaded`
150 156
 
151
-Set this to provide JavaScript that will be injected into the web page after the document element is created, but before any other content is loaded. Make sure the string evaluates to a valid type (`true` works) and doesn't otherwise throw an exception.
152
-On iOS, see [WKUserScriptInjectionTimeAtDocumentStart](https://developer.apple.com/documentation/webkit/wkuserscriptinjectiontime/wkuserscriptinjectiontimeatdocumentstart?language=objc)
157
+Set this to provide JavaScript that will be injected into the web page after the document element is created, but before other subresources finish loading.
158
+
159
+Make sure the string evaluates to a valid type (`true` works) and doesn't otherwise throw an exception.
160
+
161
+On iOS, see [`WKUserScriptInjectionTimeAtDocumentStart`](https://developer.apple.com/documentation/webkit/wkuserscriptinjectiontime/wkuserscriptinjectiontimeatdocumentstart?language=objc)
153 162
 
154 163
 | Type   | Required | Platform |
155 164
 | ------ | -------- | -------- |
156
-| string | No       | iOS, Android, macOS |
165
+| string | No       | iOS, macOS |
157 166
 
158 167
 To learn more, read the [Communicating between JS and Native](Guide.md#communicating-between-js-and-native) guide.
159 168
 
160 169
 Example:
161 170
 
162
-Post message a JSON object of `window.location` to be handled by [`onMessage`](Reference.md#onmessage)
171
+Post message a JSON object of `window.location` to be handled by [`onMessage`](Reference.md#onmessage). `window.ReactNativeWebView.postMessage` *will* be available at this time.
163 172
 
164 173
 ```jsx
165 174
 const INJECTED_JAVASCRIPT = `(function() {
@@ -175,6 +184,32 @@ const INJECTED_JAVASCRIPT = `(function() {
175 184
 
176 185
 ---
177 186
 
187
+### `injectedJavaScriptForMainFrameOnly`
188
+
189
+If `true` (default), loads the `injectedJavaScript` only into the main frame.
190
+
191
+If `false`, loads it into all frames (e.g. iframes).
192
+
193
+| Type   | Required | Platform |
194
+| ------ | -------- | -------- |
195
+| bool | No       | iOS, macOS       |
196
+
197
+---
198
+
199
+### `injectedJavaScriptBeforeContentLoadedForMainFrameOnly`
200
+
201
+If `true` (default), loads the `injectedJavaScriptBeforeContentLoaded` only into the main frame.
202
+
203
+If `false`, loads it into all frames (e.g. iframes).
204
+
205
+Warning: although support for `injectedJavaScriptBeforeContentLoadedForMainFrameOnly: false` has been implemented for iOS and macOS, [it is not clear](https://github.com/react-native-community/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.
206
+
207
+| Type   | Required | Platform |
208
+| ------ | -------- | -------- |
209
+| bool | No       | iOS, macOS       |
210
+
211
+---
212
+
178 213
 ### `mediaPlaybackRequiresUserAction`
179 214
 
180 215
 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"

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

@@ -0,0 +1,160 @@
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 on the top frame <span id="before_failed" style="display: inline-block;">failed</span><span id="before_succeeded" style="display: none;">succeeded</span>!</p>
16
+//     <p style="">afterContentLoaded on the top frame <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: 300 }}>
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
+              source={{ uri: "https://birchlabs.co.uk/linguabrowse/infopages/obsol/rnw_iframe_test.html" }}
46
+              automaticallyAdjustContentInsets={false}
47
+              style={{backgroundColor:'#00000000'}}
48
+              
49
+              /* Must be populated in order for `messagingEnabled` to be `true` to activate the
50
+               * JS injection user scripts, consistent with current behaviour. This is undesirable,
51
+               * so needs addressing in a follow-up PR. */
52
+              onMessage={() => {}}
53
+
54
+              /* We set this property in each frame */
55
+              injectedJavaScriptBeforeContentLoaded={`
56
+              console.log("executing injectedJavaScriptBeforeContentLoaded...");
57
+              if(typeof window.top.injectedIframesBeforeContentLoaded === "undefined"){
58
+                window.top.injectedIframesBeforeContentLoaded = [];
59
+              }
60
+              window.self.colourToUse = "orange";
61
+              if(window.self === window.top){
62
+                console.log("Was window.top. window.frames.length is:", window.frames.length);
63
+                window.self.numberOfFramesAtBeforeContentLoaded = window.frames.length;
64
+                function declareSuccessOfBeforeContentLoaded(head){
65
+                  var style = window.self.document.createElement('style');
66
+                  style.type = 'text/css';
67
+                  style.innerHTML = "#before_failed { display: none !important; }#before_succeeded { display: inline-block !important; }";
68
+                  head.appendChild(style);
69
+                }
70
+
71
+                const head = (window.self.document.head || window.self.document.getElementsByTagName('head')[0]);
72
+
73
+                if(head){
74
+                  declareSuccessOfBeforeContentLoaded(head);
75
+                } else {
76
+                  window.self.document.addEventListener("DOMContentLoaded", function (event) {
77
+                    const head = (window.self.document.head || window.self.document.getElementsByTagName('head')[0]);
78
+                    declareSuccessOfBeforeContentLoaded(head);
79
+                  });
80
+                }
81
+              } else {
82
+                window.top.injectedIframesBeforeContentLoaded.push(window.self.name);
83
+                console.log("wasn't window.top.");
84
+                console.log("wasn't window.top. Still going...");
85
+              }
86
+              `}
87
+              
88
+              injectedJavaScriptForMainFrameOnly={false}
89
+
90
+              /* We read the colourToUse property in each frame to recolour each frame */
91
+              injectedJavaScript={`
92
+              console.log("executing injectedJavaScript...");
93
+              if(typeof window.top.injectedIframesAfterContentLoaded === "undefined"){
94
+                window.top.injectedIframesAfterContentLoaded = [];
95
+              }
96
+
97
+              if(window.self.colourToUse){
98
+                window.self.document.body.style.backgroundColor = window.self.colourToUse;
99
+              } else {
100
+                window.self.document.body.style.backgroundColor = "cyan";
101
+              }
102
+
103
+              if(window.self === window.top){
104
+                function declareSuccessOfAfterContentLoaded(head){
105
+                  var style = window.self.document.createElement('style');
106
+                  style.type = 'text/css';
107
+                  style.innerHTML = "#after_failed { display: none !important; }#after_succeeded { display: inline-block !important; }";
108
+                  head.appendChild(style);
109
+                }
110
+
111
+                declareSuccessOfAfterContentLoaded(window.self.document.head || window.self.document.getElementsByTagName('head')[0]);
112
+
113
+                // var numberOfFramesAtBeforeContentLoadedEle = document.createElement('p');
114
+                // numberOfFramesAtBeforeContentLoadedEle.textContent = "Number of iframes upon the main frame's beforeContentLoaded: " +
115
+                // window.self.numberOfFramesAtBeforeContentLoaded;
116
+
117
+                // var numberOfFramesAtAfterContentLoadedEle = document.createElement('p');
118
+                // numberOfFramesAtAfterContentLoadedEle.textContent = "Number of iframes upon the main frame's afterContentLoaded: " + window.frames.length;
119
+                // numberOfFramesAtAfterContentLoadedEle.id = "numberOfFramesAtAfterContentLoadedEle";
120
+
121
+                var namedFramesAtBeforeContentLoadedEle = document.createElement('p');
122
+                namedFramesAtBeforeContentLoadedEle.textContent = "Names of iframes that called beforeContentLoaded: " + JSON.stringify(window.top.injectedIframesBeforeContentLoaded);
123
+                namedFramesAtBeforeContentLoadedEle.id = "namedFramesAtBeforeContentLoadedEle";
124
+
125
+                var namedFramesAtAfterContentLoadedEle = document.createElement('p');
126
+                namedFramesAtAfterContentLoadedEle.textContent = "Names of iframes that called afterContentLoaded: " + JSON.stringify(window.top.injectedIframesAfterContentLoaded);
127
+                namedFramesAtAfterContentLoadedEle.id = "namedFramesAtAfterContentLoadedEle";
128
+
129
+                // document.body.appendChild(numberOfFramesAtBeforeContentLoadedEle);
130
+                // document.body.appendChild(numberOfFramesAtAfterContentLoadedEle);
131
+                document.body.appendChild(namedFramesAtBeforeContentLoadedEle);
132
+                document.body.appendChild(namedFramesAtAfterContentLoadedEle);
133
+              } else {
134
+                window.top.injectedIframesAfterContentLoaded.push(window.self.name);
135
+                window.top.document.getElementById('namedFramesAtAfterContentLoadedEle').textContent = "Names of iframes that called afterContentLoaded: " + JSON.stringify(window.top.injectedIframesAfterContentLoaded);
136
+              }
137
+              `}
138
+            />
139
+          </View>
140
+        </View>
141
+        <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>
142
+        <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>
143
+        {/*<Text>1a) At injection time "beforeContentLoaded", a variable will be set in each frame to set 'orange' as the "colour to be used".</Text>*/}
144
+        {/*<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>*/}
145
+        {/*<Text>2a) At injection time "afterContentLoaded", that variable will be read – if present, the colour orange will be injected into all frames. Otherwise, cyan.</Text>*/}
146
+        {/*<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>*/}
147
+        <Text>✅ If the main frame becomes orange, then top-frame injection both beforeContentLoaded and afterContentLoaded is supported.</Text>
148
+        <Text>✅ If iframe_0, and iframe_1 become orange, then multi-frame injection beforeContentLoaded and afterContentLoaded is supported.</Text>
149
+        <Text>✅ If the two texts say "beforeContentLoaded on the top frame succeeded!" and "afterContentLoaded on the top frame succeeded!", then both injection times are supported at least on the main frame.</Text>
150
+        <Text>⚠️ If either of the two iframes become coloured cyan, then for that given frame, JS injection succeeded after the content loaded, but didn't occur before the content loaded - please note that for iframes, this may not be a test failure, as it is not clear whether we would expect iframes to support an injection time of beforeContentLoaded anyway.</Text>
151
+        <Text>⚠️ If "Names of iframes that called beforeContentLoaded: " is [], then see above.</Text>
152
+        <Text>❌ If "Names of iframes that called afterContentLoaded: " is [], then afterContentLoaded is not supported in iframes.</Text>
153
+        <Text>❌ If the main frame becomes coloured cyan, then JS injection succeeded after the content loaded, but didn't occur before the content loaded.</Text>
154
+        <Text>❌ If the text "beforeContentLoaded on the top frame failed" remains unchanged, then JS injection has failed on the main frame before the content loaded.</Text>
155
+        <Text>❌ If the text "afterContentLoaded on the top frame failed" remains unchanged, then JS injection has failed on the main frame after the content loaded.</Text>
156
+        <Text>❌ If the iframes remain their original colours (yellow and pink), then multi-frame injection is not supported at all.</Text>
157
+      </ScrollView>
158
+    );
159
+  }
160
+}

+ 2
- 2
example/ios/Podfile.lock View File

@@ -182,7 +182,7 @@ PODS:
182 182
     - React-cxxreact (= 0.61.5)
183 183
     - React-jsi (= 0.61.5)
184 184
   - React-jsinspector (0.61.5)
185
-  - react-native-webview (8.0.6):
185
+  - react-native-webview (8.2.0):
186 186
     - React
187 187
   - React-RCTActionSheet (0.61.5):
188 188
     - React-Core/RCTActionSheetHeaders (= 0.61.5)
@@ -326,7 +326,7 @@ SPEC CHECKSUMS:
326 326
   React-jsi: cb2cd74d7ccf4cffb071a46833613edc79cdf8f7
327 327
   React-jsiexecutor: d5525f9ed5f782fdbacb64b9b01a43a9323d2386
328 328
   React-jsinspector: fa0ecc501688c3c4c34f28834a76302233e29dc0
329
-  react-native-webview: 222d83c9c489e09b5d3541519110a637490ad4fa
329
+  react-native-webview: 1db33907230d0eb344964d6f3bb56b9ee77e25a4
330 330
   React-RCTActionSheet: 600b4d10e3aea0913b5a92256d2719c0cdd26d76
331 331
   React-RCTAnimation: 791a87558389c80908ed06cc5dfc5e7920dfa360
332 332
   React-RCTBlob: d89293cc0236d9cb0933d85e430b0bbe81ad1d72

+ 2
- 0
ios/RNCWebView.h View File

@@ -31,6 +31,8 @@
31 31
 @property (nonatomic, assign) BOOL messagingEnabled;
32 32
 @property (nonatomic, copy) NSString * _Nullable injectedJavaScript;
33 33
 @property (nonatomic, copy) NSString * _Nullable injectedJavaScriptBeforeContentLoaded;
34
+@property (nonatomic, assign) BOOL injectedJavaScriptForMainFrameOnly;
35
+@property (nonatomic, assign) BOOL injectedJavaScriptBeforeContentLoadedForMainFrameOnly;
34 36
 @property (nonatomic, assign) BOOL scrollEnabled;
35 37
 @property (nonatomic, assign) BOOL sharedCookiesEnabled;
36 38
 @property (nonatomic, assign) BOOL pagingEnabled;

+ 179
- 118
ios/RNCWebView.m View File

@@ -81,6 +81,9 @@ static NSDictionary* customCertificatesForHost;
81 81
 #else
82 82
 @property (nonatomic, copy) RNCWKWebView *webView;
83 83
 #endif // !TARGET_OS_OSX
84
+@property (nonatomic, strong) WKUserScript *postMessageScript;
85
+@property (nonatomic, strong) WKUserScript *atStartScript;
86
+@property (nonatomic, strong) WKUserScript *atEndScript;
84 87
 @end
85 88
 
86 89
 @implementation RNCWebView
@@ -122,10 +125,14 @@ static NSDictionary* customCertificatesForHost;
122 125
     _automaticallyAdjustContentInsets = YES;
123 126
     _contentInset = UIEdgeInsetsZero;
124 127
     _savedKeyboardDisplayRequiresUserAction = YES;
125
-#if !TARGET_OS_OSX
128
+    #if !TARGET_OS_OSX
126 129
     _savedStatusBarStyle = RCTSharedApplication().statusBarStyle;
127 130
     _savedStatusBarHidden = RCTSharedApplication().statusBarHidden;
128
-#endif // !TARGET_OS_OSX
131
+    #endif // !TARGET_OS_OSX
132
+    _injectedJavaScript = nil;
133
+    _injectedJavaScriptForMainFrameOnly = YES;
134
+    _injectedJavaScriptBeforeContentLoaded = nil;
135
+    _injectedJavaScriptBeforeContentLoadedForMainFrameOnly = YES;
129 136
 
130 137
 #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */
131 138
     _savedContentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
@@ -206,50 +213,7 @@ static NSDictionary* customCertificatesForHost;
206 213
   // Shim the HTML5 history API:
207 214
   [wkWebViewConfig.userContentController addScriptMessageHandler:[[RNCWeakScriptMessageDelegate alloc] initWithDelegate:self]
208 215
                                                             name:HistoryShimName];
209
-  NSString *source = [NSString stringWithFormat:
210
-    @"(function(history) {\n"
211
-    "  function notify(type) {\n"
212
-    "    setTimeout(function() {\n"
213
-    "      window.webkit.messageHandlers.%@.postMessage(type)\n"
214
-    "    }, 0)\n"
215
-    "  }\n"
216
-    "  function shim(f) {\n"
217
-    "    return function pushState() {\n"
218
-    "      notify('other')\n"
219
-    "      return f.apply(history, arguments)\n"
220
-    "    }\n"
221
-    "  }\n"
222
-    "  history.pushState = shim(history.pushState)\n"
223
-    "  history.replaceState = shim(history.replaceState)\n"
224
-    "  window.addEventListener('popstate', function() {\n"
225
-    "    notify('backforward')\n"
226
-    "  })\n"
227
-    "})(window.history)\n", HistoryShimName
228
-  ];
229
-  WKUserScript *script = [[WKUserScript alloc] initWithSource:source injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES];
230
-  [wkWebViewConfig.userContentController addUserScript:script];
231
-
232
-  if (_messagingEnabled) {
233
-    [wkWebViewConfig.userContentController addScriptMessageHandler:[[RNCWeakScriptMessageDelegate alloc] initWithDelegate:self]
234
-                                                              name:MessageHandlerName];
235
-
236
-    NSString *source = [NSString stringWithFormat:
237
-      @"window.%@ = {"
238
-       "  postMessage: function (data) {"
239
-       "    window.webkit.messageHandlers.%@.postMessage(String(data));"
240
-       "  }"
241
-       "};", MessageHandlerName, MessageHandlerName
242
-    ];
243
-
244
-    WKUserScript *script = [[WKUserScript alloc] initWithSource:source injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES];
245
-    [wkWebViewConfig.userContentController addUserScript:script];
246
-      
247
-    if (_injectedJavaScriptBeforeContentLoaded) {
248
-      // If user has provided an injectedJavascript prop, execute it at the start of the document
249
-      WKUserScript *injectedScript = [[WKUserScript alloc] initWithSource:_injectedJavaScriptBeforeContentLoaded injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES];
250
-      [wkWebViewConfig.userContentController addUserScript:injectedScript];
251
-    }
252
-  }
216
+  [self resetupScripts:wkWebViewConfig];
253 217
 
254 218
 #if !TARGET_OS_OSX
255 219
   wkWebViewConfig.allowsInlineMediaPlayback = _allowsInlineMediaPlayback;
@@ -266,68 +230,6 @@ static NSDictionary* customCertificatesForHost;
266 230
   if (_applicationNameForUserAgent) {
267 231
       wkWebViewConfig.applicationNameForUserAgent = [NSString stringWithFormat:@"%@ %@", wkWebViewConfig.applicationNameForUserAgent, _applicationNameForUserAgent];
268 232
   }
269
-
270
-  if(_sharedCookiesEnabled) {
271
-    // More info to sending cookies with WKWebView
272
-    // https://stackoverflow.com/questions/26573137/can-i-set-the-cookies-to-be-used-by-a-wkwebview/26577303#26577303
273
-    if (@available(iOS 11.0, *)) {
274
-      // Set Cookies in iOS 11 and above, initialize websiteDataStore before setting cookies
275
-      // See also https://forums.developer.apple.com/thread/97194
276
-      // check if websiteDataStore has not been initialized before
277
-      if(!_incognito && !_cacheEnabled) {
278
-        wkWebViewConfig.websiteDataStore = [WKWebsiteDataStore nonPersistentDataStore];
279
-      }
280
-      for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]) {
281
-        [wkWebViewConfig.websiteDataStore.httpCookieStore setCookie:cookie completionHandler:nil];
282
-      }
283
-    } else {
284
-      NSMutableString *script = [NSMutableString string];
285
-
286
-      // Clear all existing cookies in a direct called function. This ensures that no
287
-      // javascript error will break the web content javascript.
288
-      // We keep this code here, if someone requires that Cookies are also removed within the
289
-      // the WebView and want to extends the current sharedCookiesEnabled option with an
290
-      // additional property.
291
-      // Generates JS: document.cookie = "key=; Expires=Thu, 01 Jan 1970 00:00:01 GMT;"
292
-      // for each cookie which is already available in the WebView context.
293
-      /*
294
-      [script appendString:@"(function () {\n"];
295
-      [script appendString:@"  var cookies = document.cookie.split('; ');\n"];
296
-      [script appendString:@"  for (var i = 0; i < cookies.length; i++) {\n"];
297
-      [script appendString:@"    if (cookies[i].indexOf('=') !== -1) {\n"];
298
-      [script appendString:@"      document.cookie = cookies[i].split('=')[0] + '=; Expires=Thu, 01 Jan 1970 00:00:01 GMT';\n"];
299
-      [script appendString:@"    }\n"];
300
-      [script appendString:@"  }\n"];
301
-      [script appendString:@"})();\n\n"];
302
-      */
303
-
304
-      // Set cookies in a direct called function. This ensures that no
305
-      // javascript error will break the web content javascript.
306
-        // Generates JS: document.cookie = "key=value; Path=/; Expires=Thu, 01 Jan 20xx 00:00:01 GMT;"
307
-      // for each cookie which is available in the application context.
308
-      [script appendString:@"(function () {\n"];
309
-      for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]) {
310
-        [script appendFormat:@"document.cookie = %@ + '=' + %@",
311
-          RCTJSONStringify(cookie.name, NULL),
312
-          RCTJSONStringify(cookie.value, NULL)];
313
-        if (cookie.path) {
314
-          [script appendFormat:@" + '; Path=' + %@", RCTJSONStringify(cookie.path, NULL)];
315
-        }
316
-        if (cookie.expiresDate) {
317
-          [script appendFormat:@" + '; Expires=' + new Date(%f).toUTCString()",
318
-            cookie.expiresDate.timeIntervalSince1970 * 1000
319
-          ];
320
-        }
321
-        [script appendString:@";\n"];
322
-      }
323
-      [script appendString:@"})();\n"];
324
-
325
-      WKUserScript* cookieInScript = [[WKUserScript alloc] initWithSource:script
326
-                                                            injectionTime:WKUserScriptInjectionTimeAtDocumentStart
327
-                                                         forMainFrameOnly:YES];
328
-      [wkWebViewConfig.userContentController addUserScript:cookieInScript];
329
-    }
330
-  }
331 233
   
332 234
   return wkWebViewConfig;
333 235
 }
@@ -1136,16 +1038,7 @@ static NSDictionary* customCertificatesForHost;
1136 1038
 - (void)webView:(WKWebView *)webView
1137 1039
   didFinishNavigation:(WKNavigation *)navigation
1138 1040
 {
1139
-   if (_injectedJavaScript) {
1140
-     [self evaluateJS: _injectedJavaScript thenCall: ^(NSString *jsEvaluationValue) {
1141
-       NSMutableDictionary *event = [self baseEvent];
1142
-       event[@"jsEvaluationValue"] = jsEvaluationValue;
1143
-
1144
-       if (self.onLoadingFinish) {
1145
-         self.onLoadingFinish(event);
1146
-       }
1147
-     }];
1148
-   } else if (_onLoadingFinish) {
1041
+  if (_onLoadingFinish) {
1149 1042
     _onLoadingFinish([self baseEvent]);
1150 1043
   }
1151 1044
 }
@@ -1194,6 +1087,174 @@ static NSDictionary* customCertificatesForHost;
1194 1087
 }
1195 1088
 #endif // !TARGET_OS_OSX
1196 1089
 
1090
+
1091
+- (void)setInjectedJavaScript:(NSString *)source {
1092
+  _injectedJavaScript = source;
1093
+  
1094
+  self.atEndScript = source == nil ? nil : [[WKUserScript alloc] initWithSource:source
1095
+      injectionTime:WKUserScriptInjectionTimeAtDocumentEnd
1096
+    forMainFrameOnly:_injectedJavaScriptForMainFrameOnly];
1097
+  
1098
+  if(_webView != nil){
1099
+    [self resetupScripts:_webView.configuration];
1100
+  }
1101
+}
1102
+
1103
+- (void)setInjectedJavaScriptBeforeContentLoaded:(NSString *)source {
1104
+  _injectedJavaScriptBeforeContentLoaded = source;
1105
+  
1106
+  self.atStartScript = source == nil ? nil : [[WKUserScript alloc] initWithSource:source
1107
+       injectionTime:WKUserScriptInjectionTimeAtDocumentStart
1108
+    forMainFrameOnly:_injectedJavaScriptBeforeContentLoadedForMainFrameOnly];
1109
+  
1110
+  if(_webView != nil){
1111
+    [self resetupScripts:_webView.configuration];
1112
+  }
1113
+}
1114
+
1115
+- (void)setInjectedJavaScriptForMainFrameOnly:(BOOL)mainFrameOnly {
1116
+  _injectedJavaScriptForMainFrameOnly = mainFrameOnly;
1117
+  [self setInjectedJavaScript:_injectedJavaScript];
1118
+}
1119
+
1120
+- (void)setInjectedJavaScriptBeforeContentLoadedForMainFrameOnly:(BOOL)mainFrameOnly {
1121
+  _injectedJavaScriptBeforeContentLoadedForMainFrameOnly = mainFrameOnly;
1122
+  [self setInjectedJavaScriptBeforeContentLoaded:_injectedJavaScriptBeforeContentLoaded];
1123
+}
1124
+
1125
+- (void)setMessagingEnabled:(BOOL)messagingEnabled {
1126
+  _messagingEnabled = messagingEnabled;
1127
+  
1128
+  self.postMessageScript = _messagingEnabled ?
1129
+  [
1130
+   [WKUserScript alloc]
1131
+   initWithSource: [
1132
+                    NSString
1133
+                    stringWithFormat:
1134
+                    @"window.%@ = {"
1135
+                    "  postMessage: function (data) {"
1136
+                    "    window.webkit.messageHandlers.%@.postMessage(String(data));"
1137
+                    "  }"
1138
+                    "};", MessageHandlerName, MessageHandlerName
1139
+                    ]
1140
+   injectionTime:WKUserScriptInjectionTimeAtDocumentStart
1141
+   /* TODO: For a separate (minor) PR: use logic like this (as react-native-wkwebview does) so that messaging can be used in all frames if desired.
1142
+    *       I am keeping it as YES for consistency with previous behaviour. */
1143
+   // forMainFrameOnly:_messagingEnabledForMainFrameOnly
1144
+   forMainFrameOnly:YES
1145
+   ] :
1146
+  nil;
1147
+  
1148
+  if(_webView != nil){
1149
+    [self resetupScripts:_webView.configuration];
1150
+  }
1151
+}
1152
+
1153
+- (void)resetupScripts:(WKWebViewConfiguration *)wkWebViewConfig {
1154
+  [wkWebViewConfig.userContentController removeAllUserScripts];
1155
+  [wkWebViewConfig.userContentController removeScriptMessageHandlerForName:MessageHandlerName];
1156
+  
1157
+  NSString *html5HistoryAPIShimSource = [NSString stringWithFormat:
1158
+    @"(function(history) {\n"
1159
+    "  function notify(type) {\n"
1160
+    "    setTimeout(function() {\n"
1161
+    "      window.webkit.messageHandlers.%@.postMessage(type)\n"
1162
+    "    }, 0)\n"
1163
+    "  }\n"
1164
+    "  function shim(f) {\n"
1165
+    "    return function pushState() {\n"
1166
+    "      notify('other')\n"
1167
+    "      return f.apply(history, arguments)\n"
1168
+    "    }\n"
1169
+    "  }\n"
1170
+    "  history.pushState = shim(history.pushState)\n"
1171
+    "  history.replaceState = shim(history.replaceState)\n"
1172
+    "  window.addEventListener('popstate', function() {\n"
1173
+    "    notify('backforward')\n"
1174
+    "  })\n"
1175
+    "})(window.history)\n", HistoryShimName
1176
+  ];
1177
+  WKUserScript *script = [[WKUserScript alloc] initWithSource:html5HistoryAPIShimSource injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES];
1178
+  [wkWebViewConfig.userContentController addUserScript:script];
1179
+  
1180
+  if(_sharedCookiesEnabled) {
1181
+    // More info to sending cookies with WKWebView
1182
+    // https://stackoverflow.com/questions/26573137/can-i-set-the-cookies-to-be-used-by-a-wkwebview/26577303#26577303
1183
+    if (@available(iOS 11.0, *)) {
1184
+      // Set Cookies in iOS 11 and above, initialize websiteDataStore before setting cookies
1185
+      // See also https://forums.developer.apple.com/thread/97194
1186
+      // check if websiteDataStore has not been initialized before
1187
+      if(!_incognito && !_cacheEnabled) {
1188
+        wkWebViewConfig.websiteDataStore = [WKWebsiteDataStore nonPersistentDataStore];
1189
+      }
1190
+      for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]) {
1191
+        [wkWebViewConfig.websiteDataStore.httpCookieStore setCookie:cookie completionHandler:nil];
1192
+      }
1193
+    } else {
1194
+      NSMutableString *script = [NSMutableString string];
1195
+
1196
+      // Clear all existing cookies in a direct called function. This ensures that no
1197
+      // javascript error will break the web content javascript.
1198
+      // We keep this code here, if someone requires that Cookies are also removed within the
1199
+      // the WebView and want to extends the current sharedCookiesEnabled option with an
1200
+      // additional property.
1201
+      // Generates JS: document.cookie = "key=; Expires=Thu, 01 Jan 1970 00:00:01 GMT;"
1202
+      // for each cookie which is already available in the WebView context.
1203
+      /*
1204
+      [script appendString:@"(function () {\n"];
1205
+      [script appendString:@"  var cookies = document.cookie.split('; ');\n"];
1206
+      [script appendString:@"  for (var i = 0; i < cookies.length; i++) {\n"];
1207
+      [script appendString:@"    if (cookies[i].indexOf('=') !== -1) {\n"];
1208
+      [script appendString:@"      document.cookie = cookies[i].split('=')[0] + '=; Expires=Thu, 01 Jan 1970 00:00:01 GMT';\n"];
1209
+      [script appendString:@"    }\n"];
1210
+      [script appendString:@"  }\n"];
1211
+      [script appendString:@"})();\n\n"];
1212
+      */
1213
+
1214
+      // Set cookies in a direct called function. This ensures that no
1215
+      // javascript error will break the web content javascript.
1216
+        // Generates JS: document.cookie = "key=value; Path=/; Expires=Thu, 01 Jan 20xx 00:00:01 GMT;"
1217
+      // for each cookie which is available in the application context.
1218
+      [script appendString:@"(function () {\n"];
1219
+      for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]) {
1220
+        [script appendFormat:@"document.cookie = %@ + '=' + %@",
1221
+          RCTJSONStringify(cookie.name, NULL),
1222
+          RCTJSONStringify(cookie.value, NULL)];
1223
+        if (cookie.path) {
1224
+          [script appendFormat:@" + '; Path=' + %@", RCTJSONStringify(cookie.path, NULL)];
1225
+        }
1226
+        if (cookie.expiresDate) {
1227
+          [script appendFormat:@" + '; Expires=' + new Date(%f).toUTCString()",
1228
+            cookie.expiresDate.timeIntervalSince1970 * 1000
1229
+          ];
1230
+        }
1231
+        [script appendString:@";\n"];
1232
+      }
1233
+      [script appendString:@"})();\n"];
1234
+
1235
+      WKUserScript* cookieInScript = [[WKUserScript alloc] initWithSource:script
1236
+                                                            injectionTime:WKUserScriptInjectionTimeAtDocumentStart
1237
+                                                         forMainFrameOnly:YES];
1238
+      [wkWebViewConfig.userContentController addUserScript:cookieInScript];
1239
+    }
1240
+  }
1241
+  
1242
+  if(_messagingEnabled){
1243
+    if (self.postMessageScript){
1244
+      [wkWebViewConfig.userContentController addScriptMessageHandler:[[RNCWeakScriptMessageDelegate alloc] initWithDelegate:self]
1245
+                                                                       name:MessageHandlerName];
1246
+      [wkWebViewConfig.userContentController addUserScript:self.postMessageScript];
1247
+    }
1248
+    // FIXME: For a separate (minor) PR: these two shouldn't be gated by messagingEnabled; just keeping consistency with previous behaviour.
1249
+    if (self.atStartScript) {
1250
+      [wkWebViewConfig.userContentController addUserScript:self.atStartScript];
1251
+    }
1252
+    if (self.atEndScript) {
1253
+      [wkWebViewConfig.userContentController addUserScript:self.atEndScript];
1254
+    }
1255
+  }
1256
+}
1257
+
1197 1258
 - (NSURLRequest *)requestForSource:(id)json {
1198 1259
   NSURLRequest *request = [RCTConvert NSURLRequest:self.source];
1199 1260
 

+ 2
- 0
ios/RNCWebViewManager.m View File

@@ -43,6 +43,8 @@ RCT_EXPORT_VIEW_PROPERTY(onShouldStartLoadWithRequest, RCTDirectEventBlock)
43 43
 RCT_EXPORT_VIEW_PROPERTY(onContentProcessDidTerminate, RCTDirectEventBlock)
44 44
 RCT_EXPORT_VIEW_PROPERTY(injectedJavaScript, NSString)
45 45
 RCT_EXPORT_VIEW_PROPERTY(injectedJavaScriptBeforeContentLoaded, NSString)
46
+RCT_EXPORT_VIEW_PROPERTY(injectedJavaScriptForMainFrameOnly, BOOL)
47
+RCT_EXPORT_VIEW_PROPERTY(injectedJavaScriptBeforeContentLoadedForMainFrameOnly, BOOL)
46 48
 RCT_EXPORT_VIEW_PROPERTY(javaScriptEnabled, BOOL)
47 49
 RCT_EXPORT_VIEW_PROPERTY(allowFileAccessFromFileURLs, BOOL)
48 50
 RCT_EXPORT_VIEW_PROPERTY(allowsInlineMediaPlayback, BOOL)

+ 6
- 0
src/WebView.ios.tsx View File

@@ -290,6 +290,8 @@ class WebView extends React.Component<IOSWebViewProps, State> {
290 290
       originWhitelist,
291 291
       renderError,
292 292
       renderLoading,
293
+      injectedJavaScriptForMainFrameOnly = true,
294
+      injectedJavaScriptBeforeContentLoadedForMainFrameOnly = true,
293 295
       style,
294 296
       containerStyle,
295 297
       ...otherProps
@@ -344,6 +346,10 @@ class WebView extends React.Component<IOSWebViewProps, State> {
344 346
         onScroll={this.props.onScroll}
345 347
         onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
346 348
         onContentProcessDidTerminate={this.onContentProcessDidTerminate}
349
+        injectedJavaScript={this.props.injectedJavaScript}
350
+        injectedJavaScriptBeforeContentLoaded={this.props.injectedJavaScriptBeforeContentLoaded}
351
+        injectedJavaScriptForMainFrameOnly={injectedJavaScriptForMainFrameOnly}
352
+        injectedJavaScriptBeforeContentLoadedForMainFrameOnly={injectedJavaScriptBeforeContentLoadedForMainFrameOnly}
347 353
         ref={this.webViewRef}
348 354
         // TODO: find a better way to type this.
349 355
         source={resolveAssetSource(this.props.source as ImageSourcePropType)}

+ 16
- 0
src/WebViewTypes.ts View File

@@ -299,6 +299,8 @@ export interface IOSNativeWebViewProps extends CommonNativeWebViewProps {
299 299
   scrollEnabled?: boolean;
300 300
   useSharedProcessPool?: boolean;
301 301
   onContentProcessDidTerminate?: (event: WebViewTerminatedEvent) => void;
302
+  injectedJavaScriptForMainFrameOnly?: boolean;
303
+  injectedJavaScriptBeforeContentLoadedForMainFrameOnly?: boolean;
302 304
 }
303 305
 
304 306
 export interface MacOSNativeWebViewProps extends CommonNativeWebViewProps {
@@ -495,6 +497,20 @@ export interface IOSWebViewProps extends WebViewSharedProps {
495 497
    * @platform ios
496 498
    */
497 499
   onContentProcessDidTerminate?: (event: WebViewTerminatedEvent) => void;
500
+
501
+  /**
502
+   * If `true` (default), loads the `injectedJavaScript` only into the main frame.
503
+   * If `false`, loads it into all frames (e.g. iframes).
504
+   * @platform ios
505
+  */
506
+  injectedJavaScriptForMainFrameOnly?: boolean;
507
+
508
+  /**
509
+   * If `true` (default), loads the `injectedJavaScriptBeforeContentLoaded` only into the main frame.
510
+   * If `false`, loads it into all frames (e.g. iframes).
511
+   * @platform ios
512
+  */
513
+  injectedJavaScriptBeforeContentLoadedForMainFrameOnly?: boolean;
498 514
 }
499 515
 
500 516
 export interface MacOSWebViewProps extends WebViewSharedProps {