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
 
2
 
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)).
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
 ## Platforms Supported
9
 ## Platforms Supported
6
 
10
 
7
 - [x] iOS (both UIWebView and WKWebView)
11
 - [x] iOS (both UIWebView and WKWebView)

+ 97
- 66
android/build.gradle View File

1
 buildscript {
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
 apply plugin: 'com.android.library'
14
 apply plugin: 'com.android.library'
17
 apply plugin: 'kotlin-android'
15
 apply plugin: 'kotlin-android'
18
 
16
 
19
-
17
+def DEFAULT_TARGET_SDK_VERSION = 27
20
 def DEFAULT_COMPILE_SDK_VERSION = 27
18
 def DEFAULT_COMPILE_SDK_VERSION = 27
21
 def DEFAULT_BUILD_TOOLS_VERSION = "28.0.3"
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
 android {
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
 repositories {
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
     maven {
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
     def parentDir = rootProject.projectDir
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
 dependencies {
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
 <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.reactnativecommunity.webview">
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

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
 package com.reactnativecommunity.webview;
1
 package com.reactnativecommunity.webview;
2
 
2
 
3
 import android.annotation.TargetApi;
3
 import android.annotation.TargetApi;
4
+import android.app.DownloadManager;
4
 import android.content.Context;
5
 import android.content.Context;
5
 import com.facebook.react.uimanager.UIManagerModule;
6
 import com.facebook.react.uimanager.UIManagerModule;
7
+
8
+import java.net.MalformedURLException;
9
+import java.net.URL;
10
+import java.net.URLDecoder;
6
 import java.util.LinkedList;
11
 import java.util.LinkedList;
7
 import java.util.List;
12
 import java.util.List;
8
 import java.util.regex.Pattern;
13
 import java.util.regex.Pattern;
18
 import android.content.ActivityNotFoundException;
23
 import android.content.ActivityNotFoundException;
19
 import android.content.Intent;
24
 import android.content.Intent;
20
 import android.graphics.Bitmap;
25
 import android.graphics.Bitmap;
21
-import android.graphics.Picture;
22
 import android.net.Uri;
26
 import android.net.Uri;
23
 import android.os.Build;
27
 import android.os.Build;
28
+import android.os.Environment;
24
 import android.text.TextUtils;
29
 import android.text.TextUtils;
25
 import android.view.View;
30
 import android.view.View;
26
 import android.view.ViewGroup.LayoutParams;
31
 import android.view.ViewGroup.LayoutParams;
27
 import android.webkit.ConsoleMessage;
32
 import android.webkit.ConsoleMessage;
28
 import android.webkit.CookieManager;
33
 import android.webkit.CookieManager;
34
+import android.webkit.DownloadListener;
29
 import android.webkit.GeolocationPermissions;
35
 import android.webkit.GeolocationPermissions;
30
 import android.webkit.JavascriptInterface;
36
 import android.webkit.JavascriptInterface;
37
+import android.webkit.URLUtil;
31
 import android.webkit.ValueCallback;
38
 import android.webkit.ValueCallback;
32
 import android.webkit.WebChromeClient;
39
 import android.webkit.WebChromeClient;
40
+import android.webkit.WebResourceRequest;
33
 import android.webkit.WebSettings;
41
 import android.webkit.WebSettings;
34
 import android.webkit.WebView;
42
 import android.webkit.WebView;
35
 import android.webkit.WebViewClient;
43
 import android.webkit.WebViewClient;
44
+
36
 import com.facebook.common.logging.FLog;
45
 import com.facebook.common.logging.FLog;
37
 import com.facebook.react.bridge.Arguments;
46
 import com.facebook.react.bridge.Arguments;
38
 import com.facebook.react.bridge.LifecycleEventListener;
47
 import com.facebook.react.bridge.LifecycleEventListener;
51
 import com.facebook.react.uimanager.events.ContentSizeChangeEvent;
60
 import com.facebook.react.uimanager.events.ContentSizeChangeEvent;
52
 import com.facebook.react.uimanager.events.Event;
61
 import com.facebook.react.uimanager.events.Event;
53
 import com.facebook.react.uimanager.events.EventDispatcher;
62
 import com.facebook.react.uimanager.events.EventDispatcher;
63
+import com.facebook.react.uimanager.events.RCTEventEmitter;
54
 import com.reactnativecommunity.webview.events.TopLoadingErrorEvent;
64
 import com.reactnativecommunity.webview.events.TopLoadingErrorEvent;
55
 import com.reactnativecommunity.webview.events.TopLoadingFinishEvent;
65
 import com.reactnativecommunity.webview.events.TopLoadingFinishEvent;
56
 import com.reactnativecommunity.webview.events.TopLoadingStartEvent;
66
 import com.reactnativecommunity.webview.events.TopLoadingStartEvent;
57
 import com.reactnativecommunity.webview.events.TopMessageEvent;
67
 import com.reactnativecommunity.webview.events.TopMessageEvent;
58
 import com.reactnativecommunity.webview.events.TopLoadingProgressEvent;
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
 import org.json.JSONException;
76
 import org.json.JSONException;
60
 import org.json.JSONObject;
77
 import org.json.JSONObject;
61
 
78
 
66
  *  - GO_BACK
83
  *  - GO_BACK
67
  *  - GO_FORWARD
84
  *  - GO_FORWARD
68
  *  - RELOAD
85
  *  - RELOAD
86
+ *  - LOAD_URL
69
  *
87
  *
70
  * {@link WebView} instances could emit following direct events:
88
  * {@link WebView} instances could emit following direct events:
71
  *  - topLoadingFinish
89
  *  - topLoadingFinish
72
  *  - topLoadingStart
90
  *  - topLoadingStart
73
  *  - topLoadingStart
91
  *  - topLoadingStart
74
  *  - topLoadingProgress
92
  *  - topLoadingProgress
93
+ *  - topShouldStartLoadWithRequest
75
  *
94
  *
76
  * Each event will carry the following properties:
95
  * Each event will carry the following properties:
77
  *  - target - view's react tag
96
  *  - target - view's react tag
85
 public class RNCWebViewManager extends SimpleViewManager<WebView> {
104
 public class RNCWebViewManager extends SimpleViewManager<WebView> {
86
 
105
 
87
   protected static final String REACT_CLASS = "RNCWebView";
106
   protected static final String REACT_CLASS = "RNCWebView";
107
+  private RNCWebViewPackage aPackage;
88
 
108
 
89
   protected static final String HTML_ENCODING = "UTF-8";
109
   protected static final String HTML_ENCODING = "UTF-8";
90
   protected static final String HTML_MIME_TYPE = "text/html";
110
   protected static final String HTML_MIME_TYPE = "text/html";
98
   public static final int COMMAND_STOP_LOADING = 4;
118
   public static final int COMMAND_STOP_LOADING = 4;
99
   public static final int COMMAND_POST_MESSAGE = 5;
119
   public static final int COMMAND_POST_MESSAGE = 5;
100
   public static final int COMMAND_INJECT_JAVASCRIPT = 6;
120
   public static final int COMMAND_INJECT_JAVASCRIPT = 6;
121
+  public static final int COMMAND_LOAD_URL = 7;
101
 
122
 
102
   // Use `webView.loadUrl("about:blank")` to reliably reset the view
123
   // Use `webView.loadUrl("about:blank")` to reliably reset the view
103
   // state and release page resources (including any running JavaScript).
124
   // state and release page resources (including any running JavaScript).
104
   protected static final String BLANK_URL = "about:blank";
125
   protected static final String BLANK_URL = "about:blank";
105
 
126
 
106
   protected WebViewConfig mWebViewConfig;
127
   protected WebViewConfig mWebViewConfig;
107
-  protected @Nullable WebView.PictureListener mPictureListener;
108
 
128
 
109
   protected static class RNCWebViewClient extends WebViewClient {
129
   protected static class RNCWebViewClient extends WebViewClient {
110
 
130
 
111
     protected boolean mLastLoadFailed = false;
131
     protected boolean mLastLoadFailed = false;
112
     protected @Nullable ReadableArray mUrlPrefixesForDefaultIntent;
132
     protected @Nullable ReadableArray mUrlPrefixesForDefaultIntent;
113
-    protected @Nullable List<Pattern> mOriginWhitelist;
114
 
133
 
115
     @Override
134
     @Override
116
     public void onPageFinished(WebView webView, String url) {
135
     public void onPageFinished(WebView webView, String url) {
138
 
157
 
139
     @Override
158
     @Override
140
     public boolean shouldOverrideUrlLoading(WebView view, String url) {
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
       return true;
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
     @Override
172
     @Override
230
     public void setUrlPrefixesForDefaultIntent(ReadableArray specialUrls) {
215
     public void setUrlPrefixesForDefaultIntent(ReadableArray specialUrls) {
231
       mUrlPrefixesForDefaultIntent = specialUrls;
216
       mUrlPrefixesForDefaultIntent = specialUrls;
232
     }
217
     }
233
-
234
-    public void setOriginWhitelist(List<Pattern> originWhitelist) {
235
-      mOriginWhitelist = originWhitelist;
236
-    }
237
   }
218
   }
238
 
219
 
239
   /**
220
   /**
244
     protected @Nullable String injectedJS;
225
     protected @Nullable String injectedJS;
245
     protected boolean messagingEnabled = false;
226
     protected boolean messagingEnabled = false;
246
     protected @Nullable RNCWebViewClient mRNCWebViewClient;
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
     protected class RNCWebViewBridge {
234
     protected class RNCWebViewBridge {
249
       RNCWebView mContext;
235
       RNCWebView mContext;
284
       cleanupCallbacksAndDestroy();
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
     @Override
289
     @Override
288
     public void setWebViewClient(WebViewClient client) {
290
     public void setWebViewClient(WebViewClient client) {
289
       super.setWebViewClient(client);
291
       super.setWebViewClient(client);
427
       public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
429
       public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
428
         callback.invoke(origin, true, false);
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
     reactContext.addLifecycleEventListener(webView);
452
     reactContext.addLifecycleEventListener(webView);
432
     mWebViewConfig.configWebView(webView);
453
     mWebViewConfig.configWebView(webView);
453
       WebView.setWebContentsDebuggingEnabled(true);
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
     return webView;
524
     return webView;
457
   }
525
   }
458
 
526
 
592
 
660
 
593
   @ReactProp(name = "onContentSizeChange")
661
   @ReactProp(name = "onContentSizeChange")
594
   public void setOnContentSizeChange(WebView view, boolean sendContentSizeChangeEvents) {
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
   @ReactProp(name = "mixedContentMode")
666
   @ReactProp(name = "mixedContentMode")
636
     view.getSettings().setGeolocationEnabled(isGeolocationEnabled != null && isGeolocationEnabled);
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
   @Override
703
   @Override
654
   protected void addEventEmitters(ThemedReactContext reactContext, WebView view) {
704
   protected void addEventEmitters(ThemedReactContext reactContext, WebView view) {
655
     // Do not register default touch emitter and let WebView implementation handle touches
705
     // Do not register default touch emitter and let WebView implementation handle touches
658
 
708
 
659
   @Override
709
   @Override
660
   public Map getExportedCustomDirectEventTypeConstants() {
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
   @Override
720
   @Override
671
         "reload", COMMAND_RELOAD,
725
         "reload", COMMAND_RELOAD,
672
         "stopLoading", COMMAND_STOP_LOADING,
726
         "stopLoading", COMMAND_STOP_LOADING,
673
         "postMessage", COMMAND_POST_MESSAGE,
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
         RNCWebView reactWebView = (RNCWebView) root;
769
         RNCWebView reactWebView = (RNCWebView) root;
715
         reactWebView.evaluateJavascriptWithFallback(args.getString(0));
770
         reactWebView.evaluateJavascriptWithFallback(args.getString(0));
716
         break;
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
     ((RNCWebView) webView).cleanupCallbacksAndDestroy();
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
   protected static void dispatchEvent(WebView webView, Event event) {
788
   protected static void dispatchEvent(WebView webView, Event event) {
745
     ReactContext reactContext = (ReactContext) webView.getContext();
789
     ReactContext reactContext = (ReactContext) webView.getContext();
746
     EventDispatcher eventDispatcher =
790
     EventDispatcher eventDispatcher =
747
       reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher();
791
       reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher();
748
     eventDispatcher.dispatchEvent(event);
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
 
1
 
2
 package com.reactnativecommunity.webview;
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
 import com.facebook.react.bridge.ReactApplicationContext;
25
 import com.facebook.react.bridge.ReactApplicationContext;
5
 import com.facebook.react.bridge.ReactContextBaseJavaModule;
26
 import com.facebook.react.bridge.ReactContextBaseJavaModule;
6
 import com.facebook.react.bridge.ReactMethod;
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
   private final ReactApplicationContext reactContext;
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
   public RNCWebViewModule(ReactApplicationContext reactContext) {
54
   public RNCWebViewModule(ReactApplicationContext reactContext) {
14
     super(reactContext);
55
     super(reactContext);
15
     this.reactContext = reactContext;
56
     this.reactContext = reactContext;
57
+    reactContext.addActivityEventListener(this);
16
   }
58
   }
17
 
59
 
18
   @Override
60
   @Override
19
   public String getName() {
61
   public String getName() {
20
     return "RNCWebView";
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
 
1
 
2
 package com.reactnativecommunity.webview;
2
 package com.reactnativecommunity.webview;
3
 
3
 
4
+import java.util.ArrayList;
4
 import java.util.Arrays;
5
 import java.util.Arrays;
5
 import java.util.Collections;
6
 import java.util.Collections;
6
 import java.util.List;
7
 import java.util.List;
10
 import com.facebook.react.bridge.ReactApplicationContext;
11
 import com.facebook.react.bridge.ReactApplicationContext;
11
 import com.facebook.react.uimanager.ViewManager;
12
 import com.facebook.react.uimanager.ViewManager;
12
 import com.facebook.react.bridge.JavaScriptModule;
13
 import com.facebook.react.bridge.JavaScriptModule;
14
+
13
 public class RNCWebViewPackage implements ReactPackage {
15
 public class RNCWebViewPackage implements ReactPackage {
16
+
17
+    private RNCWebViewManager manager;
18
+    private RNCWebViewModule module;
19
+
14
     @Override
20
     @Override
15
     public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
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
     // Deprecated from RN 0.47
29
     // Deprecated from RN 0.47
23
 
33
 
24
     @Override
34
     @Override
25
     public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
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

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

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
 }
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
 - [`html`](Reference.md#html)
44
 - [`html`](Reference.md#html)
45
 - [`hideKeyboardAccessoryView`](Reference.md#hidekeyboardaccessoryview)
45
 - [`hideKeyboardAccessoryView`](Reference.md#hidekeyboardaccessoryview)
46
 - [`allowsBackForwardNavigationGestures`](Reference.md#allowsbackforwardnavigationgestures)
46
 - [`allowsBackForwardNavigationGestures`](Reference.md#allowsbackforwardnavigationgestures)
47
+- [`incognito`](Reference.md#incognito)
47
 - [`allowFileAccess`](Reference.md#allowFileAccess)
48
 - [`allowFileAccess`](Reference.md#allowFileAccess)
48
 - [`saveFormDataDisabled`](Reference.md#saveFormDataDisabled)
49
 - [`saveFormDataDisabled`](Reference.md#saveFormDataDisabled)
50
+- [`pagingEnabled`](Reference.md#pagingEnabled)
51
+- [`allowsLinkPreview`](Reference.md#allowsLinkPreview)
49
 
52
 
50
 ## Methods Index
53
 ## Methods Index
51
 
54
 
100
 
103
 
101
 ### `injectedJavaScript`
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
 | Type   | Required |
108
 | Type   | Required |
106
 | ------ | -------- |
109
 | ------ | -------- |
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
 ### `decelerationRate`
289
 ### `decelerationRate`
277
 
290
 
278
 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:
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
 
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
 ### `allowFileAccess`
525
 ### `allowFileAccess`
503
 
526
 
504
 If true, this will allow access to the file system via `file://` URI's. The default value is `false`.
527
 If true, this will allow access to the file system via `file://` URI's. The default value is `false`.
517
 | ------- | -------- | -------- |
540
 | ------- | -------- | -------- |
518
 | boolean | No       | Android  |
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
 ## Methods
563
 ## Methods
521
 
564
 
522
 ### `extraNativeComponentConfig()`
565
 ### `extraNativeComponentConfig()`

+ 15
- 0
ios/RNCWKProcessPoolManager.h View File

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

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

+ 115
- 40
ios/RNCWKWebView.m View File

8
 #import "RNCWKWebView.h"
8
 #import "RNCWKWebView.h"
9
 #import <React/RCTConvert.h>
9
 #import <React/RCTConvert.h>
10
 #import <React/RCTAutoInsetsProtocol.h>
10
 #import <React/RCTAutoInsetsProtocol.h>
11
+#import "RNCWKProcessPoolManager.h"
12
+#import <UIKit/UIKit.h>
11
 
13
 
12
 #import "objc/runtime.h"
14
 #import "objc/runtime.h"
13
 
15
 
14
-static NSTimer *keyboardTimer;
15
 static NSString *const MessageHanderName = @"ReactNative";
16
 static NSString *const MessageHanderName = @"ReactNative";
16
 
17
 
17
 // runtime trick to remove WKWebView keyboard default toolbar
18
 // runtime trick to remove WKWebView keyboard default toolbar
40
   BOOL _savedHideKeyboardAccessoryView;
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
  * See https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/DisplayWebContent/Tasks/WebKitAvail.html.
47
  * See https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/DisplayWebContent/Tasks/WebKitAvail.html.
65
   return _webkitAvailable;
61
   return _webkitAvailable;
66
 }
62
 }
67
 
63
 
68
-
69
 - (instancetype)initWithFrame:(CGRect)frame
64
 - (instancetype)initWithFrame:(CGRect)frame
70
 {
65
 {
71
   if ((self = [super initWithFrame:frame])) {
66
   if ((self = [super initWithFrame:frame])) {
75
     _automaticallyAdjustContentInsets = YES;
70
     _automaticallyAdjustContentInsets = YES;
76
     _contentInset = UIEdgeInsetsZero;
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
   return self;
73
   return self;
92
 }
74
 }
93
 
75
 
99
     };
81
     };
100
 
82
 
101
     WKWebViewConfiguration *wkWebViewConfig = [WKWebViewConfiguration new];
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
     wkWebViewConfig.userContentController = [WKUserContentController new];
90
     wkWebViewConfig.userContentController = [WKUserContentController new];
103
     [wkWebViewConfig.userContentController addScriptMessageHandler: self name: MessageHanderName];
91
     [wkWebViewConfig.userContentController addScriptMessageHandler: self name: MessageHanderName];
104
     wkWebViewConfig.allowsInlineMediaPlayback = _allowsInlineMediaPlayback;
92
     wkWebViewConfig.allowsInlineMediaPlayback = _allowsInlineMediaPlayback;
116
     _webView.UIDelegate = self;
104
     _webView.UIDelegate = self;
117
     _webView.navigationDelegate = self;
105
     _webView.navigationDelegate = self;
118
     _webView.scrollView.scrollEnabled = _scrollEnabled;
106
     _webView.scrollView.scrollEnabled = _scrollEnabled;
107
+    _webView.scrollView.pagingEnabled = _pagingEnabled;
119
     _webView.scrollView.bounces = _bounces;
108
     _webView.scrollView.bounces = _bounces;
109
+    _webView.allowsLinkPreview = _allowsLinkPreview;
120
     [_webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
110
     [_webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
121
     _webView.allowsBackForwardNavigationGestures = _allowsBackForwardNavigationGestures;
111
     _webView.allowsBackForwardNavigationGestures = _allowsBackForwardNavigationGestures;
112
+
122
     if (_userAgent) {
113
     if (_userAgent) {
123
       _webView.customUserAgent = _userAgent;
114
       _webView.customUserAgent = _userAgent;
124
     }
115
     }
131
     [self addSubview:_webView];
122
     [self addSubview:_webView];
132
     [self setHideKeyboardAccessoryView: _savedHideKeyboardAccessoryView];
123
     [self setHideKeyboardAccessoryView: _savedHideKeyboardAccessoryView];
133
     [self visitSource];
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
 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
140
 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
329
 
311
 
330
 #pragma mark - WKNavigationDelegate methods
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
  * Decides whether to allow or cancel a navigation.
397
  * Decides whether to allow or cancel a navigation.
334
  * @see https://fburl.com/42r9fxob
398
  * @see https://fburl.com/42r9fxob
402
       return;
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
     NSMutableDictionary<NSString *, id> *event = [self baseEvent];
476
     NSMutableDictionary<NSString *, id> *event = [self baseEvent];
406
     [event addEntriesFromDictionary:@{
477
     [event addEntriesFromDictionary:@{
407
       @"didFailProvisionalNavigation": @YES,
478
       @"didFailProvisionalNavigation": @YES,
419
           thenCall: (void (^)(NSString*)) callback
490
           thenCall: (void (^)(NSString*)) callback
420
 {
491
 {
421
   [self.webView evaluateJavaScript: js completionHandler: ^(id result, NSError *error) {
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
 RCT_EXPORT_VIEW_PROPERTY(automaticallyAdjustContentInsets, BOOL)
45
 RCT_EXPORT_VIEW_PROPERTY(automaticallyAdjustContentInsets, BOOL)
46
 RCT_EXPORT_VIEW_PROPERTY(hideKeyboardAccessoryView, BOOL)
46
 RCT_EXPORT_VIEW_PROPERTY(hideKeyboardAccessoryView, BOOL)
47
 RCT_EXPORT_VIEW_PROPERTY(allowsBackForwardNavigationGestures, BOOL)
47
 RCT_EXPORT_VIEW_PROPERTY(allowsBackForwardNavigationGestures, BOOL)
48
+RCT_EXPORT_VIEW_PROPERTY(incognito, BOOL)
49
+RCT_EXPORT_VIEW_PROPERTY(pagingEnabled, BOOL)
48
 RCT_EXPORT_VIEW_PROPERTY(userAgent, NSString)
50
 RCT_EXPORT_VIEW_PROPERTY(userAgent, NSString)
51
+RCT_EXPORT_VIEW_PROPERTY(allowsLinkPreview, BOOL)
49
 
52
 
50
 /**
53
 /**
51
  * Expose methods to enable messaging the webview.
54
  * Expose methods to enable messaging the webview.
69
   view.bounces = json == nil ? true : [RCTConvert BOOL: json];
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
 RCT_CUSTOM_VIEW_PROPERTY(scrollEnabled, BOOL, RNCWKWebView) {
79
 RCT_CUSTOM_VIEW_PROPERTY(scrollEnabled, BOOL, RNCWKWebView) {
73
   view.scrollEnabled = json == nil ? true : [RCTConvert BOOL: json];
80
   view.scrollEnabled = json == nil ? true : [RCTConvert BOOL: json];
74
 }
81
 }

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

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

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

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

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

25
 import invariant from 'fbjs/lib/invariant';
25
 import invariant from 'fbjs/lib/invariant';
26
 import keyMirror from 'fbjs/lib/keyMirror';
26
 import keyMirror from 'fbjs/lib/keyMirror';
27
 
27
 
28
-import WebViewShared from './WebViewShared';
28
+import {
29
+  defaultOriginWhitelist,
30
+  createOnShouldStartLoadWithRequest,
31
+} from './WebViewShared';
29
 import type {
32
 import type {
30
   WebViewEvent,
33
   WebViewEvent,
31
   WebViewError,
34
   WebViewError,
130
 
133
 
131
   static defaultProps = {
134
   static defaultProps = {
132
     useWebKit: true,
135
     useWebKit: true,
133
-    originWhitelist: WebViewShared.defaultOriginWhitelist,
136
+    originWhitelist: defaultOriginWhitelist,
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
   state = {
145
   state = {
137
     viewState: this.props.startInLoadingState
146
     viewState: this.props.startInLoadingState
138
       ? WebViewState.LOADING
147
       ? WebViewState.LOADING
159
         'The allowsBackForwardNavigationGestures property is not supported when useWebKit = false',
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
   render() {
182
   render() {
199
 
217
 
200
     const nativeConfig = this.props.nativeConfig || {};
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
     const decelerationRate = processDecelerationRate(
226
     const decelerationRate = processDecelerationRate(
238
       this.props.decelerationRate,
227
       this.props.decelerationRate,
264
         injectedJavaScript={this.props.injectedJavaScript}
253
         injectedJavaScript={this.props.injectedJavaScript}
265
         bounces={this.props.bounces}
254
         bounces={this.props.bounces}
266
         scrollEnabled={this.props.scrollEnabled}
255
         scrollEnabled={this.props.scrollEnabled}
256
+        pagingEnabled={this.props.pagingEnabled}
267
         decelerationRate={decelerationRate}
257
         decelerationRate={decelerationRate}
268
         contentInset={this.props.contentInset}
258
         contentInset={this.props.contentInset}
269
         automaticallyAdjustContentInsets={
259
         automaticallyAdjustContentInsets={
271
         }
261
         }
272
         hideKeyboardAccessoryView={this.props.hideKeyboardAccessoryView}
262
         hideKeyboardAccessoryView={this.props.hideKeyboardAccessoryView}
273
         allowsBackForwardNavigationGestures={this.props.allowsBackForwardNavigationGestures}
263
         allowsBackForwardNavigationGestures={this.props.allowsBackForwardNavigationGestures}
264
+        incognito={this.props.incognito}
274
         userAgent={this.props.userAgent}
265
         userAgent={this.props.userAgent}
275
         onLoadingStart={this._onLoadingStart}
266
         onLoadingStart={this._onLoadingStart}
276
         onLoadingFinish={this._onLoadingFinish}
267
         onLoadingFinish={this._onLoadingFinish}
285
           this.props.mediaPlaybackRequiresUserAction
276
           this.props.mediaPlaybackRequiresUserAction
286
         }
277
         }
287
         dataDetectorTypes={this.props.dataDetectorTypes}
278
         dataDetectorTypes={this.props.dataDetectorTypes}
279
+        useSharedProcessPool={this.props.useSharedProcessPool}
280
+        allowsLinkPreview={this.props.allowsLinkPreview}
288
         {...nativeConfig.props}
281
         {...nativeConfig.props}
289
       />
282
       />
290
     );
283
     );
434
   };
427
   };
435
 
428
 
436
   _onLoadingProgress = (event: WebViewProgressEvent) => {
429
   _onLoadingProgress = (event: WebViewProgressEvent) => {
437
-    const {onLoadProgress} = this.props;
430
+    const { onLoadProgress } = this.props;
438
     onLoadProgress && onLoadProgress(event);
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
   componentDidUpdate(prevProps: WebViewSharedProps) {
450
   componentDidUpdate(prevProps: WebViewSharedProps) {
442
     if (!(prevProps.useWebKit && this.props.useWebKit)) {
451
     if (!(prevProps.useWebKit && this.props.useWebKit)) {
444
     }
453
     }
445
 
454
 
446
     this._showRedboxOnPropChanges(prevProps, 'allowsInlineMediaPlayback');
455
     this._showRedboxOnPropChanges(prevProps, 'allowsInlineMediaPlayback');
456
+    this._showRedboxOnPropChanges(prevProps, 'incognito');
447
     this._showRedboxOnPropChanges(prevProps, 'mediaPlaybackRequiresUserAction');
457
     this._showRedboxOnPropChanges(prevProps, 'mediaPlaybackRequiresUserAction');
448
     this._showRedboxOnPropChanges(prevProps, 'dataDetectorTypes');
458
     this._showRedboxOnPropChanges(prevProps, 'dataDetectorTypes');
449
 
459
 

+ 53
- 14
js/WebViewShared.js View File

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

+ 61
- 25
js/WebViewTypes.js View File

10
 
10
 
11
 'use strict';
11
 'use strict';
12
 
12
 
13
-import type {Node, Element, ComponentType} from 'react';
13
+import type { Node, Element, ComponentType } from 'react';
14
 
14
 
15
-import type {SyntheticEvent} from 'CoreEventTypes';
16
-import type {EdgeInsetsProp} from 'EdgeInsetsPropType';
17
-import type {ViewStyleProp} from 'StyleSheet';
18
-import type {ViewProps} from 'ViewPropTypes';
15
+import type { SyntheticEvent } from 'CoreEventTypes';
16
+import type { EdgeInsetsProp } from 'EdgeInsetsPropType';
17
+import type { ViewStyleProp } from 'StyleSheet';
18
+import type { ViewProps } from 'ViewPropTypes';
19
 
19
 
20
 export type WebViewNativeEvent = $ReadOnly<{|
20
 export type WebViewNativeEvent = $ReadOnly<{|
21
   url: string,
21
   url: string,
23
   title: string,
23
   title: string,
24
   canGoBack: boolean,
24
   canGoBack: boolean,
25
   canGoForward: boolean,
25
   canGoForward: boolean,
26
+  lockIdentifier: number,
26
 |}>;
27
 |}>;
27
 
28
 
28
 export type WebViewProgressEvent = $ReadOnly<{|
29
 export type WebViewProgressEvent = $ReadOnly<{|
29
-    ...WebViewNativeEvent,
30
-    progress: number,
31
-|}>
30
+  ...WebViewNativeEvent,
31
+  progress: number,
32
+|}>;
32
 
33
 
33
 export type WebViewNavigation = $ReadOnly<{|
34
 export type WebViewNavigation = $ReadOnly<{|
34
   ...WebViewNativeEvent,
35
   ...WebViewNativeEvent,
118
 export type WebViewSource = WebViewSourceUri | WebViewSourceHtml;
119
 export type WebViewSource = WebViewSourceUri | WebViewSourceHtml;
119
 
120
 
120
 export type WebViewNativeConfig = $ReadOnly<{|
121
 export type WebViewNativeConfig = $ReadOnly<{|
121
-  /*
122
+  /**
122
    * The native component used to render the WebView.
123
    * The native component used to render the WebView.
123
    */
124
    */
124
   component?: ComponentType<WebViewSharedProps>,
125
   component?: ComponentType<WebViewSharedProps>,
125
-  /*
126
+  /**
126
    * Set props directly on the native component WebView. Enables custom props which the
127
    * Set props directly on the native component WebView. Enables custom props which the
127
    * original WebView doesn't pass through.
128
    * original WebView doesn't pass through.
128
    */
129
    */
129
   props?: ?Object,
130
   props?: ?Object,
130
-  /*
131
+  /**
131
    * Set the ViewManager to use for communication with the native side.
132
    * Set the ViewManager to use for communication with the native side.
132
    * @platform ios
133
    * @platform ios
133
    */
134
    */
134
   viewManager?: ?Object,
135
   viewManager?: ?Object,
135
 |}>;
136
 |}>;
136
 
137
 
138
+export type OnShouldStartLoadWithRequest = (
139
+  event: WebViewNavigation,
140
+) => boolean;
141
+
137
 export type IOSWebViewProps = $ReadOnly<{|
142
 export type IOSWebViewProps = $ReadOnly<{|
138
   /**
143
   /**
139
    * If true, use WKWebView instead of UIWebView.
144
    * If true, use WKWebView instead of UIWebView.
168
    */
173
    */
169
   scrollEnabled?: ?boolean,
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
    * The amount by which the web view content is inset from the edges of
185
    * The amount by which the web view content is inset from the edges of
173
    * the scroll view. Defaults to {top: 0, left: 0, bottom: 0, right: 0}.
186
    * the scroll view. Defaults to {top: 0, left: 0, bottom: 0, right: 0}.
197
    *
210
    *
198
    * @platform ios
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
    * Boolean that determines whether HTML5 videos play inline or use the
216
    * Boolean that determines whether HTML5 videos play inline or use the
229
    * back-forward list navigations.
232
    * back-forward list navigations.
230
    */
233
    */
231
   allowsBackForwardNavigationGestures?: ?boolean,
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
    * The custom user agent string.
243
    * The custom user agent string.
234
    */
244
    */
235
   userAgent?: ?string,
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
 export type AndroidWebViewProps = $ReadOnly<{|
258
 export type AndroidWebViewProps = $ReadOnly<{|
277
    */
297
    */
278
   saveFormDataDisabled?: ?boolean,
298
   saveFormDataDisabled?: ?boolean,
279
 
299
 
280
-  /*
300
+  /**
281
    * Used on Android only, controls whether the given list of URL prefixes should
301
    * Used on Android only, controls whether the given list of URL prefixes should
282
    * make {@link com.facebook.react.views.webview.ReactWebViewClient} to launch a
302
    * make {@link com.facebook.react.views.webview.ReactWebViewClient} to launch a
283
    * default activity intent for those URL instead of loading it within the webview.
303
    * default activity intent for those URL instead of loading it within the webview.
327
   mixedContentMode?: ?('never' | 'always' | 'compatibility'),
347
   mixedContentMode?: ?('never' | 'always' | 'compatibility'),
328
 |}>;
348
 |}>;
329
 
349
 
330
-export type WebViewSharedProps =  $ReadOnly<{|
350
+export type WebViewSharedProps = $ReadOnly<{|
331
   ...ViewProps,
351
   ...ViewProps,
332
   ...IOSWebViewProps,
352
   ...IOSWebViewProps,
333
   ...AndroidWebViewProps,
353
   ...AndroidWebViewProps,
345
    */
365
    */
346
   source?: ?WebViewSource,
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
    * Function that returns a view to show if there's an error.
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
    * Function that returns a loading indicator.
383
    * Function that returns a loading indicator.
439
    */
468
    */
440
   originWhitelist?: $ReadOnlyArray<string>,
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
    * Override the native component used to render the WebView. Enables a custom native
479
    * Override the native component used to render the WebView. Enables a custom native
444
    * WebView which uses the same JavaScript as the original WebView.
480
    * WebView which uses the same JavaScript as the original WebView.

+ 1
- 1
package.json View File

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

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

9
   readonly canGoForward: boolean;
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
 export interface WebViewProgressEvent extends WebViewNativeEvent {
18
 export interface WebViewProgressEvent extends WebViewNativeEvent {
13
   readonly progress: number;
19
   readonly progress: number;
14
 }
20
 }
145
    */
151
    */
146
   scrollEnabled?: boolean;
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
    * The amount by which the web view content is inset from the edges of
163
    * The amount by which the web view content is inset from the edges of
150
    * the scroll view. Defaults to {top: 0, left: 0, bottom: 0, right: 0}.
164
    * the scroll view. Defaults to {top: 0, left: 0, bottom: 0, right: 0}.
182
    * to stop loading.
196
    * to stop loading.
183
    * @platform ios
197
    * @platform ios
184
    */
198
    */
185
-  onShouldStartLoadWithRequest?: (event: WebViewNativeEvent) => any;
199
+  onShouldStartLoadWithRequest?: (event: WebViewIOSLoadRequestEvent) => any;
186
 
200
 
187
   /**
201
   /**
188
    * Boolean that determines whether HTML5 videos play inline or use the
202
    * Boolean that determines whether HTML5 videos play inline or use the
199
    * backward compatible.
213
    * backward compatible.
200
    */
214
    */
201
   hideKeyboardAccessoryView?: boolean;
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
 export interface AndroidWebViewProps {
232
 export interface AndroidWebViewProps {
370
    * Boolean value that forces the `WebView` to show the loading view
398
    * Boolean value that forces the `WebView` to show the loading view
371
    * on the first load.
399
    * on the first load.
372
    */
400
    */
373
-  startInLoadingState?: string;
401
+  startInLoadingState?: boolean;
374
 
402
 
375
   /**
403
   /**
376
    * Set this to provide JavaScript that will be injected into the web page
404
    * Set this to provide JavaScript that will be injected into the web page
417
   public goBack: () => void;
445
   public goBack: () => void;
418
   public reload: () => void;
446
   public reload: () => void;
419
   public stopLoading: () => void;
447
   public stopLoading: () => void;
448
+  public postMessage: (msg: string) => void;
449
+  public injectJavaScript: (js: string) => void;
420
 }
450
 }