Merge branch 'master' into master
This commit is contained in:
commit
665c80239c
|
@ -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
|
||||
}
|
17
README.md
17
README.md
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
```
|
|
@ -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`.
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
|
|
|
@ -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.");
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue