Browse Source

Merge branch 'master' into docs/nav-state-changes

Jamon Holmgren 6 years ago
parent
commit
06c1bb0657
No account linked to committer's email address

+ 4
- 0
README.md View File

@@ -2,6 +2,10 @@
2 2
 
3 3
 **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 4
 
5
+> We just swapped out the React Native WebView in our app with the version from React Native Community. The swap took less than a day, required almost no code modifications, and is faster and CSS works better. Props to everyone in the community (including those at Infinite Red) that helped get that component split out.
6
+
7
+_Garrett McCullough, mobile engineer at Virta Health_
8
+
5 9
 ## Platforms Supported
6 10
 
7 11
 - [x] iOS (both UIWebView and WKWebView)

+ 97
- 66
android/build.gradle View File

@@ -1,91 +1,122 @@
1 1
 buildscript {
2
-    ext.kotlin_version = '1.2.71'
3
-    repositories {
4
-        google()
5
-        jcenter()
6
-        maven {
7
-            url 'https://maven.fabric.io/public'
8
-        }
9
-    }
10
-    dependencies {
11
-        classpath 'com.android.tools.build:gradle:3.2.1'
12
-        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
13
-    }
2
+  ext.kotlin_version = '1.2.71'
3
+  repositories {
4
+    google()
5
+    jcenter()
6
+  }
7
+  dependencies {
8
+    classpath 'com.android.tools.build:gradle:3.2.1'
9
+    //noinspection DifferentKotlinGradleVersion
10
+    classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
11
+  }
14 12
 }
15 13
 
16 14
 apply plugin: 'com.android.library'
17 15
 apply plugin: 'kotlin-android'
18 16
 
19
-
17
+def DEFAULT_TARGET_SDK_VERSION = 27
20 18
 def DEFAULT_COMPILE_SDK_VERSION = 27
21 19
 def DEFAULT_BUILD_TOOLS_VERSION = "28.0.3"
22
-def DEFAULT_TARGET_SDK_VERSION = 27
20
+
21
+def getExtOrDefault(name, defaultValue) {
22
+  return rootProject.ext.has(name) ? rootProject.ext.get(name) : defaultValue
23
+}
23 24
 
