Browse Source

feat(Android Webview): Support onShouldStartLoadWithRequest on Android (#107)

This PR adds support for `onShouldStartLoadWithRequest` on android.

The initial PR was #59

The issue for this PR is: #106

fixes #106
Thibault Malbranche 6 years ago
parent
commit
b1b662628e

+ 1
- 0
android/build.gradle View File

48
 
48
 
49
 repositories {
49
 repositories {
50
     mavenCentral()
50
     mavenCentral()
51
+    jcenter()
51
     maven {
52
     maven {
52
         url 'https://maven.google.com/'
53
         url 'https://maven.google.com/'
53
         name 'Google'
54
         name 'Google'

+ 33
- 63
android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java View File

30
 import android.webkit.JavascriptInterface;
30
 import android.webkit.JavascriptInterface;
31
 import android.webkit.ValueCallback;
31
 import android.webkit.ValueCallback;
32
 import android.webkit.WebChromeClient;
32
 import android.webkit.WebChromeClient;
33
+import android.webkit.WebResourceRequest;
33
 import android.webkit.WebSettings;
34
 import android.webkit.WebSettings;
34
 import android.webkit.WebView;
35
 import android.webkit.WebView;
35
 import android.webkit.WebViewClient;
36
 import android.webkit.WebViewClient;
51
 import com.facebook.react.uimanager.events.ContentSizeChangeEvent;
52
 import com.facebook.react.uimanager.events.ContentSizeChangeEvent;
52
 import com.facebook.react.uimanager.events.Event;
53
 import com.facebook.react.uimanager.events.Event;
53
 import com.facebook.react.uimanager.events.EventDispatcher;
54
 import com.facebook.react.uimanager.events.EventDispatcher;
55
+import com.facebook.react.uimanager.events.RCTEventEmitter;
54
 import com.reactnativecommunity.webview.events.TopLoadingErrorEvent;
56
 import com.reactnativecommunity.webview.events.TopLoadingErrorEvent;
55
 import com.reactnativecommunity.webview.events.TopLoadingFinishEvent;
57
 import com.reactnativecommunity.webview.events.TopLoadingFinishEvent;
56
 import com.reactnativecommunity.webview.events.TopLoadingStartEvent;
58
 import com.reactnativecommunity.webview.events.TopLoadingStartEvent;
57
 import com.reactnativecommunity.webview.events.TopMessageEvent;
59
 import com.reactnativecommunity.webview.events.TopMessageEvent;
58
 import com.reactnativecommunity.webview.events.TopLoadingProgressEvent;
60
 import com.reactnativecommunity.webview.events.TopLoadingProgressEvent;
61
+import com.reactnativecommunity.webview.events.TopShouldStartLoadWithRequestEvent;
62
+import java.io.UnsupportedEncodingException;
63
+import java.util.ArrayList;
64
+import java.util.HashMap;
65
+import java.util.Locale;
66
+import java.util.Map;
67
+import javax.annotation.Nullable;
59
 import org.json.JSONException;
68
 import org.json.JSONException;
60
 import org.json.JSONObject;
69
 import org.json.JSONObject;
61
 
70
 
66
  *  - GO_BACK
75
  *  - GO_BACK
67
  *  - GO_FORWARD
76
  *  - GO_FORWARD
68
  *  - RELOAD
77
  *  - RELOAD
78
+ *  - LOAD_URL
69
  *
79
  *
70
  * {@link WebView} instances could emit following direct events:
80
  * {@link WebView} instances could emit following direct events:
71
  *  - topLoadingFinish
81
  *  - topLoadingFinish
72
  *  - topLoadingStart
82
  *  - topLoadingStart
73
  *  - topLoadingStart
83
  *  - topLoadingStart
74
  *  - topLoadingProgress
84
  *  - topLoadingProgress
85
+ *  - topShouldStartLoadWithRequest
75
  *
86
  *
76
  * Each event will carry the following properties:
87
  * Each event will carry the following properties:
77
  *  - target - view's react tag
88
  *  - target - view's react tag
99
   public static final int COMMAND_STOP_LOADING = 4;
110
   public static final int COMMAND_STOP_LOADING = 4;
100
   public static final int COMMAND_POST_MESSAGE = 5;
111
   public static final int COMMAND_POST_MESSAGE = 5;
101
   public static final int COMMAND_INJECT_JAVASCRIPT = 6;
112
   public static final int COMMAND_INJECT_JAVASCRIPT = 6;
113
+  public static final int COMMAND_LOAD_URL = 7;
102
 
114
 
103
   // Use `webView.loadUrl("about:blank")` to reliably reset the view
115
   // Use `webView.loadUrl("about:blank")` to reliably reset the view
104
   // state and release page resources (including any running JavaScript).
116
   // state and release page resources (including any running JavaScript).
111
 
123
 
112
     protected boolean mLastLoadFailed = false;
124
     protected boolean mLastLoadFailed = false;
113
     protected @Nullable ReadableArray mUrlPrefixesForDefaultIntent;
125
     protected @Nullable ReadableArray mUrlPrefixesForDefaultIntent;
114
-    protected @Nullable List<Pattern> mOriginWhitelist;
115
 
126
 
116
     @Override
127
     @Override
117
     public void onPageFinished(WebView webView, String url) {
128
     public void onPageFinished(WebView webView, String url) {
139
 
150
 
140
     @Override
151
     @Override
141
     public boolean shouldOverrideUrlLoading(WebView view, String url) {
152
     public boolean shouldOverrideUrlLoading(WebView view, String url) {
142
-      if (url.equals(BLANK_URL)) return false;
143
-
144
-      // url blacklisting
145
-      if (mUrlPrefixesForDefaultIntent != null && mUrlPrefixesForDefaultIntent.size() > 0) {
146
-        ArrayList<Object> urlPrefixesForDefaultIntent =
147
-            mUrlPrefixesForDefaultIntent.toArrayList();
148
-        for (Object urlPrefix : urlPrefixesForDefaultIntent) {
149
-          if (url.startsWith((String) urlPrefix)) {
150
-            launchIntent(view.getContext(), url);
151
-            return true;
152
-          }
153
-        }
154
-      }
155
-
156
-      if (mOriginWhitelist != null && shouldHandleURL(mOriginWhitelist, url)) {
157
-        return false;
158
-      }
159
-
160
-      launchIntent(view.getContext(), url);
153
+      dispatchEvent(view, new TopShouldStartLoadWithRequestEvent(view.getId(), url));
161
       return true;
154
       return true;
162
     }
155
     }
163
 
156
 
164
-    private void launchIntent(Context context, String url) {
165
-      try {
166
-        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
167
-        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
168
-        intent.addCategory(Intent.CATEGORY_BROWSABLE);
169
-        context.startActivity(intent);
170
-      } catch (ActivityNotFoundException e) {
171
-        FLog.w(ReactConstants.TAG, "activity not found to handle uri scheme for: " + url, e);
172
-      }
173
-    }
174
 
157
 
175
-    private boolean shouldHandleURL(List<Pattern> originWhitelist, String url) {
176
-      Uri uri = Uri.parse(url);
177
-      String scheme = uri.getScheme() != null ? uri.getScheme() : "";
178
-      String authority = uri.getAuthority() != null ? uri.getAuthority() : "";
179
-      String urlToCheck = scheme + "://" + authority;
180
-      for (Pattern pattern : originWhitelist) {
181
-        if (pattern.matcher(urlToCheck).matches()) {
182
-          return true;
183
-        }
184
-      }
185
-      return false;
158
+    @TargetApi(Build.VERSION_CODES.N)
159
+    @Override
160
+    public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
161
+      dispatchEvent(view, new TopShouldStartLoadWithRequestEvent(view.getId(), request.getUrl().toString()));
162
+      return true;
186
     }
163
     }
187
 
164
 
188
     @Override
165
     @Override
231
     public void setUrlPrefixesForDefaultIntent(ReadableArray specialUrls) {
208
     public void setUrlPrefixesForDefaultIntent(ReadableArray specialUrls) {
232
       mUrlPrefixesForDefaultIntent = specialUrls;
209
       mUrlPrefixesForDefaultIntent = specialUrls;
233
     }
210
     }
234
-
235
-    public void setOriginWhitelist(List<Pattern> originWhitelist) {
236
-      mOriginWhitelist = originWhitelist;
237
-    }
238
   }
211
   }
239
 
212
 
240
   /**
213
   /**
656
     view.getSettings().setGeolocationEnabled(isGeolocationEnabled != null && isGeolocationEnabled);
629
     view.getSettings().setGeolocationEnabled(isGeolocationEnabled != null && isGeolocationEnabled);
657
   }
630
   }
658
 
631
 
659
-  @ReactProp(name = "originWhitelist")
660
-  public void setOriginWhitelist(
661
-    WebView view,
662
-    @Nullable ReadableArray originWhitelist) {
663
-    RNCWebViewClient client = ((RNCWebView) view).getRNCWebViewClient();
664
-    if (client != null && originWhitelist != null) {
665
-      List<Pattern> whiteList = new LinkedList<>();
666
-      for (int i = 0 ; i < originWhitelist.size() ; i++) {
667
-        whiteList.add(Pattern.compile(originWhitelist.getString(i)));
668
-      }
669
-      client.setOriginWhitelist(whiteList);
670
-    }
671
-  }
672
-
673
   @Override
632
   @Override
674
   protected void addEventEmitters(ThemedReactContext reactContext, WebView view) {
633
   protected void addEventEmitters(ThemedReactContext reactContext, WebView view) {
675
     // Do not register default touch emitter and let WebView implementation handle touches
634
     // Do not register default touch emitter and let WebView implementation handle touches
678
 
637
 
679
   @Override
638
   @Override
680
   public Map getExportedCustomDirectEventTypeConstants() {
639
   public Map getExportedCustomDirectEventTypeConstants() {
681
-    MapBuilder.Builder builder = MapBuilder.builder();
682
-    builder.put("topLoadingProgress", MapBuilder.of("registrationName", "onLoadingProgress"));
683
-    return builder.build();
640
+    Map export = super.getExportedCustomDirectEventTypeConstants();
641
+    if (export == null) {
642
+      export = MapBuilder.newHashMap();
643
+    }
644
+    export.put(TopLoadingProgressEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoadingProgress"));
645
+    export.put(TopShouldStartLoadWithRequestEvent.EVENT_NAME, MapBuilder.of("registrationName", "onShouldStartLoadWithRequest"));
646
+    return export;
684
   }
647
   }
685
 
648
 
686
   @Override
649
   @Override
691
         "reload", COMMAND_RELOAD,
654
         "reload", COMMAND_RELOAD,
692
         "stopLoading", COMMAND_STOP_LOADING,
655
         "stopLoading", COMMAND_STOP_LOADING,
693
         "postMessage", COMMAND_POST_MESSAGE,
656
         "postMessage", COMMAND_POST_MESSAGE,
694
-        "injectJavaScript", COMMAND_INJECT_JAVASCRIPT
657
+        "injectJavaScript", COMMAND_INJECT_JAVASCRIPT,
658
+        "loadUrl", COMMAND_LOAD_URL
695
       );
659
       );
696
   }
660
   }
697
 
661
 
734
         RNCWebView reactWebView = (RNCWebView) root;
698
         RNCWebView reactWebView = (RNCWebView) root;
735
         reactWebView.evaluateJavascriptWithFallback(args.getString(0));
699
         reactWebView.evaluateJavascriptWithFallback(args.getString(0));
736
         break;
700
         break;
701
+      case COMMAND_LOAD_URL:
702
+        if (args == null) {
703
+          throw new RuntimeException("Arguments for loading an url are null!");
704
+        }
705
+        root.loadUrl(args.getString(0));
706
+        break;
737
     }
707
     }
738
   }
708
   }
739
 
709
 

+ 40
- 0
android/src/main/java/com/reactnativecommunity/webview/events/TopShouldStartLoadWithRequestEvent.java View File

1
+package com.reactnativecommunity.webview.events;
2
+
3
+import com.facebook.react.bridge.Arguments;
4
+import com.facebook.react.bridge.WritableMap;
5
+import com.facebook.react.uimanager.events.Event;
6
+import com.facebook.react.uimanager.events.RCTEventEmitter;
7
+
8
+public class TopShouldStartLoadWithRequestEvent extends Event<TopMessageEvent> {
9
+    public static final String EVENT_NAME = "topShouldStartLoadWithRequest";
10
+    private final String mUrl;
11
+
12
+    public TopShouldStartLoadWithRequestEvent(int viewId, String url) {
13
+        super(viewId);
14
+        mUrl = url;
15
+    }
16
+
17
+    @Override
18
+    public String getEventName() {
19
+        return EVENT_NAME;
20
+    }
21
+
22
+    @Override
23
+    public boolean canCoalesce() {
24
+        return false;
25
+    }
26
+
27
+    @Override
28
+    public short getCoalescingKey() {
29
+        // All events for a given view can be coalesced.
30
+        return 0;
31
+    }
32
+
33
+    @Override
34
+    public void dispatch(RCTEventEmitter rctEventEmitter) {
35
+        WritableMap data = Arguments.createMap();
36
+        data.putString("url", mUrl);
37
+        data.putString("navigationType", "other");
38
+        rctEventEmitter.receiveEvent(getViewTag(), EVENT_NAME, data);
39
+    }
40
+}

+ 35
- 20
js/WebView.android.js View File

8
  * @flow
8
  * @flow
9
  */
9
  */
10
 
10
 
11
-'use strict';
12
-
13
 import React from 'react';
11
 import React from 'react';
14
 
12
 
15
-import ReactNative from 'react-native';
16
-import {
13
+import ReactNative, {
17
   ActivityIndicator,
14
   ActivityIndicator,
15
+  Image,
16
+  requireNativeComponent,
18
   StyleSheet,
17
   StyleSheet,
19
   UIManager,
18
   UIManager,
20
   View,
19
   View,
21
-  Image,
22
-  requireNativeComponent,
23
   NativeModules
20
   NativeModules
24
 } from 'react-native';
21
 } from 'react-native';
25
 
22
 
26
 import invariant from 'fbjs/lib/invariant';
23
 import invariant from 'fbjs/lib/invariant';
27
 import keyMirror from 'fbjs/lib/keyMirror';
24
 import keyMirror from 'fbjs/lib/keyMirror';
28
 
25
 
29
-import WebViewShared from './WebViewShared';
26
+import {
27
+  defaultOriginWhitelist,
28
+  createOnShouldStartLoadWithRequest,
29
+} from './WebViewShared';
30
 import type {
30
 import type {
31
-  WebViewEvent,
32
   WebViewError,
31
   WebViewError,
33
   WebViewErrorEvent,
32
   WebViewErrorEvent,
34
   WebViewMessageEvent,
33
   WebViewMessageEvent,
35
-  WebViewNavigation,
36
   WebViewNavigationEvent,
34
   WebViewNavigationEvent,
35
+  WebViewProgressEvent,
37
   WebViewSharedProps,
36
   WebViewSharedProps,
38
   WebViewSource,
37
   WebViewSource,
39
-  WebViewProgressEvent,
40
 } from './WebViewTypes';
38
 } from './WebViewTypes';
41
 
39
 
42
 const resolveAssetSource = Image.resolveAssetSource;
40
 const resolveAssetSource = Image.resolveAssetSource;
69
     scalesPageToFit: true,
67
     scalesPageToFit: true,
70
     allowFileAccess: false,
68
     allowFileAccess: false,
71
     saveFormDataDisabled: false,
69
     saveFormDataDisabled: false,
72
-    originWhitelist: WebViewShared.defaultOriginWhitelist,
70
+    originWhitelist: defaultOriginWhitelist,
73
   };
71
   };
74
 
72
 
75
   static isFileUploadSupported = async () => {
73
   static isFileUploadSupported = async () => {
78
   }
76
   }
79
 
77
 
80
   state = {
78
   state = {
81
-    viewState: this.props.startInLoadingState ? WebViewState.LOADING : WebViewState.IDLE,
79
+    viewState: this.props.startInLoadingState
80
+      ? WebViewState.LOADING
81
+      : WebViewState.IDLE,
82
     lastErrorEvent: null,
82
     lastErrorEvent: null,
83
   };
83
   };
84
 
84
 
131
 
131
 
132
     const nativeConfig = this.props.nativeConfig || {};
132
     const nativeConfig = this.props.nativeConfig || {};
133
 
133
 
134
-    const originWhitelist = (this.props.originWhitelist || []).map(
135
-      WebViewShared.originWhitelistToRegex,
136
-    );
137
-
138
     let NativeWebView = nativeConfig.component || RNCWebView;
134
     let NativeWebView = nativeConfig.component || RNCWebView;
139
 
135
 
136
+    const onShouldStartLoadWithRequest = createOnShouldStartLoadWithRequest(
137
+      this.onShouldStartLoadWithRequestCallback,
138
+      this.props.originWhitelist,
139
+      this.props.onShouldStartLoadWithRequest,
140
+    );
141
+
140
     const webView = (
142
     const webView = (
141
       <NativeWebView
143
       <NativeWebView
142
         ref={this.webViewRef}
144
         ref={this.webViewRef}
157
         automaticallyAdjustContentInsets={
159
         automaticallyAdjustContentInsets={
158
           this.props.automaticallyAdjustContentInsets
160
           this.props.automaticallyAdjustContentInsets
159
         }
161
         }
162
+        onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
160
         onContentSizeChange={this.props.onContentSizeChange}
163
         onContentSizeChange={this.props.onContentSizeChange}
161
         onLoadingStart={this.onLoadingStart}
164
         onLoadingStart={this.onLoadingStart}
162
         onLoadingFinish={this.onLoadingFinish}
165
         onLoadingFinish={this.onLoadingFinish}
170
         allowUniversalAccessFromFileURLs={
173
         allowUniversalAccessFromFileURLs={
171
           this.props.allowUniversalAccessFromFileURLs
174
           this.props.allowUniversalAccessFromFileURLs
172
         }
175
         }
173
-        originWhitelist={originWhitelist}
174
         mixedContentMode={this.props.mixedContentMode}
176
         mixedContentMode={this.props.mixedContentMode}
175
         saveFormDataDisabled={this.props.saveFormDataDisabled}
177
         saveFormDataDisabled={this.props.saveFormDataDisabled}
176
         urlPrefixesForDefaultIntent={this.props.urlPrefixesForDefaultIntent}
178
         urlPrefixesForDefaultIntent={this.props.urlPrefixesForDefaultIntent}
290
     const { onMessage } = this.props;
292
     const { onMessage } = this.props;
291
     onMessage && onMessage(event);
293
     onMessage && onMessage(event);
292
   };
294
   };
293
-  
295
+
294
   onLoadingProgress = (event: WebViewProgressEvent) => {
296
   onLoadingProgress = (event: WebViewProgressEvent) => {
295
-    const { onLoadProgress} = this.props;
297
+    const { onLoadProgress } = this.props;
296
     onLoadProgress && onLoadProgress(event);
298
     onLoadProgress && onLoadProgress(event);
297
-  }
299
+  };
300
+
301
+  onShouldStartLoadWithRequestCallback = (
302
+    shouldStart: boolean,
303
+    url: string,
304
+  ) => {
305
+    if (shouldStart) {
306
+      UIManager.dispatchViewManagerCommand(
307
+        this.getWebViewHandle(),
308
+        UIManager.RNCWebView.Commands.loadUrl,
309
+        [String(url)],
310
+      );
311
+    }
312
+  };
298
 }
313
 }
