Browse Source

Merge upstream

Alexander Stammbach 6 years ago
parent
commit
f1cbe918a4

+ 21
- 0
.all-contributorsrc View File

83
         "code",
83
         "code",
84
         "doc"
84
         "doc"
85
       ]
85
       ]
86
+    },
87
+    {
88
+      "login": "jordansexton",
89
+      "name": "Jordan Sexton",
90
+      "avatar_url": "https://avatars2.githubusercontent.com/u/1173161?v=4",
91
+      "profile": "https://stylisted.com",
92
+      "contributions": [
93
+        "code",
94
+        "doc"
95
+      ]
96
+    },
97
+    {
98
+      "login": "MalcolmScruggs",
99
+      "name": "Malcolm Scruggs",
100
+      "avatar_url": "https://avatars1.githubusercontent.com/u/22333355?v=4",
101
+      "profile": "https://github.com/MalcolmScruggs",
102
+      "contributions": [
103
+        "code",
104
+        "tool",
105
+        "test"
106
+      ]
86
     }
107
     }
87
   ],
108
   ],
88
   "contributorsPerLine": 7
109
   "contributorsPerLine": 7

+ 4
- 3
README.md View File

1
 # React Native WebView - a Modern, Cross-Platform WebView for React Native
1
 # React Native WebView - a Modern, Cross-Platform WebView for React Native