24 25
 android {
25
-    compileSdkVersion rootProject.hasProperty('compileSdkVersion') ? rootProject.compileSdkVersion : DEFAULT_COMPILE_SDK_VERSION
26
-    buildToolsVersion rootProject.hasProperty('buildToolsVersion') ? rootProject.buildToolsVersion : DEFAULT_BUILD_TOOLS_VERSION
27
-    defaultConfig {
28
-        minSdkVersion 16
29
-        targetSdkVersion rootProject.hasProperty('targetSdkVersion') ? rootProject.targetSdkVersion : DEFAULT_TARGET_SDK_VERSION
30
-        versionCode 1
31
-        versionName "1.0"
32
-    }
33
-    buildTypes {
34
-        release {
35
-            minifyEnabled false
36
-        }
37
-    }
38
-    productFlavors {
39
-    }
40
-    lintOptions {
41
-        disable 'GradleCompatible'
42
-    }
43
-    compileOptions {
44
-        sourceCompatibility JavaVersion.VERSION_1_8
45
-        targetCompatibility JavaVersion.VERSION_1_8
26
+  compileSdkVersion getExtOrDefault('compileSdkVersion', DEFAULT_COMPILE_SDK_VERSION)
27
+  buildToolsVersion getExtOrDefault('buildToolsVersion', DEFAULT_BUILD_TOOLS_VERSION)
28
+  defaultConfig {
29
+    minSdkVersion 16
30
+    targetSdkVersion getExtOrDefault('targetSdkVersion', DEFAULT_TARGET_SDK_VERSION)
31
+    versionCode 1
32
+    versionName "1.0"
33
+  }
34
+  buildTypes {
35
+    release {
36
+      minifyEnabled false
46 37
     }
38
+  }
39
+  lintOptions {
40
+    disable 'GradleCompatible'
41
+  }
42
+  compileOptions {
43
+    sourceCompatibility JavaVersion.VERSION_1_8
44
+    targetCompatibility JavaVersion.VERSION_1_8
45
+  }
47 46
 }
48 47
 
49 48
 repositories {
50
-    mavenCentral()
49
+  mavenCentral()
50
+  jcenter()
51
+
52
+  def found = false
53
+  def defaultDir = null
54
+  def androidSourcesName = 'React Native sources'
55
+
56
+  if (rootProject.ext.has('reactNativeAndroidRoot')) {
57
+    defaultDir = rootProject.ext.get('reactNativeAndroidRoot')
58
+  } else {
59
+    defaultDir = new File(
60
+      projectDir,
61
+      '/../../../node_modules/react-native/android'
62
+    )
63
+  }
64
+
65
+  if (defaultDir.exists()) {
51 66
     maven {
52
-        url 'https://maven.google.com/'
53
-        name 'Google'
67
+      url defaultDir.toString()
68
+      name androidSourcesName
54 69
     }
55 70
 
56
-    // Stolen from react-native-firebase, thanks dudes!
57
-    def found = false
71
+    logger.quiet(":${project.name}:reactNativeAndroidRoot ${defaultDir.canonicalPath}")
72
+    found = true
73
+  } else {
58 74
     def parentDir = rootProject.projectDir
59
-    def reactNativeAndroidName = 'React Native (Node Modules)'
60
-
61
-    1.upto(4, {
62
-        if (found) return true
63
-        parentDir = parentDir.parentFile
64
-        def reactNativeAndroid = new File(
65
-                parentDir,
66
-                'node_modules/react-native/android'
67
-        )
68
-
69
-        if (reactNativeAndroid.exists()) {
70
-            maven {
71
-                url reactNativeAndroid.toString()
72
-                name reactNativeAndroidName
73
-            }
74
-
75
-            println "${project.name}: using React Native sources from ${reactNativeAndroid.toString()}"
76
-            found = true
75
+
76
+    1.upto(5, {
77
+      if (found) return true
78
+      parentDir = parentDir.parentFile
79
+
80
+      def androidSourcesDir = new File(
81
+        parentDir,
82
+        'node_modules/react-native'
83
+      )
84
+
85
+      def androidPrebuiltBinaryDir = new File(
86
+        parentDir,
87
+        'node_modules/react-native/android'
88
+      )
89
+
90
+      if (androidPrebuiltBinaryDir.exists()) {
91
+        maven {
92
+          url androidPrebuiltBinaryDir.toString()
93
+          name androidSourcesName
94
+        }
95
+
96
+        logger.quiet(":${project.name}:reactNativeAndroidRoot ${androidPrebuiltBinaryDir.canonicalPath}")
97
+        found = true
98
+      } else if (androidSourcesDir.exists()) {
99
+        maven {
100
+          url androidSourcesDir.toString()
101
+          name androidSourcesName
77 102
         }
103
+
104
+        logger.quiet(":${project.name}:reactNativeAndroidRoot ${androidSourcesDir.canonicalPath}")
105
+        found = true
106
+      }
78 107
     })
108
+  }
79 109
 
80
-    if (!found) {
81
-        throw new GradleException(
82
-                "${project.name}: unable to locate React Native Android sources, " +
83
-                        "ensure you have you installed React Native as a dependency and try again."
84
-        )
85
-    }
110
+  if (!found) {
111
+    throw new GradleException(
112
+      "${project.name}: unable to locate React Native android sources. " +
113
+        "Ensure you have you installed React Native as a dependency in your project and try again."
114
+    )
115
+  }
86 116
 }
87 117
 
88 118
 dependencies {
89
-    implementation 'com.facebook.react:react-native:+'
90
-    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
119
+  //noinspection GradleDynamicVersion
120
+  api 'com.facebook.react:react-native:+'
121
+  implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
91 122
 }

+ 12
- 1
android/src/main/AndroidManifest.xml View File

@@ -1,2 +1,13 @@
1 1
 <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.reactnativecommunity.webview">
2
-</manifest>
2
+    <application>
3
+        <provider
4
+        android:name=".RNCWebViewFileProvider"
5
+        android:authorities="${applicationId}.fileprovider"
6
+        android:exported="false"
7
+        android:grantUriPermissions="true">
8
+            <meta-data
9
+                android:name="android.support.FILE_PROVIDER_PATHS"
10
+                android:resource="@xml/file_provider_paths" />
11
+        </provider>
12
+    </application>
13
+</manifest>

+ 14
- 0
android/src/main/java/com/reactnativecommunity/webview/RNCWebViewFileProvider.java View File

@@ -0,0 +1,14 @@
1
+package com.reactnativecommunity.webview;
2
+
3
+import android.support.v4.content.FileProvider;
4
+
5
+/**
6
+ * Providing a custom {@code FileProvider} prevents manifest {@code <provider>} name collisions.
7
+ *
8
+ * See https://developer.android.com/guide/topics/manifest/provider-element.html for details.
9
+ */
10
+public class RNCWebViewFileProvider extends FileProvider {
11
+
12
+  // This class intentionally left blank.
13
+
14
+}

+ 143
- 87
android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java View File

@@ -1,8 +1,13 @@
1 1
 package com.reactnativecommunity.webview;
2 2
 
3 3
 import android.annotation.TargetApi;
4
+import android.app.DownloadManager;
4 5
 import android.content.Context;
5 6
 import com.facebook.react.uimanager.UIManagerModule;
7
+
8
+import java.net.MalformedURLException;
9
+import java.net.URL;
10
+import java.net.URLDecoder;
6 11
 import java.util.LinkedList;
7 12
 import java.util.List;
8 13
 import java.util.regex.Pattern;
@@ -18,21 +23,25 @@ import java.util.Map;
18 23
 import android.content.ActivityNotFoundException;
19 24
 import android.content.Intent;
20 25
 import android.graphics.Bitmap;
21
-import android.graphics.Picture;
22 26
 import android.net.Uri;
23 27
 import android.os.Build;
28
+import android.os.Environment;
24 29
 import android.text.TextUtils;
25 30
 import android.view.View;
26 31
 import android.view.ViewGroup.LayoutParams;
27 32
 import android.webkit.ConsoleMessage;
28 33
 import android.webkit.CookieManager;
34
+import android.webkit.DownloadListener;
29 35
 import android.webkit.GeolocationPermissions;
30 36
 import android.webkit.JavascriptInterface;
37
+import android.webkit.URLUtil;
31 38
 import android.webkit.ValueCallback;
32 39
 import android.webkit.WebChromeClient;
40
+import android.webkit.WebResourceRequest;
33 41
 import android.webkit.WebSettings;
34 42
 import android.webkit.WebView;
35 43
 import android.webkit.WebViewClient;
44
+
36 45
 import com.facebook.common.logging.FLog;
37 46
 import com.facebook.react.bridge.Arguments;
38 47
 import com.facebook.react.bridge.LifecycleEventListener;
@@ -51,11 +60,19 @@ import com.facebook.react.uimanager.annotations.ReactProp;
51 60
 import com.facebook.react.uimanager.events.ContentSizeChangeEvent;
52 61
 import com.facebook.react.uimanager.events.Event;
53 62
 import com.facebook.react.uimanager.events.EventDispatcher;
63
+import com.facebook.react.uimanager.events.RCTEventEmitter;
54 64
 import com.reactnativecommunity.webview.events.TopLoadingErrorEvent;
55 65
 import com.reactnativecommunity.webview.events.TopLoadingFinishEvent;
56 66
 import com.reactnativecommunity.webview.events.TopLoadingStartEvent;
57 67
 import com.reactnativecommunity.webview.events.TopMessageEvent;
58 68
 import com.reactnativecommunity.webview.events.TopLoadingProgressEvent;
69
+import com.reactnativecommunity.webview.events.TopShouldStartLoadWithRequestEvent;
70
+import java.io.UnsupportedEncodingException;
71
+import java.util.ArrayList;
72
+import java.util.HashMap;
73
+import java.util.Locale;
74
+import java.util.Map;
75
+import javax.annotation.Nullable;
59 76
 import org.json.JSONException;
60 77
 import org.json.JSONObject;
61 78
 
@@ -66,12 +83,14 @@ import org.json.JSONObject;
66 83
  *  - GO_BACK
67 84
  *  - GO_FORWARD
68 85
  *  - RELOAD
86
+ *  - LOAD_URL
69 87
  *
70 88
  * {@link WebView} instances could emit following direct events:
71 89
  *  - topLoadingFinish
72 90
  *  - topLoadingStart
73 91
  *  - topLoadingStart
74 92
  *  - topLoadingProgress
93
+ *  - topShouldStartLoadWithRequest
75 94
  *
76 95
  * Each event will carry the following properties:
77 96
  *  - target - view's react tag
@@ -85,6 +104,7 @@ import org.json.JSONObject;
85 104
 public class RNCWebViewManager extends SimpleViewManager<WebView> {
86 105
 
87 106
   protected static final String REACT_CLASS = "RNCWebView";
107
+  private RNCWebViewPackage aPackage;
88 108
 
89 109
   protected static final String HTML_ENCODING = "UTF-8";
90 110
   protected static final String HTML_MIME_TYPE = "text/html";
@@ -98,19 +118,18 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
98 118
   public static final int COMMAND_STOP_LOADING = 4;
99 119
   public static final int COMMAND_POST_MESSAGE = 5;
100 120
   public static final int COMMAND_INJECT_JAVASCRIPT = 6;
121
+  public static final int COMMAND_LOAD_URL = 7;
101 122
 
102 123
   // Use `webView.loadUrl("about:blank")` to reliably reset the view
103 124
   // state and release page resources (including any running JavaScript).
104 125
   protected static final String BLANK_URL = "about:blank";
105 126
 
106 127
   protected WebViewConfig mWebViewConfig;
107
-  protected @Nullable WebView.PictureListener mPictureListener;
108 128
 
109 129
   protected static class RNCWebViewClient extends WebViewClient {
110 130
 
111 131
     protected boolean mLastLoadFailed = false;
112 132
     protected @Nullable ReadableArray mUrlPrefixesForDefaultIntent;
113
-    protected @Nullable List<Pattern> mOriginWhitelist;
114 133
 
115 134
     @Override
116 135
     public void onPageFinished(WebView webView, String url) {
@@ -138,50 +157,16 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
138 157
 
139 158
     @Override
140 159
     public boolean shouldOverrideUrlLoading(WebView view, String url) {
141
-      if (url.equals(BLANK_URL)) return false;
142
-
143
-      // url blacklisting
144
-      if (mUrlPrefixesForDefaultIntent != null && mUrlPrefixesForDefaultIntent.size() > 0) {
145
-        ArrayList<Object> urlPrefixesForDefaultIntent =
146
-            mUrlPrefixesForDefaultIntent.toArrayList();
147
-        for (Object urlPrefix : urlPrefixesForDefaultIntent) {
148
-          if (url.startsWith((String) urlPrefix)) {
149
-            launchIntent(view.getContext(), url);
150
-            return true;
151
-          }
152
-        }
153
-      }
154
-
155
-      if (mOriginWhitelist != null && shouldHandleURL(mOriginWhitelist, url)) {
156
-        return false;
157
-      }
158
-
159
-      launchIntent(view.getContext(), url);
160
+      dispatchEvent(view, new TopShouldStartLoadWithRequestEvent(view.getId(), url));
160 161
       return true;
161 162
     }
162 163
 
163
-    private void launchIntent(Context context, String url) {
164
-      try {
165
-        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
166
-        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
167
-        intent.addCategory(Intent.CATEGORY_BROWSABLE);
168
-        context.startActivity(intent);
169
-      } catch (ActivityNotFoundException e) {
170
-        FLog.w(ReactConstants.TAG, "activity not found to handle uri scheme for: " + url, e);
171
-      }
172
-    }
173 164
 
174
-    private boolean shouldHandleURL(List<Pattern> originWhitelist, String url) {
175
-      Uri uri = Uri.parse(url);
176
-      String scheme = uri.getScheme() != null ? uri.getScheme() : "";
177
-      String authority = uri.getAuthority() != null ? uri.getAuthority() : "";
178
-      String urlToCheck = scheme + "://" + authority;
179
-      for (Pattern pattern : originWhitelist) {
180
-        if (pattern.matcher(urlToCheck).matches()) {
181
-          return true;
182
-        }
183
-      }
184
-      return false;
165
+    @TargetApi(Build.VERSION_CODES.N)
166
+    @Override
167
+    public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
168
+      dispatchEvent(view, new TopShouldStartLoadWithRequestEvent(view.getId(), request.getUrl().toString()));
169
+      return true;
185 170
     }
186 171
 
187 172
     @Override
@@ -230,10 +215,6 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
230 215
     public void setUrlPrefixesForDefaultIntent(ReadableArray specialUrls) {
231 216
       mUrlPrefixesForDefaultIntent = specialUrls;
232 217
     }
233
-
234
-    public void setOriginWhitelist(List<Pattern> originWhitelist) {
235
-      mOriginWhitelist = originWhitelist;
236
-    }
237 218
   }
238 219
 
239 220
   /**
@@ -244,6 +225,11 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
244 225
     protected @Nullable String injectedJS;
245 226
     protected boolean messagingEnabled = false;
246 227
     protected @Nullable RNCWebViewClient mRNCWebViewClient;
228
+    protected boolean sendContentSizeChangeEvents = false;
229
+    public void setSendContentSizeChangeEvents(boolean sendContentSizeChangeEvents) {
230
+      this.sendContentSizeChangeEvents = sendContentSizeChangeEvents;
231
+    }
232
+
247 233
 
248 234
     protected class RNCWebViewBridge {
249 235
       RNCWebView mContext;
@@ -284,6 +270,22 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
284 270
       cleanupCallbacksAndDestroy();
285 271
     }
286 272
 
273
+    @Override
274
+    protected void onSizeChanged(int w, int h, int ow, int oh) {
275
+      super.onSizeChanged(w, h, ow, oh);
276
+
277
+      if (sendContentSizeChangeEvents) {
278
+        dispatchEvent(
279
+          this,
280
+          new ContentSizeChangeEvent(
281
+            this.getId(),
282
+            w,
283
+            h
284
+          )
285
+        );
286
+      }
287
+    }
288
+
287 289
     @Override
288 290
     public void setWebViewClient(WebViewClient client) {
289 291
       super.setWebViewClient(client);
@@ -427,6 +429,25 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
427 429
       public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
428 430
         callback.invoke(origin, true, false);
429 431
       }
432
+
433
+      protected void openFileChooser(ValueCallback<Uri> filePathCallback, String acceptType) {
434
+        getModule().startPhotoPickerIntent(filePathCallback, acceptType);
435
+      }
436
+      protected void openFileChooser(ValueCallback<Uri> filePathCallback) {
437
+        getModule().startPhotoPickerIntent(filePathCallback, "");
438
+      }
439
+      protected void openFileChooser(ValueCallback<Uri> filePathCallback, String acceptType, String capture) {
440
+        getModule().startPhotoPickerIntent(filePathCallback, acceptType);
441
+      }
442
+
443
+      @TargetApi(Build.VERSION_CODES.LOLLIPOP)
444
+      @Override
445
+      public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
446
+        String[] acceptTypes = fileChooserParams.getAcceptTypes();
447
+        boolean allowMultiple = fileChooserParams.getMode() == WebChromeClient.FileChooserParams.MODE_OPEN_MULTIPLE;
448
+        Intent intent = fileChooserParams.createIntent();
449
+        return getModule().startPhotoPickerIntent(filePathCallback, intent, acceptTypes, allowMultiple);
450
+      }
430 451
     });
431 452
     reactContext.addLifecycleEventListener(webView);
432 453
     mWebViewConfig.configWebView(webView);
@@ -453,6 +474,53 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
453 474
       WebView.setWebContentsDebuggingEnabled(true);
454 475
     }
455 476
 
477
+    webView.setDownloadListener(new DownloadListener() {
478
+      public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) {
479
+        RNCWebViewModule module = getModule();
480
+
481
+        DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
482
+
483
+        //Try to extract filename from contentDisposition, otherwise guess using URLUtil
484
+        String fileName = "";
485
+        try {
486
+          fileName = contentDisposition.replaceFirst("(?i)^.*filename=\"?([^\"]+)\"?.*$", "$1");
487
+          fileName = URLDecoder.decode(fileName, "UTF-8");
488
+        } catch (Exception e) {
489
+          System.out.println("Error extracting filename from contentDisposition: " + e);
490
+          System.out.println("Falling back to URLUtil.guessFileName");
491
+          fileName = URLUtil.guessFileName(url,contentDisposition,mimetype);
492
+        }
493
+        String downloadMessage = "Downloading " + fileName;
494
+
495
+        //Attempt to add cookie, if it exists
496
+        URL urlObj = null;
497
+        try {
498
+          urlObj = new URL(url);
499
+          String baseUrl = urlObj.getProtocol() + "://" + urlObj.getHost();
500
+          String cookie = CookieManager.getInstance().getCookie(baseUrl);
501
+          request.addRequestHeader("Cookie", cookie);
502
+          System.out.println("Got cookie for DownloadManager: " + cookie);
503
+        } catch (MalformedURLException e) {
504
+          System.out.println("Error getting cookie for DownloadManager: " + e.toString());
505
+          e.printStackTrace();
506
+        }
507
+
508
+        //Finish setting up request
509
+        request.addRequestHeader("User-Agent", userAgent);
510
+        request.setTitle(fileName);
511
+        request.setDescription(downloadMessage);
512
+        request.allowScanningByMediaScanner();
513
+        request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
514
+        request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
515
+
516
+        module.setDownloadRequest(request);
517
+
518
+        if (module.grantFileDownloaderPermissions()) {
519
+          module.downloadFile();
520
+        }
521
+      }
522
+    });
523
+
456 524
     return webView;
457 525
   }
458 526
 
@@ -592,11 +660,7 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
592 660
 
593 661
   @ReactProp(name = "onContentSizeChange")
594 662
   public void setOnContentSizeChange(WebView view, boolean sendContentSizeChangeEvents) {
595
-    if (sendContentSizeChangeEvents) {
596
-      view.setPictureListener(getPictureListener());
597
-    } else {
598
-      view.setPictureListener(null);
599
-    }
663
+    ((RNCWebView) view).setSendContentSizeChangeEvents(sendContentSizeChangeEvents);
600 664
   }
601 665
 
602 666
   @ReactProp(name = "mixedContentMode")
@@ -636,20 +700,6 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
636 700
     view.getSettings().setGeolocationEnabled(isGeolocationEnabled != null && isGeolocationEnabled);
637 701
   }
638 702
 
639
-  @ReactProp(name = "originWhitelist")
640
-  public void setOriginWhitelist(
641
-    WebView view,
642
-    @Nullable ReadableArray originWhitelist) {
643
-    RNCWebViewClient client = ((RNCWebView) view).getRNCWebViewClient();
644
-    if (client != null && originWhitelist != null) {
645
-      List<Pattern> whiteList = new LinkedList<>();
646
-      for (int i = 0 ; i < originWhitelist.size() ; i++) {
647
-        whiteList.add(Pattern.compile(originWhitelist.getString(i)));
648
-      }
649
-      client.setOriginWhitelist(whiteList);
650
-    }
651
-  }
652
-
653 703
   @Override
654 704
   protected void addEventEmitters(ThemedReactContext reactContext, WebView view) {
655 705
     // Do not register default touch emitter and let WebView implementation handle touches
@@ -658,9 +708,13 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
658 708
 
659 709
   @Override
660 710
   public Map getExportedCustomDirectEventTypeConstants() {
661
-    MapBuilder.Builder builder = MapBuilder.builder();
662
-    builder.put("topLoadingProgress", MapBuilder.of("registrationName", "onLoadingProgress"));
663
-    return builder.build();
711
+    Map export = super.getExportedCustomDirectEventTypeConstants();
712
+    if (export == null) {
713
+      export = MapBuilder.newHashMap();
714
+    }
715
+    export.put(TopLoadingProgressEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoadingProgress"));
716
+    export.put(TopShouldStartLoadWithRequestEvent.EVENT_NAME, MapBuilder.of("registrationName", "onShouldStartLoadWithRequest"));
717
+    return export;
664 718
   }
665 719
 
666 720
   @Override
@@ -671,7 +725,8 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
671 725
         "reload", COMMAND_RELOAD,
672 726
         "stopLoading", COMMAND_STOP_LOADING,
673 727
         "postMessage", COMMAND_POST_MESSAGE,
674
-        "injectJavaScript", COMMAND_INJECT_JAVASCRIPT
728
+        "injectJavaScript", COMMAND_INJECT_JAVASCRIPT,
729
+        "loadUrl", COMMAND_LOAD_URL
675 730
       );
676 731
   }
677 732
 
@@ -714,6 +769,12 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
714 769
         RNCWebView reactWebView = (RNCWebView) root;
715 770
         reactWebView.evaluateJavascriptWithFallback(args.getString(0));
716 771
         break;
772
+      case COMMAND_LOAD_URL:
773
+        if (args == null) {
774
+          throw new RuntimeException("Arguments for loading an url are null!");
775
+        }
776
+        root.loadUrl(args.getString(0));
777
+        break;
717 778
     }
718 779
   }
719 780
 
@@ -724,27 +785,22 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
724 785
     ((RNCWebView) webView).cleanupCallbacksAndDestroy();
725 786
   }
726 787
 
727
-  protected WebView.PictureListener getPictureListener() {
728
-    if (mPictureListener == null) {
729
-      mPictureListener = new WebView.PictureListener() {
730
-        @Override
731
-        public void onNewPicture(WebView webView, Picture picture) {
732
-          dispatchEvent(
733
-            webView,
734
-            new ContentSizeChangeEvent(
735
-              webView.getId(),
736
-              webView.getWidth(),
737
-              webView.getContentHeight()));
738
-        }
739
-      };
740
-    }
741
-    return mPictureListener;
742
-  }
743
-
744 788
   protected static void dispatchEvent(WebView webView, Event event) {
745 789
     ReactContext reactContext = (ReactContext) webView.getContext();
746 790
     EventDispatcher eventDispatcher =
747 791
       reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher();
748 792
     eventDispatcher.dispatchEvent(event);
749 793
   }
794
+
795
+  public RNCWebViewPackage getPackage() {
796
+    return this.aPackage;
797
+  }
798
+
799
+  public void setPackage(RNCWebViewPackage aPackage) {
800
+    this.aPackage = aPackage;
801
+  }
802
+
803
+  public RNCWebViewModule getModule() {
804
+    return this.aPackage.getModule();
805
+  }
750 806
 }

+ 362
- 3
android/src/main/java/com/reactnativecommunity/webview/RNCWebViewModule.java View File

@@ -1,22 +1,381 @@
1 1
 
2 2
 package com.reactnativecommunity.webview;
3 3
 
4
+import android.Manifest;
5
+import android.app.Activity;
6
+import android.app.DownloadManager;
7
+import android.content.Context;
8
+import android.content.Intent;
9
+import android.content.pm.PackageManager;
10
+import android.net.Uri;
11
+import android.os.Build;
12
+import android.os.Environment;
13
+import android.os.Parcelable;
14
+import android.provider.MediaStore;
15
+import android.support.annotation.RequiresApi;
16
+import android.support.v4.content.ContextCompat;
17
+import android.support.v4.content.FileProvider;
18
+import android.util.Log;
19
+import android.webkit.ValueCallback;
20
+import android.webkit.WebChromeClient;
21
+import android.widget.Toast;
22
+
23
+import com.facebook.react.bridge.ActivityEventListener;
24
+import com.facebook.react.bridge.Promise;
4 25
 import com.facebook.react.bridge.ReactApplicationContext;
5 26
 import com.facebook.react.bridge.ReactContextBaseJavaModule;
6 27
 import com.facebook.react.bridge.ReactMethod;
7
-import com.facebook.react.bridge.Callback;
28
+import com.facebook.react.modules.core.PermissionAwareActivity;
29
+import com.facebook.react.modules.core.PermissionListener;
30
+
31
+import java.io.File;
32
+import java.io.IOException;
33
+import java.util.ArrayList;
34
+
35
+import static android.app.Activity.RESULT_OK;
8 36
 
9
-public class RNCWebViewModule extends ReactContextBaseJavaModule {
37
+public class RNCWebViewModule extends ReactContextBaseJavaModule implements ActivityEventListener {
10 38
 
11 39
   private final ReactApplicationContext reactContext;
40
+  private RNCWebViewPackage aPackage;
41
+
42
+  private static final int PICKER = 1;
43
+  private static final int PICKER_LEGACY = 3;
44
+
45
+  private ValueCallback<Uri> filePathCallbackLegacy;
46
+  private ValueCallback<Uri[]> filePathCallback;
47
+  private Uri outputFileUri;
48
+
49
+  private DownloadManager.Request downloadRequest;
50
+  private static final int FILE_DOWNLOAD_PERMISSION_REQUEST = 1;
51
+
52
+  final String DEFAULT_MIME_TYPES = "*/*";
12 53
 
13 54
   public RNCWebViewModule(ReactApplicationContext reactContext) {
14 55
     super(reactContext);
15 56
     this.reactContext = reactContext;
57
+    reactContext.addActivityEventListener(this);
16 58
   }
17 59
 
18 60
   @Override
19 61
   public String getName() {
20 62
     return "RNCWebView";
21 63
   }
22
-}
64
+
65
+  @ReactMethod
66
+  public void isFileUploadSupported(final Promise promise) {
67
+      Boolean result = false;
68
+      int current = Build.VERSION.SDK_INT;
69
+      if (current >= Build.VERSION_CODES.LOLLIPOP) {
70
+          result = true;
71
+      }
72
+      if (current >= Build.VERSION_CODES.JELLY_BEAN && current <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
73
+          result = true;
74
+      }
75
+      promise.resolve(result);
76
+  }
77
+
78
+  public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
79
+
80
+    if (filePathCallback == null && filePathCallbackLegacy == null) {
81
+        return;
82
+    }
83
+
84
+    // based off of which button was pressed, we get an activity result and a file
85
+    // the camera activity doesn't properly return the filename* (I think?) so we use
86
+    // this filename instead
87
+    switch (requestCode) {
88
+    case PICKER:
89
+        if (resultCode != RESULT_OK) {
90
+            if (filePathCallback != null) {
91
+                filePathCallback.onReceiveValue(null);
92
+            }
93
+        } else {
94
+            Uri result[] = this.getSelectedFiles(data, resultCode);
95
+            if (result != null) {
96
+                filePathCallback.onReceiveValue(result);
97
+            } else {
98
+                filePathCallback.onReceiveValue(new Uri[] { outputFileUri });
99
+            }
100
+        }
101
+        break;
102
+    case PICKER_LEGACY:
103
+        Uri result = resultCode != Activity.RESULT_OK ? null : data == null ? outputFileUri : data.getData();
104
+        filePathCallbackLegacy.onReceiveValue(result);
105
+        break;
106
+
107
+    }
108
+    filePathCallback = null;
109
+    filePathCallbackLegacy= null;
110
+    outputFileUri = null;
111
+  }
112
+
113
+  public void onNewIntent(Intent intent) {
114
+  }
115
+
116
+  private Uri[] getSelectedFiles(Intent data, int resultCode) {
117
+    if (data == null) {
118
+        return null;
119
+    }
120
+
121
+    // we have one file selected
122
+    if (data.getData() != null) {
123
+        if (resultCode == RESULT_OK && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
124
+            return WebChromeClient.FileChooserParams.parseResult(resultCode, data);
125
+        } else {
126
+            return null;
127
+        }
128
+    }
129
+
130
+    // we have multiple files selected
131
+    if (data.getClipData() != null) {
132
+        final int numSelectedFiles = data.getClipData().getItemCount();
133
+        Uri[] result = new Uri[numSelectedFiles];
134
+        for (int i = 0; i < numSelectedFiles; i++) {
135
+            result[i] = data.getClipData().getItemAt(i).getUri();
136
+        }
137
+        return result;
138
+    }
139
+    return null;
140
+  }
141
+
142
+  public void startPhotoPickerIntent(ValueCallback<Uri> filePathCallback, String acceptType) {
143
+      filePathCallbackLegacy = filePathCallback;
144
+
145
+      Intent fileChooserIntent = getFileChooserIntent(acceptType);
146
+      Intent chooserIntent = Intent.createChooser(fileChooserIntent, "");
147
+
148
+      ArrayList<Parcelable> extraIntents = new ArrayList<>();
149
+      if (acceptsImages(acceptType)) {
150
+          extraIntents.add(getPhotoIntent());
151
+      }
152
+      if (acceptsVideo(acceptType)) {
153
+          extraIntents.add(getVideoIntent());
154
+      }
155
+      chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Parcelable[]{}));
156
+
157
+      if (chooserIntent.resolveActivity(getCurrentActivity().getPackageManager()) != null) {
158
+          getCurrentActivity().startActivityForResult(chooserIntent, PICKER_LEGACY);
159
+      } else {
160
+          Log.w("RNCWebViewModule", "there is no Activity to handle this Intent");
161
+      }
162
+  }
163
+
164
+  @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
165
+  public boolean startPhotoPickerIntent(final ValueCallback<Uri[]> callback, final Intent intent, final String[] acceptTypes, final boolean allowMultiple) {
166
+    filePathCallback = callback;
167
+
168
+    ArrayList<Parcelable> extraIntents = new ArrayList<>();
169
+    if (acceptsImages(acceptTypes)) {
170
+      extraIntents.add(getPhotoIntent());
171
+    }
172
+    if (acceptsVideo(acceptTypes)) {
173
+      extraIntents.add(getVideoIntent());
174
+    }
175
+
176
+    Intent fileSelectionIntent = getFileChooserIntent(acceptTypes, allowMultiple);
177
+
178
+    Intent chooserIntent = new Intent(Intent.ACTION_CHOOSER);
179
+    chooserIntent.putExtra(Intent.EXTRA_INTENT, fileSelectionIntent);
180
+    chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Parcelable[]{}));
181
+
182
+    if (chooserIntent.resolveActivity(getCurrentActivity().getPackageManager()) != null) {
183
+        getCurrentActivity().startActivityForResult(chooserIntent, PICKER);
184
+    } else {
185
+        Log.w("RNCWebViewModule", "there is no Activity to handle this Intent");
186
+    }
187
+
188
+    return true;
189
+  }
190
+
191
+  public void setDownloadRequest(DownloadManager.Request request) {
192
+    this.downloadRequest = request;
193
+  }
194
+
195
+  public void downloadFile() {
196
+    DownloadManager dm = (DownloadManager) getCurrentActivity().getBaseContext().getSystemService(Context.DOWNLOAD_SERVICE);
197
+    String downloadMessage = "Downloading";
198
+
199
+    dm.enqueue(this.downloadRequest);
200
+
201
+    Toast.makeText(getCurrentActivity().getApplicationContext(), downloadMessage, Toast.LENGTH_LONG).show();
202
+  }
203
+
204
+  public boolean grantFileDownloaderPermissions() {
205
+    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
206
+      return true;
207
+    }
208
+
209
+    boolean result = true;
210
+    if (ContextCompat.checkSelfPermission(getCurrentActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
211
+      result = false;
212
+    }
213
+
214
+    if (!result) {
215
+      PermissionAwareActivity activity = getPermissionAwareActivity();
216
+      activity.requestPermissions(new String[]{ Manifest.permission.WRITE_EXTERNAL_STORAGE }, FILE_DOWNLOAD_PERMISSION_REQUEST, webviewFileDownloaderPermissionListener);
217
+    }
218
+
219
+    return result;
220
+  }
221
+
222
+  public RNCWebViewPackage getPackage() {
223
+    return this.aPackage;
224
+  }
225
+
226
+  public void setPackage(RNCWebViewPackage aPackage) {
227
+    this.aPackage = aPackage;
228
+  }
229
+
230
+  private Intent getPhotoIntent() {
231
+    Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
232
+    outputFileUri = getOutputUri(MediaStore.ACTION_IMAGE_CAPTURE);
233
+    intent.putExtra(MediaStore.EXTRA_OUTPUT, outputFileUri);
234
+    return intent;
235
+  }
236
+
237
+  private Intent getVideoIntent() {
238
+    Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
239
+    // @todo from experience, for Videos we get the data onActivityResult
240
+    // so there's no need to store the Uri
241
+    Uri outputVideoUri = getOutputUri(MediaStore.ACTION_VIDEO_CAPTURE);
242
+    intent.putExtra(MediaStore.EXTRA_OUTPUT, outputVideoUri);
243
+    return intent;
244
+  }
245
+
246
+  private Intent getFileChooserIntent(String acceptTypes) {
247
+    String _acceptTypes = acceptTypes;
248
+    if (acceptTypes.isEmpty()) {
249
+      _acceptTypes = DEFAULT_MIME_TYPES;
250
+    }
251
+    Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
252
+    intent.addCategory(Intent.CATEGORY_OPENABLE);
253
+    intent.setType(_acceptTypes);
254
+    return intent;
255
+  }
256
+
257
+  private Intent getFileChooserIntent(String[] acceptTypes, boolean allowMultiple) {
258
+    Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
259
+    intent.addCategory(Intent.CATEGORY_OPENABLE);
260
+    intent.setType("*/*");
261
+    intent.putExtra(Intent.EXTRA_MIME_TYPES, getAcceptedMimeType(acceptTypes));
262
+    intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple);
263
+    return intent;
264
+  }
265
+
266
+  private Boolean acceptsImages(String types) {
267
+    return types.isEmpty() || types.toLowerCase().contains("image");
268
+  }
269
+  private Boolean acceptsImages(String[] types) {
270
+    return isArrayEmpty(types) || arrayContainsString(types, "image");
271
+  }
272
+
273
+  private Boolean acceptsVideo(String types) {
274
+    return types.isEmpty() || types.toLowerCase().contains("video");
275
+  }
276
+  private Boolean acceptsVideo(String[] types) {
277
+    return isArrayEmpty(types) || arrayContainsString(types, "video");
278
+  }
279
+
280
+  private Boolean arrayContainsString(String[] array, String pattern){
281
+    for(String content : array){
282
+        if(content.contains(pattern)){
283
+            return true;
284
+        }
285
+    }
286
+    return false;
287
+  }
288
+
289
+  private String[] getAcceptedMimeType(String[] types) {
290
+    if (isArrayEmpty(types)) {
291
+        return new String[]{DEFAULT_MIME_TYPES};
292
+    }
293
+    return types;
294
+  }
295
+
296
+  private Uri getOutputUri(String intentType) {
297
+    File capturedFile = null;
298
+    try {
299
+        capturedFile = getCapturedFile(intentType);
300
+    } catch (IOException e) {
301
+        Log.e("CREATE FILE", "Error occurred while creating the File", e);
302
+        e.printStackTrace();
303
+    }
304
+
305
+    // for versions below 6.0 (23) we use the old File creation & permissions model
306
+    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
307
+        return Uri.fromFile(capturedFile);
308
+    }
309
+
310
+    // for versions 6.0+ (23) we use the FileProvider to avoid runtime permissions
311
+    String packageName = getReactApplicationContext().getPackageName();
312
+    return FileProvider.getUriForFile(getReactApplicationContext(), packageName+".fileprovider", capturedFile);
313
+  }
314
+
315
+  private File getCapturedFile(String intentType) throws IOException {
316
+    String prefix = "";
317
+    String suffix = "";
318
+    String dir = "";
319
+    String filename = "";
320
+
321
+    if (intentType.equals(MediaStore.ACTION_IMAGE_CAPTURE)) {
322
+      prefix = "image-";
323
+      suffix = ".jpg";
324
+      dir = Environment.DIRECTORY_PICTURES;
325
+    } else if (intentType.equals(MediaStore.ACTION_VIDEO_CAPTURE)) {
326
+      prefix = "video-";
327
+      suffix = ".mp4";
328
+      dir = Environment.DIRECTORY_MOVIES;
329
+    }
330
+
331
+    filename = prefix + String.valueOf(System.currentTimeMillis()) + suffix;
332
+
333
+    // for versions below 6.0 (23) we use the old File creation & permissions model
334
+    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
335
+        // only this Directory works on all tested Android versions
336
+        // ctx.getExternalFilesDir(dir) was failing on Android 5.0 (sdk 21)
337
+        File storageDir = Environment.getExternalStoragePublicDirectory(dir);
338
+        return new File(storageDir, filename);
339
+    }
340
+
341
+    File storageDir = getReactApplicationContext().getExternalFilesDir(null);
342
+    return File.createTempFile(filename, suffix, storageDir);
343
+  }
344
+
345
+  private Boolean isArrayEmpty(String[] arr) {
346
+    // when our array returned from getAcceptTypes() has no values set from the webview
347
+    // i.e. <input type="file" />, without any "accept" attr
348
+    // will be an array with one empty string element, afaik
349
+    return arr.length == 0 || (arr.length == 1 && arr[0].length() == 0);
350
+  }
351
+
352
+  private PermissionAwareActivity getPermissionAwareActivity() {
353
+    Activity activity = getCurrentActivity();
354
+    if (activity == null) {
355
+        throw new IllegalStateException("Tried to use permissions API while not attached to an Activity.");
356
+    } else if (!(activity instanceof PermissionAwareActivity)) {
357
+        throw new IllegalStateException("Tried to use permissions API but the host Activity doesn't implement PermissionAwareActivity.");
358
+    }
359
+    return (PermissionAwareActivity) activity;
360
+  }
361
+
362
+  private PermissionListener webviewFileDownloaderPermissionListener = new PermissionListener() {
363
+    @Override
364
+    public boolean onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
365
+      switch (requestCode) {
366
+        case FILE_DOWNLOAD_PERMISSION_REQUEST: {
367
+          // If request is cancelled, the result arrays are empty.
368
+          if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
369
+            if (downloadRequest != null) {
370
+              downloadFile();
371
+            }
372
+          } else {
373
+            Toast.makeText(getCurrentActivity().getApplicationContext(), "Cannot download files as permission was denied. Please provide permission to write to storage, in order to download files.", Toast.LENGTH_LONG).show();
374
+          }
375
+          return true;
376
+        }
377
+      }
378
+      return false;
379
+    }
380
+  };
381
+}

+ 19
- 4
android/src/main/java/com/reactnativecommunity/webview/RNCWebViewPackage.java View File

@@ -1,6 +1,7 @@
1 1
 
2 2
 package com.reactnativecommunity.webview;
3 3
 
4
+import java.util.ArrayList;
4 5
 import java.util.Arrays;
5 6
 import java.util.Collections;
6 7
 import java.util.List;
@@ -10,10 +11,19 @@ import com.facebook.react.bridge.NativeModule;
10 11
 import com.facebook.react.bridge.ReactApplicationContext;
11 12
 import com.facebook.react.uimanager.ViewManager;
12 13
 import com.facebook.react.bridge.JavaScriptModule;
14
+
13 15
 public class RNCWebViewPackage implements ReactPackage {
16
+
17
+    private RNCWebViewManager manager;
18
+    private RNCWebViewModule module;
19
+
14 20
     @Override
15 21
     public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
16
-      return Arrays.<NativeModule>asList(new RNCWebViewModule(reactContext));
22
+      List<NativeModule> modulesList = new ArrayList<>();
23
+      module = new RNCWebViewModule(reactContext);
24
+      module.setPackage(this);
25
+      modulesList.add(module);
26
+      return modulesList;
17 27
     }
18 28
 
19 29
     // Deprecated from RN 0.47
