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

This commit is contained in:
Jamon Holmgren 2019-01-11 08:28:10 -08:00 committed by GitHub
commit 06c1bb0657
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1235 additions and 299 deletions

View File

@ -2,6 +2,10 @@
**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)

View File

@ -1,91 +1,122 @@
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 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()
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."
)
}
}
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"
}

View File

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

View File

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

View File

@ -1,8 +1,13 @@
package com.reactnativecommunity.webview;
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;
@ -18,21 +23,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;
@ -51,11 +60,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;
@ -66,12 +83,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
@ -85,6 +104,7 @@ import org.json.JSONObject;
public class RNCWebViewManager extends SimpleViewManager<WebView> {
protected static final String REACT_CLASS = "RNCWebView";
private RNCWebViewPackage aPackage;
protected static final String HTML_ENCODING = "UTF-8";
protected static final String HTML_MIME_TYPE = "text/html";
@ -98,19 +118,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) {
@ -138,50 +157,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
@ -230,10 +215,6 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
public void setUrlPrefixesForDefaultIntent(ReadableArray specialUrls) {
mUrlPrefixesForDefaultIntent = specialUrls;
}
public void setOriginWhitelist(List<Pattern> originWhitelist) {
mOriginWhitelist = originWhitelist;
}
}
/**
@ -244,6 +225,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;
@ -284,6 +270,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);
@ -427,6 +429,25 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
callback.invoke(origin, true, false);
}
protected void openFileChooser(ValueCallback<Uri> filePathCallback, String acceptType) {
getModule().startPhotoPickerIntent(filePathCallback, acceptType);
}
protected void openFileChooser(ValueCallback<Uri> filePathCallback) {
getModule().startPhotoPickerIntent(filePathCallback, "");
}
protected void openFileChooser(ValueCallback<Uri> filePathCallback, String acceptType, String capture) {
getModule().startPhotoPickerIntent(filePathCallback, acceptType);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
String[] acceptTypes = fileChooserParams.getAcceptTypes();
boolean allowMultiple = fileChooserParams.getMode() == WebChromeClient.FileChooserParams.MODE_OPEN_MULTIPLE;
Intent intent = fileChooserParams.createIntent();
return getModule().startPhotoPickerIntent(filePathCallback, intent, acceptTypes, allowMultiple);
}
});
reactContext.addLifecycleEventListener(webView);
mWebViewConfig.configWebView(webView);
@ -453,6 +474,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;
}
@ -592,11 +660,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")
@ -636,20 +700,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
@ -658,9 +708,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
@ -671,7 +725,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
);
}
@ -714,6 +769,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;
}
}
@ -724,27 +785,22 @@ 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 =
reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher();
eventDispatcher.dispatchEvent(event);
}
public RNCWebViewPackage getPackage() {
return this.aPackage;
}
public void setPackage(RNCWebViewPackage aPackage) {
this.aPackage = aPackage;
}
public RNCWebViewModule getModule() {
return this.aPackage.getModule();
}
}

View File

@ -1,22 +1,381 @@
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.bridge.Callback;
import com.facebook.react.modules.core.PermissionAwareActivity;
import com.facebook.react.modules.core.PermissionListener;
public class RNCWebViewModule extends ReactContextBaseJavaModule {
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import static android.app.Activity.RESULT_OK;
public class RNCWebViewModule extends ReactContextBaseJavaModule implements ActivityEventListener {
private final ReactApplicationContext reactContext;
private RNCWebViewPackage aPackage;
private static final int PICKER = 1;
private static final int PICKER_LEGACY = 3;
private ValueCallback<Uri> filePathCallbackLegacy;
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) {
super(reactContext);
this.reactContext = reactContext;
reactContext.addActivityEventListener(this);
}
@Override
public String getName() {
return "RNCWebView";
}
}
@ReactMethod
public void isFileUploadSupported(final Promise promise) {
Boolean result = false;
int current = Build.VERSION.SDK_INT;
if (current >= Build.VERSION_CODES.LOLLIPOP) {
result = true;
}
if (current >= Build.VERSION_CODES.JELLY_BEAN && current <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
result = true;
}
promise.resolve(result);
}
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
if (filePathCallback == null && filePathCallbackLegacy == null) {
return;
}
// based off of which button was pressed, we get an activity result and a file
// the camera activity doesn't properly return the filename* (I think?) so we use
// this filename instead
switch (requestCode) {
case PICKER:
if (resultCode != RESULT_OK) {
if (filePathCallback != null) {
filePathCallback.onReceiveValue(null);
}
} else {
Uri result[] = this.getSelectedFiles(data, resultCode);
if (result != null) {
filePathCallback.onReceiveValue(result);
} else {
filePathCallback.onReceiveValue(new Uri[] { outputFileUri });
}
}
break;
case PICKER_LEGACY:
Uri result = resultCode != Activity.RESULT_OK ? null : data == null ? outputFileUri : data.getData();
filePathCallbackLegacy.onReceiveValue(result);
break;
}
filePathCallback = null;
filePathCallbackLegacy= null;
outputFileUri = null;
}
public void onNewIntent(Intent intent) {
}
private Uri[] getSelectedFiles(Intent data, int resultCode) {
if (data == null) {
return null;
}
// we have one file selected
if (data.getData() != null) {
if (resultCode == RESULT_OK && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return WebChromeClient.FileChooserParams.parseResult(resultCode, data);
} else {
return null;
}
}
// we have multiple files selected
if (data.getClipData() != null) {
final int numSelectedFiles = data.getClipData().getItemCount();
Uri[] result = new Uri[numSelectedFiles];
for (int i = 0; i < numSelectedFiles; i++) {
result[i] = data.getClipData().getItemAt(i).getUri();
}
return result;
}
return null;
}
public void startPhotoPickerIntent(ValueCallback<Uri> filePathCallback, String acceptType) {
filePathCallbackLegacy = filePathCallback;
Intent fileChooserIntent = getFileChooserIntent(acceptType);
Intent chooserIntent = Intent.createChooser(fileChooserIntent, "");
ArrayList<Parcelable> extraIntents = new ArrayList<>();
if (acceptsImages(acceptType)) {
extraIntents.add(getPhotoIntent());
}
if (acceptsVideo(acceptType)) {
extraIntents.add(getVideoIntent());
}
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Parcelable[]{}));
if (chooserIntent.resolveActivity(getCurrentActivity().getPackageManager()) != null) {
getCurrentActivity().startActivityForResult(chooserIntent, PICKER_LEGACY);
} else {
Log.w("RNCWebViewModule", "there is no Activity to handle this Intent");
}
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public boolean startPhotoPickerIntent(final ValueCallback<Uri[]> callback, final Intent intent, final String[] acceptTypes, final boolean allowMultiple) {
filePathCallback = callback;
ArrayList<Parcelable> extraIntents = new ArrayList<>();
if (acceptsImages(acceptTypes)) {
extraIntents.add(getPhotoIntent());
}
if (acceptsVideo(acceptTypes)) {
extraIntents.add(getVideoIntent());
}
Intent fileSelectionIntent = getFileChooserIntent(acceptTypes, allowMultiple);
Intent chooserIntent = new Intent(Intent.ACTION_CHOOSER);
chooserIntent.putExtra(Intent.EXTRA_INTENT, fileSelectionIntent);
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Parcelable[]{}));
if (chooserIntent.resolveActivity(getCurrentActivity().getPackageManager()) != null) {
getCurrentActivity().startActivityForResult(chooserIntent, PICKER);
} else {
Log.w("RNCWebViewModule", "there is no Activity to handle this Intent");
}
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;
}
public void setPackage(RNCWebViewPackage aPackage) {
this.aPackage = aPackage;
}
private Intent getPhotoIntent() {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
outputFileUri = getOutputUri(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, outputFileUri);
return intent;
}
private Intent getVideoIntent() {
Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
// @todo from experience, for Videos we get the data onActivityResult
// so there's no need to store the Uri
Uri outputVideoUri = getOutputUri(MediaStore.ACTION_VIDEO_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, outputVideoUri);
return intent;
}
private Intent getFileChooserIntent(String acceptTypes) {
String _acceptTypes = acceptTypes;
if (acceptTypes.isEmpty()) {
_acceptTypes = DEFAULT_MIME_TYPES;
}
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType(_acceptTypes);
return intent;
}
private Intent getFileChooserIntent(String[] acceptTypes, boolean allowMultiple) {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
intent.putExtra(Intent.EXTRA_MIME_TYPES, getAcceptedMimeType(acceptTypes));
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple);
return intent;
}
private Boolean acceptsImages(String types) {
return types.isEmpty() || types.toLowerCase().contains("image");
}
private Boolean acceptsImages(String[] types) {
return isArrayEmpty(types) || arrayContainsString(types, "image");
}
private Boolean acceptsVideo(String types) {
return types.isEmpty() || types.toLowerCase().contains("video");
}
private Boolean acceptsVideo(String[] types) {
return isArrayEmpty(types) || arrayContainsString(types, "video");
}
private Boolean arrayContainsString(String[] array, String pattern){
for(String content : array){
if(content.contains(pattern)){
return true;
}
}
return false;
}
private String[] getAcceptedMimeType(String[] types) {
if (isArrayEmpty(types)) {
return new String[]{DEFAULT_MIME_TYPES};
}
return types;
}
private Uri getOutputUri(String intentType) {
File capturedFile = null;
try {
capturedFile = getCapturedFile(intentType);
} catch (IOException e) {
Log.e("CREATE FILE", "Error occurred while creating the File", e);
e.printStackTrace();
}
// for versions below 6.0 (23) we use the old File creation & permissions model
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return Uri.fromFile(capturedFile);
}
// for versions 6.0+ (23) we use the FileProvider to avoid runtime permissions
String packageName = getReactApplicationContext().getPackageName();
return FileProvider.getUriForFile(getReactApplicationContext(), packageName+".fileprovider", capturedFile);
}
private File getCapturedFile(String intentType) throws IOException {
String prefix = "";
String suffix = "";
String dir = "";
String filename = "";
if (intentType.equals(MediaStore.ACTION_IMAGE_CAPTURE)) {
prefix = "image-";
suffix = ".jpg";
dir = Environment.DIRECTORY_PICTURES;
} else if (intentType.equals(MediaStore.ACTION_VIDEO_CAPTURE)) {
prefix = "video-";
suffix = ".mp4";
dir = Environment.DIRECTORY_MOVIES;
}
filename = prefix + String.valueOf(System.currentTimeMillis()) + suffix;
// for versions below 6.0 (23) we use the old File creation & permissions model
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
// only this Directory works on all tested Android versions
// ctx.getExternalFilesDir(dir) was failing on Android 5.0 (sdk 21)
File storageDir = Environment.getExternalStoragePublicDirectory(dir);
return new File(storageDir, filename);
}
File storageDir = getReactApplicationContext().getExternalFilesDir(null);
return File.createTempFile(filename, suffix, storageDir);
}
private Boolean isArrayEmpty(String[] arr) {
// when our array returned from getAcceptTypes() has no values set from the webview
// i.e. <input type="file" />, without any "accept" attr
// 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

@ -1,6 +1,7 @@
package com.reactnativecommunity.webview;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@ -10,10 +11,19 @@ import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import com.facebook.react.bridge.JavaScriptModule;
public class RNCWebViewPackage implements ReactPackage {
private RNCWebViewManager manager;
private RNCWebViewModule module;
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Arrays.<NativeModule>asList(new RNCWebViewModule(reactContext));
List<NativeModule> modulesList = new ArrayList<>();
module = new RNCWebViewModule(reactContext);
module.setPackage(this);
modulesList.add(module);
return modulesList;
}
// Deprecated from RN 0.47
@ -23,7 +33,12 @@ public class RNCWebViewPackage implements ReactPackage {
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
RNCWebViewManager viewManager = new RNCWebViewManager();
return Arrays.<ViewManager>asList(viewManager);
manager = new RNCWebViewManager();
manager.setPackage(this);
return Arrays.<ViewManager>asList(manager);
}
}
public RNCWebViewModule getModule() {
return module;
}
}

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

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

View File

@ -110,3 +110,83 @@ class MyWeb extends Component {
}
```
### Add support for File Upload
##### iOS
For iOS, all you need to do is specify the permissions in your `ios/[project]/Info.plist` file:
Photo capture:
```
<key>NSCameraUsageDescription</key>
<string>Take pictures for certain activities</string>
```
Gallery selection:
```
<key>NSPhotoLibraryUsageDescription</key>
<string>Select pictures for certain activities</string>
```
Video recording:
```
<key>NSMicrophoneUsageDescription</key>
<string>Need microphone access for recording videos</string>
```
##### Android
Add permission in AndroidManifest.xml:
```xml
<manifest ...>
......
<!-- this is required only for Android 4.1-5.1 (api 16-22) -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
......
</manifest>
```
##### Check for File Upload support, with `static isFileUploadSupported()`
File Upload using `<input type="file" />` is not supported for Android 4.4 KitKat (see [details](https://github.com/delight-im/Android-AdvancedWebView/issues/4#issuecomment-70372146)):
```
import { WebView } from "react-native-webview";
WebView.isFileUploadSupported().then(res => {
if (res === true) {
// file upload is supported
} else {
// not file upload support
}
});
```
### 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,8 +44,11 @@ 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)
- [`pagingEnabled`](Reference.md#pagingEnabled)
- [`allowsLinkPreview`](Reference.md#allowsLinkPreview)
## Methods Index
@ -100,7 +103,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 |
| ------ | -------- |
@ -273,6 +276,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 not 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:
@ -499,6 +512,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`.
@ -517,6 +540,26 @@ Sets whether the WebView should disable saving form data. The default value is `
| ------- | -------- | -------- |
| boolean | No | Android |
---
### `pagingEnabled`
If the value of this property is true, the scroll view stops on multiples of the scroll views bounds when the user scrolls. The default value is false.
| Type | Required | Platform |
| ------- | -------- | -------- |
| boolean | No | iOS |
---
### `allowsLinkPreview`
A Boolean value that determines whether pressing on a link displays a preview of the destination for the link. In iOS this property is available on devices that support 3D Touch. In iOS 10 and later, the default value is true; before that, the default value is false.
| Type | Required | Platform |
| ------- | -------- | -------- |
| boolean | No | iOS |
## Methods
### `extraNativeComponentConfig()`

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
@ -26,6 +26,7 @@ shouldStartLoadForRequest:(NSMutableDictionary<NSString *, id> *)request
@property (nonatomic, assign) BOOL messagingEnabled;
@property (nonatomic, copy) NSString *injectedJavaScript;
@property (nonatomic, assign) BOOL scrollEnabled;
@property (nonatomic, assign) BOOL pagingEnabled;
@property (nonatomic, assign) CGFloat decelerationRate;
@property (nonatomic, assign) BOOL allowsInlineMediaPlayback;
@property (nonatomic, assign) BOOL bounces;
@ -37,7 +38,10 @@ 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 allowsLinkPreview;
- (void)postMessage:(NSString *)message;
- (void)injectJavaScript:(NSString *)script;

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
@ -40,12 +41,7 @@ static NSString *const MessageHanderName = @"ReactNative";
BOOL _savedHideKeyboardAccessoryView;
}
- (void)dealloc
{
if(_webView){
[_webView removeObserver:self forKeyPath:@"estimatedProgress"];
}
}
- (void)dealloc{}
/**
* See https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/DisplayWebContent/Tasks/WebKitAvail.html.
@ -65,7 +61,6 @@ static NSString *const MessageHanderName = @"ReactNative";
return _webkitAvailable;
}
- (instancetype)initWithFrame:(CGRect)frame
{
if ((self = [super initWithFrame:frame])) {
@ -75,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;
}
@ -99,6 +81,12 @@ static NSString *const MessageHanderName = @"ReactNative";
};
WKWebViewConfiguration *wkWebViewConfig = [WKWebViewConfiguration new];
if (_incognito) {
wkWebViewConfig.websiteDataStore = [WKWebsiteDataStore nonPersistentDataStore];
}
if(self.useSharedProcessPool) {
wkWebViewConfig.processPool = [[RNCWKProcessPoolManager sharedManager] sharedProcessPool];
}
wkWebViewConfig.userContentController = [WKUserContentController new];
[wkWebViewConfig.userContentController addScriptMessageHandler: self name: MessageHanderName];
wkWebViewConfig.allowsInlineMediaPlayback = _allowsInlineMediaPlayback;
@ -116,9 +104,12 @@ static NSString *const MessageHanderName = @"ReactNative";
_webView.UIDelegate = self;
_webView.navigationDelegate = self;
_webView.scrollView.scrollEnabled = _scrollEnabled;
_webView.scrollView.pagingEnabled = _pagingEnabled;
_webView.scrollView.bounces = _bounces;
_webView.allowsLinkPreview = _allowsLinkPreview;
[_webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
_webView.allowsBackForwardNavigationGestures = _allowsBackForwardNavigationGestures;
if (_userAgent) {
_webView.customUserAgent = _userAgent;
}
@ -131,28 +122,19 @@ static NSString *const MessageHanderName = @"ReactNative";
[self addSubview:_webView];
[self setHideKeyboardAccessoryView: _savedHideKeyboardAccessoryView];
[self visitSource];
} else {
[_webView.configuration.userContentController removeScriptMessageHandlerForName:MessageHanderName];
}
}
-(void)keyboardWillHide
- (void)removeFromSuperview
{
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];
if (_webView) {
[_webView.configuration.userContentController removeScriptMessageHandlerForName:MessageHanderName];
[_webView removeObserver:self forKeyPath:@"estimatedProgress"];
[_webView removeFromSuperview];
_webView = nil;
}
}
-(void)keyboardDisplacementFix
{
// https://stackoverflow.com/a/9637807/824966
[UIView animateWithDuration:.25 animations:^{
self.webView.scrollView.contentOffset = CGPointMake(0, 0);
}];
[super removeFromSuperview];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
@ -329,6 +311,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
@ -402,6 +466,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,
@ -419,8 +490,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,7 +45,10 @@ 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(allowsLinkPreview, BOOL)
/**
* Expose methods to enable messaging the webview.
@ -69,6 +72,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,34 +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;
@ -68,11 +67,18 @@ class WebView extends React.Component<WebViewSharedProps, State> {
scalesPageToFit: true,
allowFileAccess: false,
saveFormDataDisabled: false,
originWhitelist: WebViewShared.defaultOriginWhitelist,
originWhitelist: defaultOriginWhitelist,
};
static isFileUploadSupported = async () => {
// native implementation should return "true" only for Android 5+
return NativeModules.RNCWebView.isFileUploadSupported();
}
state = {
viewState: this.props.startInLoadingState ? WebViewState.LOADING : WebViewState.IDLE,
viewState: this.props.startInLoadingState
? WebViewState.LOADING
: WebViewState.IDLE,
lastErrorEvent: null,
};
@ -125,12 +131,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}
@ -151,6 +159,7 @@ class WebView extends React.Component<WebViewSharedProps, State> {
automaticallyAdjustContentInsets={
this.props.automaticallyAdjustContentInsets
}
onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
onContentSizeChange={this.props.onContentSizeChange}
onLoadingStart={this.onLoadingStart}
onLoadingFinish={this.onLoadingFinish}
@ -164,7 +173,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}
@ -284,11 +292,24 @@ class WebView extends React.Component<WebViewSharedProps, State> {
const { onMessage } = this.props;
onMessage && onMessage(event);
};
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,
@ -130,9 +133,15 @@ class WebView extends React.Component<WebViewSharedProps, State> {
static defaultProps = {
useWebKit: true,
originWhitelist: WebViewShared.defaultOriginWhitelist,
originWhitelist: defaultOriginWhitelist,
useSharedProcessPool: true,
};
static isFileUploadSupported = async () => {
// no native implementation for iOS, depends only on permissions
return true;
}
state = {
viewState: this.props.startInLoadingState
? WebViewState.LOADING
@ -159,6 +168,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() {
@ -199,40 +217,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,
@ -264,6 +253,7 @@ class WebView extends React.Component<WebViewSharedProps, State> {
injectedJavaScript={this.props.injectedJavaScript}
bounces={this.props.bounces}
scrollEnabled={this.props.scrollEnabled}
pagingEnabled={this.props.pagingEnabled}
decelerationRate={decelerationRate}
contentInset={this.props.contentInset}
automaticallyAdjustContentInsets={
@ -271,6 +261,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}
@ -285,6 +276,8 @@ class WebView extends React.Component<WebViewSharedProps, State> {
this.props.mediaPlaybackRequiresUserAction
}
dataDetectorTypes={this.props.dataDetectorTypes}
useSharedProcessPool={this.props.useSharedProcessPool}
allowsLinkPreview={this.props.allowsLinkPreview}
{...nativeConfig.props}
/>
);
@ -434,9 +427,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)) {
@ -444,6 +453,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.
@ -168,6 +173,14 @@ export type IOSWebViewProps = $ReadOnly<{|
*/
scrollEnabled?: ?boolean,
/**
* If the value of this property is true, the scroll view stops on multiples
* of the scroll views bounds when the user scrolls.
* The default value is false.
* @platform ios
*/
pagingEnabled?: ?boolean,
/**
* The amount by which the web view content is inset from the edges of
* the scroll view. Defaults to {top: 0, left: 0, bottom: 0, right: 0}.
@ -197,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
@ -229,10 +232,27 @@ 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.
*/
userAgent?: ?string,
/**
* A Boolean value that determines whether pressing on a link
* displays a preview of the destination for the link.
*
* This property is available on devices that support 3D Touch.
* In iOS 10 and later, the default value is `true`; before that, the default value is `false`.
* @platform ios
*/
allowsLinkPreview?: ?boolean,
|}>;
export type AndroidWebViewProps = $ReadOnly<{|
@ -277,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.
@ -327,7 +347,7 @@ export type AndroidWebViewProps = $ReadOnly<{|
mixedContentMode?: ?('never' | 'always' | 'compatibility'),
|}>;
export type WebViewSharedProps = $ReadOnly<{|
export type WebViewSharedProps = $ReadOnly<{|
...ViewProps,
...IOSWebViewProps,
...AndroidWebViewProps,
@ -345,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.
@ -439,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.8.0",
"version": "3.1.1",
"homepage": "https://github.com/react-native-community/react-native-webview#readme",
"scripts": {
"test:ios:flow": "flow check",

34
typings/index.d.ts vendored
View File

@ -9,6 +9,12 @@ export interface WebViewNativeEvent {
readonly canGoForward: boolean;
}
export interface WebViewIOSLoadRequestEvent extends WebViewNativeEvent {
target: number;
lockIdentifier: number;
navigationType: "click" | "formsubmit" | "backforward" | "reload" | "formresubmit" | "other";
}
export interface WebViewProgressEvent extends WebViewNativeEvent {
readonly progress: number;
}
@ -145,6 +151,14 @@ export interface IOSWebViewProps {
*/
scrollEnabled?: boolean;
/**
* If the value of this property is true, the scroll view stops on multiples
* of the scroll views bounds when the user scrolls.
* The default value is false.
* @platform ios
*/
pagingEnabled?: boolean,
/**
* The amount by which the web view content is inset from the edges of
* the scroll view. Defaults to {top: 0, left: 0, bottom: 0, right: 0}.
@ -182,7 +196,7 @@ export interface IOSWebViewProps {
* to stop loading.
* @platform ios
*/
onShouldStartLoadWithRequest?: (event: WebViewNativeEvent) => any;
onShouldStartLoadWithRequest?: (event: WebViewIOSLoadRequestEvent) => any;
/**
* Boolean that determines whether HTML5 videos play inline or use the
@ -199,6 +213,20 @@ export interface IOSWebViewProps {
* backward compatible.
*/
hideKeyboardAccessoryView?: boolean;
/**
* If true, this will be able horizontal swipe gestures when using the WKWebView. The default value is `false`.
*/
allowsBackForwardNavigationGestures?: boolean;
/**
* A Boolean value that determines whether pressing on a link
* displays a preview of the destination for the link.
*
* This property is available on devices that support 3D Touch.
* In iOS 10 and later, the default value is `true`; before that, the default value is `false`.
* @platform ios
*/
allowsLinkPreview?: boolean;
}
export interface AndroidWebViewProps {
@ -370,7 +398,7 @@ export interface WebViewSharedProps extends ViewProps, IOSWebViewProps, AndroidW
* Boolean value that forces the `WebView` to show the loading view
* on the first load.
*/
startInLoadingState?: string;
startInLoadingState?: boolean;
/**
* Set this to provide JavaScript that will be injected into the web page
@ -417,4 +445,6 @@ export class WebView extends Component<WebViewSharedProps> {
public goBack: () => void;
public reload: () => void;
public stopLoading: () => void;
public postMessage: (msg: string) => void;
public injectJavaScript: (js: string) => void;
}