2
-[![star this repo](http://githubbadges.com/star.svg?user=react-native-community&repo=react-native-webview&style=flat)](https://github.com/react-native-community/react-native-webview) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) [![All Contributors](https://img.shields.io/badge/all_contributors-6-orange.svg?style=flat-square)](#contributors) [![Known Vulnerabilities](https://snyk.io/test/github/react-native-community/react-native-webview/badge.svg?style=flat-square)](https://snyk.io/test/github/react-native-community/react-native-webview) 
2
+[![star this repo](http://githubbadges.com/star.svg?user=react-native-community&repo=react-native-webview&style=flat)](https://github.com/react-native-community/react-native-webview) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) [![All Contributors](https://img.shields.io/badge/all_contributors-8-orange.svg?style=flat-square)](#contributors) [![Known Vulnerabilities](https://snyk.io/test/github/react-native-community/react-native-webview/badge.svg?style=flat-square)](https://snyk.io/test/github/react-native-community/react-native-webview) 
3
 
3
 
4
 **React Native WebView** is a modern, well-supported, and cross-platform WebView for React Native. It is intended to be a replacement for the built-in WebView (which will be [removed from core](https://github.com/react-native-community/discussions-and-proposals/pull/3)).
4
 **React Native WebView** is a modern, well-supported, and cross-platform WebView for React Native. It is intended to be a replacement for the built-in WebView (which will be [removed from core](https://github.com/react-native-community/discussions-and-proposals/pull/3)).
5
 
5
 
84
 
84
 
85
 <!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
85
 <!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
86
 <!-- prettier-ignore -->
86
 <!-- prettier-ignore -->
87
-| [<img src="https://avatars1.githubusercontent.com/u/6181446?v=4" width="100px;" alt="Thibault Malbranche"/><br /><sub><b>Thibault Malbranche</b></sub>](https://twitter.com/titozzz)<br />[๐Ÿ’ป](https://github.com/react-native-community/react-native-webview/commits?author=titozzz "Code") [๐Ÿค”](#ideas-titozzz "Ideas, Planning, & Feedback") [๐Ÿ‘€](#review-titozzz "Reviewed Pull Requests") [๐Ÿ“–](https://github.com/react-native-community/react-native-webview/commits?author=titozzz "Documentation") [๐Ÿšง](#maintenance-titozzz "Maintenance") [โš ๏ธ](https://github.com/react-native-community/react-native-webview/commits?author=titozzz "Tests") [๐Ÿš‡](#infra-titozzz "Infrastructure (Hosting, Build-Tools, etc)") [๐Ÿ’ฌ](#question-titozzz "Answering Questions") | [<img src="https://avatars3.githubusercontent.com/u/1479215?v=4" width="100px;" alt="Jamon Holmgren"/><br /><sub><b>Jamon Holmgren</b></sub>](https://jamonholmgren.com)<br />[๐Ÿ’ป](https://github.com/react-native-community/react-native-webview/commits?author=jamonholmgren "Code") [๐Ÿค”](#ideas-jamonholmgren "Ideas, Planning, & Feedback") [๐Ÿ‘€](#review-jamonholmgren "Reviewed Pull Requests") [๐Ÿ“–](https://github.com/react-native-community/react-native-webview/commits?author=jamonholmgren "Documentation") [๐Ÿšง](#maintenance-jamonholmgren "Maintenance") [โš ๏ธ](https://github.com/react-native-community/react-native-webview/commits?author=jamonholmgren "Tests") [๐Ÿ’ก](#example-jamonholmgren "Examples") [๐Ÿ’ฌ](#question-jamonholmgren "Answering Questions") | [<img src="https://avatars1.githubusercontent.com/u/2570562?v=4" width="100px;" alt="Andrei Pfeiffer"/><br /><sub><b>Andrei Pfeiffer</b></sub>](https://github.com/andreipfeiffer)<br />[๐Ÿ’ป](https://github.com/react-native-community/react-native-webview/commits?author=andreipfeiffer "Code") [๐Ÿ‘€](#review-andreipfeiffer "Reviewed Pull Requests") [๐Ÿค”](#ideas-andreipfeiffer "Ideas, Planning, & Feedback") | [<img src="https://avatars0.githubusercontent.com/u/5347038?v=4" width="100px;" alt="Michael Diarmid"/><br /><sub><b>Michael Diarmid</b></sub>](https://twitter.com/mikediarmid)<br />[๐Ÿ’ป](https://github.com/react-native-community/react-native-webview/commits?author=Salakar "Code") [๐Ÿ‘€](#review-Salakar "Reviewed Pull Requests") [๐Ÿค”](#ideas-Salakar "Ideas, Planning, & Feedback") [๐Ÿ”ง](#tool-Salakar "Tools") | [<img src="https://avatars3.githubusercontent.com/u/932981?v=4" width="100px;" alt="Scott Mathson"/><br /><sub><b>Scott Mathson</b></sub>](http://smathson.github.io)<br />[๐Ÿ’ป](https://github.com/react-native-community/react-native-webview/commits?author=smathson "Code") [๐Ÿ“–](https://github.com/react-native-community/react-native-webview/commits?author=smathson "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/8221990?v=4" width="100px;" alt="Margaret"/><br /><sub><b>Margaret</b></sub>](https://github.com/YangXiaomei)<br />[๐Ÿ’ป](https://github.com/react-native-community/react-native-webview/commits?author=YangXiaomei "Code") [๐Ÿ“–](https://github.com/react-native-community/react-native-webview/commits?author=YangXiaomei "Documentation") |
88
-| :---: | :---: | :---: | :---: | :---: | :---: |
87
+| [<img src="https://avatars1.githubusercontent.com/u/6181446?v=4" width="100px;" alt="Thibault Malbranche"/><br /><sub><b>Thibault Malbranche</b></sub>](https://twitter.com/titozzz)<br />[๐Ÿ’ป](https://github.com/react-native-community/react-native-webview/commits?author=titozzz "Code") [๐Ÿค”](#ideas-titozzz "Ideas, Planning, & Feedback") [๐Ÿ‘€](#review-titozzz "Reviewed Pull Requests") [๐Ÿ“–](https://github.com/react-native-community/react-native-webview/commits?author=titozzz "Documentation") [๐Ÿšง](#maintenance-titozzz "Maintenance") [โš ๏ธ](https://github.com/react-native-community/react-native-webview/commits?author=titozzz "Tests") [๐Ÿš‡](#infra-titozzz "Infrastructure (Hosting, Build-Tools, etc)") [๐Ÿ’ฌ](#question-titozzz "Answering Questions") | [<img src="https://avatars3.githubusercontent.com/u/1479215?v=4" width="100px;" alt="Jamon Holmgren"/><br /><sub><b>Jamon Holmgren</b></sub>](https://jamonholmgren.com)<br />[๐Ÿ’ป](https://github.com/react-native-community/react-native-webview/commits?author=jamonholmgren "Code") [๐Ÿค”](#ideas-jamonholmgren "Ideas, Planning, & Feedback") [๐Ÿ‘€](#review-jamonholmgren "Reviewed Pull Requests") [๐Ÿ“–](https://github.com/react-native-community/react-native-webview/commits?author=jamonholmgren "Documentation") [๐Ÿšง](#maintenance-jamonholmgren "Maintenance") [โš ๏ธ](https://github.com/react-native-community/react-native-webview/commits?author=jamonholmgren "Tests") [๐Ÿ’ก](#example-jamonholmgren "Examples") [๐Ÿ’ฌ](#question-jamonholmgren "Answering Questions") | [<img src="https://avatars1.githubusercontent.com/u/2570562?v=4" width="100px;" alt="Andrei Pfeiffer"/><br /><sub><b>Andrei Pfeiffer</b></sub>](https://github.com/andreipfeiffer)<br />[๐Ÿ’ป](https://github.com/react-native-community/react-native-webview/commits?author=andreipfeiffer "Code") [๐Ÿ‘€](#review-andreipfeiffer "Reviewed Pull Requests") [๐Ÿค”](#ideas-andreipfeiffer "Ideas, Planning, & Feedback") | [<img src="https://avatars0.githubusercontent.com/u/5347038?v=4" width="100px;" alt="Michael Diarmid"/><br /><sub><b>Michael Diarmid</b></sub>](https://twitter.com/mikediarmid)<br />[๐Ÿ’ป](https://github.com/react-native-community/react-native-webview/commits?author=Salakar "Code") [๐Ÿ‘€](#review-Salakar "Reviewed Pull Requests") [๐Ÿค”](#ideas-Salakar "Ideas, Planning, & Feedback") [๐Ÿ”ง](#tool-Salakar "Tools") | [<img src="https://avatars3.githubusercontent.com/u/932981?v=4" width="100px;" alt="Scott Mathson"/><br /><sub><b>Scott Mathson</b></sub>](http://smathson.github.io)<br />[๐Ÿ’ป](https://github.com/react-native-community/react-native-webview/commits?author=smathson "Code") [๐Ÿ“–](https://github.com/react-native-community/react-native-webview/commits?author=smathson "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/8221990?v=4" width="100px;" alt="Margaret"/><br /><sub><b>Margaret</b></sub>](https://github.com/YangXiaomei)<br />[๐Ÿ’ป](https://github.com/react-native-community/react-native-webview/commits?author=YangXiaomei "Code") [๐Ÿ“–](https://github.com/react-native-community/react-native-webview/commits?author=YangXiaomei "Documentation") | [<img src="https://avatars2.githubusercontent.com/u/1173161?v=4" width="100px;" alt="Jordan Sexton"/><br /><sub><b>Jordan Sexton</b></sub>](https://stylisted.com)<br />[๐Ÿ’ป](https://github.com/react-native-community/react-native-webview/commits?author=jordansexton "Code") [๐Ÿ“–](https://github.com/react-native-community/react-native-webview/commits?author=jordansexton "Documentation") |
88
+| :---: | :---: | :---: | :---: | :---: | :---: | :---: |
89
+| [<img src="https://avatars1.githubusercontent.com/u/22333355?v=4" width="100px;" alt="Malcolm Scruggs"/><br /><sub><b>Malcolm Scruggs</b></sub>](https://github.com/MalcolmScruggs)<br />[๐Ÿ’ป](https://github.com/react-native-community/react-native-webview/commits?author=MalcolmScruggs "Code") [๐Ÿ”ง](#tool-MalcolmScruggs "Tools") [โš ๏ธ](https://github.com/react-native-community/react-native-webview/commits?author=MalcolmScruggs "Tests") |
89
 <!-- ALL-CONTRIBUTORS-LIST:END -->
90
 <!-- ALL-CONTRIBUTORS-LIST:END -->
90
 
91
 
91
 This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
92
 This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!

+ 3
- 3
android/build.gradle View File

1
 buildscript {
1
 buildscript {
2
   //Buildscript is evaluated before everything else so we can't use getExtOrDefault
2
   //Buildscript is evaluated before everything else so we can't use getExtOrDefault
3
-  def kotlin_version = rootProject.ext.has('kotlinVersion') ? rootProject.ext.get('kotlinVersion') : project.properties['ReactNativeWebview_kotlinVersion']
3
+  def kotlin_version = rootProject.ext.has('kotlinVersion') ? rootProject.ext.get('kotlinVersion') : project.properties['ReactNativeWebView_kotlinVersion']
4
 
4
 
5
   repositories {
5
   repositories {
6
     google()
6
     google()
15
 }
15
 }
16
 
16
 
17
 def getExtOrDefault(name) {
17
 def getExtOrDefault(name) {
18
-  return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['ReactNativeWebview_' + name]
18
+  return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['ReactNativeWebView_' + name]
19
 }
19
 }
20
 
20
 
21
 def getExtOrIntegerDefault(name) {
21
 def getExtOrIntegerDefault(name) {
22
-  return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties['ReactNativeWebview_' + name]).toInteger()
22
+  return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties['ReactNativeWebView_' + name]).toInteger()
23
 }
23
 }
24
 
24
 
25
 apply plugin: 'com.android.library'
25
 apply plugin: 'com.android.library'

+ 5
- 5
android/gradle.properties View File

1
-ReactNativeWebview_kotlinVersion=1.3.11
2
-ReactNativeWebview_compileSdkVersion=28
3
-ReactNativeWebview_buildToolsVersion=28.0.3
4
-ReactNativeWebview_targetSdkVersion=28
5
-ReactNativeWebview_supportLibVersion=28.0.0
1
+ReactNativeWebView_kotlinVersion=1.3.11
2
+ReactNativeWebView_compileSdkVersion=28
3
+ReactNativeWebView_buildToolsVersion=28.0.3
4
+ReactNativeWebView_targetSdkVersion=28
5
+ReactNativeWebView_supportLibVersion=28.0.0

+ 10
- 29
android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java View File

109
 
109
 
110
   protected static final String HTML_ENCODING = "UTF-8";
110
   protected static final String HTML_ENCODING = "UTF-8";
111
   protected static final String HTML_MIME_TYPE = "text/html";
111
   protected static final String HTML_MIME_TYPE = "text/html";
112
-  protected static final String BRIDGE_NAME = "__REACT_WEB_VIEW_BRIDGE";
112
+  protected static final String JAVASCRIPT_INTERFACE = "ReactNativeWebView";
113
 
113
 
114
   protected static final String HTTP_METHOD_POST = "POST";
114
   protected static final String HTTP_METHOD_POST = "POST";
115
 
115
 
138
 
138
 
139
       if (!mLastLoadFailed) {
139
       if (!mLastLoadFailed) {
140
         RNCWebView reactWebView = (RNCWebView) webView;
140
         RNCWebView reactWebView = (RNCWebView) webView;
141
+
141
         reactWebView.callInjectedJavaScript();
142
         reactWebView.callInjectedJavaScript();
142
-        reactWebView.linkBridge();
143
+
143
         emitFinishEvent(webView, url);
144
         emitFinishEvent(webView, url);
144
       }
145
       }
145
     }
146
     }
239
         mContext = c;
240
         mContext = c;
240
       }
241
       }
241
 
242
 
243
+      /**
244
+       * This method is called whenever JavaScript running within the web view calls:
245
+       *   - window[JAVASCRIPT_INTERFACE].postMessage
246
+       */
242
       @JavascriptInterface
247
       @JavascriptInterface
243
       public void postMessage(String message) {
248
       public void postMessage(String message) {
244
         mContext.onMessage(message);
249
         mContext.onMessage(message);
312
       }
317
       }
313
 
318
 
314
       messagingEnabled = enabled;
319
       messagingEnabled = enabled;
320
+
315
       if (enabled) {
321
       if (enabled) {
316
-        addJavascriptInterface(createRNCWebViewBridge(this), BRIDGE_NAME);
317
-        linkBridge();
322
+        addJavascriptInterface(createRNCWebViewBridge(this), JAVASCRIPT_INTERFACE);
318
       } else {
323
       } else {
319
-        removeJavascriptInterface(BRIDGE_NAME);
324
+        removeJavascriptInterface(JAVASCRIPT_INTERFACE);
320
       }
325
       }
321
     }
326
     }
322
 
327
 
342
       }
347
       }
343
     }
348
     }
344
 
349
 
345
-    public void linkBridge() {
346
-      if (messagingEnabled) {
347
-        if (ReactBuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
348
-          // See isNative in lodash
349
-          String testPostMessageNative = "String(window.postMessage) === String(Object.hasOwnProperty).replace('hasOwnProperty', 'postMessage')";
350
-          evaluateJavascript(testPostMessageNative, new ValueCallback<String>() {
351
-            @Override
352
-            public void onReceiveValue(String value) {
353
-              if (value.equals("true")) {
354
-                FLog.w(ReactConstants.TAG, "Setting onMessage on a WebView overrides existing values of window.postMessage, but a previous value was defined");
355
-              }
356
-            }
357
-          });
358
-        }
359
-
360
-        evaluateJavascriptWithFallback("(" +
361
-          "window.originalPostMessage = window.postMessage," +
362
-          "window.postMessage = function(data) {" +
363
-            BRIDGE_NAME + ".postMessage(String(data));" +
364
-          "}" +
365
-        ")");
366
-      }
367
-    }
368
-
369
     public void onMessage(String message) {
350
     public void onMessage(String message) {
370
       dispatchEvent(this, new TopMessageEvent(this.getId(), message));
351
       dispatchEvent(this, new TopMessageEvent(this.getId(), message));
371
     }
352
     }

+ 12
- 0
babel.config.js View File

1
+module.exports = function (api) {
2
+    api && api.cache(false);
3
+    return {
4
+      env: {
5
+        test: {
6
+          presets: [
7
+            "module:metro-react-native-babel-preset"
8
+          ],
9
+        }
10
+      }
11
+    };
12
+  }

+ 1
- 0
docs/Contributing.md View File

49
 - After pulling this repo and installing all dependencies, you can run flow on iOS and Android-specific files using the commands:
49
 - After pulling this repo and installing all dependencies, you can run flow on iOS and Android-specific files using the commands:
50
   - `yarn test:ios:flow` for iOS
50
   - `yarn test:ios:flow` for iOS
51
   - `yarn test:android:flow` for Android
51
   - `yarn test:android:flow` for Android
52
+- You can run Jest tests using the command: `yarn test:js`
52
 - If you want to add another React Native platform to this repository, you will need to create another `.flowconfig` for it. If your platform is `example`, copy the main flowconfig and rename it to `.flowconfig.example`. Then edit the config to ignore other platforms, and add `.*/*[.]example.js` to the ignore lists of the other platforms. Then add an entry to `package.json` like this:
53
 - If you want to add another React Native platform to this repository, you will need to create another `.flowconfig` for it. If your platform is `example`, copy the main flowconfig and rename it to `.flowconfig.example`. Then edit the config to ignore other platforms, and add `.*/*[.]example.js` to the ignore lists of the other platforms. Then add an entry to `package.json` like this:
53
   - `"test:example:flow": "flow check --flowconfig-name .flowconfig.example"`
54
   - `"test:example:flow": "flow check --flowconfig-name .flowconfig.example"`
54
 - Currently you need to install React Native 0.57 to be able to test these types - `flow check` will not pass against 0.56.
55
 - Currently you need to install React Native 0.57 to be able to test these types - `flow check` will not pass against 0.56.

+ 2
- 4
docs/Reference.md View File

2
 
2
 
3
 This document lays out the current public properties and methods for the React Native WebView.
3
 This document lays out the current public properties and methods for the React Native WebView.
4
 
4
 
5
-> **Security Warning:** Currently, `onMessage` and `postMessage` do not allow specifying an origin. This can lead to cross-site scripting attacks if an unexpected document is loaded within a `WebView` instance. Please refer to the MDN documentation for [`Window.postMessage()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) for more details on the security implications of this.
6
-
7
 ## Props Index
5
 ## Props Index
8
 
6
 
9
 - [`source`](Reference.md#source)
7
 - [`source`](Reference.md#source)
196
 
194
 
197
 ### `onMessage`
195
 ### `onMessage`
198
 
196
 
199
-A function that is invoked when the webview calls `window.postMessage`. Setting this property will inject a `postMessage` global into your webview, but will still call pre-existing values of `postMessage`.
197
+Function that is invoked when the webview calls `window.ReactNativeWebView.postMessage`. Setting this property will inject this global into your webview.
200
 
198
 
201
-`window.postMessage` accepts one argument, `data`, which will be available on the event object, `event.nativeEvent.data`. `data` must be a string.
199
+`window.ReactNativeWebView.postMessage` accepts one argument, `data`, which will be available on the event object, `event.nativeEvent.data`. `data` must be a string.
202
 
200
 
203
 | Type     | Required |
201
 | Type     | Required |
204
 | -------- | -------- |
202
 | -------- | -------- |

+ 24
- 36
ios/RNCUIWebView.m View File

11
 
11
 
12
 NSString *const RNCJSNavigationScheme = @"react-js-navigation";
12
 NSString *const RNCJSNavigationScheme = @"react-js-navigation";
13
 
13
 
14
-static NSString *const kPostMessageHost = @"postMessage";
14
+static NSString *const MessageHandlerName = @"ReactNativeWebView";
15
 
15
 
16
 @interface RNCUIWebView () <UIWebViewDelegate, RCTAutoInsetsProtocol>
16
 @interface RNCUIWebView () <UIWebViewDelegate, RCTAutoInsetsProtocol>
17
 
17
 
86
     @"data": message,
86
     @"data": message,
87
   };
87
   };
88
   NSString *source = [NSString
88
   NSString *source = [NSString
89
-    stringWithFormat:@"document.dispatchEvent(new MessageEvent('message', %@));",
89
+    stringWithFormat:@"window.dispatchEvent(new MessageEvent('message', %@));",
90
     RCTJSONStringify(eventInitDict, NULL)
90
     RCTJSONStringify(eventInitDict, NULL)
91
   ];
91
   ];
92
   [_webView stringByEvaluatingJavaScriptFromString:source];
92
   [_webView stringByEvaluatingJavaScriptFromString:source];
236
     }
236
     }
237
   }
237
   }
238
 
238
 
239
-  if (isJSNavigation && [request.URL.host isEqualToString:kPostMessageHost]) {
239
+  if (isJSNavigation && [request.URL.host isEqualToString:MessageHandlerName]) {
240
     NSString *data = request.URL.query;
240
     NSString *data = request.URL.query;
241
     data = [data stringByReplacingOccurrencesOfString:@"+" withString:@" "];
241
     data = [data stringByReplacingOccurrencesOfString:@"+" withString:@" "];
242
     data = [data stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
242
     data = [data stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
246
       @"data": data,
246
       @"data": data,
247
     }];
247
     }];
248
 
248
 
249
-    NSString *source = @"document.dispatchEvent(new MessageEvent('message:received'));";
249
+    NSString *source = [NSString stringWithFormat:@"window.%@.messageReceived();", MessageHandlerName];
250
 
250
 
251
     [_webView stringByEvaluatingJavaScriptFromString:source];
251
     [_webView stringByEvaluatingJavaScriptFromString:source];
252
 
252
 
289
 - (void)webViewDidFinishLoad:(UIWebView *)webView
289
 - (void)webViewDidFinishLoad:(UIWebView *)webView
290
 {
290
 {
291
   if (_messagingEnabled) {
291
   if (_messagingEnabled) {
292
-    #if RCT_DEV
293
-    // See isNative in lodash
294
-    NSString *testPostMessageNative = @"String(window.postMessage) === String(Object.hasOwnProperty).replace('hasOwnProperty', 'postMessage')";
295
-    BOOL postMessageIsNative = [
296
-      [webView stringByEvaluatingJavaScriptFromString:testPostMessageNative]
297
-      isEqualToString:@"true"
298
-    ];
299
-    if (!postMessageIsNative) {
300
-      RCTLogError(@"Setting onMessage on a WebView overrides existing values of window.postMessage, but a previous value was defined");
301
-    }
302
-    #endif
303
     NSString *source = [NSString stringWithFormat:
292
     NSString *source = [NSString stringWithFormat:
304
       @"(function() {"
293
       @"(function() {"
305
-        "window.originalPostMessage = window.postMessage;"
306
-
307
-        "var messageQueue = [];"
308
-        "var messagePending = false;"
309
-
310
-        "function processQueue() {"
311
-          "if (!messageQueue.length || messagePending) return;"
312
-          "messagePending = true;"
313
-          "window.location = '%@://%@?' + encodeURIComponent(messageQueue.shift());"
314
-        "}"
315
-
316
-        "window.postMessage = function(data) {"
317
-          "messageQueue.push(String(data));"
318
-          "processQueue();"
319
-        "};"
320
-
321
-        "document.addEventListener('message:received', function(e) {"
322
-          "messagePending = false;"
323
-          "processQueue();"
324
-        "});"
325
-      "})();", RNCJSNavigationScheme, kPostMessageHost
294
+       "  var messageQueue = [];"
295
+       "  var messagePending = false;"
296
+
297
+       "  function processQueue () {"
298
+       "    if (!messageQueue.length || messagePending) return;"
299
+       "    messagePending = true;"
300
+       "    document.location = '%@://%@?' + encodeURIComponent(messageQueue.shift());"
301
+       "  }"
302
+
303
+       "  window.%@ = {"
304
+       "    postMessage: function (data) {"
305
+       "      messageQueue.push(String(data));"
306
+       "      processQueue();"
307
+       "    },"
308
+       "    messageReceived: function () {"
309
+       "      messagePending = false;"
310
+       "      processQueue();"
311
+       "    }"
312
+       "  };"
313
+       "})();", RNCJSNavigationScheme, MessageHandlerName, MessageHandlerName
326
     ];
314
     ];
327
     [webView stringByEvaluatingJavaScriptFromString:source];
315
     [webView stringByEvaluatingJavaScriptFromString:source];
328
   }
316
   }

+ 34
- 33
ios/RNCWKWebView.m View File

14
 #import "objc/runtime.h"
14
 #import "objc/runtime.h"
15
 
15
 
16
 static NSTimer *keyboardTimer;
16
 static NSTimer *keyboardTimer;
17
-static NSString *const MessageHanderName = @"ReactNative";
17
+static NSString *const MessageHandlerName = @"ReactNativeWebView";
18
 
18
 
19
 // runtime trick to remove WKWebView keyboard default toolbar
19
 // runtime trick to remove WKWebView keyboard default toolbar
20
 // see: http://stackoverflow.com/questions/19033292/ios-7-uiwebview-keyboard-issue/19042279#19042279
20
 // see: http://stackoverflow.com/questions/19033292/ios-7-uiwebview-keyboard-issue/19042279#19042279
88
   return self;
88
   return self;
89
 }
89
 }
90
 
90
 
91
+/**
92
+ * See https://stackoverflow.com/questions/25713069/why-is-wkwebview-not-opening-links-with-target-blank/25853806#25853806 for details.
93
+ */
94
+- (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures
95
+{
96
+  if (!navigationAction.targetFrame.isMainFrame) {
97
+    [webView loadRequest:navigationAction.request];
98
+  }
99
+  return nil;
100
+}
101
+
91
 - (void)didMoveToWindow
102
 - (void)didMoveToWindow
92
 {
103
 {
93
   if (self.window != nil && _webView == nil) {
104
   if (self.window != nil && _webView == nil) {
105
       wkWebViewConfig.processPool = [[RNCWKProcessPoolManager sharedManager] sharedProcessPool];
116
       wkWebViewConfig.processPool = [[RNCWKProcessPoolManager sharedManager] sharedProcessPool];
106
     }
117
     }
107
     wkWebViewConfig.userContentController = [WKUserContentController new];
118
     wkWebViewConfig.userContentController = [WKUserContentController new];
108
-    [wkWebViewConfig.userContentController addScriptMessageHandler: self name: MessageHanderName];
119
+
120
+    if (_messagingEnabled) {
121
+      [wkWebViewConfig.userContentController addScriptMessageHandler:self name:MessageHandlerName];
122
+
123
+      NSString *source = [NSString stringWithFormat:
124
+        @"window.%@ = {"
125
+         "  postMessage: function (data) {"
126
+         "    window.webkit.messageHandlers.%@.postMessage(String(data));"
127
+         "  }"
128
+         "};", MessageHandlerName, MessageHandlerName
129
+      ];
130
+
131
+      WKUserScript *script = [[WKUserScript alloc] initWithSource:source injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES];
132
+      [wkWebViewConfig.userContentController addUserScript:script];
133
+    }
134
+
109
     wkWebViewConfig.allowsInlineMediaPlayback = _allowsInlineMediaPlayback;
135
     wkWebViewConfig.allowsInlineMediaPlayback = _allowsInlineMediaPlayback;
110
 #if WEBKIT_IOS_10_APIS_AVAILABLE
136
 #if WEBKIT_IOS_10_APIS_AVAILABLE
111
     wkWebViewConfig.mediaTypesRequiringUserActionForPlayback = _mediaPlaybackRequiresUserAction
137
     wkWebViewConfig.mediaTypesRequiringUserActionForPlayback = _mediaPlaybackRequiresUserAction
152
 - (void)removeFromSuperview
178
 - (void)removeFromSuperview
153
 {
179
 {
154
     if (_webView) {
180
     if (_webView) {
155
-        [_webView.configuration.userContentController removeScriptMessageHandlerForName:MessageHanderName];
181
+        [_webView.configuration.userContentController removeScriptMessageHandlerForName:MessageHandlerName];
156
         [_webView removeObserver:self forKeyPath:@"estimatedProgress"];
182
         [_webView removeObserver:self forKeyPath:@"estimatedProgress"];
157
         [_webView removeFromSuperview];
183
         [_webView removeFromSuperview];
158
         _webView = nil;
184
         _webView = nil;
215
 
241
 
216
 /**
242
 /**
217
  * This method is called whenever JavaScript running within the web view calls:
243
  * This method is called whenever JavaScript running within the web view calls:
218
- *   - window.webkit.messageHandlers.[MessageHanderName].postMessage
244
+ *   - window.webkit.messageHandlers[MessageHandlerName].postMessage
219
  */
245
  */
220
 - (void)userContentController:(WKUserContentController *)userContentController
246
 - (void)userContentController:(WKUserContentController *)userContentController
221
        didReceiveScriptMessage:(WKScriptMessage *)message
247
        didReceiveScriptMessage:(WKScriptMessage *)message
284
 
310
 
285
 -(void)setHideKeyboardAccessoryView:(BOOL)hideKeyboardAccessoryView
311
 -(void)setHideKeyboardAccessoryView:(BOOL)hideKeyboardAccessoryView
286
 {
312
 {
287
-
288
     if (_webView == nil) {
313
     if (_webView == nil) {
289
         _savedHideKeyboardAccessoryView = hideKeyboardAccessoryView;
314
         _savedHideKeyboardAccessoryView = hideKeyboardAccessoryView;
290
         return;
315
         return;
295
     }
320
     }
296
 
321
 
297
     UIView* subview;
322
     UIView* subview;
323
+
298
     for (UIView* view in _webView.scrollView.subviews) {
324
     for (UIView* view in _webView.scrollView.subviews) {
299
         if([[view.class description] hasPrefix:@"WK"])
325
         if([[view.class description] hasPrefix:@"WK"])
300
             subview = view;
326
             subview = view;
334
 {
360
 {
335
   NSDictionary *eventInitDict = @{@"data": message};
361
   NSDictionary *eventInitDict = @{@"data": message};
336
   NSString *source = [NSString
362
   NSString *source = [NSString
337
-    stringWithFormat:@"document.dispatchEvent(new MessageEvent('message', %@));",
363
+    stringWithFormat:@"window.dispatchEvent(new MessageEvent('message', %@));",
338
     RCTJSONStringify(eventInitDict, NULL)
364
     RCTJSONStringify(eventInitDict, NULL)
339
   ];
365
   ];
340
-  [self evaluateJS: source thenCall: nil];
366
+  [self injectJavaScript: source];
341
 }
367
 }
342
 
368
 
343
 - (void)layoutSubviews
369
 - (void)layoutSubviews
551
   }];
577
   }];
552
 }
578
 }
553
 
579
 
554
-
555
 /**
580
 /**
556
  * Called when the navigation is complete.
581
  * Called when the navigation is complete.
557
  * @see https://fburl.com/rtys6jlb
582
  * @see https://fburl.com/rtys6jlb
559
 - (void)      webView:(WKWebView *)webView
584
 - (void)      webView:(WKWebView *)webView
560
   didFinishNavigation:(WKNavigation *)navigation
585
   didFinishNavigation:(WKNavigation *)navigation
561
 {
586
 {
562
-  if (_messagingEnabled) {
563
-    #if RCT_DEV
564
-
565
-    // Implementation inspired by Lodash.isNative.
566
-    NSString *isPostMessageNative = @"String(String(window.postMessage) === String(Object.hasOwnProperty).replace('hasOwnProperty', 'postMessage'))";
567
-    [self evaluateJS: isPostMessageNative thenCall: ^(NSString *result) {
568
-      if (! [result isEqualToString:@"true"]) {
569
-        RCTLogError(@"Setting onMessage on a WebView overrides existing values of window.postMessage, but a previous value was defined");
570
-      }
571
-    }];
572
-    #endif
573
-
574
-    NSString *source = [NSString stringWithFormat:
575
-      @"(function() {"
576
-        "window.originalPostMessage = window.postMessage;"
577
-
578
-        "window.postMessage = function(data) {"
579
-          "window.webkit.messageHandlers.%@.postMessage(String(data));"
580
-        "};"
581
-      "})();",
582
-      MessageHanderName
583
-    ];
584
-    [self evaluateJS: source thenCall: nil];
585
-  }
586
-
587
   if (_injectedJavaScript) {
587
   if (_injectedJavaScript) {
588
     [self evaluateJS: _injectedJavaScript thenCall: ^(NSString *jsEvaluationValue) {
588
     [self evaluateJS: _injectedJavaScript thenCall: ^(NSString *jsEvaluationValue) {
589
       NSMutableDictionary *event = [self baseEvent];
589
       NSMutableDictionary *event = [self baseEvent];
590
       event[@"jsEvaluationValue"] = jsEvaluationValue;
590
       event[@"jsEvaluationValue"] = jsEvaluationValue;
591
+
591
       if (self.onLoadingFinish) {
592
       if (self.onLoadingFinish) {
592
         self.onLoadingFinish(event);
593
         self.onLoadingFinish(event);
593
       }
594
       }

+ 0
- 8
ios/RNCWebView.xcworkspace/contents.xcworkspacedata View File

1
-// !$*UTF8*$!
2
-<?xml version="1.0" encoding="UTF-8"?>
3
-<Workspace
4
-   version = "1.0">
5
-   <FileRef
6
-      location = "group:RNCWebView.xcodeproj">
7
-   </FileRef>
8
-</Workspace>

+ 185
- 0
jest.config.js View File

1
+// For a detailed explanation regarding each configuration property, visit:
2
+// https://jestjs.io/docs/en/configuration.html
3
+
4
+module.exports = {
5
+  // All imported modules in your tests should be mocked automatically
6
+  // automock: false,
7
+
8
+  // Stop running tests after `n` failures
9
+  // bail: 0,
10
+
11
+  // Respect "browser" field in package.json when resolving modules
12
+  // browser: false,
13
+
14
+  // The directory where Jest should store its cached dependency information
15
+  // cacheDirectory: "/private/var/folders/8f/kgcy219d1dvfvbcqky441_d00000gp/T/jest_dy",
16
+
17
+  // Automatically clear mock calls and instances between every test
18
+  clearMocks: true,
19
+
20
+  // Indicates whether the coverage information should be collected while executing the test
21
+  // collectCoverage: false,
22
+
23
+  // An array of glob patterns indicating a set of files for which coverage information should be collected
24
+  // collectCoverageFrom: null,
25
+
26
+  // The directory where Jest should output its coverage files
27
+  // coverageDirectory: "coverage",
28
+
29
+  // An array of regexp pattern strings used to skip coverage collection
30
+  // coveragePathIgnorePatterns: [
31
+  //   "/node_modules/"
32
+  // ],
33
+
34
+  // A list of reporter names that Jest uses when writing coverage reports
35
+  // coverageReporters: [
36
+  //   "json",
37
+  //   "text",
38
+  //   "lcov",
39
+  //   "clover"
40
+  // ],
41
+
42
+  // An object that configures minimum threshold enforcement for coverage results
43
+  // coverageThreshold: null,
44
+
45
+  // A path to a custom dependency extractor
46
+  // dependencyExtractor: null,
47
+
48
+  // Make calling deprecated APIs throw helpful error messages
49
+  // errorOnDeprecated: false,
50
+
51
+  // Force coverage collection from ignored files usin a array of glob patterns
52
+  // forceCoverageMatch: [],
53
+
54
+  // A path to a module which exports an async function that is triggered once before all test suites
55
+  // globalSetup: null,
56
+
57
+  // A path to a module which exports an async function that is triggered once after all test suites
58
+  // globalTeardown: null,
59
+
60
+  // A set of global variables that need to be available in all test environments
61
+  // globals: {},
62
+
63
+  // An array of directory names to be searched recursively up from the requiring module's location
64
+  // moduleDirectories: [
65
+  //   "node_modules"
66
+  // ],
67
+
68
+  // An array of file extensions your modules use
69
+  // moduleFileExtensions: [
70
+  //   "js",
71
+  //   "json",
72
+  //   "jsx",
73
+  //   "ts",
74
+  //   "tsx",
75
+  //   "node"
76
+  // ],
77
+
78
+  // A map from regular expressions to module names that allow to stub out resources with a single module
79
+  // moduleNameMapper: {},
80
+
81
+  // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
82
+  // modulePathIgnorePatterns: [],
83
+
84
+  // Activates notifications for test results
85
+  // notify: false,
86
+
87
+  // An enum that specifies notification mode. Requires { notify: true }
88
+  // notifyMode: "failure-change",
89
+
90
+  // A preset that is used as a base for Jest's configuration
91
+  preset: "react-native",
92
+
93
+  // Run tests from one or more projects
94
+  // projects: null,
95
+
96
+  // Use this configuration option to add custom reporters to Jest
97
+  // reporters: undefined,
98
+
99
+  // Automatically reset mock state between every test
100
+  // resetMocks: false,
101
+
102
+  // Reset the module registry before running each individual test
103
+  // resetModules: false,
104
+
105
+  // A path to a custom resolver
106
+  // resolver: null,
107
+
108
+  // Automatically restore mock state between every test
109
+  // restoreMocks: false,
110
+
111
+  // The root directory that Jest should scan for tests and modules within
112
+  // rootDir: null,
113
+
114
+  // A list of paths to directories that Jest should use to search for files in
115
+  // roots: [
116
+  //   "<rootDir>"
117
+  // ],
118
+
119
+  // Allows you to use a custom runner instead of Jest's default test runner
120
+  // runner: "jest-runner",
121
+
122
+  // The paths to modules that run some code to configure or set up the testing environment before each test
123
+  // setupFiles: [],
124
+
125
+  // A list of paths to modules that run some code to configure or set up the testing framework before each test
126
+  // setupFilesAfterEnv: [],
127
+
128
+  // A list of paths to snapshot serializer modules Jest should use for snapshot testing
129
+  // snapshotSerializers: [],
130
+
131
+  // The test environment that will be used for testing
132
+  testEnvironment: "node",
133
+
134
+  // Options that will be passed to the testEnvironment
135
+  // testEnvironmentOptions: {},
136
+
137
+  // Adds a location field to test results
138
+  // testLocationInResults: false,
139
+
140
+  // The glob patterns Jest uses to detect test files
141
+  // testMatch: [
142
+  //   "**/__tests__/**/*.[jt]s?(x)",
143
+  //   "**/?(*.)+(spec|test).[tj]s?(x)"
144
+  // ],
145
+
146
+  // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
147
+  // testPathIgnorePatterns: [
148
+  //   "/node_modules/"
149
+  // ],
150
+
151
+  // The regexp pattern or array of patterns that Jest uses to detect test files
152
+  // testRegex: [],
153
+
154
+  // This option allows the use of a custom results processor
155
+  // testResultsProcessor: null,
156
+
157
+  // This option allows use of a custom test runner
158
+  // testRunner: "jasmine2",
159
+
160
+  // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
161
+  // testURL: "http://localhost",
162
+
163
+  // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
164
+  // timers: "real",
165
+
166
+  // A map from regular expressions to paths to transformers
167
+  // transform: null,
168
+
169
+  // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
170
+  // transformIgnorePatterns: [
171
+  //   "/node_modules/"
172
+  // ],
173
+
174
+  // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
175
+  // unmockedModulePathPatterns: undefined,
176
+
177
+  // Indicates whether each individual test should be reported during the run
178
+  // verbose: null,
179
+
180
+  // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
181
+  // watchPathIgnorePatterns: [],
182
+
183
+  // Whether to use watchman for file crawling
184
+  // watchman: true,
185
+};

+ 1
- 1
js/WebView.android.js View File

157
         }
157
         }
158
         thirdPartyCookiesEnabled={this.props.thirdPartyCookiesEnabled}
158
         thirdPartyCookiesEnabled={this.props.thirdPartyCookiesEnabled}
159
         domStorageEnabled={this.props.domStorageEnabled}
159
         domStorageEnabled={this.props.domStorageEnabled}
160
-        messagingEnabled={typeof this.props.onMessage === 'function'}
161
         cacheEnabled={this.props.cacheEnabled}
160
         cacheEnabled={this.props.cacheEnabled}
162
         onMessage={this.onMessage}
161
         onMessage={this.onMessage}
162
+        messagingEnabled={typeof this.props.onMessage === 'function'}
163
         overScrollMode={this.props.overScrollMode}
163
         overScrollMode={this.props.overScrollMode}
164
         contentInset={this.props.contentInset}
164
         contentInset={this.props.contentInset}
165
         automaticallyAdjustContentInsets={
165
         automaticallyAdjustContentInsets={

+ 1
- 3
js/WebView.ios.js View File

232
       source = { uri: this.props.url };
232
       source = { uri: this.props.url };
233
     }
233
     }
234
 
234
 
235
-    const messagingEnabled = typeof this.props.onMessage === 'function';
236
-
237
     let NativeWebView = nativeConfig.component;
235
     let NativeWebView = nativeConfig.component;
238
 
236
 
239
     if (this.props.useWebKit) {
237
     if (this.props.useWebKit) {
268
         onLoadingFinish={this._onLoadingFinish}
266
         onLoadingFinish={this._onLoadingFinish}
269
         onLoadingError={this._onLoadingError}
267
         onLoadingError={this._onLoadingError}
270
         onLoadingProgress={this._onLoadingProgress}
268
         onLoadingProgress={this._onLoadingProgress}
271
-        messagingEnabled={messagingEnabled}
272
         onMessage={this._onMessage}
269
         onMessage={this._onMessage}
270
+        messagingEnabled={typeof this.props.onMessage === 'function'}
273
         onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
271
         onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
274
         scalesPageToFit={scalesPageToFit}
272
         scalesPageToFit={scalesPageToFit}
275
         allowsInlineMediaPlayback={this.props.allowsInlineMediaPlayback}
273
         allowsInlineMediaPlayback={this.props.allowsInlineMediaPlayback}

+ 2
- 2
js/WebViewShared.js View File

19
 const defaultOriginWhitelist = ['http://*', 'https://*'];
19
 const defaultOriginWhitelist = ['http://*', 'https://*'];
20
 
20
 
21
 const extractOrigin = (url: string): string => {
21
 const extractOrigin = (url: string): string => {
22
-  const result = /^[A-Za-z0-9]+:(\/\/)?[^/]*/.exec(url);
22
+  const result = /^[A-Za-z][A-Za-z0-9\+\-\.]+:(\/\/)?[^/]*/.exec(url);
23
   return result === null ? '' : result[0];
23
   return result === null ? '' : result[0];
24
 };
24
 };
25
 
25
 
26
 const originWhitelistToRegex = (originWhitelist: string): string =>
26
 const originWhitelistToRegex = (originWhitelist: string): string =>
27
-  escapeStringRegexp(originWhitelist).replace(/\\\*/g, '.*');
27
+    `^${escapeStringRegexp(originWhitelist).replace(/\\\*/g, '.*')}`;
28
 
28
 
29
 const passesWhitelist = (compiledWhitelist: Array<string>, url: string) => {
29
 const passesWhitelist = (compiledWhitelist: Array<string>, url: string) => {
30
   const origin = extractOrigin(url);
30
   const origin = extractOrigin(url);

+ 4
- 6
js/WebViewTypes.js View File

424
   onNavigationStateChange?: (event: WebViewNavigation) => mixed,
424
   onNavigationStateChange?: (event: WebViewNavigation) => mixed,
425
 
425
 
426
   /**
426
   /**
427
-   * A function that is invoked when the webview calls `window.postMessage`.
428
-   * Setting this property will inject a `postMessage` global into your
429
-   * webview, but will still call pre-existing values of `postMessage`.
427
+   * Function that is invoked when the webview calls `window.ReactNativeWebView.postMessage`.
428
+   * Setting this property will inject this global into your webview.
430
    *
429
    *
431
-   * `window.postMessage` accepts one argument, `data`, which will be
432
-   * available on the event object, `event.nativeEvent.data`. `data`
433
-   * must be a string.
430
+   * `window.ReactNativeWebView.postMessage` accepts one argument, `data`, which will be
431
+   * available on the event object, `event.nativeEvent.data`. `data` must be a string.
434
    */
432
    */
435
   onMessage?: (event: WebViewMessageEvent) => mixed,
433
   onMessage?: (event: WebViewMessageEvent) => mixed,
436
 
434
 

+ 136
- 0
js/__tests__/WebViewShared-test.js View File

1
+import { Linking } from 'react-native';
2
+
3
+import {
4
+  defaultOriginWhitelist,
5
+  createOnShouldStartLoadWithRequest,
6
+} from '../WebViewShared';
7
+
8
+describe('WebViewShared', () => {
9
+  test('exports defaultOriginWhitelist', () => {
10
+    expect(defaultOriginWhitelist).toMatchSnapshot();
11
+  });
12
+
13
+  describe('createOnShouldStartLoadWithRequest', () => {
14
+    const alwaysTrueOnShouldStartLoadWithRequest = (nativeEvent) => {
15
+      return true;
16
+    };
17
+
18
+    const alwaysFalseOnShouldStartLoadWithRequest = (nativeEvent) => {
19
+      return false;
20
+    };
21
+
22
+    const loadRequest = jest.fn();
23
+
24
+    test('loadRequest is called without onShouldStartLoadWithRequest override', () => {
25
+      const onShouldStartLoadWithRequest = createOnShouldStartLoadWithRequest(
26
+        loadRequest,
27
+        defaultOriginWhitelist,
28
+      );
29
+
30
+      onShouldStartLoadWithRequest({ nativeEvent: { url: 'https://www.example.com/', lockIdentifier: 1 } });
31
+      expect(Linking.openURL).toHaveBeenCalledTimes(0);
32
+      expect(loadRequest).toHaveBeenCalledWith(true, 'https://www.example.com/', 1);
33
+    });
34
+
35
+    test('Linking.openURL is called without onShouldStartLoadWithRequest override', () => {
36
+      const onShouldStartLoadWithRequest = createOnShouldStartLoadWithRequest(
37
+        loadRequest,
38
+        defaultOriginWhitelist,
39
+      );
40
+
41
+      onShouldStartLoadWithRequest({ nativeEvent: { url: 'invalid://example.com/', lockIdentifier: 2 } });
42
+      expect(Linking.openURL).toHaveBeenCalledWith('invalid://example.com/');
43
+      expect(loadRequest).toHaveBeenCalledWith(false, 'invalid://example.com/', 2);
44
+    });
45
+
46
+    test('loadRequest with true onShouldStartLoadWithRequest override is called', () => {
47
+      const onShouldStartLoadWithRequest = createOnShouldStartLoadWithRequest(
48
+        loadRequest,
49
+        defaultOriginWhitelist,
50
+        alwaysTrueOnShouldStartLoadWithRequest,
51
+      );
52
+
53
+      onShouldStartLoadWithRequest({ nativeEvent: { url: 'https://www.example.com/', lockIdentifier: 1 } });
54
+      expect(Linking.openURL).toHaveBeenCalledTimes(0);
55
+      expect(loadRequest).toHaveBeenLastCalledWith(true, 'https://www.example.com/', 1);
56
+    });
57
+
58
+    test('Linking.openURL with true onShouldStartLoadWithRequest override is called for links not passing the whitelist', () => {
59
+      const onShouldStartLoadWithRequest = createOnShouldStartLoadWithRequest(
60
+        loadRequest,
61
+        defaultOriginWhitelist,
62
+        alwaysTrueOnShouldStartLoadWithRequest,
63
+      );
64
+
65
+      onShouldStartLoadWithRequest({ nativeEvent: { url: 'invalid://example.com/', lockIdentifier: 1 } });
66
+      expect(Linking.openURL).toHaveBeenLastCalledWith('invalid://example.com/');
67
+      expect(loadRequest).toHaveBeenLastCalledWith(true, 'invalid://example.com/', 1);
68
+    });
69
+
70
+    test('loadRequest with false onShouldStartLoadWithRequest override is called', () => {
71
+      const onShouldStartLoadWithRequest = createOnShouldStartLoadWithRequest(
72
+        loadRequest,
73
+        defaultOriginWhitelist,
74
+        alwaysFalseOnShouldStartLoadWithRequest,
75
+      );
76
+
77
+      onShouldStartLoadWithRequest({ nativeEvent: { url: 'https://www.example.com/', lockIdentifier: 1 } });
78
+      expect(Linking.openURL).toHaveBeenCalledTimes(0);
79
+      expect(loadRequest).toHaveBeenLastCalledWith(false, 'https://www.example.com/', 1);
80
+    });
81
+
82
+    test('loadRequest with limited whitelist', () => {
83
+      const onShouldStartLoadWithRequest = createOnShouldStartLoadWithRequest(
84
+        loadRequest,
85
+        ['https://*'],
86
+      );
87
+
88
+      onShouldStartLoadWithRequest({ nativeEvent: { url: 'https://www.example.com/', lockIdentifier: 1 } });
89
+      expect(Linking.openURL).toHaveBeenCalledTimes(0);
90
+      expect(loadRequest).toHaveBeenLastCalledWith(true, 'https://www.example.com/', 1);
91
+
92
+      onShouldStartLoadWithRequest({ nativeEvent: { url: 'http://insecure.com/', lockIdentifier: 2 } });
93
+      expect(Linking.openURL).toHaveBeenLastCalledWith('http://insecure.com/');
94
+      expect(loadRequest).toHaveBeenLastCalledWith(false, 'http://insecure.com/', 2);
95
+
96
+      onShouldStartLoadWithRequest({ nativeEvent: { url: 'git+https://insecure.com/', lockIdentifier: 3 } });
97
+      expect(Linking.openURL).toHaveBeenLastCalledWith('git+https://insecure.com/');
98
+      expect(loadRequest).toHaveBeenLastCalledWith(false, 'git+https://insecure.com/', 3);
99
+
100
+      onShouldStartLoadWithRequest({ nativeEvent: { url: 'fakehttps://insecure.com/', lockIdentifier: 4 } });
101
+      expect(Linking.openURL).toHaveBeenLastCalledWith('fakehttps://insecure.com/');
102
+      expect(loadRequest).toHaveBeenLastCalledWith(false, 'fakehttps://insecure.com/', 4);
103
+    });
104
+
105
+    test('loadRequest allows for valid URIs', () => {
106
+      const onShouldStartLoadWithRequest = createOnShouldStartLoadWithRequest(
107
+          loadRequest,
108
+          ['plus+https://*', 'DOT.https://*', 'dash-https://*', '0invalid://*', '+invalid://*'],
109
+      );
110
+
111
+      onShouldStartLoadWithRequest({ nativeEvent: { url: 'plus+https://www.example.com/', lockIdentifier: 1 } });
112
+      expect(Linking.openURL).toHaveBeenCalledTimes(0);
113
+      expect(loadRequest).toHaveBeenLastCalledWith(true, 'plus+https://www.example.com/', 1);
114
+
115
+      onShouldStartLoadWithRequest({ nativeEvent: { url: 'DOT.https://www.example.com/', lockIdentifier: 2 } });
116
+      expect(Linking.openURL).toHaveBeenCalledTimes(0);
117
+      expect(loadRequest).toHaveBeenLastCalledWith(true, 'DOT.https://www.example.com/', 2);
118
+
119
+      onShouldStartLoadWithRequest({ nativeEvent: { url: 'dash-https://www.example.com/', lockIdentifier: 3 } });
120
+      expect(Linking.openURL).toHaveBeenCalledTimes(0);
121
+      expect(loadRequest).toHaveBeenLastCalledWith(true, 'dash-https://www.example.com/', 3);
122
+
123
+      onShouldStartLoadWithRequest({ nativeEvent: { url: '0invalid://www.example.com/', lockIdentifier: 4 } });
124
+      expect(Linking.openURL).toHaveBeenLastCalledWith('0invalid://www.example.com/');
125
+      expect(loadRequest).toHaveBeenLastCalledWith(false, '0invalid://www.example.com/', 4);
126
+
127
+      onShouldStartLoadWithRequest({ nativeEvent: { url: '+invalid://www.example.com/', lockIdentifier: 5 } });
128
+      expect(Linking.openURL).toHaveBeenLastCalledWith('+invalid://www.example.com/');
129
+      expect(loadRequest).toHaveBeenLastCalledWith(false, '+invalid://www.example.com/', 5);
130
+
131
+      onShouldStartLoadWithRequest({ nativeEvent: { url: 'FAKE+plus+https://www.example.com/', lockIdentifier: 6 } });
132
+      expect(Linking.openURL).toHaveBeenLastCalledWith('FAKE+plus+https://www.example.com/');
133
+      expect(loadRequest).toHaveBeenLastCalledWith(false, 'FAKE+plus+https://www.example.com/', 6);
134
+    });
135
+  });
136
+});

+ 8
- 0
js/__tests__/__snapshots__/WebViewShared-test.js.snap View File

1
+// Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+exports[`WebViewShared exports defaultOriginWhitelist 1`] = `
4
+Array [
5
+  "http://*",
6
+  "https://*",
7
+]
8
+`;

+ 8
- 2
package.json View File

8
     "Thibault Malbranche <malbranche.thibault@gmail.com>"
8
     "Thibault Malbranche <malbranche.thibault@gmail.com>"
9
   ],
9
   ],
10
   "license": "MIT",
10
   "license": "MIT",
11
-  "version": "4.0.0",
11
+  "version": "5.0.2",
12
   "homepage": "https://github.com/react-native-community/react-native-webview#readme",
12
   "homepage": "https://github.com/react-native-community/react-native-webview#readme",
13
   "scripts": {
13
   "scripts": {
14
+    "test:js": "jest",
14
     "test:ios:flow": "flow check",
15
     "test:ios:flow": "flow check",
15
     "test:android:flow": "flow check --flowconfig-name .flowconfig.android",
16
     "test:android:flow": "flow check --flowconfig-name .flowconfig.android",
16
     "ci:publish": "yarn semantic-release",
17
     "ci:publish": "yarn semantic-release",
17
-    "ci:test": "yarn ci:test:flow",
18
+    "ci:test": "yarn ci:test:flow && yarn ci:test:js",
18
     "ci:test:flow": "yarn test:ios:flow && yarn test:android:flow",
19
     "ci:test:flow": "yarn test:ios:flow && yarn test:android:flow",
20
+    "ci:test:js": "yarn test:js",
19
     "semantic-release": "semantic-release"
21
     "semantic-release": "semantic-release"
20
   },
22
   },
21
   "peerDependencies": {
23
   "peerDependencies": {
27
     "fbjs": "^0.8.17"
29
     "fbjs": "^0.8.17"
28
   },
30
   },
29
   "devDependencies": {
31
   "devDependencies": {
32
+    "@babel/core": "^7.2.2",
30
     "@semantic-release/git": "7.0.5",
33
     "@semantic-release/git": "7.0.5",
31
     "@types/react": "^16.4.18",
34
     "@types/react": "^16.4.18",
32
     "@types/react-native": "^0.57.6",
35
     "@types/react-native": "^0.57.6",
36
+    "babel-jest": "^24.0.0",
33
     "flow-bin": "^0.80.0",
37
     "flow-bin": "^0.80.0",
38
+    "jest": "^24.0.0",
39
+    "metro-react-native-babel-preset": "^0.51.1",
34
     "react-native": "^0.57",
40
     "react-native": "^0.57",
35
     "semantic-release": "15.10.3"
41
     "semantic-release": "15.10.3"
36
   },
42
   },

+ 1287
- 13
yarn.lock
File diff suppressed because it is too large
View File