@@ -23,7 +33,12 @@ public class RNCWebViewPackage implements ReactPackage {
23 33
 
24 34
     @Override
25 35
     public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
26
-        RNCWebViewManager viewManager = new RNCWebViewManager();
27
-        return Arrays.<ViewManager>asList(viewManager);
36
+      manager = new RNCWebViewManager();
37
+      manager.setPackage(this);
38
+      return Arrays.<ViewManager>asList(manager);
39
+    }
40
+
41
+    public RNCWebViewModule getModule() {
42
+      return module;
28 43
     }
29
-}
44
+}

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

@@ -0,0 +1,40 @@
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
+}

+ 4
- 0
android/src/main/res/xml/file_provider_paths.xml View File

@@ -0,0 +1,4 @@
1
+<?xml version="1.0" encoding="utf-8"?>
2
+<paths xmlns:android="http://schemas.android.com/apk/res/android">
3
+    <external-path name="shared" path="." />
4
+</paths>

+ 80
- 0
docs/Guide.md View File

@@ -110,3 +110,83 @@ class MyWeb extends Component {
110 110
 }
111 111
 ```
112 112
 
113
+### Add support for File Upload
114
+
115
+##### iOS
116
+
117
+For iOS, all you need to do is specify the permissions in your `ios/[project]/Info.plist` file:
118
+
119
+Photo capture:
120
+```
121
+<key>NSCameraUsageDescription</key>
122
+<string>Take pictures for certain activities</string>
123
+```
124
+
125
+Gallery selection:
126
+```
127
+<key>NSPhotoLibraryUsageDescription</key>
128
+<string>Select pictures for certain activities</string>
129
+```
130
+
131
+Video recording:
132
+```
133
+<key>NSMicrophoneUsageDescription</key>
134
+<string>Need microphone access for recording videos</string>
135
+```
136
+
137
+##### Android
138
+
139
+Add permission in AndroidManifest.xml:
140
+```xml
141
+<manifest ...>
142
+  ......
143
+
144
+  <!-- this is required only for Android 4.1-5.1 (api 16-22)  -->
145
+  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
146
+
147
+  ......
148
+</manifest>
149
+```
150
+
151
+##### Check for File Upload support, with `static isFileUploadSupported()`
152
+
153
+File Upload using `<input type="file" />` is not supported for Android 4.4 KitKat (see [details](https://github.com/delight-im/Android-AdvancedWebView/issues/4#issuecomment-70372146)):
154
+
155
+```
156
+import { WebView } from "react-native-webview";
157
+
158
+WebView.isFileUploadSupported().then(res => {
159
+  if (res === true) {
160
+    // file upload is supported
161
+  } else {
162
+    // not file upload support
163
+  }
164
+});
165
+
166
+```
167
+
168
+### Add support for File Download
169
+
170
+##### iOS
171
+
172
+For iOS, all you need to do is specify the permissions in your `ios/[project]/Info.plist` file:
173
+
174
+Save to gallery:
175
+```
176
+<key>NSPhotoLibraryAddUsageDescription</key>
177
+<string>Save pictures for certain activities.</string>
178
+```
179
+
180
+##### Android
181
+
182
+Add permission in AndroidManifest.xml:
183
+```xml
184
+<manifest ...>
185
+  ......
186
+
187
+  <!-- this is required to save files on Android  -->
188
+  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
189
+
190
+  ......
191
+</manifest>
192
+```

+ 44
- 1
docs/Reference.md View File

@@ -44,8 +44,11 @@ This document lays out the current public properties and methods for the React N
44 44
 - [`html`](Reference.md#html)
45 45
 - [`hideKeyboardAccessoryView`](Reference.md#hidekeyboardaccessoryview)
46 46
 - [`allowsBackForwardNavigationGestures`](Reference.md#allowsbackforwardnavigationgestures)
47
+- [`incognito`](Reference.md#incognito)
47 48
 - [`allowFileAccess`](Reference.md#allowFileAccess)
48 49
 - [`saveFormDataDisabled`](Reference.md#saveFormDataDisabled)
50
+- [`pagingEnabled`](Reference.md#pagingEnabled)
51
+- [`allowsLinkPreview`](Reference.md#allowsLinkPreview)
49 52
 
50 53
 ## Methods Index
51 54
 
@@ -100,7 +103,7 @@ Controls whether to adjust the content inset for web views that are placed behin
100 103
 
101 104
 ### `injectedJavaScript`
102 105
 
103
-Set this to provide JavaScript that will be injected into the web page when the view loads.
106
+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.
104 107
 
105 108
 | Type   | Required |
106 109
 | ------ | -------- |
@@ -273,6 +276,16 @@ Boolean value that forces the `WebView` to show the loading view on the first lo
273 276
 
274 277
 ---
275 278
 
279
+### `style`
280
+
281
+A style object that allow you to customize the `WebView` style. Please not that there are default styles (example: you need to add `flex: 0` to the style if you want to use `height` property).
282
+
283
+| Type  | Required |
284
+| ----- | -------- |
285
+| style | No       |
286
+
287
+---
288
+
276 289
 ### `decelerationRate`
277 290
 
278 291
 A floating-point number that determines how quickly the scroll view decelerates after the user lifts their finger. You may also use the string shortcuts `"normal"` and `"fast"` which match the underlying iOS settings for `UIScrollViewDecelerationRateNormal` and `UIScrollViewDecelerationRateFast` respectively:
@@ -499,6 +512,16 @@ If true, this will be able horizontal swipe gestures when using the WKWebView. T
499 512
 
500 513
 ---
501 514
 
515
+### `incognito`
516
+
517
+Does not store any data within the lifetime of the WebView.
518
+
519
+| Type    | Required | Platform      |
520
+| ------- | -------- | ------------- |
521
+| boolean | No       | iOS WKWebView |
522
+
523
+---
524
+
502 525
 ### `allowFileAccess`
503 526
 
504 527
 If true, this will allow access to the file system via `file://` URI's. The default value is `false`.