299
 
314
 
300
 const RNCWebView = requireNativeComponent('RNCWebView');
315
 const RNCWebView = requireNativeComponent('RNCWebView');

+ 28
- 38
js/WebView.ios.js View File

25
 import invariant from 'fbjs/lib/invariant';
25
 import invariant from 'fbjs/lib/invariant';
26
 import keyMirror from 'fbjs/lib/keyMirror';
26
 import keyMirror from 'fbjs/lib/keyMirror';
27
 
27
 
28
-import WebViewShared from './WebViewShared';
28
+import {
29
+  defaultOriginWhitelist,
30
+  createOnShouldStartLoadWithRequest,
31
+} from './WebViewShared';
29
 import type {
32
 import type {
30
   WebViewEvent,
33
   WebViewEvent,
31
   WebViewError,
34
   WebViewError,
130
 
133
 
131
   static defaultProps = {
134
   static defaultProps = {
132
     useWebKit: true,
135
     useWebKit: true,
133
-    originWhitelist: WebViewShared.defaultOriginWhitelist,
136
+    originWhitelist: defaultOriginWhitelist,
134
   };
137
   };
135
 
138
 
136
   static isFileUploadSupported = async () => {
139
   static isFileUploadSupported = async () => {
204
 
207
 
205
     const nativeConfig = this.props.nativeConfig || {};
208
     const nativeConfig = this.props.nativeConfig || {};
206
 
209
 
207
-    let viewManager = nativeConfig.viewManager;
208
-
209
-    if (this.props.useWebKit) {
210
-      viewManager = viewManager || RNCWKWebViewManager;
211
-    } else {
212
-      viewManager = viewManager || RNCUIWebViewManager;
213
-    }
214
-
215
-    const compiledWhitelist = [
216
-      'about:blank',
217
-      ...(this.props.originWhitelist || []),
218
-    ].map(WebViewShared.originWhitelistToRegex);
219
-    const onShouldStartLoadWithRequest = event => {
220
-      let shouldStart = true;
221
-      const { url } = event.nativeEvent;
222
-      const origin = WebViewShared.extractOrigin(url);
223
-      const passesWhitelist = compiledWhitelist.some(x =>
224
-        new RegExp(x).test(origin),
225
-      );
226
-      shouldStart = shouldStart && passesWhitelist;
227
-      if (!passesWhitelist) {
228
-        Linking.openURL(url);
229
-      }
230
-      if (this.props.onShouldStartLoadWithRequest) {
231
-        shouldStart =
232
-          shouldStart &&
233
-          this.props.onShouldStartLoadWithRequest(event.nativeEvent);
234
-      }
235
-      invariant(viewManager != null, 'viewManager expected to be non-null');
236
-      viewManager.startLoadWithResult(
237
-        !!shouldStart,
238
-        event.nativeEvent.lockIdentifier,
239
-      );
240
-    };
210
+    const onShouldStartLoadWithRequest = createOnShouldStartLoadWithRequest(
211
+      this.onShouldStartLoadWithRequestCallback,
212
+      this.props.originWhitelist,
213
+      this.props.onShouldStartLoadWithRequest,
214
+    );
241
 
215
 
242
     const decelerationRate = processDecelerationRate(
216
     const decelerationRate = processDecelerationRate(
243
       this.props.decelerationRate,
217
       this.props.decelerationRate,
441
   };
415
   };
442
 
416
 
443
   _onLoadingProgress = (event: WebViewProgressEvent) => {
417
   _onLoadingProgress = (event: WebViewProgressEvent) => {
444
-    const {onLoadProgress} = this.props;
418
+    const { onLoadProgress } = this.props;
445
     onLoadProgress && onLoadProgress(event);
419
     onLoadProgress && onLoadProgress(event);
446
-  }
420
+  };
421
+
422
+  onShouldStartLoadWithRequestCallback = (
423
+    shouldStart: boolean,
424
+    url: string,
425
+    lockIdentifier: number,
426
+  ) => {
427
+    let viewManager = (this.props.nativeConfig || {}).viewManager;
428
+
429
+    if (this.props.useWebKit) {
430
+      viewManager = viewManager || RNCWKWebViewManager;
431
+    } else {
432
+      viewManager = viewManager || RNCUIWebViewManager;
433
+    }
434
+    invariant(viewManager != null, 'viewManager expected to be non-null');
435
+    viewManager.startLoadWithResult(!!shouldStart, lockIdentifier);
436
+  };
447
 
437
 
448
   componentDidUpdate(prevProps: WebViewSharedProps) {
438
   componentDidUpdate(prevProps: WebViewSharedProps) {
449
     if (!(prevProps.useWebKit && this.props.useWebKit)) {
439
     if (!(prevProps.useWebKit && this.props.useWebKit)) {

+ 53
- 14
js/WebViewShared.js View File

8
  * @flow
8
  * @flow
9
  */
9
  */
10
 
10
 
11
-'use strict';
12
-
13
-const escapeStringRegexp = require('escape-string-regexp');
14
-
15
-const WebViewShared = {
16
-  defaultOriginWhitelist: ['http://*', 'https://*'],
17
-  extractOrigin: (url: string): string => {
18
-    const result = /^[A-Za-z0-9]+:(\/\/)?[^/]*/.exec(url);
19
-    return result === null ? '' : result[0];
20
-  },
21
-  originWhitelistToRegex: (originWhitelist: string): string => {
22
-    return escapeStringRegexp(originWhitelist).replace(/\\\*/g, '.*');
23
-  },
11
+import escapeStringRegexp from 'escape-string-regexp';
12
+import { Linking } from 'react-native';
13
+import type {
14
+  WebViewNavigationEvent,
15
+  WebViewNavigation,
16
+  OnShouldStartLoadWithRequest,
17
+} from './WebViewTypes';
18
+
19
+const defaultOriginWhitelist = ['http://*', 'https://*'];
20
+
21
+const extractOrigin = (url: string): string => {
22
+  const result = /^[A-Za-z0-9]+:(\/\/)?[^/]*/.exec(url);
23
+  return result === null ? '' : result[0];
24
+};
25
+
26
+const originWhitelistToRegex = (originWhitelist: string): string =>
27
+  escapeStringRegexp(originWhitelist).replace(/\\\*/g, '.*');
28
+
29
+const passesWhitelist = (compiledWhitelist: Array<string>, url: string) => {
30
+  const origin = extractOrigin(url);
31
+  return compiledWhitelist.some(x => new RegExp(x).test(origin));
32
+};
33
+
34
+const compileWhitelist = (
35
+  originWhitelist: ?$ReadOnlyArray<string>,
36
+): Array<string> =>
37
+  ['about:blank', ...(originWhitelist || [])].map(originWhitelistToRegex);
38
+
39
+const createOnShouldStartLoadWithRequest = (
40
+  loadRequest: (
41
+    shouldStart: boolean,
42
+    url: string,
43
+    lockIdentifier: number,
44
+  ) => void,
45
+  originWhitelist: ?$ReadOnlyArray<string>,
46
+  onShouldStartLoadWithRequest: ?OnShouldStartLoadWithRequest,
47
+) => {
48
+  return ({ nativeEvent }: WebViewNavigationEvent) => {
49
+    let shouldStart = true;
50
+    const { url, lockIdentifier } = nativeEvent;
51
+
52
+    if (!passesWhitelist(compileWhitelist(originWhitelist), url)) {
53
+      Linking.openURL(url);
54
+      shouldStart = false
55
+    }
56
+
57
+    if (onShouldStartLoadWithRequest) {
58
+      shouldStart = onShouldStartLoadWithRequest(nativeEvent);
59
+    }
60
+
61
+    loadRequest(shouldStart, url, lockIdentifier);
62
+  };
24
 };
63
 };
25
 
64
 
26
-module.exports = WebViewShared;
65
+export { defaultOriginWhitelist, createOnShouldStartLoadWithRequest };

+ 31
- 25
js/WebViewTypes.js View File

10
 
10
 
11
 'use strict';
11
 'use strict';
12
 
12
 
13
-import type {Node, Element, ComponentType} from 'react';
13
+import type { Node, Element, ComponentType } from 'react';
14
 
14
 
15
-import type {SyntheticEvent} from 'CoreEventTypes';
16
-import type {EdgeInsetsProp} from 'EdgeInsetsPropType';
17
-import type {ViewStyleProp} from 'StyleSheet';
18
-import type {ViewProps} from 'ViewPropTypes';
15
+import type { SyntheticEvent } from 'CoreEventTypes';
16
+import type { EdgeInsetsProp } from 'EdgeInsetsPropType';
17
+import type { ViewStyleProp } from 'StyleSheet';
18
+import type { ViewProps } from 'ViewPropTypes';
19
 
19
 
20
 export type WebViewNativeEvent = $ReadOnly<{|
20
 export type WebViewNativeEvent = $ReadOnly<{|
21
   url: string,
21
   url: string,
23
   title: string,
23
   title: string,
24
   canGoBack: boolean,
24
   canGoBack: boolean,
25
   canGoForward: boolean,
25
   canGoForward: boolean,
26
+  lockIdentifier: number,
26
 |}>;
27
 |}>;
27
 
28
 
28
 export type WebViewProgressEvent = $ReadOnly<{|
29
 export type WebViewProgressEvent = $ReadOnly<{|
29
-    ...WebViewNativeEvent,
30
-    progress: number,
31
-|}>
30
+  ...WebViewNativeEvent,
31
+  progress: number,
32
+|}>;
32
 
33
 
33
 export type WebViewNavigation = $ReadOnly<{|
34
 export type WebViewNavigation = $ReadOnly<{|
34
   ...WebViewNativeEvent,
35
   ...WebViewNativeEvent,
118
 export type WebViewSource = WebViewSourceUri | WebViewSourceHtml;
119
 export type WebViewSource = WebViewSourceUri | WebViewSourceHtml;
119
 
120
 
120
 export type WebViewNativeConfig = $ReadOnly<{|
121
 export type WebViewNativeConfig = $ReadOnly<{|
121
-  /*
122
+  /**
122
    * The native component used to render the WebView.
123
    * The native component used to render the WebView.
123
    */
124
    */
124
   component?: ComponentType<WebViewSharedProps>,
125
   component?: ComponentType<WebViewSharedProps>,
125
-  /*
126
+  /**
126
    * Set props directly on the native component WebView. Enables custom props which the
127
    * Set props directly on the native component WebView. Enables custom props which the
127
    * original WebView doesn't pass through.
128
    * original WebView doesn't pass through.
128
    */
129
    */
129
   props?: ?Object,
130
   props?: ?Object,
130
-  /*
131
+  /**
131
    * Set the ViewManager to use for communication with the native side.
132
    * Set the ViewManager to use for communication with the native side.
132
    * @platform ios
133
    * @platform ios
133
    */
134
    */
134
   viewManager?: ?Object,
135
   viewManager?: ?Object,
135
 |}>;
136
 |}>;
136
 
137
 
138
+export type OnShouldStartLoadWithRequest = (
139
+  event: WebViewNavigation,
140
+) => boolean;
141
+
137
 export type IOSWebViewProps = $ReadOnly<{|
142
 export type IOSWebViewProps = $ReadOnly<{|
138
   /**
143
   /**
139
    * If true, use WKWebView instead of UIWebView.
144
    * If true, use WKWebView instead of UIWebView.
205
    *
210
    *
206
    * @platform ios
211
    * @platform ios
207
    */
212
    */
208
-  dataDetectorTypes?:
209
-    | ?DataDetectorTypes
210
-    | $ReadOnlyArray<DataDetectorTypes>,
211
-
212
-  /**
213
-   * Function that allows custom handling of any web view requests. Return
214
-   * `true` from the function to continue loading the request and `false`
215
-   * to stop loading.
216
-   * @platform ios
217
-   */
218
-  onShouldStartLoadWithRequest?: (event: WebViewEvent) => mixed,
213
+  dataDetectorTypes?: ?DataDetectorTypes | $ReadOnlyArray<DataDetectorTypes>,
219
 
214
 
220
   /**
215
   /**
221
    * Boolean that determines whether HTML5 videos play inline or use the
216
    * Boolean that determines whether HTML5 videos play inline or use the
295
    */
290
    */
296
   saveFormDataDisabled?: ?boolean,
291
   saveFormDataDisabled?: ?boolean,
297
 
292
 
298
-  /*
293
+  /**
299
    * Used on Android only, controls whether the given list of URL prefixes should
294
    * Used on Android only, controls whether the given list of URL prefixes should
300
    * make {@link com.facebook.react.views.webview.ReactWebViewClient} to launch a
295
    * make {@link com.facebook.react.views.webview.ReactWebViewClient} to launch a
301
    * default activity intent for those URL instead of loading it within the webview.
296
    * default activity intent for those URL instead of loading it within the webview.
345
   mixedContentMode?: ?('never' | 'always' | 'compatibility'),
340
   mixedContentMode?: ?('never' | 'always' | 'compatibility'),
346
 |}>;
341
 |}>;
347
 
342
 
348
-export type WebViewSharedProps =  $ReadOnly<{|
343
+export type WebViewSharedProps = $ReadOnly<{|
349
   ...ViewProps,
344
   ...ViewProps,
350
   ...IOSWebViewProps,
345
   ...IOSWebViewProps,
351
   ...AndroidWebViewProps,
346
   ...AndroidWebViewProps,
366
   /**
361
   /**
367
    * Function that returns a view to show if there's an error.
362
    * Function that returns a view to show if there's an error.
368
    */
363
    */
369
-  renderError: (errorDomain: ?string, errorCode: number, errorDesc: string) => Element<any>, // view to show if there's an error
364
+  renderError: (
365
+    errorDomain: ?string,
366
+    errorCode: number,
367
+    errorDesc: string,
368
+  ) => Element<any>, // view to show if there's an error
370
 
369
 
371
   /**
370
   /**
372
    * Function that returns a loading indicator.
371
    * Function that returns a loading indicator.
457
    */
456
    */
458
   originWhitelist?: $ReadOnlyArray<string>,
457
   originWhitelist?: $ReadOnlyArray<string>,
459
 
458
 
459
+  /**
460
+   * Function that allows custom handling of any web view requests. Return
461
+   * `true` from the function to continue loading the request and `false`
462
+   * to stop loading. The `navigationType` is always `other` on android.
463
+   */
464
+  onShouldStartLoadWithRequest?: OnShouldStartLoadWithRequest,
465
+
460
   /**
466
   /**
461
    * Override the native component used to render the WebView. Enables a custom native
467
    * Override the native component used to render the WebView. Enables a custom native
462
    * WebView which uses the same JavaScript as the original WebView.
468
    * WebView which uses the same JavaScript as the original WebView.