Merge branch 'master' into master

This commit is contained in:
Michael Diarmid 2019-01-21 07:17:00 +00:00 committed by GitHub
commit 665c80239c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 810 additions and 287 deletions

79
.all-contributorsrc Normal file
View File

@ -0,0 +1,79 @@
{
"projectName": "react-native-webview",
"projectOwner": "react-native-community",
"repoType": "github",
"repoHost": "https://github.com",
"files": [
"README.md"
],
"imageSize": 100,
"commit": false,
"contributors": [
{
"login": "titozzz",
"name": "Thibault Malbranche",
"avatar_url": "https://avatars1.githubusercontent.com/u/6181446?v=4",
"profile": "https://twitter.com/titozzz",
"contributions": [
"code",
"ideas",
"review",
"doc",
"maintenance",
"test",
"infra",
"question"
]
},
{
"login": "jamonholmgren",
"name": "Jamon Holmgren",
"avatar_url": "https://avatars3.githubusercontent.com/u/1479215?v=4",
"profile": "https://jamonholmgren.com",
"contributions": [
"code",
"ideas",
"review",
"doc",
"maintenance",
"test",
"example",
"question"
]
},
{
"login": "andreipfeiffer",
"name": "Andrei Pfeiffer",
"avatar_url": "https://avatars1.githubusercontent.com/u/2570562?v=4",
"profile": "https://github.com/andreipfeiffer",
"contributions": [
"code",
"review",
"ideas"
]
},
{
"login": "Salakar",
"name": "Michael Diarmid",
"avatar_url": "https://avatars0.githubusercontent.com/u/5347038?v=4",
"profile": "https://twitter.com/mikediarmid",
"contributions": [
"code",
"review",
"ideas",
"tool"
]
},
{
"login": "smathson",
"name": "Scott Mathson",
"avatar_url": "https://avatars3.githubusercontent.com/u/932981?v=4",
"profile": "http://smathson.github.io",
"contributions": [
"code",
"doc"
]
}
],
"contributorsPerLine": 7
}

View File