@@ -517,6 +540,26 @@ Sets whether the WebView should disable saving form data. The default value is `
517 540
 | ------- | -------- | -------- |
518 541
 | boolean | No       | Android  |
519 542
 
543
+---
544
+
545
+### `pagingEnabled`
546
+
547
+If the value of this property is true, the scroll view stops on multiples of the scroll view’s bounds when the user scrolls. The default value is false.
548
+
549
+| Type    | Required | Platform |
550
+| ------- | -------- | -------- |
551
+| boolean | No       | iOS      |
552
+
553
+---
554
+
555
+### `allowsLinkPreview`
556
+
557
+A Boolean value that determines whether pressing on a link displays a preview of the destination for the link. In iOS this property is available on devices that support 3D Touch. In iOS 10 and later, the default value is true; before that, the default value is false.
558
+
559
+| Type    | Required | Platform |
560
+| ------- | -------- | -------- |
561
+| boolean | No       | iOS      |
562
+
520 563
 ## Methods
521 564
 
522 565
 ### `extraNativeComponentConfig()`

+ 15
- 0
ios/RNCWKProcessPoolManager.h View File

@@ -0,0 +1,15 @@
1
+/**
2
+ * Copyright (c) 2015-present, Facebook, Inc.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+#import <WebKit/WebKit.h>
9
+
10
+@interface RNCWKProcessPoolManager : NSObject
11
+
12
++ (instancetype) sharedManager;
13
+- (WKProcessPool *)sharedProcessPool;
14
+
15
+@end

+ 36
- 0
ios/RNCWKProcessPoolManager.m View File

@@ -0,0 +1,36 @@
1
+/**
2
+ * Copyright (c) 2015-present, Facebook, Inc.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+#import <Foundation/Foundation.h>
9
+#import "RNCWKProcessPoolManager.h"
10
+
11
+@interface RNCWKProcessPoolManager() {
12
+    WKProcessPool *_sharedProcessPool;
13
+}
14
+@end
15
+
16
+@implementation RNCWKProcessPoolManager
17
+
18
++ (id) sharedManager {
19
+    static RNCWKProcessPoolManager *_sharedManager = nil;
20
+    @synchronized(self) {
21
+        if(_sharedManager == nil) {
22
+            _sharedManager = [[super alloc] init];
23
+        }
24
+        return _sharedManager;
25
+    }
26
+}
27
+
28
+- (WKProcessPool *)sharedProcessPool {
29
+    if (!_sharedProcessPool) {
30
+        _sharedProcessPool = [[WKProcessPool alloc] init];
31
+    }
32
+    return _sharedProcessPool;
33
+}
34
+
35
+@end
36
+

+ 5
- 1
ios/RNCWKWebView.h View File

@@ -14,7 +14,7 @@
14 14
 @protocol RNCWKWebViewDelegate <NSObject>
15 15
 
16 16
 - (BOOL)webView:(RNCWKWebView *)webView
17
-shouldStartLoadForRequest:(NSMutableDictionary<NSString *, id> *)request
17
+   shouldStartLoadForRequest:(NSMutableDictionary<NSString *, id> *)request
18 18
    withCallback:(RCTDirectEventBlock)callback;
19 19
 
20 20
 @end
@@ -26,6 +26,7 @@ shouldStartLoadForRequest:(NSMutableDictionary<NSString *, id> *)request
26 26
 @property (nonatomic, assign) BOOL messagingEnabled;
27 27
 @property (nonatomic, copy) NSString *injectedJavaScript;
28 28
 @property (nonatomic, assign) BOOL scrollEnabled;
29
+@property (nonatomic, assign) BOOL pagingEnabled;
29 30
 @property (nonatomic, assign) CGFloat decelerationRate;
30 31
 @property (nonatomic, assign) BOOL allowsInlineMediaPlayback;
31 32
 @property (nonatomic, assign) BOOL bounces;
@@ -37,7 +38,10 @@ shouldStartLoadForRequest:(NSMutableDictionary<NSString *, id> *)request
37 38
 @property (nonatomic, assign) BOOL automaticallyAdjustContentInsets;
38 39
 @property (nonatomic, assign) BOOL hideKeyboardAccessoryView;
39 40
 @property (nonatomic, assign) BOOL allowsBackForwardNavigationGestures;
41
+@property (nonatomic, assign) BOOL incognito;
42
+@property (nonatomic, assign) BOOL useSharedProcessPool;
40 43
 @property (nonatomic, copy) NSString *userAgent;
44
+@property (nonatomic, assign) BOOL allowsLinkPreview;
41 45
 
42 46
 - (void)postMessage:(NSString *)message;
43 47
 - (void)injectJavaScript:(NSString *)script;

+ 115
- 40
ios/RNCWKWebView.m View File

@@ -8,10 +8,11 @@
8 8
 #import "RNCWKWebView.h"
9 9
 #import <React/RCTConvert.h>
10 10
 #import <React/RCTAutoInsetsProtocol.h>
11
+#import "RNCWKProcessPoolManager.h"
12
+#import <UIKit/UIKit.h>
11 13
 
12 14
 #import "objc/runtime.h"
13 15
 
14
-static NSTimer *keyboardTimer;
15 16
 static NSString *const MessageHanderName = @"ReactNative";
16 17
 
17 18
 // runtime trick to remove WKWebView keyboard default toolbar
@@ -40,12 +41,7 @@ static NSString *const MessageHanderName = @"ReactNative";
40 41
   BOOL _savedHideKeyboardAccessoryView;
41 42
 }
42 43
 
43
-- (void)dealloc
44
-{
45
-    if(_webView){
46
-        [_webView removeObserver:self forKeyPath:@"estimatedProgress"];
47
-    }
48
-}
44
+- (void)dealloc{}
49 45
 
50 46
 /**
51 47
  * See https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/DisplayWebContent/Tasks/WebKitAvail.html.
@@ -65,7 +61,6 @@ static NSString *const MessageHanderName = @"ReactNative";
65 61
   return _webkitAvailable;
66 62
 }
67 63
 
68
-
69 64
 - (instancetype)initWithFrame:(CGRect)frame
70 65
 {
71 66
   if ((self = [super initWithFrame:frame])) {
@@ -75,19 +70,6 @@ static NSString *const MessageHanderName = @"ReactNative";
75 70
     _automaticallyAdjustContentInsets = YES;
76 71
     _contentInset = UIEdgeInsetsZero;
77 72
   }
78
-    
79
-  // Workaround for a keyboard dismissal bug present in iOS 12
80
-  // https://openradar.appspot.com/radar?id=5018321736957952
81
-  if (@available(iOS 12.0, *)) {
82
-    [[NSNotificationCenter defaultCenter]
83
-      addObserver:self
84
-      selector:@selector(keyboardWillHide)
85
-      name:UIKeyboardWillHideNotification object:nil];
86
-    [[NSNotificationCenter defaultCenter]
87
-      addObserver:self
88
-      selector:@selector(keyboardWillShow)
89
-      name:UIKeyboardWillShowNotification object:nil];
90
-  }
91 73
   return self;
92 74
 }
93 75
 
@@ -99,6 +81,12 @@ static NSString *const MessageHanderName = @"ReactNative";
99 81
     };
100 82
 
101 83
     WKWebViewConfiguration *wkWebViewConfig = [WKWebViewConfiguration new];
84
+    if (_incognito) {
85
+      wkWebViewConfig.websiteDataStore = [WKWebsiteDataStore nonPersistentDataStore];
86
+    }
87
+    if(self.useSharedProcessPool) {
88
+        wkWebViewConfig.processPool = [[RNCWKProcessPoolManager sharedManager] sharedProcessPool];
89
+    }
102 90
     wkWebViewConfig.userContentController = [WKUserContentController new];
103 91
     [wkWebViewConfig.userContentController addScriptMessageHandler: self name: MessageHanderName];
104 92
     wkWebViewConfig.allowsInlineMediaPlayback = _allowsInlineMediaPlayback;
@@ -116,9 +104,12 @@ static NSString *const MessageHanderName = @"ReactNative";
116 104
     _webView.UIDelegate = self;
117 105
     _webView.navigationDelegate = self;
118 106
     _webView.scrollView.scrollEnabled = _scrollEnabled;
107
+    _webView.scrollView.pagingEnabled = _pagingEnabled;
119 108
     _webView.scrollView.bounces = _bounces;
109
+    _webView.allowsLinkPreview = _allowsLinkPreview;
120 110
     [_webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
121 111
     _webView.allowsBackForwardNavigationGestures = _allowsBackForwardNavigationGestures;
112
+
122 113
     if (_userAgent) {
123 114
       _webView.customUserAgent = _userAgent;
124 115
     }
@@ -131,28 +122,19 @@ static NSString *const MessageHanderName = @"ReactNative";
131 122
     [self addSubview:_webView];
132 123
     [self setHideKeyboardAccessoryView: _savedHideKeyboardAccessoryView];
133 124
     [self visitSource];
134
-  } else {
135
-    [_webView.configuration.userContentController removeScriptMessageHandlerForName:MessageHanderName];
136 125
   }
137 126
 }
138 127
 
139
--(void)keyboardWillHide
128
+- (void)removeFromSuperview
140 129
 {
141
-    keyboardTimer = [NSTimer scheduledTimerWithTimeInterval:0 target:self selector:@selector(keyboardDisplacementFix) userInfo:nil repeats:false];
142
-    [[NSRunLoop mainRunLoop] addTimer:keyboardTimer forMode:NSRunLoopCommonModes];
143
-}
144
--(void)keyboardWillShow
145
-{
146
-    if (keyboardTimer != nil) {
147
-        [keyboardTimer invalidate];
130
+    if (_webView) {
131
+        [_webView.configuration.userContentController removeScriptMessageHandlerForName:MessageHanderName];
132
+        [_webView removeObserver:self forKeyPath:@"estimatedProgress"];
133
+        [_webView removeFromSuperview];
134
+        _webView = nil;
148 135
     }
149
-}
150
--(void)keyboardDisplacementFix
151
-{
152
-    // https://stackoverflow.com/a/9637807/824966
153
-    [UIView animateWithDuration:.25 animations:^{
154
-        self.webView.scrollView.contentOffset = CGPointMake(0, 0);
155
-    }];
136
+
137
+    [super removeFromSuperview];
156 138
 }
157 139
 
158 140
 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
@@ -329,6 +311,88 @@ static NSString *const MessageHanderName = @"ReactNative";
329 311
 
330 312
 #pragma mark - WKNavigationDelegate methods
331 313
 
314
+/**
315
+* alert
316
+*/
317
+- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler
318
+{
319
+    UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert];
320
+    [alert addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
321
+        completionHandler();
322
+    }]];
323
+    [[self topViewController] presentViewController:alert animated:YES completion:NULL];
324
+
325
+}
326
+
327
+/**
328
+* confirm
329
+*/
330
+- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler{
331
+    UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert];
332
+    [alert addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
333
+        completionHandler(YES);
334
+    }]];
335
+    [alert addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
336
+        completionHandler(NO);
337
+    }]];
338
+    [[self topViewController] presentViewController:alert animated:YES completion:NULL];
339
+}
340
+
341
+/**
342
+* prompt
343
+*/
344
+- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *))completionHandler{
345
+    UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:prompt preferredStyle:UIAlertControllerStyleAlert];
346
+    [alert addTextFieldWithConfigurationHandler:^(UITextField *textField) {
347
+        textField.textColor = [UIColor lightGrayColor];
348
+        textField.placeholder = defaultText;
349
+    }];
350
+    [alert addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
351
+        completionHandler([[alert.textFields lastObject] text]);
352
+    }]];
353
+    [[self topViewController] presentViewController:alert animated:YES completion:NULL];
354
+}
355
+
356
+/**
357
+ * topViewController
358
+ */
359
+-(UIViewController *)topViewController{
360
+   UIViewController *controller = [self topViewControllerWithRootViewController:[self getCurrentWindow].rootViewController];
361
+   return controller;
362
+}
363
+
364
+/**
365
+ * topViewControllerWithRootViewController
366
+ */
367
+-(UIViewController *)topViewControllerWithRootViewController:(UIViewController *)viewController{
368
+  if (viewController==nil) return nil;
369
+  if (viewController.presentedViewController!=nil) {
370
+    return [self topViewControllerWithRootViewController:viewController.presentedViewController];
371
+  } else if ([viewController isKindOfClass:[UITabBarController class]]){
372
+    return [self topViewControllerWithRootViewController:[(UITabBarController *)viewController selectedViewController]];
373
+  } else if ([viewController isKindOfClass:[UINavigationController class]]){
374
+    return [self topViewControllerWithRootViewController:[(UINavigationController *)viewController visibleViewController]];
375
+  } else {
376
+    return viewController;
377
+  }
378
+}
379
+/**
380
+ * getCurrentWindow
381
+ */
382
+-(UIWindow *)getCurrentWindow{
383
+  UIWindow *window = [UIApplication sharedApplication].keyWindow;
384
+  if (window.windowLevel!=UIWindowLevelNormal) {
385
+    for (UIWindow *wid in [UIApplication sharedApplication].windows) {
386
+      if (window.windowLevel==UIWindowLevelNormal) {
387
+        window = wid;
388
+        break;
389
+      }
390
+    }
391
+  }
392
+  return window;
393
+}
394
+
395
+
332 396
 /**
333 397
  * Decides whether to allow or cancel a navigation.
334 398
  * @see https://fburl.com/42r9fxob
@@ -402,6 +466,13 @@ static NSString *const MessageHanderName = @"ReactNative";
402 466
       return;
403 467
     }
404 468
 
469
+    if ([error.domain isEqualToString:@"WebKitErrorDomain"] && error.code == 102) {
470
+      // Error code 102 "Frame load interrupted" is raised by the WKWebView
471
+      // when the URL is from an http redirect. This is a common pattern when
472
+      // implementing OAuth with a WebView.
473
+      return;
474
+    }
475
+
405 476
     NSMutableDictionary<NSString *, id> *event = [self baseEvent];
406 477
     [event addEntriesFromDictionary:@{
407 478
       @"didFailProvisionalNavigation": @YES,
@@ -419,8 +490,12 @@ static NSString *const MessageHanderName = @"ReactNative";
419 490
           thenCall: (void (^)(NSString*)) callback
420 491
 {
421 492
   [self.webView evaluateJavaScript: js completionHandler: ^(id result, NSError *error) {
422
-    if (error == nil && callback != nil) {
423
-      callback([NSString stringWithFormat:@"%@", result]);
493
+    if (error == nil) {
494
+      if (callback != nil) {
495
+        callback([NSString stringWithFormat:@"%@", result]);
496
+      }
497
+    } else {
498
+      RCTLogError(@"Error evaluating injectedJavaScript: This is possibly due to an unsupported return type. Try adding true to the end of your injectedJavaScript string.");
424 499
     }
425 500
   }];
426 501
 }

+ 7
- 0
ios/RNCWKWebViewManager.m View File

@@ -45,7 +45,10 @@ RCT_EXPORT_VIEW_PROPERTY(contentInset, UIEdgeInsets)
45 45
 RCT_EXPORT_VIEW_PROPERTY(automaticallyAdjustContentInsets, BOOL)
46 46
 RCT_EXPORT_VIEW_PROPERTY(hideKeyboardAccessoryView, BOOL)
47 47
 RCT_EXPORT_VIEW_PROPERTY(allowsBackForwardNavigationGestures, BOOL)
48
+RCT_EXPORT_VIEW_PROPERTY(incognito, BOOL)
49
+RCT_EXPORT_VIEW_PROPERTY(pagingEnabled, BOOL)
48 50
 RCT_EXPORT_VIEW_PROPERTY(userAgent, NSString)
51
+RCT_EXPORT_VIEW_PROPERTY(allowsLinkPreview, BOOL)
49 52
 
50 53
 /**
51 54
  * Expose methods to enable messaging the webview.
@@ -69,6 +72,10 @@ RCT_CUSTOM_VIEW_PROPERTY(bounces, BOOL, RNCWKWebView) {
69 72
   view.bounces = json == nil ? true : [RCTConvert BOOL: json];
70 73
 }
71 74
 
75
+RCT_CUSTOM_VIEW_PROPERTY(useSharedProcessPool, BOOL, RNCWKWebView) {
76
+  view.useSharedProcessPool = json == nil ? true : [RCTConvert BOOL: json];
77
+}
78
+
72 79
 RCT_CUSTOM_VIEW_PROPERTY(scrollEnabled, BOOL, RNCWKWebView) {
73 80
   view.scrollEnabled = json == nil ? true : [RCTConvert BOOL: json];
74 81
 }

+ 6
- 0
ios/RNCWebView.xcodeproj/project.pbxproj View File

@@ -7,6 +7,7 @@
7 7
 	objects = {
8 8
 
9 9
 /* Begin PBXBuildFile section */
10
+		3515965E21A3C86000623BFA /* RNCWKProcessPoolManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 3515965D21A3C86000623BFA /* RNCWKProcessPoolManager.m */; };
10 11
 		E914DBF6214474710071092B /* RNCUIWebViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = E914DBF3214474710071092B /* RNCUIWebViewManager.m */; };
11 12
 		E914DBF7214474710071092B /* RNCUIWebView.m in Sources */ = {isa = PBXBuildFile; fileRef = E914DBF4214474710071092B /* RNCUIWebView.m */; };
12 13
 		E91B351D21446E6C00F9801F /* RNCWKWebViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = E91B351B21446E6C00F9801F /* RNCWKWebViewManager.m */; };
@@ -27,6 +28,8 @@
27 28
 
28 29
 /* Begin PBXFileReference section */
29 30
 		134814201AA4EA6300B7C361 /* libRNCWebView.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNCWebView.a; sourceTree = BUILT_PRODUCTS_DIR; };
31
+		3515965D21A3C86000623BFA /* RNCWKProcessPoolManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNCWKProcessPoolManager.m; sourceTree = "<group>"; };
32
+		3515965F21A3C87E00623BFA /* RNCWKProcessPoolManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNCWKProcessPoolManager.h; sourceTree = "<group>"; };
30 33
 		E914DBF2214474710071092B /* RNCUIWebView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNCUIWebView.h; sourceTree = "<group>"; };
31 34
 		E914DBF3214474710071092B /* RNCUIWebViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNCUIWebViewManager.m; sourceTree = "<group>"; };
32 35
 		E914DBF4214474710071092B /* RNCUIWebView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNCUIWebView.m; sourceTree = "<group>"; };
@@ -67,6 +70,8 @@
67 70
 				E91B351C21446E6C00F9801F /* RNCWKWebView.m */,
68 71
 				E91B351921446E6C00F9801F /* RNCWKWebViewManager.h */,
69 72
 				E91B351B21446E6C00F9801F /* RNCWKWebViewManager.m */,
73
+				3515965D21A3C86000623BFA /* RNCWKProcessPoolManager.m */,
74
+				3515965F21A3C87E00623BFA /* RNCWKProcessPoolManager.h */,
70 75
 				134814211AA4EA7D00B7C361 /* Products */,
71 76
 			);
72 77
 			sourceTree = "<group>";
@@ -131,6 +136,7 @@
131 136
 				E914DBF7214474710071092B /* RNCUIWebView.m in Sources */,
132 137
 				E914DBF6214474710071092B /* RNCUIWebViewManager.m in Sources */,
133 138
 				E91B351E21446E6C00F9801F /* RNCWKWebView.m in Sources */,
139
+				3515965E21A3C86000623BFA /* RNCWKProcessPoolManager.m in Sources */,
134 140
 			);
135 141
 			runOnlyForDeploymentPostprocessing = 0;
136 142
 		};

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

@@ -8,34 +8,33 @@
8 8
  * @flow
9 9
  */
10 10
 
11
-'use strict';
12
-
13 11
 import React from 'react';
14 12
 
15
-import ReactNative from 'react-native';
16
-import {
13
+import ReactNative, {
17 14
   ActivityIndicator,
15
+  Image,
16
+  requireNativeComponent,
18 17
   StyleSheet,
19 18
   UIManager,
20 19
   View,
21
-  Image,
22
-  requireNativeComponent
20
+  NativeModules
23 21
 } from 'react-native';
24 22
 
25 23
 import invariant from 'fbjs/lib/invariant';
26 24
 import keyMirror from 'fbjs/lib/keyMirror';
27 25
 
28
-import WebViewShared from './WebViewShared';
26
+import {
27
+  defaultOriginWhitelist,
28
+  createOnShouldStartLoadWithRequest,
29
+} from './WebViewShared';
29 30
 import type {
30
-  WebViewEvent,
31 31
   WebViewError,
32 32
   WebViewErrorEvent,
33 33
   WebViewMessageEvent,
34
-  WebViewNavigation,
35 34
   WebViewNavigationEvent,
35
+  WebViewProgressEvent,
36 36
   WebViewSharedProps,
37 37
   WebViewSource,
38
-  WebViewProgressEvent,
39 38
 } from './WebViewTypes';
40 39
 
41 40
 const resolveAssetSource = Image.resolveAssetSource;
@@ -68,11 +67,18 @@ class WebView extends React.Component<WebViewSharedProps, State> {
68 67
     scalesPageToFit: true,
69 68
     allowFileAccess: false,
70 69
     saveFormDataDisabled: false,
71
-    originWhitelist: WebViewShared.defaultOriginWhitelist,
70
+    originWhitelist: defaultOriginWhitelist,
72 71
   };
73 72
 
73
+  static isFileUploadSupported = async () => {
74
+    // native implementation should return "true" only for Android 5+
75
+    return NativeModules.RNCWebView.isFileUploadSupported();
76
+  }
77
+
74 78
   state = {
75
-    viewState: this.props.startInLoadingState ? WebViewState.LOADING : WebViewState.IDLE,
79
+    viewState: this.props.startInLoadingState
80
+      ? WebViewState.LOADING
81
+      : WebViewState.IDLE,
76 82
     lastErrorEvent: null,
77 83
   };
78 84
 
@@ -125,12 +131,14 @@ class WebView extends React.Component<WebViewSharedProps, State> {
125 131
 
126 132
     const nativeConfig = this.props.nativeConfig || {};
127 133
 
128
-    const originWhitelist = (this.props.originWhitelist || []).map(
129
-      WebViewShared.originWhitelistToRegex,
130
-    );
131
-
132 134
     let NativeWebView = nativeConfig.component || RNCWebView;
133 135
 
136
+    const onShouldStartLoadWithRequest = createOnShouldStartLoadWithRequest(
137
+      this.onShouldStartLoadWithRequestCallback,
138
+      this.props.originWhitelist,
139
+      this.props.onShouldStartLoadWithRequest,
140
+    );
141
+
134 142
     const webView = (
135 143
       <NativeWebView
136 144
         ref={this.webViewRef}
@@ -151,6 +159,7 @@ class WebView extends React.Component<WebViewSharedProps, State> {
151 159
         automaticallyAdjustContentInsets={
152 160
           this.props.automaticallyAdjustContentInsets
153 161
         }
162
+        onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
154 163
         onContentSizeChange={this.props.onContentSizeChange}
155 164
         onLoadingStart={this.onLoadingStart}
156 165
         onLoadingFinish={this.onLoadingFinish}
@@ -164,7 +173,6 @@ class WebView extends React.Component<WebViewSharedProps, State> {
164 173
         allowUniversalAccessFromFileURLs={
165 174
           this.props.allowUniversalAccessFromFileURLs
166 175
         }
167
-        originWhitelist={originWhitelist}
168 176
         mixedContentMode={this.props.mixedContentMode}
169 177
         saveFormDataDisabled={this.props.saveFormDataDisabled}
170 178
         urlPrefixesForDefaultIntent={this.props.urlPrefixesForDefaultIntent}
@@ -284,11 +292,24 @@ class WebView extends React.Component<WebViewSharedProps, State> {
284 292
     const { onMessage } = this.props;
285 293
     onMessage && onMessage(event);
286 294
   };
287
-  
295
+
288 296
   onLoadingProgress = (event: WebViewProgressEvent) => {
289
-    const { onLoadProgress} = this.props;
297
+    const { onLoadProgress } = this.props;
290 298
     onLoadProgress && onLoadProgress(event);
291
-  }
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
+  };
292 313
 }
293 314
 
294 315
 const RNCWebView = requireNativeComponent('RNCWebView');

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

@@ -25,7 +25,10 @@ import {
25 25
 import invariant from 'fbjs/lib/invariant';
26 26
 import keyMirror from 'fbjs/lib/keyMirror';
27 27
 
28
-import WebViewShared from './WebViewShared';
28
+import {
29
+  defaultOriginWhitelist,
30
+  createOnShouldStartLoadWithRequest,
31
+} from './WebViewShared';
29 32
 import type {
30 33
   WebViewEvent,
31 34
   WebViewError,
@@ -130,9 +133,15 @@ class WebView extends React.Component<WebViewSharedProps, State> {
130 133
 
131 134
   static defaultProps = {
132 135
     useWebKit: true,
133
-    originWhitelist: WebViewShared.defaultOriginWhitelist,
136
+    originWhitelist: defaultOriginWhitelist,
137
+    useSharedProcessPool: true,
134 138
   };
135 139
 
140
+  static isFileUploadSupported = async () => {
141
+    // no native implementation for iOS, depends only on permissions
142
+    return true;
143
+  }
144
+
136 145
   state = {
137 146
     viewState: this.props.startInLoadingState
138 147
       ? WebViewState.LOADING
@@ -159,6 +168,15 @@ class WebView extends React.Component<WebViewSharedProps, State> {
159 168
         'The allowsBackForwardNavigationGestures property is not supported when useWebKit = false',
160 169
       );
161 170
     }
171
+
172
+    if (
173
+      !this.props.useWebKit &&
174
+      this.props.incognito
175
+    ) {
176
+      console.warn(
177
+        'The incognito property is not supported when useWebKit = false',
178
+      );
179
+    }
162 180
   }
163 181
 
164 182
   render() {
@@ -199,40 +217,11 @@ class WebView extends React.Component<WebViewSharedProps, State> {
199 217
 
200 218
     const nativeConfig = this.props.nativeConfig || {};
201 219
 
202
-    let viewManager = nativeConfig.viewManager;
203
-
204
-    if (this.props.useWebKit) {
205
-      viewManager = viewManager || RNCWKWebViewManager;
206
-    } else {
207
-      viewManager = viewManager || RNCUIWebViewManager;
208
-    }
209
-
210
-    const compiledWhitelist = [
211
-      'about:blank',
212
-      ...(this.props.originWhitelist || []),
213
-    ].map(WebViewShared.originWhitelistToRegex);
214
-    const onShouldStartLoadWithRequest = event => {
215
-      let shouldStart = true;
216
-      const { url } = event.nativeEvent;
217
-      const origin = WebViewShared.extractOrigin(url);
218
-      const passesWhitelist = compiledWhitelist.some(x =>
219
-        new RegExp(x).test(origin),
220
-      );
221
-      shouldStart = shouldStart && passesWhitelist;
222
-      if (!passesWhitelist) {
223
-        Linking.openURL(url);
224
-      }
225
-      if (this.props.onShouldStartLoadWithRequest) {
226
-        shouldStart =
227
-          shouldStart &&
228
-          this.props.onShouldStartLoadWithRequest(event.nativeEvent);
229
-      }
230
-      invariant(viewManager != null, 'viewManager expected to be non-null');
231
-      viewManager.startLoadWithResult(
232
-        !!shouldStart,
233
-        event.nativeEvent.lockIdentifier,
234
-      );
235
-    };
220
+    const onShouldStartLoadWithRequest = createOnShouldStartLoadWithRequest(
221
+      this.onShouldStartLoadWithRequestCallback,
222
+      this.props.originWhitelist,
223
+      this.props.onShouldStartLoadWithRequest,
224
+    );
236 225
 
237 226
     const decelerationRate = processDecelerationRate(
238 227
       this.props.decelerationRate,
@@ -264,6 +253,7 @@ class WebView extends React.Component<WebViewSharedProps, State> {
264 253
         injectedJavaScript={this.props.injectedJavaScript}
265 254
         bounces={this.props.bounces}
266 255
         scrollEnabled={this.props.scrollEnabled}
256
+        pagingEnabled={this.props.pagingEnabled}
267 257
         decelerationRate={decelerationRate}
268 258
         contentInset={this.props.contentInset}
269 259
         automaticallyAdjustContentInsets={
@@ -271,6 +261,7 @@ class WebView extends React.Component<WebViewSharedProps, State> {
271 261
         }
272 262
         hideKeyboardAccessoryView={this.props.hideKeyboardAccessoryView}
273 263
         allowsBackForwardNavigationGestures={this.props.allowsBackForwardNavigationGestures}
264
+        incognito={this.props.incognito}
274 265
         userAgent={this.props.userAgent}
275 266
         onLoadingStart={this._onLoadingStart}
276 267
         onLoadingFinish={this._onLoadingFinish}
@@ -285,6 +276,8 @@ class WebView extends React.Component<WebViewSharedProps, State> {
285 276
           this.props.mediaPlaybackRequiresUserAction
286 277
         }
287 278
         dataDetectorTypes={this.props.dataDetectorTypes}
279
+        useSharedProcessPool={this.props.useSharedProcessPool}
280
+        allowsLinkPreview={this.props.allowsLinkPreview}
288 281
         {...nativeConfig.props}
289 282
       />
290 283
     );
@@ -434,9 +427,25 @@ class WebView extends React.Component<WebViewSharedProps, State> {
434 427
   };
435 428
 
436 429
   _onLoadingProgress = (event: WebViewProgressEvent) => {
437
-    const {onLoadProgress} = this.props;
430
+    const { onLoadProgress } = this.props;
438 431
     onLoadProgress && onLoadProgress(event);
439
-  }
432
+  };
433
+
434
+  onShouldStartLoadWithRequestCallback = (
435
+    shouldStart: boolean,
436
+    url: string,
437
+    lockIdentifier: number,
438
+  ) => {
439
+    let viewManager = (this.props.nativeConfig || {}).viewManager;
440
+
441
+    if (this.props.useWebKit) {
442
+      viewManager = viewManager || RNCWKWebViewManager;
443
+    } else {
444
+      viewManager = viewManager || RNCUIWebViewManager;
445
+    }
446
+    invariant(viewManager != null, 'viewManager expected to be non-null');
447
+    viewManager.startLoadWithResult(!!shouldStart, lockIdentifier);
448
+  };
440 449
 
441 450
   componentDidUpdate(prevProps: WebViewSharedProps) {
442 451
     if (!(prevProps.useWebKit && this.props.useWebKit)) {
@@ -444,6 +453,7 @@ class WebView extends React.Component<WebViewSharedProps, State> {
444 453
     }
445 454
 
446 455
     this._showRedboxOnPropChanges(prevProps, 'allowsInlineMediaPlayback');
456
+    this._showRedboxOnPropChanges(prevProps, 'incognito');
447 457
     this._showRedboxOnPropChanges(prevProps, 'mediaPlaybackRequiresUserAction');
448 458
     this._showRedboxOnPropChanges(prevProps, 'dataDetectorTypes');
449 459
 

+ 53
- 14
js/WebViewShared.js View File

@@ -8,19 +8,58 @@
8 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 };

+ 61
- 25
js/WebViewTypes.js View File

@@ -10,12 +10,12 @@
10 10
 
11 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 20
 export type WebViewNativeEvent = $ReadOnly<{|
21 21
   url: string,
@@ -23,12 +23,13 @@ export type WebViewNativeEvent = $ReadOnly<{|
23 23
   title: string,
24 24
   canGoBack: boolean,
25 25
   canGoForward: boolean,
26
+  lockIdentifier: number,
26 27
 |}>;
27 28
 
28 29
 export type WebViewProgressEvent = $ReadOnly<{|
29
-    ...WebViewNativeEvent,
30
-    progress: number,
31
-|}>
30
+  ...WebViewNativeEvent,
31
+  progress: number,
32
+|}>;
32 33
 
33 34
 export type WebViewNavigation = $ReadOnly<{|
34 35
   ...WebViewNativeEvent,
@@ -118,22 +119,26 @@ export type WebViewSourceHtml = $ReadOnly<{|
118 119
 export type WebViewSource = WebViewSourceUri | WebViewSourceHtml;
119 120
 
120 121
 export type WebViewNativeConfig = $ReadOnly<{|
121
-  /*
122
+  /**
122 123
    * The native component used to render the WebView.
123 124
    */
124 125
   component?: ComponentType<WebViewSharedProps>,
125
-  /*
126
+  /**
126 127
    * Set props directly on the native component WebView. Enables custom props which the
127 128
    * original WebView doesn't pass through.
128 129
    */
129 130
   props?: ?Object,
130
-  /*
131
+  /**
131 132
    * Set the ViewManager to use for communication with the native side.
132 133
    * @platform ios
133 134
    */
134 135
   viewManager?: ?Object,
135 136
 |}>;
136 137
 
138
+export type OnShouldStartLoadWithRequest = (
139
+  event: WebViewNavigation,
140
+) => boolean;
141
+
137 142
 export type IOSWebViewProps = $ReadOnly<{|
138 143
   /**
139 144
    * If true, use WKWebView instead of UIWebView.
@@ -168,6 +173,14 @@ export type IOSWebViewProps = $ReadOnly<{|
168 173
    */
169 174
   scrollEnabled?: ?boolean,
170 175
 
176
+  /**
177
+   * If the value of this property is true, the scroll view stops on multiples
178
+   * of the scroll view’s bounds when the user scrolls.
179
+   * The default value is false.
180
+   * @platform ios
181
+   */
182
+  pagingEnabled?: ?boolean,
183
+
171 184
   /**
172 185
    * The amount by which the web view content is inset from the edges of
173 186
    * the scroll view. Defaults to {top: 0, left: 0, bottom: 0, right: 0}.
@@ -197,17 +210,7 @@ export type IOSWebViewProps = $ReadOnly<{|
197 210
    *
198 211
    * @platform ios
199 212
    */
200
-  dataDetectorTypes?:
201
-    | ?DataDetectorTypes
202
-    | $ReadOnlyArray<DataDetectorTypes>,
203
-
204
-  /**
205
-   * Function that allows custom handling of any web view requests. Return
206
-   * `true` from the function to continue loading the request and `false`
207
-   * to stop loading.
208
-   * @platform ios
209
-   */
210
-  onShouldStartLoadWithRequest?: (event: WebViewEvent) => mixed,
213
+  dataDetectorTypes?: ?DataDetectorTypes | $ReadOnlyArray<DataDetectorTypes>,
211 214
 
212 215
   /**
213 216
    * Boolean that determines whether HTML5 videos play inline or use the
@@ -229,10 +232,27 @@ export type IOSWebViewProps = $ReadOnly<{|
229 232
    * back-forward list navigations.
230 233
    */
231 234
   allowsBackForwardNavigationGestures?: ?boolean,
235
+  /**
236
+   * A Boolean value indicating whether WebKit WebView should be created using a shared
237
+   * process pool, enabling WebViews to share cookies and localStorage between each other.
238
+   * Default is true but can be set to false for backwards compatibility.
239
+   * @platform ios
240
+   */
241
+  useSharedProcessPool?: ?boolean,
232 242
   /**
233 243
    * The custom user agent string.
234 244
    */
235 245
   userAgent?: ?string,
246
+
247
+  /**
248
+   * A Boolean value that determines whether pressing on a link
249
+   * displays a preview of the destination for the link.
250
+   *
251
+   * This property is available on devices that support 3D Touch.
252
+   * In iOS 10 and later, the default value is `true`; before that, the default value is `false`.
253
+   * @platform ios
254
+   */
255
+  allowsLinkPreview?: ?boolean,
236 256
 |}>;
237 257
 
238 258
 export type AndroidWebViewProps = $ReadOnly<{|
@@ -277,7 +297,7 @@ export type AndroidWebViewProps = $ReadOnly<{|
277 297
    */
278 298
   saveFormDataDisabled?: ?boolean,
279 299
 
280
-  /*
300
+  /**
281 301
    * Used on Android only, controls whether the given list of URL prefixes should
282 302
    * make {@link com.facebook.react.views.webview.ReactWebViewClient} to launch a
283 303
    * default activity intent for those URL instead of loading it within the webview.
@@ -327,7 +347,7 @@ export type AndroidWebViewProps = $ReadOnly<{|
327 347
   mixedContentMode?: ?('never' | 'always' | 'compatibility'),
328 348
 |}>;
329 349
 
330
-export type WebViewSharedProps =  $ReadOnly<{|
350
+export type WebViewSharedProps = $ReadOnly<{|
331 351
   ...ViewProps,
332 352
   ...IOSWebViewProps,
333 353
   ...AndroidWebViewProps,
@@ -345,10 +365,19 @@ export type WebViewSharedProps =  $ReadOnly<{|
345 365
    */
346 366
   source?: ?WebViewSource,
347 367
 
368
+  /**
369
+   * Does not store any data within the lifetime of the WebView.
370
+   */
371
+  incognito?: ?boolean,
372
+
348 373
   /**
349 374
    * Function that returns a view to show if there's an error.
350 375
    */
351
-  renderError: (errorDomain: ?string, errorCode: number, errorDesc: string) => Element<any>, // view to show if there's an error
376
+  renderError: (
377
+    errorDomain: ?string,
378
+    errorCode: number,
379
+    errorDesc: string,
380
+  ) => Element<any>, // view to show if there's an error
352 381
 
353 382
   /**
354 383
    * Function that returns a loading indicator.
@@ -439,6 +468,13 @@ export type WebViewSharedProps =  $ReadOnly<{|
439 468
    */
440 469
   originWhitelist?: $ReadOnlyArray<string>,
441 470
 
471
+  /**
472
+   * Function that allows custom handling of any web view requests. Return
473
+   * `true` from the function to continue loading the request and `false`
474
+   * to stop loading. The `navigationType` is always `other` on android.
475
+   */
476
+  onShouldStartLoadWithRequest?: OnShouldStartLoadWithRequest,
477
+
442 478
   /**
443 479
    * Override the native component used to render the WebView. Enables a custom native
444 480
    * WebView which uses the same JavaScript as the original WebView.

+ 1
- 1
package.json View File

@@ -8,7 +8,7 @@
8 8
     "Thibault Malbranche <malbranche.thibault@gmail.com>"
9 9
   ],
10 10
   "license": "MIT",
11
-  "version": "2.8.0",
11
+  "version": "3.1.1",
12 12
   "homepage": "https://github.com/react-native-community/react-native-webview#readme",
13 13
   "scripts": {
14 14
     "test:ios:flow": "flow check",

+ 32
- 2
typings/index.d.ts View File

@@ -9,6 +9,12 @@ export interface WebViewNativeEvent {
9 9
   readonly canGoForward: boolean;
10 10
 }
11 11
 
12
+export interface WebViewIOSLoadRequestEvent extends WebViewNativeEvent {
13
+  target: number;
14
+  lockIdentifier: number;
15
+  navigationType: "click" | "formsubmit" | "backforward" | "reload" | "formresubmit" | "other";
16
+}
17
+
12 18
 export interface WebViewProgressEvent extends WebViewNativeEvent {
13 19
   readonly progress: number;
14 20
 }
@@ -145,6 +151,14 @@ export interface IOSWebViewProps {
145 151
    */
146 152
   scrollEnabled?: boolean;
147 153
 
154
+  /**
155
+   * If the value of this property is true, the scroll view stops on multiples
156
+   * of the scroll view’s bounds when the user scrolls.
157
+   * The default value is false.
158
+   * @platform ios
159
+   */
160
+  pagingEnabled?: boolean,
161
+
148 162
   /**
149 163
    * The amount by which the web view content is inset from the edges of
150 164
    * the scroll view. Defaults to {top: 0, left: 0, bottom: 0, right: 0}.
@@ -182,7 +196,7 @@ export interface IOSWebViewProps {
182 196
    * to stop loading.
183 197
    * @platform ios
184 198
    */
185
-  onShouldStartLoadWithRequest?: (event: WebViewNativeEvent) => any;
199
+  onShouldStartLoadWithRequest?: (event: WebViewIOSLoadRequestEvent) => any;
186 200
 
187 201
   /**
188 202
    * Boolean that determines whether HTML5 videos play inline or use the
@@ -199,6 +213,20 @@ export interface IOSWebViewProps {
199 213
    * backward compatible.
200 214
    */
201 215
   hideKeyboardAccessoryView?: boolean;
216
+  /**
217
+   * If true, this will be able horizontal swipe gestures when using the WKWebView. The default value is `false`.
218
+   */
219
+  allowsBackForwardNavigationGestures?: boolean;
220
+
221
+  /**
222
+   * A Boolean value that determines whether pressing on a link
223
+   * displays a preview of the destination for the link.
224
+   *
225
+   * This property is available on devices that support 3D Touch.
226
+   * In iOS 10 and later, the default value is `true`; before that, the default value is `false`.
227
+   * @platform ios
228
+   */
229
+  allowsLinkPreview?: boolean;
202 230
 }
203 231
 
204 232
 export interface AndroidWebViewProps {
@@ -370,7 +398,7 @@ export interface WebViewSharedProps extends ViewProps, IOSWebViewProps, AndroidW
370 398
    * Boolean value that forces the `WebView` to show the loading view
371 399
    * on the first load.
372 400
    */
373
-  startInLoadingState?: string;
401
+  startInLoadingState?: boolean;
374 402
 
375 403
   /**
376 404
    * Set this to provide JavaScript that will be injected into the web page
@@ -417,4 +445,6 @@ export class WebView extends Component<WebViewSharedProps> {
417 445
   public goBack: () => void;
418 446
   public reload: () => void;
419 447
   public stopLoading: () => void;
448
+  public postMessage: (msg: string) => void;
449
+  public injectJavaScript: (js: string) => void;
420 450
 }