@ -1,7 +1,12 @@
# React Native WebView - a Modern, Cross-Platform WebView for React Native
[![All Contributors](https://img.shields.io/badge/all_contributors-5-orange.svg?style=flat-square)](#contributors)
**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)).
> 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.
_Garrett McCullough, mobile engineer at Virta Health_
## Platforms Supported
- [x] iOS (both UIWebView and WKWebView)
@ -73,6 +78,18 @@ Simply install React Native WebView and then use it in place of the core WebView
- [Thibault Malbranche](https://github.com/Titozzz) ([Twitter @titozzz](https://twitter.com/titozzz)) from [Brigad](https://brigad.co/about)
- [Empyrical](https://github.com/empyrical) ([Twitter @empyrical](https://twitter.com/empyrical))
## Contributors
Thanks goes to these wonderful people ([emoji key](https://github.com/all-contributors/all-contributors#emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore -->
| [<img src="https://avatars1.githubusercontent.com/u/6181446?v=4" width="100px;" alt="Thibault Malbranche"/><br /><sub><b>Thibault Malbranche</b></sub>](https://twitter.com/titozzz)<br />[💻](https://github.com/react-native-community/react-native-webview/commits?author=titozzz "Code") [🤔](#ideas-titozzz "Ideas, Planning, & Feedback") [👀](#review-titozzz "Reviewed Pull Requests") [📖](https://github.com/react-native-community/react-native-webview/commits?author=titozzz "Documentation") [🚧](#maintenance-titozzz "Maintenance") [⚠️](https://github.com/react-native-community/react-native-webview/commits?author=titozzz "Tests") [🚇](#infra-titozzz "Infrastructure (Hosting, Build-Tools, etc)") [💬](#question-titozzz "Answering Questions") | [<img src="https://avatars3.githubusercontent.com/u/1479215?v=4" width="100px;" alt="Jamon Holmgren"/><br /><sub><b>Jamon Holmgren</b></sub>](https://jamonholmgren.com)<br />[💻](https://github.com/react-native-community/react-native-webview/commits?author=jamonholmgren "Code") [🤔](#ideas-jamonholmgren "Ideas, Planning, & Feedback") [👀](#review-jamonholmgren "Reviewed Pull Requests") [📖](https://github.com/react-native-community/react-native-webview/commits?author=jamonholmgren "Documentation") [🚧](#maintenance-jamonholmgren "Maintenance") [⚠️](https://github.com/react-native-community/react-native-webview/commits?author=jamonholmgren "Tests") [💡](#example-jamonholmgren "Examples") [💬](#question-jamonholmgren "Answering Questions") | [<img src="https://avatars1.githubusercontent.com/u/2570562?v=4" width="100px;" alt="Andrei Pfeiffer"/><br /><sub><b>Andrei Pfeiffer</b></sub>](https://github.com/andreipfeiffer)<br />[💻](https://github.com/react-native-community/react-native-webview/commits?author=andreipfeiffer "Code") [👀](#review-andreipfeiffer "Reviewed Pull Requests") [🤔](#ideas-andreipfeiffer "Ideas, Planning, & Feedback") | [<img src="https://avatars0.githubusercontent.com/u/5347038?v=4" width="100px;" alt="Michael Diarmid"/><br /><sub><b>Michael Diarmid</b></sub>](https://twitter.com/mikediarmid)<br />[💻](https://github.com/react-native-community/react-native-webview/commits?author=Salakar "Code") [👀](#review-Salakar "Reviewed Pull Requests") [🤔](#ideas-Salakar "Ideas, Planning, & Feedback") [🔧](#tool-Salakar "Tools") | [<img src="https://avatars3.githubusercontent.com/u/932981?v=4" width="100px;" alt="Scott Mathson"/><br /><sub><b>Scott Mathson</b></sub>](http://smathson.github.io)<br />[💻](https://github.com/react-native-community/react-native-webview/commits?author=smathson "Code") [📖](https://github.com/react-native-community/react-native-webview/commits?author=smathson "Documentation") |
| :---: | :---: | :---: | :---: | :---: |
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
## License
MIT

View File

@ -1,91 +1,127 @@
buildscript {
ext.kotlin_version = '1.2.71'
repositories {
google()
jcenter()
maven {
url 'https://maven.fabric.io/public'
}
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
ext.kotlin_version = '1.2.71'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
//noinspection DifferentKotlinGradleVersion
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
def DEFAULT_TARGET_SDK_VERSION = 27
def DEFAULT_COMPILE_SDK_VERSION = 27
def DEFAULT_BUILD_TOOLS_VERSION = "28.0.3"
def DEFAULT_TARGET_SDK_VERSION = 27
def DEFAULT_SUPPORT_LIB_VERSION = "28.0.0"
def getExtOrDefault(name, defaultValue) {
return rootProject.ext.has(name) ? rootProject.ext.get(name) : defaultValue
}
android {
compileSdkVersion rootProject.hasProperty('compileSdkVersion') ? rootProject.compileSdkVersion : DEFAULT_COMPILE_SDK_VERSION
buildToolsVersion rootProject.hasProperty('buildToolsVersion') ? rootProject.buildToolsVersion : DEFAULT_BUILD_TOOLS_VERSION
defaultConfig {
minSdkVersion 16
targetSdkVersion rootProject.hasProperty('targetSdkVersion') ? rootProject.targetSdkVersion : DEFAULT_TARGET_SDK_VERSION
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
}
}
productFlavors {
}
lintOptions {
disable 'GradleCompatible'
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
compileSdkVersion getExtOrDefault('compileSdkVersion', DEFAULT_COMPILE_SDK_VERSION)
buildToolsVersion getExtOrDefault('buildToolsVersion', DEFAULT_BUILD_TOOLS_VERSION)
defaultConfig {
minSdkVersion 16
targetSdkVersion getExtOrDefault('targetSdkVersion', DEFAULT_TARGET_SDK_VERSION)
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
}
}
lintOptions {
disable 'GradleCompatible'
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
repositories {
mavenCentral()
mavenCentral()
jcenter()
google()
def found = false
def defaultDir = null
def androidSourcesName = 'React Native sources'
if (rootProject.ext.has('reactNativeAndroidRoot')) {
defaultDir = rootProject.ext.get('reactNativeAndroidRoot')
} else {
defaultDir = new File(
projectDir,
'/../../../node_modules/react-native/android'
)
}
if (defaultDir.exists()) {
maven {
url 'https://maven.google.com/'
name 'Google'
url defaultDir.toString()
name androidSourcesName
}
// Stolen from react-native-firebase, thanks dudes!
def found = false
logger.quiet(":${project.name}:reactNativeAndroidRoot ${defaultDir.canonicalPath}")
found = true
} else {
def parentDir = rootProject.projectDir
def reactNativeAndroidName = 'React Native (Node Modules)'
1.upto(4, {
if (found) return true
parentDir = parentDir.parentFile
def reactNativeAndroid = new File(
parentDir,
'node_modules/react-native/android'
)
1.upto(5, {
if (found) return true
parentDir = parentDir.parentFile
if (reactNativeAndroid.exists()) {
maven {
url reactNativeAndroid.toString()
name reactNativeAndroidName
}
def androidSourcesDir = new File(
parentDir,
'node_modules/react-native'
)
println "${project.name}: using React Native sources from ${reactNativeAndroid.toString()}"
found = true
def androidPrebuiltBinaryDir = new File(
parentDir,
'node_modules/react-native/android'
)
if (androidPrebuiltBinaryDir.exists()) {
maven {
url androidPrebuiltBinaryDir.toString()
name androidSourcesName
}
})
if (!found) {
throw new GradleException(
"${project.name}: unable to locate React Native Android sources, " +
"ensure you have you installed React Native as a dependency and try again."
)
}
logger.quiet(":${project.name}:reactNativeAndroidRoot ${androidPrebuiltBinaryDir.canonicalPath}")
found = true
} else if (androidSourcesDir.exists()) {
maven {
url androidSourcesDir.toString()
name androidSourcesName
}
logger.quiet(":${project.name}:reactNativeAndroidRoot ${androidSourcesDir.canonicalPath}")
found = true
}
})
}
if (!found) {
throw new GradleException(
"${project.name}: unable to locate React Native android sources. " +
"Ensure you have you installed React Native as a dependency in your project and try again."
)
}
}
def support_version = getExtOrDefault('supportLibVersion', DEFAULT_SUPPORT_LIB_VERSION)
dependencies {
implementation 'com.facebook.react:react-native:+'
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
//noinspection GradleDynamicVersion
api 'com.facebook.react:react-native:+'
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "com.android.support:appcompat-v7:$support_version"
}

View File

@ -2,10 +2,14 @@ package com.reactnativecommunity.webview;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.DownloadManager;
import android.content.Context;
import com.facebook.react.uimanager.UIManagerModule;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Pattern;
@ -21,21 +25,25 @@ import java.util.Map;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Picture;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.webkit.ConsoleMessage;
import android.webkit.CookieManager;
import android.webkit.DownloadListener;
import android.webkit.GeolocationPermissions;
import android.webkit.JavascriptInterface;
import android.webkit.URLUtil;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebResourceRequest;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import com.facebook.common.logging.FLog;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.LifecycleEventListener;
@ -54,11 +62,19 @@ import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.uimanager.events.ContentSizeChangeEvent;
import com.facebook.react.uimanager.events.Event;
import com.facebook.react.uimanager.events.EventDispatcher;
import com.facebook.react.uimanager.events.RCTEventEmitter;
import com.reactnativecommunity.webview.events.TopLoadingErrorEvent;
import com.reactnativecommunity.webview.events.TopLoadingFinishEvent;
import com.reactnativecommunity.webview.events.TopLoadingStartEvent;
import com.reactnativecommunity.webview.events.TopMessageEvent;
import com.reactnativecommunity.webview.events.TopLoadingProgressEvent;
import com.reactnativecommunity.webview.events.TopShouldStartLoadWithRequestEvent;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import javax.annotation.Nullable;
import org.json.JSONException;
import org.json.JSONObject;
@ -69,12 +85,14 @@ import org.json.JSONObject;
* - GO_BACK
* - GO_FORWARD
* - RELOAD
* - LOAD_URL
*
* {@link WebView} instances could emit following direct events:
* - topLoadingFinish
* - topLoadingStart
* - topLoadingStart
* - topLoadingProgress
* - topShouldStartLoadWithRequest
*
* Each event will carry the following properties:
* - target - view's react tag
@ -102,19 +120,18 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
public static final int COMMAND_STOP_LOADING = 4;
public static final int COMMAND_POST_MESSAGE = 5;
public static final int COMMAND_INJECT_JAVASCRIPT = 6;
public static final int COMMAND_LOAD_URL = 7;
// Use `webView.loadUrl("about:blank")` to reliably reset the view
// state and release page resources (including any running JavaScript).
protected static final String BLANK_URL = "about:blank";
protected WebViewConfig mWebViewConfig;
protected @Nullable WebView.PictureListener mPictureListener;
protected static class RNCWebViewClient extends WebViewClient {
protected boolean mLastLoadFailed = false;
protected @Nullable ReadableArray mUrlPrefixesForDefaultIntent;
protected @Nullable List<Pattern> mOriginWhitelist;
@Override
public void onPageFinished(WebView webView, String url) {
@ -142,50 +159,16 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url.equals(BLANK_URL)) return false;
// url blacklisting
if (mUrlPrefixesForDefaultIntent != null && mUrlPrefixesForDefaultIntent.size() > 0) {
ArrayList<Object> urlPrefixesForDefaultIntent =
mUrlPrefixesForDefaultIntent.toArrayList();
for (Object urlPrefix : urlPrefixesForDefaultIntent) {
if (url.startsWith((String) urlPrefix)) {
launchIntent(view.getContext(), url);
return true;
}
}
}
if (mOriginWhitelist != null && shouldHandleURL(mOriginWhitelist, url)) {
return false;
}
launchIntent(view.getContext(), url);
dispatchEvent(view, new TopShouldStartLoadWithRequestEvent(view.getId(), url));
return true;
}
private void launchIntent(Context context, String url) {
try {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addCategory(Intent.CATEGORY_BROWSABLE);
context.startActivity(intent);
} catch (ActivityNotFoundException e) {
FLog.w(ReactConstants.TAG, "activity not found to handle uri scheme for: " + url, e);
}
}
private boolean shouldHandleURL(List<Pattern> originWhitelist, String url) {
Uri uri = Uri.parse(url);
String scheme = uri.getScheme() != null ? uri.getScheme() : "";
String authority = uri.getAuthority() != null ? uri.getAuthority() : "";
String urlToCheck = scheme + "://" + authority;
for (Pattern pattern : originWhitelist) {
if (pattern.matcher(urlToCheck).matches()) {
return true;
}
}
return false;
@TargetApi(Build.VERSION_CODES.N)
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
dispatchEvent(view, new TopShouldStartLoadWithRequestEvent(view.getId(), request.getUrl().toString()));
return true;
}
@Override
@ -234,10 +217,6 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
public void setUrlPrefixesForDefaultIntent(ReadableArray specialUrls) {
mUrlPrefixesForDefaultIntent = specialUrls;
}
public void setOriginWhitelist(List<Pattern> originWhitelist) {
mOriginWhitelist = originWhitelist;
}
}
/**
@ -248,6 +227,11 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
protected @Nullable String injectedJS;
protected boolean messagingEnabled = false;
protected @Nullable RNCWebViewClient mRNCWebViewClient;
protected boolean sendContentSizeChangeEvents = false;
public void setSendContentSizeChangeEvents(boolean sendContentSizeChangeEvents) {
this.sendContentSizeChangeEvents = sendContentSizeChangeEvents;
}
protected class RNCWebViewBridge {
RNCWebView mContext;
@ -288,6 +272,22 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
cleanupCallbacksAndDestroy();
}
@Override
protected void onSizeChanged(int w, int h, int ow, int oh) {
super.onSizeChanged(w, h, ow, oh);
if (sendContentSizeChangeEvents) {
dispatchEvent(
this,
new ContentSizeChangeEvent(
this.getId(),
w,
h
)
);
}
}
@Override
public void setWebViewClient(WebViewClient client) {
super.setWebViewClient(client);
@ -477,6 +477,53 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
WebView.setWebContentsDebuggingEnabled(true);
}
webView.setDownloadListener(new DownloadListener() {
public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) {
RNCWebViewModule module = getModule();
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
//Try to extract filename from contentDisposition, otherwise guess using URLUtil
String fileName = "";
try {
fileName = contentDisposition.replaceFirst("(?i)^.*filename=\"?([^\"]+)\"?.*$", "$1");
fileName = URLDecoder.decode(fileName, "UTF-8");
} catch (Exception e) {
System.out.println("Error extracting filename from contentDisposition: " + e);
System.out.println("Falling back to URLUtil.guessFileName");
fileName = URLUtil.guessFileName(url,contentDisposition,mimetype);
}
String downloadMessage = "Downloading " + fileName;
//Attempt to add cookie, if it exists
URL urlObj = null;
try {
urlObj = new URL(url);
String baseUrl = urlObj.getProtocol() + "://" + urlObj.getHost();
String cookie = CookieManager.getInstance().getCookie(baseUrl);
request.addRequestHeader("Cookie", cookie);
System.out.println("Got cookie for DownloadManager: " + cookie);
} catch (MalformedURLException e) {
System.out.println("Error getting cookie for DownloadManager: " + e.toString());
e.printStackTrace();
}
//Finish setting up request
request.addRequestHeader("User-Agent", userAgent);
request.setTitle(fileName);
request.setDescription(downloadMessage);
request.allowScanningByMediaScanner();
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
module.setDownloadRequest(request);
if (module.grantFileDownloaderPermissions()) {
module.downloadFile();
}
}
});
return webView;
}
@ -631,11 +678,7 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
@ReactProp(name = "onContentSizeChange")
public void setOnContentSizeChange(WebView view, boolean sendContentSizeChangeEvents) {
if (sendContentSizeChangeEvents) {
view.setPictureListener(getPictureListener());
} else {
view.setPictureListener(null);
}
((RNCWebView) view).setSendContentSizeChangeEvents(sendContentSizeChangeEvents);
}
@ReactProp(name = "mixedContentMode")
@ -675,20 +718,6 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
view.getSettings().setGeolocationEnabled(isGeolocationEnabled != null && isGeolocationEnabled);
}
@ReactProp(name = "originWhitelist")
public void setOriginWhitelist(
WebView view,
@Nullable ReadableArray originWhitelist) {
RNCWebViewClient client = ((RNCWebView) view).getRNCWebViewClient();
if (client != null && originWhitelist != null) {
List<Pattern> whiteList = new LinkedList<>();
for (int i = 0 ; i < originWhitelist.size() ; i++) {
whiteList.add(Pattern.compile(originWhitelist.getString(i)));
}
client.setOriginWhitelist(whiteList);
}
}
@Override
protected void addEventEmitters(ThemedReactContext reactContext, WebView view) {
// Do not register default touch emitter and let WebView implementation handle touches
@ -697,9 +726,13 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
@Override
public Map getExportedCustomDirectEventTypeConstants() {
MapBuilder.Builder builder = MapBuilder.builder();
builder.put("topLoadingProgress", MapBuilder.of("registrationName", "onLoadingProgress"));
return builder.build();
Map export = super.getExportedCustomDirectEventTypeConstants();
if (export == null) {
export = MapBuilder.newHashMap();
}
export.put(TopLoadingProgressEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoadingProgress"));
export.put(TopShouldStartLoadWithRequestEvent.EVENT_NAME, MapBuilder.of("registrationName", "onShouldStartLoadWithRequest"));
return export;
}
@Override
@ -710,7 +743,8 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
"reload", COMMAND_RELOAD,
"stopLoading", COMMAND_STOP_LOADING,
"postMessage", COMMAND_POST_MESSAGE,
"injectJavaScript", COMMAND_INJECT_JAVASCRIPT
"injectJavaScript", COMMAND_INJECT_JAVASCRIPT,
"loadUrl", COMMAND_LOAD_URL
);
}
@ -753,6 +787,12 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
RNCWebView reactWebView = (RNCWebView) root;
reactWebView.evaluateJavascriptWithFallback(args.getString(0));
break;
case COMMAND_LOAD_URL:
if (args == null) {
throw new RuntimeException("Arguments for loading an url are null!");
}
root.loadUrl(args.getString(0));
break;
}
}
@ -763,23 +803,6 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
((RNCWebView) webView).cleanupCallbacksAndDestroy();
}
protected WebView.PictureListener getPictureListener() {
if (mPictureListener == null) {
mPictureListener = new WebView.PictureListener() {
@Override
public void onNewPicture(WebView webView, Picture picture) {
dispatchEvent(
webView,
new ContentSizeChangeEvent(
webView.getId(),
webView.getWidth(),
webView.getContentHeight()));
}
};
}
return mPictureListener;
}
protected static void dispatchEvent(WebView webView, Event event) {
ReactContext reactContext = (ReactContext) webView.getContext();
EventDispatcher eventDispatcher =

View File

@ -1,24 +1,32 @@
package com.reactnativecommunity.webview;
import android.Manifest;
import android.app.Activity;
import android.app.DownloadManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.os.Parcelable;
import android.provider.MediaStore;
import android.support.annotation.RequiresApi;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.FileProvider;
import android.util.Log;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.widget.Toast;
import com.facebook.react.bridge.ActivityEventListener;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.modules.core.PermissionAwareActivity;
import com.facebook.react.modules.core.PermissionListener;
import java.io.File;
import java.io.IOException;
@ -38,6 +46,9 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
private ValueCallback<Uri[]> filePathCallback;
private Uri outputFileUri;
private DownloadManager.Request downloadRequest;
private static final int FILE_DOWNLOAD_PERMISSION_REQUEST = 1;
final String DEFAULT_MIME_TYPES = "*/*";
public RNCWebViewModule(ReactApplicationContext reactContext) {
@ -177,6 +188,37 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
return true;
}
public void setDownloadRequest(DownloadManager.Request request) {
this.downloadRequest = request;
}
public void downloadFile() {
DownloadManager dm = (DownloadManager) getCurrentActivity().getBaseContext().getSystemService(Context.DOWNLOAD_SERVICE);
String downloadMessage = "Downloading";
dm.enqueue(this.downloadRequest);
Toast.makeText(getCurrentActivity().getApplicationContext(), downloadMessage, Toast.LENGTH_LONG).show();
}
public boolean grantFileDownloaderPermissions() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return true;
}
boolean result = true;
if (ContextCompat.checkSelfPermission(getCurrentActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
result = false;
}
if (!result) {
PermissionAwareActivity activity = getPermissionAwareActivity();
activity.requestPermissions(new String[]{ Manifest.permission.WRITE_EXTERNAL_STORAGE }, FILE_DOWNLOAD_PERMISSION_REQUEST, webviewFileDownloaderPermissionListener);
}
return result;
}
public RNCWebViewPackage getPackage() {
return this.aPackage;
}
@ -306,4 +348,34 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
// will be an array with one empty string element, afaik
return arr.length == 0 || (arr.length == 1 && arr[0].length() == 0);
}
private PermissionAwareActivity getPermissionAwareActivity() {
Activity activity = getCurrentActivity();
if (activity == null) {
throw new IllegalStateException("Tried to use permissions API while not attached to an Activity.");
} else if (!(activity instanceof PermissionAwareActivity)) {
throw new IllegalStateException("Tried to use permissions API but the host Activity doesn't implement PermissionAwareActivity.");
}
return (PermissionAwareActivity) activity;
}
private PermissionListener webviewFileDownloaderPermissionListener = new PermissionListener() {
@Override
public boolean onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
switch (requestCode) {
case FILE_DOWNLOAD_PERMISSION_REQUEST: {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
if (downloadRequest != null) {
downloadFile();
}
} else {
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();
}
return true;
}
}
return false;
}
};
}

View File

@ -0,0 +1,40 @@
package com.reactnativecommunity.webview.events;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.events.Event;
import com.facebook.react.uimanager.events.RCTEventEmitter;
public class TopShouldStartLoadWithRequestEvent extends Event<TopMessageEvent> {
public static final String EVENT_NAME = "topShouldStartLoadWithRequest";
private final String mUrl;
public TopShouldStartLoadWithRequestEvent(int viewId, String url) {
super(viewId);
mUrl = url;
}
@Override
public String getEventName() {
return EVENT_NAME;
}
@Override
public boolean canCoalesce() {
return false;
}
@Override
public short getCoalescingKey() {
// All events for a given view can be coalesced.
return 0;
}
@Override
public void dispatch(RCTEventEmitter rctEventEmitter) {
WritableMap data = Arguments.createMap();
data.putString("url", mUrl);
data.putString("navigationType", "other");
rctEventEmitter.receiveEvent(getViewTag(), EVENT_NAME, data);
}
}

View File

@ -105,3 +105,28 @@ WebView.isFileUploadSupported().then(res => {
```
### Add support for File Download
##### iOS
For iOS, all you need to do is specify the permissions in your `ios/[project]/Info.plist` file:
Save to gallery:
```
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Save pictures for certain activities.</string>
```
##### Android
Add permission in AndroidManifest.xml:
```xml
<manifest ...>
......
<!-- this is required to save files on Android -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
......
</manifest>
```

View File

@ -44,6 +44,7 @@ This document lays out the current public properties and methods for the React N
- [`html`](Reference.md#html)
- [`hideKeyboardAccessoryView`](Reference.md#hidekeyboardaccessoryview)
- [`allowsBackForwardNavigationGestures`](Reference.md#allowsbackforwardnavigationgestures)
- [`incognito`](Reference.md#incognito)
- [`allowFileAccess`](Reference.md#allowFileAccess)
- [`saveFormDataDisabled`](Reference.md#saveFormDataDisabled)
- [`enableCache`](Reference.md#enableCache)
@ -103,7 +104,7 @@ Controls whether to adjust the content inset for web views that are placed behin
### `injectedJavaScript`
Set this to provide JavaScript that will be injected into the web page when the view loads.
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.
| Type | Required |
| ------ | -------- |
@ -276,6 +277,16 @@ Boolean value that forces the `WebView` to show the loading view on the first lo
---
### `style`
A style object that allow you to customize the `WebView` style. Please note that there are default styles (example: you need to add `flex: 0` to the style if you want to use `height` property).
| Type | Required |
| ----- | -------- |
| style | No |
---
### `decelerationRate`
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:
@ -502,6 +513,16 @@ If true, this will be able horizontal swipe gestures when using the WKWebView. T
---
### `incognito`
Does not store any data within the lifetime of the WebView.
| Type | Required | Platform |
| ------- | -------- | ------------- |
| boolean | No | iOS WKWebView |
---
### `allowFileAccess`
If true, this will allow access to the file system via `file://` URI's. The default value is `false`.

View File

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

View File

@ -0,0 +1,36 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <Foundation/Foundation.h>
#import "RNCWKProcessPoolManager.h"
@interface RNCWKProcessPoolManager() {
WKProcessPool *_sharedProcessPool;
}
@end
@implementation RNCWKProcessPoolManager
+ (id) sharedManager {
static RNCWKProcessPoolManager *_sharedManager = nil;
@synchronized(self) {
if(_sharedManager == nil) {
_sharedManager = [[super alloc] init];
}
return _sharedManager;
}
}
- (WKProcessPool *)sharedProcessPool {
if (!_sharedProcessPool) {
_sharedProcessPool = [[WKProcessPool alloc] init];
}
return _sharedProcessPool;
}
@end

View File

@ -14,7 +14,7 @@
@protocol RNCWKWebViewDelegate <NSObject>
- (BOOL)webView:(RNCWKWebView *)webView
shouldStartLoadForRequest:(NSMutableDictionary<NSString *, id> *)request
shouldStartLoadForRequest:(NSMutableDictionary<NSString *, id> *)request
withCallback:(RCTDirectEventBlock)callback;
@end
@ -38,6 +38,8 @@ shouldStartLoadForRequest:(NSMutableDictionary<NSString *, id> *)request
@property (nonatomic, assign) BOOL automaticallyAdjustContentInsets;
@property (nonatomic, assign) BOOL hideKeyboardAccessoryView;
@property (nonatomic, assign) BOOL allowsBackForwardNavigationGestures;
@property (nonatomic, assign) BOOL incognito;
@property (nonatomic, assign) BOOL useSharedProcessPool;
@property (nonatomic, copy) NSString *userAgent;
@property (nonatomic, assign) BOOL enableCache;
@property (nonatomic, assign) BOOL allowsLinkPreview;

View File

@ -8,10 +8,11 @@
#import "RNCWKWebView.h"
#import <React/RCTConvert.h>
#import <React/RCTAutoInsetsProtocol.h>
#import "RNCWKProcessPoolManager.h"
#import <UIKit/UIKit.h>
#import "objc/runtime.h"
static NSTimer *keyboardTimer;
static NSString *const MessageHanderName = @"ReactNative";
// runtime trick to remove WKWebView keyboard default toolbar
@ -60,7 +61,6 @@ static NSString *const MessageHanderName = @"ReactNative";
return _webkitAvailable;
}
- (instancetype)initWithFrame:(CGRect)frame
{
if ((self = [super initWithFrame:frame])) {
@ -70,19 +70,6 @@ static NSString *const MessageHanderName = @"ReactNative";
_automaticallyAdjustContentInsets = YES;
_contentInset = UIEdgeInsetsZero;
}
// Workaround for a keyboard dismissal bug present in iOS 12
// https://openradar.appspot.com/radar?id=5018321736957952
if (@available(iOS 12.0, *)) {
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(keyboardWillHide)
name:UIKeyboardWillHideNotification object:nil];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(keyboardWillShow)
name:UIKeyboardWillShowNotification object:nil];
}
return self;
}
@ -94,9 +81,14 @@ static NSString *const MessageHanderName = @"ReactNative";
};
WKWebViewConfiguration *wkWebViewConfig = [WKWebViewConfiguration new];
if (_enableCache) {
if (_incognito) {
wkWebViewConfig.websiteDataStore = [WKWebsiteDataStore nonPersistentDataStore];
} else if (_enableCache) {
wkWebViewConfig.websiteDataStore = [WKWebsiteDataStore defaultDataStore];
}
if(self.useSharedProcessPool) {
wkWebViewConfig.processPool = [[RNCWKProcessPoolManager sharedManager] sharedProcessPool];
}
wkWebViewConfig.userContentController = [WKUserContentController new];
[wkWebViewConfig.userContentController addScriptMessageHandler: self name: MessageHanderName];
wkWebViewConfig.allowsInlineMediaPlayback = _allowsInlineMediaPlayback;
@ -135,6 +127,13 @@ static NSString *const MessageHanderName = @"ReactNative";
}
}
// Update webview property when the component prop changes.
- (void)setAllowsBackForwardNavigationGestures:(BOOL)allowsBackForwardNavigationGestures {
_allowsBackForwardNavigationGestures = allowsBackForwardNavigationGestures;
_webView.allowsBackForwardNavigationGestures = _allowsBackForwardNavigationGestures;
}
- (void)removeFromSuperview
{
if (_webView) {
@ -147,27 +146,6 @@ static NSString *const MessageHanderName = @"ReactNative";
[super removeFromSuperview];
}
-(void)keyboardWillHide
{
keyboardTimer = [NSTimer scheduledTimerWithTimeInterval:0 target:self selector:@selector(keyboardDisplacementFix) userInfo:nil repeats:false];
[[NSRunLoop mainRunLoop] addTimer:keyboardTimer forMode:NSRunLoopCommonModes];
}
-(void)keyboardWillShow
{
if (keyboardTimer != nil) {
[keyboardTimer invalidate];
}
}
-(void)keyboardDisplacementFix
{
// https://stackoverflow.com/a/9637807/824966
[UIView animateWithDuration:.25 animations:^{
self.webView.scrollView.contentOffset = CGPointMake(0, 0);
}];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if ([keyPath isEqual:@"estimatedProgress"] && object == self.webView) {
if(_onLoadingProgress){
@ -342,6 +320,88 @@ static NSString *const MessageHanderName = @"ReactNative";
#pragma mark - WKNavigationDelegate methods
/**
* alert
*/
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler
{
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
completionHandler();
}]];
[[self topViewController] presentViewController:alert animated:YES completion:NULL];
}
/**
* confirm
*/
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler{
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
completionHandler(YES);
}]];
[alert addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
completionHandler(NO);
}]];
[[self topViewController] presentViewController:alert animated:YES completion:NULL];
}
/**
* prompt
*/
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *))completionHandler{
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" message:prompt preferredStyle:UIAlertControllerStyleAlert];
[alert addTextFieldWithConfigurationHandler:^(UITextField *textField) {
textField.textColor = [UIColor lightGrayColor];
textField.placeholder = defaultText;
}];
[alert addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
completionHandler([[alert.textFields lastObject] text]);
}]];
[[self topViewController] presentViewController:alert animated:YES completion:NULL];
}
/**
* topViewController
*/
-(UIViewController *)topViewController{
   UIViewController *controller = [self topViewControllerWithRootViewController:[self getCurrentWindow].rootViewController];
   return controller;
}
/**
* topViewControllerWithRootViewController
*/
-(UIViewController *)topViewControllerWithRootViewController:(UIViewController *)viewController{
if (viewController==nil) return nil;
if (viewController.presentedViewController!=nil) {
return [self topViewControllerWithRootViewController:viewController.presentedViewController];
} else if ([viewController isKindOfClass:[UITabBarController class]]){
return [self topViewControllerWithRootViewController:[(UITabBarController *)viewController selectedViewController]];
} else if ([viewController isKindOfClass:[UINavigationController class]]){
return [self topViewControllerWithRootViewController:[(UINavigationController *)viewController visibleViewController]];
} else {
return viewController;
}
}
/**
* getCurrentWindow
*/
-(UIWindow *)getCurrentWindow{
UIWindow *window = [UIApplication sharedApplication].keyWindow;
if (window.windowLevel!=UIWindowLevelNormal) {
for (UIWindow *wid in [UIApplication sharedApplication].windows) {
if (window.windowLevel==UIWindowLevelNormal) {
window = wid;
break;
}
}
}
return window;
}
/**
* Decides whether to allow or cancel a navigation.
* @see https://fburl.com/42r9fxob
@ -415,6 +475,13 @@ static NSString *const MessageHanderName = @"ReactNative";
return;
}
if ([error.domain isEqualToString:@"WebKitErrorDomain"] && error.code == 102) {
// Error code 102 "Frame load interrupted" is raised by the WKWebView
// when the URL is from an http redirect. This is a common pattern when
// implementing OAuth with a WebView.
return;
}
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
[event addEntriesFromDictionary:@{
@"didFailProvisionalNavigation": @YES,
@ -432,8 +499,12 @@ static NSString *const MessageHanderName = @"ReactNative";
thenCall: (void (^)(NSString*)) callback
{
[self.webView evaluateJavaScript: js completionHandler: ^(id result, NSError *error) {
if (error == nil && callback != nil) {
callback([NSString stringWithFormat:@"%@", result]);
if (error == nil) {
if (callback != nil) {
callback([NSString stringWithFormat:@"%@", result]);
}
} else {
RCTLogError(@"Error evaluating injectedJavaScript: This is possibly due to an unsupported return type. Try adding true to the end of your injectedJavaScript string.");
}
}];
}

View File

@ -45,6 +45,7 @@ RCT_EXPORT_VIEW_PROPERTY(contentInset, UIEdgeInsets)
RCT_EXPORT_VIEW_PROPERTY(automaticallyAdjustContentInsets, BOOL)
RCT_EXPORT_VIEW_PROPERTY(hideKeyboardAccessoryView, BOOL)
RCT_EXPORT_VIEW_PROPERTY(allowsBackForwardNavigationGestures, BOOL)
RCT_EXPORT_VIEW_PROPERTY(incognito, BOOL)
RCT_EXPORT_VIEW_PROPERTY(pagingEnabled, BOOL)
RCT_EXPORT_VIEW_PROPERTY(userAgent, NSString)
RCT_EXPORT_VIEW_PROPERTY(enableCache, BOOL)
@ -72,6 +73,10 @@ RCT_CUSTOM_VIEW_PROPERTY(bounces, BOOL, RNCWKWebView) {
view.bounces = json == nil ? true : [RCTConvert BOOL: json];
}
RCT_CUSTOM_VIEW_PROPERTY(useSharedProcessPool, BOOL, RNCWKWebView) {
view.useSharedProcessPool = json == nil ? true : [RCTConvert BOOL: json];
}
RCT_CUSTOM_VIEW_PROPERTY(scrollEnabled, BOOL, RNCWKWebView) {
view.scrollEnabled = json == nil ? true : [RCTConvert BOOL: json];
}

View File

@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
3515965E21A3C86000623BFA /* RNCWKProcessPoolManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 3515965D21A3C86000623BFA /* RNCWKProcessPoolManager.m */; };
E914DBF6214474710071092B /* RNCUIWebViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = E914DBF3214474710071092B /* RNCUIWebViewManager.m */; };
E914DBF7214474710071092B /* RNCUIWebView.m in Sources */ = {isa = PBXBuildFile; fileRef = E914DBF4214474710071092B /* RNCUIWebView.m */; };
E91B351D21446E6C00F9801F /* RNCWKWebViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = E91B351B21446E6C00F9801F /* RNCWKWebViewManager.m */; };
@ -27,6 +28,8 @@
/* Begin PBXFileReference section */
134814201AA4EA6300B7C361 /* libRNCWebView.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNCWebView.a; sourceTree = BUILT_PRODUCTS_DIR; };
3515965D21A3C86000623BFA /* RNCWKProcessPoolManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNCWKProcessPoolManager.m; sourceTree = "<group>"; };
3515965F21A3C87E00623BFA /* RNCWKProcessPoolManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNCWKProcessPoolManager.h; sourceTree = "<group>"; };
E914DBF2214474710071092B /* RNCUIWebView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNCUIWebView.h; sourceTree = "<group>"; };
E914DBF3214474710071092B /* RNCUIWebViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNCUIWebViewManager.m; sourceTree = "<group>"; };
E914DBF4214474710071092B /* RNCUIWebView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNCUIWebView.m; sourceTree = "<group>"; };
@ -67,6 +70,8 @@
E91B351C21446E6C00F9801F /* RNCWKWebView.m */,
E91B351921446E6C00F9801F /* RNCWKWebViewManager.h */,
E91B351B21446E6C00F9801F /* RNCWKWebViewManager.m */,
3515965D21A3C86000623BFA /* RNCWKProcessPoolManager.m */,
3515965F21A3C87E00623BFA /* RNCWKProcessPoolManager.h */,
134814211AA4EA7D00B7C361 /* Products */,
);
sourceTree = "<group>";
@ -131,6 +136,7 @@
E914DBF7214474710071092B /* RNCUIWebView.m in Sources */,
E914DBF6214474710071092B /* RNCUIWebViewManager.m in Sources */,
E91B351E21446E6C00F9801F /* RNCWKWebView.m in Sources */,
3515965E21A3C86000623BFA /* RNCWKProcessPoolManager.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@ -8,35 +8,33 @@
* @flow
*/
'use strict';
import React from 'react';
import ReactNative from 'react-native';
import {
import ReactNative, {
ActivityIndicator,
Image,
requireNativeComponent,
StyleSheet,
UIManager,
View,
Image,
requireNativeComponent,
NativeModules
} from 'react-native';
import invariant from 'fbjs/lib/invariant';
import keyMirror from 'fbjs/lib/keyMirror';
import WebViewShared from './WebViewShared';
import {
defaultOriginWhitelist,
createOnShouldStartLoadWithRequest,
} from './WebViewShared';
import type {
WebViewEvent,
WebViewError,
WebViewErrorEvent,
WebViewMessageEvent,
WebViewNavigation,
WebViewNavigationEvent,
WebViewProgressEvent,
WebViewSharedProps,
WebViewSource,
WebViewProgressEvent,
} from './WebViewTypes';
const resolveAssetSource = Image.resolveAssetSource;
@ -69,8 +67,8 @@ class WebView extends React.Component<WebViewSharedProps, State> {
scalesPageToFit: true,
allowFileAccess: false,
saveFormDataDisabled: false,
originWhitelist: WebViewShared.defaultOriginWhitelist,
enableCache: true,
originWhitelist: defaultOriginWhitelist,
};
static isFileUploadSupported = async () => {
@ -79,7 +77,9 @@ class WebView extends React.Component<WebViewSharedProps, State> {
}
state = {
viewState: this.props.startInLoadingState ? WebViewState.LOADING : WebViewState.IDLE,
viewState: this.props.startInLoadingState
? WebViewState.LOADING
: WebViewState.IDLE,
lastErrorEvent: null,
};
@ -132,12 +132,14 @@ class WebView extends React.Component<WebViewSharedProps, State> {
const nativeConfig = this.props.nativeConfig || {};
const originWhitelist = (this.props.originWhitelist || []).map(
WebViewShared.originWhitelistToRegex,
);
let NativeWebView = nativeConfig.component || RNCWebView;
const onShouldStartLoadWithRequest = createOnShouldStartLoadWithRequest(
this.onShouldStartLoadWithRequestCallback,
this.props.originWhitelist,
this.props.onShouldStartLoadWithRequest,
);
const webView = (
<NativeWebView
ref={this.webViewRef}
@ -158,6 +160,7 @@ class WebView extends React.Component<WebViewSharedProps, State> {
automaticallyAdjustContentInsets={
this.props.automaticallyAdjustContentInsets
}
onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
onContentSizeChange={this.props.onContentSizeChange}
onLoadingStart={this.onLoadingStart}
onLoadingFinish={this.onLoadingFinish}
@ -171,7 +174,6 @@ class WebView extends React.Component<WebViewSharedProps, State> {
allowUniversalAccessFromFileURLs={
this.props.allowUniversalAccessFromFileURLs
}
originWhitelist={originWhitelist}
mixedContentMode={this.props.mixedContentMode}
saveFormDataDisabled={this.props.saveFormDataDisabled}
urlPrefixesForDefaultIntent={this.props.urlPrefixesForDefaultIntent}
@ -293,9 +295,22 @@ class WebView extends React.Component<WebViewSharedProps, State> {
};
onLoadingProgress = (event: WebViewProgressEvent) => {
const { onLoadProgress} = this.props;
const { onLoadProgress } = this.props;
onLoadProgress && onLoadProgress(event);
}
};
onShouldStartLoadWithRequestCallback = (
shouldStart: boolean,
url: string,
) => {
if (shouldStart) {
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
UIManager.RNCWebView.Commands.loadUrl,
[String(url)],
);
}
};
}
const RNCWebView = requireNativeComponent('RNCWebView');

View File

@ -25,7 +25,10 @@ import {
import invariant from 'fbjs/lib/invariant';
import keyMirror from 'fbjs/lib/keyMirror';
import WebViewShared from './WebViewShared';
import {
defaultOriginWhitelist,
createOnShouldStartLoadWithRequest,
} from './WebViewShared';
import type {
WebViewEvent,
WebViewError,
@ -131,7 +134,8 @@ class WebView extends React.Component<WebViewSharedProps, State> {
static defaultProps = {
useWebKit: true,
enableCache: true,
originWhitelist: WebViewShared.defaultOriginWhitelist,
originWhitelist: defaultOriginWhitelist,
useSharedProcessPool: true,
};
static isFileUploadSupported = async () => {
@ -165,6 +169,15 @@ class WebView extends React.Component<WebViewSharedProps, State> {
'The allowsBackForwardNavigationGestures property is not supported when useWebKit = false',
);
}
if (
!this.props.useWebKit &&
this.props.incognito
) {
console.warn(
'The incognito property is not supported when useWebKit = false',
);
}
}
render() {
@ -205,40 +218,11 @@ class WebView extends React.Component<WebViewSharedProps, State> {
const nativeConfig = this.props.nativeConfig || {};
let viewManager = nativeConfig.viewManager;
if (this.props.useWebKit) {
viewManager = viewManager || RNCWKWebViewManager;
} else {
viewManager = viewManager || RNCUIWebViewManager;
}
const compiledWhitelist = [
'about:blank',
...(this.props.originWhitelist || []),
].map(WebViewShared.originWhitelistToRegex);
const onShouldStartLoadWithRequest = event => {
let shouldStart = true;
const { url } = event.nativeEvent;
const origin = WebViewShared.extractOrigin(url);
const passesWhitelist = compiledWhitelist.some(x =>
new RegExp(x).test(origin),
);
shouldStart = shouldStart && passesWhitelist;
if (!passesWhitelist) {
Linking.openURL(url);
}
if (this.props.onShouldStartLoadWithRequest) {
shouldStart =
shouldStart &&
this.props.onShouldStartLoadWithRequest(event.nativeEvent);
}
invariant(viewManager != null, 'viewManager expected to be non-null');
viewManager.startLoadWithResult(
!!shouldStart,
event.nativeEvent.lockIdentifier,
);
};
const onShouldStartLoadWithRequest = createOnShouldStartLoadWithRequest(
this.onShouldStartLoadWithRequestCallback,
this.props.originWhitelist,
this.props.onShouldStartLoadWithRequest,
);
const decelerationRate = processDecelerationRate(
this.props.decelerationRate,
@ -278,6 +262,7 @@ class WebView extends React.Component<WebViewSharedProps, State> {
}
hideKeyboardAccessoryView={this.props.hideKeyboardAccessoryView}
allowsBackForwardNavigationGestures={this.props.allowsBackForwardNavigationGestures}
incognito={this.props.incognito}
userAgent={this.props.userAgent}
onLoadingStart={this._onLoadingStart}
onLoadingFinish={this._onLoadingFinish}
@ -292,6 +277,7 @@ class WebView extends React.Component<WebViewSharedProps, State> {
this.props.mediaPlaybackRequiresUserAction
}
dataDetectorTypes={this.props.dataDetectorTypes}
useSharedProcessPool={this.props.useSharedProcessPool}
allowsLinkPreview={this.props.allowsLinkPreview}
{...nativeConfig.props}
/>
@ -442,9 +428,25 @@ class WebView extends React.Component<WebViewSharedProps, State> {
};
_onLoadingProgress = (event: WebViewProgressEvent) => {
const {onLoadProgress} = this.props;
const { onLoadProgress } = this.props;
onLoadProgress && onLoadProgress(event);
}
};
onShouldStartLoadWithRequestCallback = (
shouldStart: boolean,
url: string,
lockIdentifier: number,
) => {
let viewManager = (this.props.nativeConfig || {}).viewManager;
if (this.props.useWebKit) {
viewManager = viewManager || RNCWKWebViewManager;
} else {
viewManager = viewManager || RNCUIWebViewManager;
}
invariant(viewManager != null, 'viewManager expected to be non-null');
viewManager.startLoadWithResult(!!shouldStart, lockIdentifier);
};
componentDidUpdate(prevProps: WebViewSharedProps) {
if (!(prevProps.useWebKit && this.props.useWebKit)) {
@ -452,6 +454,7 @@ class WebView extends React.Component<WebViewSharedProps, State> {
}
this._showRedboxOnPropChanges(prevProps, 'allowsInlineMediaPlayback');
this._showRedboxOnPropChanges(prevProps, 'incognito');
this._showRedboxOnPropChanges(prevProps, 'mediaPlaybackRequiresUserAction');
this._showRedboxOnPropChanges(prevProps, 'dataDetectorTypes');

View File

@ -8,19 +8,58 @@
* @flow
*/
'use strict';
import escapeStringRegexp from 'escape-string-regexp';
import { Linking } from 'react-native';
import type {
WebViewNavigationEvent,
WebViewNavigation,
OnShouldStartLoadWithRequest,
} from './WebViewTypes';
const escapeStringRegexp = require('escape-string-regexp');
const defaultOriginWhitelist = ['http://*', 'https://*'];
const WebViewShared = {
defaultOriginWhitelist: ['http://*', 'https://*'],
extractOrigin: (url: string): string => {
const result = /^[A-Za-z0-9]+:(\/\/)?[^/]*/.exec(url);
return result === null ? '' : result[0];
},
originWhitelistToRegex: (originWhitelist: string): string => {
return escapeStringRegexp(originWhitelist).replace(/\\\*/g, '.*');
},
const extractOrigin = (url: string): string => {
const result = /^[A-Za-z0-9]+:(\/\/)?[^/]*/.exec(url);
return result === null ? '' : result[0];
};
module.exports = WebViewShared;
const originWhitelistToRegex = (originWhitelist: string): string =>
escapeStringRegexp(originWhitelist).replace(/\\\*/g, '.*');
const passesWhitelist = (compiledWhitelist: Array<string>, url: string) => {
const origin = extractOrigin(url);
return compiledWhitelist.some(x => new RegExp(x).test(origin));
};
const compileWhitelist = (
originWhitelist: ?$ReadOnlyArray<string>,
): Array<string> =>
['about:blank', ...(originWhitelist || [])].map(originWhitelistToRegex);
const createOnShouldStartLoadWithRequest = (
loadRequest: (
shouldStart: boolean,
url: string,
lockIdentifier: number,
) => void,
originWhitelist: ?$ReadOnlyArray<string>,
onShouldStartLoadWithRequest: ?OnShouldStartLoadWithRequest,
) => {
return ({ nativeEvent }: WebViewNavigationEvent) => {
let shouldStart = true;
const { url, lockIdentifier } = nativeEvent;
if (!passesWhitelist(compileWhitelist(originWhitelist), url)) {
Linking.openURL(url);
shouldStart = false
}
if (onShouldStartLoadWithRequest) {
shouldStart = onShouldStartLoadWithRequest(nativeEvent);
}
loadRequest(shouldStart, url, lockIdentifier);
};
};
export { defaultOriginWhitelist, createOnShouldStartLoadWithRequest };

View File

@ -10,12 +10,12 @@
'use strict';
import type {Node, Element, ComponentType} from 'react';
import type { Node, Element, ComponentType } from 'react';
import type {SyntheticEvent} from 'CoreEventTypes';
import type {EdgeInsetsProp} from 'EdgeInsetsPropType';
import type {ViewStyleProp} from 'StyleSheet';
import type {ViewProps} from 'ViewPropTypes';
import type { SyntheticEvent } from 'CoreEventTypes';
import type { EdgeInsetsProp } from 'EdgeInsetsPropType';
import type { ViewStyleProp } from 'StyleSheet';
import type { ViewProps } from 'ViewPropTypes';
export type WebViewNativeEvent = $ReadOnly<{|
url: string,
@ -23,12 +23,13 @@ export type WebViewNativeEvent = $ReadOnly<{|
title: string,
canGoBack: boolean,
canGoForward: boolean,
lockIdentifier: number,
|}>;
export type WebViewProgressEvent = $ReadOnly<{|
...WebViewNativeEvent,
progress: number,
|}>
...WebViewNativeEvent,
progress: number,
|}>;
export type WebViewNavigation = $ReadOnly<{|
...WebViewNativeEvent,
@ -118,22 +119,26 @@ export type WebViewSourceHtml = $ReadOnly<{|
export type WebViewSource = WebViewSourceUri | WebViewSourceHtml;
export type WebViewNativeConfig = $ReadOnly<{|
/*
/**
* The native component used to render the WebView.
*/
component?: ComponentType<WebViewSharedProps>,
/*
/**
* Set props directly on the native component WebView. Enables custom props which the
* original WebView doesn't pass through.
*/
props?: ?Object,
/*
/**
* Set the ViewManager to use for communication with the native side.
* @platform ios
*/
viewManager?: ?Object,
|}>;
export type OnShouldStartLoadWithRequest = (
event: WebViewNavigation,
) => boolean;
export type IOSWebViewProps = $ReadOnly<{|
/**
* If true, use WKWebView instead of UIWebView.
@ -205,17 +210,7 @@ export type IOSWebViewProps = $ReadOnly<{|
*
* @platform ios
*/
dataDetectorTypes?:
| ?DataDetectorTypes
| $ReadOnlyArray<DataDetectorTypes>,
/**
* Function that allows custom handling of any web view requests. Return
* `true` from the function to continue loading the request and `false`
* to stop loading.
* @platform ios
*/
onShouldStartLoadWithRequest?: (event: WebViewEvent) => mixed,
dataDetectorTypes?: ?DataDetectorTypes | $ReadOnlyArray<DataDetectorTypes>,
/**
* Boolean that determines whether HTML5 videos play inline or use the
@ -237,6 +232,13 @@ export type IOSWebViewProps = $ReadOnly<{|
* back-forward list navigations.
*/
allowsBackForwardNavigationGestures?: ?boolean,
/**
* A Boolean value indicating whether WebKit WebView should be created using a shared
* process pool, enabling WebViews to share cookies and localStorage between each other.
* Default is true but can be set to false for backwards compatibility.
* @platform ios
*/
useSharedProcessPool?: ?boolean,
/**
* The custom user agent string.
*/
@ -295,7 +297,7 @@ export type AndroidWebViewProps = $ReadOnly<{|
*/
saveFormDataDisabled?: ?boolean,
/*
/**
* Used on Android only, controls whether the given list of URL prefixes should
* make {@link com.facebook.react.views.webview.ReactWebViewClient} to launch a
* default activity intent for those URL instead of loading it within the webview.
@ -345,7 +347,7 @@ export type AndroidWebViewProps = $ReadOnly<{|
mixedContentMode?: ?('never' | 'always' | 'compatibility'),
|}>;
export type WebViewSharedProps = $ReadOnly<{|
export type WebViewSharedProps = $ReadOnly<{|
...ViewProps,
...IOSWebViewProps,
...AndroidWebViewProps,
@ -363,10 +365,19 @@ export type WebViewSharedProps = $ReadOnly<{|
*/
source?: ?WebViewSource,
/**
* Does not store any data within the lifetime of the WebView.
*/
incognito?: ?boolean,
/**
* Function that returns a view to show if there's an error.
*/
renderError: (errorDomain: ?string, errorCode: number, errorDesc: string) => Element<any>, // view to show if there's an error
renderError: (
errorDomain: ?string,
errorCode: number,
errorDesc: string,
) => Element<any>, // view to show if there's an error
/**
* Function that returns a loading indicator.
@ -457,6 +468,13 @@ export type WebViewSharedProps = $ReadOnly<{|
*/
originWhitelist?: $ReadOnlyArray<string>,
/**
* Function that allows custom handling of any web view requests. Return
* `true` from the function to continue loading the request and `false`
* to stop loading. The `navigationType` is always `other` on android.
*/
onShouldStartLoadWithRequest?: OnShouldStartLoadWithRequest,
/**
* Override the native component used to render the WebView. Enables a custom native
* WebView which uses the same JavaScript as the original WebView.

View File

@ -8,7 +8,7 @@
"Thibault Malbranche <malbranche.thibault@gmail.com>"
],
"license": "MIT",
"version": "2.13.0",
"version": "3.1.3",
"homepage": "https://github.com/react-native-community/react-native-webview#readme",
"scripts": {
"test:ios:flow": "flow check",