From e89a8b74761cd5a6ea27a4bd236bdba3072914df Mon Sep 17 00:00:00 2001 From: aleqsio Date: Mon, 19 Jun 2023 10:31:03 +0200 Subject: [PATCH] Updated vendored react-native-webview to v13.2.2 (#22913) --- .../webview/RNCBasicAuthCredential.java | 11 + .../webview/RNCWebChromeClient.java | 346 +++ .../api/components/webview/RNCWebView.java | 418 ++++ .../components/webview/RNCWebViewClient.java | 318 +++ ...bViewConfig.java => RNCWebViewConfig.java} | 7 +- .../webview/RNCWebViewFileProvider.java | 2 +- .../components/webview/RNCWebViewManager.java | 1897 ++--------------- .../webview/RNCWebViewManagerImpl.kt | 652 ++++++ .../components/webview/RNCWebViewModule.java | 547 +---- .../webview/RNCWebViewModuleImpl.java | 554 +++++ .../components/webview/RNCWebViewPackage.java | 57 + .../components/webview/RNCWebViewPackage.kt | 15 - .../events/TopCustomMenuSelectionEvent.kt | 24 + apps/bare-expo/package.json | 2 +- apps/native-component-list/package.json | 2 +- ios/Podfile.lock | 4 +- .../react-native-webview/apple/RNCWebView.h | 134 +- .../react-native-webview/apple/RNCWebView.mm | 504 +++++ .../apple/RNCWebViewDecisionManager.h | 20 + .../apple/RNCWebViewDecisionManager.m | 47 + .../apple/RNCWebViewImpl.h | 148 ++ .../apple/{RNCWebView.m => RNCWebViewImpl.m} | 477 +++-- .../apple/RNCWebViewManager.h | 12 +- ...CWebViewManager.m => RNCWebViewManager.mm} | 251 +-- .../react-native-webview.podspec.json | 6 +- packages/expo/bundledNativeModules.json | 2 +- tools/src/vendoring/config/expoGoConfig.ts | 33 +- tools/src/vendoring/legacy.ts | 18 + yarn.lock | 8 +- 29 files changed, 3766 insertions(+), 2750 deletions(-) create mode 100644 android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCBasicAuthCredential.java create mode 100644 android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebChromeClient.java create mode 100644 android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebView.java create mode 100644 android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebViewClient.java rename android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/{WebViewConfig.java => RNCWebViewConfig.java} (79%) create mode 100644 android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebViewManagerImpl.kt create mode 100644 android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebViewModuleImpl.java create mode 100644 android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebViewPackage.java delete mode 100644 android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebViewPackage.kt create mode 100644 android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/events/TopCustomMenuSelectionEvent.kt create mode 100644 ios/vendored/unversioned/react-native-webview/apple/RNCWebView.mm create mode 100644 ios/vendored/unversioned/react-native-webview/apple/RNCWebViewDecisionManager.h create mode 100644 ios/vendored/unversioned/react-native-webview/apple/RNCWebViewDecisionManager.m create mode 100644 ios/vendored/unversioned/react-native-webview/apple/RNCWebViewImpl.h rename ios/vendored/unversioned/react-native-webview/apple/{RNCWebView.m => RNCWebViewImpl.m} (89%) rename ios/vendored/unversioned/react-native-webview/apple/{RNCWebViewManager.m => RNCWebViewManager.mm} (51%) diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCBasicAuthCredential.java b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCBasicAuthCredential.java new file mode 100644 index 0000000000000..35dd54d4cd9ee --- /dev/null +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCBasicAuthCredential.java @@ -0,0 +1,11 @@ +package versioned.host.exp.exponent.modules.api.components.webview; + +class RNCBasicAuthCredential { + String username; + String password; + + RNCBasicAuthCredential(String username, String password) { + this.username = username; + this.password = password; + } +} \ No newline at end of file diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebChromeClient.java b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebChromeClient.java new file mode 100644 index 0000000000000..8bd0205e3d9a2 --- /dev/null +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebChromeClient.java @@ -0,0 +1,346 @@ +package versioned.host.exp.exponent.modules.api.components.webview; + +import android.Manifest; +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Build; +import android.os.Message; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.webkit.ConsoleMessage; +import android.webkit.GeolocationPermissions; +import android.webkit.PermissionRequest; +import android.webkit.ValueCallback; +import android.webkit.WebChromeClient; +import android.webkit.WebView; +import android.widget.FrameLayout; + +import androidx.annotation.RequiresApi; +import androidx.core.content.ContextCompat; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.LifecycleEventListener; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.common.build.ReactBuildConfig; +import com.facebook.react.modules.core.PermissionAwareActivity; +import com.facebook.react.modules.core.PermissionListener; +import com.facebook.react.uimanager.UIManagerHelper; +import versioned.host.exp.exponent.modules.api.components.webview.events.TopLoadingProgressEvent; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class RNCWebChromeClient extends WebChromeClient implements LifecycleEventListener { + protected static final FrameLayout.LayoutParams FULLSCREEN_LAYOUT_PARAMS = new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER); + + protected static final int FULLSCREEN_SYSTEM_UI_VISIBILITY = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | + View.SYSTEM_UI_FLAG_LAYOUT_STABLE | + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_FULLSCREEN | + View.SYSTEM_UI_FLAG_IMMERSIVE | + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; + + protected static final int COMMON_PERMISSION_REQUEST = 3; + + protected RNCWebView mWebView; + + protected View mVideoView; + protected WebChromeClient.CustomViewCallback mCustomViewCallback; + + /* + * - Permissions - + * As native permissions are asynchronously handled by the PermissionListener, many fields have + * to be stored to send permissions results to the webview + */ + + // Webview camera & audio permission callback + protected PermissionRequest permissionRequest; + // Webview camera & audio permission already granted + protected List grantedPermissions; + + // Webview geolocation permission callback + protected GeolocationPermissions.Callback geolocationPermissionCallback; + // Webview geolocation permission origin callback + protected String geolocationPermissionOrigin; + + // true if native permissions dialog is shown, false otherwise + protected boolean permissionsRequestShown = false; + // Pending Android permissions for the next request + protected List pendingPermissions = new ArrayList<>(); + + protected RNCWebView.ProgressChangedFilter progressChangedFilter = null; + protected boolean mAllowsProtectedMedia = false; + + public RNCWebChromeClient(RNCWebView webView) { + this.mWebView = webView; + } + + @Override + public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) { + + final WebView newWebView = new WebView(view.getContext()); + final WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj; + transport.setWebView(newWebView); + resultMsg.sendToTarget(); + + return true; + } + + @Override + public boolean onConsoleMessage(ConsoleMessage message) { + if (ReactBuildConfig.DEBUG) { + return super.onConsoleMessage(message); + } + // Ignore console logs in non debug builds. + return true; + } + + @Override + public void onProgressChanged(WebView webView, int newProgress) { + super.onProgressChanged(webView, newProgress); + final String url = webView.getUrl(); + if (progressChangedFilter.isWaitingForCommandLoadUrl()) { + return; + } + WritableMap event = Arguments.createMap(); + event.putDouble("target", webView.getId()); + event.putString("title", webView.getTitle()); + event.putString("url", url); + event.putBoolean("canGoBack", webView.canGoBack()); + event.putBoolean("canGoForward", webView.canGoForward()); + event.putDouble("progress", (float) newProgress / 100); + + int reactTag = webView.getId(); + UIManagerHelper.getEventDispatcherForReactTag(this.mWebView.getThemedReactContext(), reactTag).dispatchEvent(new TopLoadingProgressEvent(reactTag, event)); + } + + @Override + public void onPermissionRequest(final PermissionRequest request) { + + grantedPermissions = new ArrayList<>(); + + ArrayList requestedAndroidPermissions = new ArrayList<>(); + for (String requestedResource : request.getResources()) { + String androidPermission = null; + + if (requestedResource.equals(PermissionRequest.RESOURCE_AUDIO_CAPTURE)) { + androidPermission = Manifest.permission.RECORD_AUDIO; + } else if (requestedResource.equals(PermissionRequest.RESOURCE_VIDEO_CAPTURE)) { + androidPermission = Manifest.permission.CAMERA; + } else if(requestedResource.equals(PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID)) { + if (mAllowsProtectedMedia) { + grantedPermissions.add(requestedResource); + } else { + /** + * Legacy handling (Kept in case it was working under some conditions (given Android version or something)) + * + * Try to ask user to grant permission using Activity.requestPermissions + * + * Find more details here: https://github.com/react-native-webview/react-native-webview/pull/2732 + */ + androidPermission = PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID; + } } + // TODO: RESOURCE_MIDI_SYSEX, RESOURCE_PROTECTED_MEDIA_ID. + if (androidPermission != null) { + if (ContextCompat.checkSelfPermission(this.mWebView.getThemedReactContext(), androidPermission) == PackageManager.PERMISSION_GRANTED) { + grantedPermissions.add(requestedResource); + } else { + requestedAndroidPermissions.add(androidPermission); + } + } + } + + // If all the permissions are already granted, send the response to the WebView synchronously + if (requestedAndroidPermissions.isEmpty()) { + request.grant(grantedPermissions.toArray(new String[0])); + grantedPermissions = null; + return; + } + + // Otherwise, ask to Android System for native permissions asynchronously + + this.permissionRequest = request; + + requestPermissions(requestedAndroidPermissions); + } + + + @Override + public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) { + + if (ContextCompat.checkSelfPermission(this.mWebView.getThemedReactContext(), Manifest.permission.ACCESS_FINE_LOCATION) + != PackageManager.PERMISSION_GRANTED) { + + /* + * Keep the trace of callback and origin for the async permission request + */ + geolocationPermissionCallback = callback; + geolocationPermissionOrigin = origin; + + requestPermissions(Collections.singletonList(Manifest.permission.ACCESS_FINE_LOCATION)); + + } else { + callback.invoke(origin, true, false); + } + } + + private PermissionAwareActivity getPermissionAwareActivity() { + Activity activity = this.mWebView.getThemedReactContext().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 synchronized void requestPermissions(List permissions) { + + /* + * If permissions request dialog is displayed on the screen and another request is sent to the + * activity, the last permission asked is skipped. As a work-around, we use pendingPermissions + * to store next required permissions. + */ + + if (permissionsRequestShown) { + pendingPermissions.addAll(permissions); + return; + } + + PermissionAwareActivity activity = getPermissionAwareActivity(); + permissionsRequestShown = true; + + activity.requestPermissions( + permissions.toArray(new String[0]), + COMMON_PERMISSION_REQUEST, + webviewPermissionsListener + ); + + // Pending permissions have been sent, the list can be cleared + pendingPermissions.clear(); + } + + + private PermissionListener webviewPermissionsListener = (requestCode, permissions, grantResults) -> { + + permissionsRequestShown = false; + + /* + * As a "pending requests" approach is used, requestCode cannot help to define if the request + * came from geolocation or camera/audio. This is why shouldAnswerToPermissionRequest is used + */ + boolean shouldAnswerToPermissionRequest = false; + + for (int i = 0; i < permissions.length; i++) { + + String permission = permissions[i]; + boolean granted = grantResults[i] == PackageManager.PERMISSION_GRANTED; + + if (permission.equals(Manifest.permission.ACCESS_FINE_LOCATION) + && geolocationPermissionCallback != null + && geolocationPermissionOrigin != null) { + + if (granted) { + geolocationPermissionCallback.invoke(geolocationPermissionOrigin, true, false); + } else { + geolocationPermissionCallback.invoke(geolocationPermissionOrigin, false, false); + } + + geolocationPermissionCallback = null; + geolocationPermissionOrigin = null; + } + + if (permission.equals(Manifest.permission.RECORD_AUDIO)) { + if (granted && grantedPermissions != null) { + grantedPermissions.add(PermissionRequest.RESOURCE_AUDIO_CAPTURE); + } + shouldAnswerToPermissionRequest = true; + } + + if (permission.equals(Manifest.permission.CAMERA)) { + if (granted && grantedPermissions != null) { + grantedPermissions.add(PermissionRequest.RESOURCE_VIDEO_CAPTURE); + } + shouldAnswerToPermissionRequest = true; + } + + if (permission.equals(PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID)) { + if (granted && grantedPermissions != null) { + grantedPermissions.add(PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID); + } + shouldAnswerToPermissionRequest = true; + } + } + + if (shouldAnswerToPermissionRequest + && permissionRequest != null + && grantedPermissions != null) { + permissionRequest.grant(grantedPermissions.toArray(new String[0])); + permissionRequest = null; + grantedPermissions = null; + } + + if (!pendingPermissions.isEmpty()) { + requestPermissions(pendingPermissions); + return false; + } + + return true; + }; + + protected void openFileChooser(ValueCallback filePathCallback, String acceptType) { + this.mWebView.getThemedReactContext().getNativeModule(RNCWebViewModule.class).startPhotoPickerIntent(filePathCallback, acceptType); + } + + protected void openFileChooser(ValueCallback filePathCallback) { + this.mWebView.getThemedReactContext().getNativeModule(RNCWebViewModule.class).startPhotoPickerIntent(filePathCallback, ""); + } + + protected void openFileChooser(ValueCallback filePathCallback, String acceptType, String capture) { + this.mWebView.getThemedReactContext().getNativeModule(RNCWebViewModule.class).startPhotoPickerIntent(filePathCallback, acceptType); + } + + @Override + public boolean onShowFileChooser(WebView webView, ValueCallback filePathCallback, FileChooserParams fileChooserParams) { + String[] acceptTypes = fileChooserParams.getAcceptTypes(); + boolean allowMultiple = fileChooserParams.getMode() == WebChromeClient.FileChooserParams.MODE_OPEN_MULTIPLE; + + return this.mWebView.getThemedReactContext().getNativeModule(RNCWebViewModule.class).startPhotoPickerIntent(filePathCallback, acceptTypes, allowMultiple, fileChooserParams.isCaptureEnabled()); + } + + @Override + public void onHostResume() { + if (mVideoView != null && mVideoView.getSystemUiVisibility() != FULLSCREEN_SYSTEM_UI_VISIBILITY) { + mVideoView.setSystemUiVisibility(FULLSCREEN_SYSTEM_UI_VISIBILITY); + } + } + + @Override + public void onHostPause() { } + + @Override + public void onHostDestroy() { } + + protected ViewGroup getRootView() { + return this.mWebView.getThemedReactContext().getCurrentActivity().findViewById(android.R.id.content); + } + + public void setProgressChangedFilter(RNCWebView.ProgressChangedFilter filter) { + progressChangedFilter = filter; + } + + /** + * Set whether or not protected media should be allowed + * /!\ Setting this to false won't revoke permission already granted to the current webpage. + * In order to do so, you'd need to reload the page /!\ + */ + public void setAllowsProtectedMedia(boolean enabled) { + mAllowsProtectedMedia = enabled; + } +} \ No newline at end of file diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebView.java b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebView.java new file mode 100644 index 0000000000000..68c90e3f81e9e --- /dev/null +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebView.java @@ -0,0 +1,418 @@ +package versioned.host.exp.exponent.modules.api.components.webview; + +import androidx.annotation.Nullable; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Rect; +import android.os.Build; +import android.text.TextUtils; +import android.util.AttributeSet; + +import android.view.ActionMode; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.webkit.JavascriptInterface; +import android.webkit.ValueCallback; +import android.webkit.WebChromeClient; +import android.webkit.WebView; +import android.webkit.WebViewClient; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.CatalystInstance; +import com.facebook.react.bridge.LifecycleEventListener; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeArray; +import com.facebook.react.bridge.WritableNativeMap; +import com.facebook.react.uimanager.ThemedReactContext; +import com.facebook.react.uimanager.UIManagerHelper; +import com.facebook.react.uimanager.UIManagerModule; +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.facebook.react.views.scroll.OnScrollDispatchHelper; +import com.facebook.react.views.scroll.ScrollEvent; +import com.facebook.react.views.scroll.ScrollEventType; +import versioned.host.exp.exponent.modules.api.components.webview.events.TopCustomMenuSelectionEvent; +import versioned.host.exp.exponent.modules.api.components.webview.events.TopLoadingProgressEvent; +import versioned.host.exp.exponent.modules.api.components.webview.events.TopMessageEvent; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.List; +import java.util.Map; + +public class RNCWebView extends WebView implements LifecycleEventListener { + protected @Nullable + String injectedJS; + protected @Nullable + String injectedJSBeforeContentLoaded; + protected static final String JAVASCRIPT_INTERFACE = "ReactNativeWebView"; + + /** + * android.webkit.WebChromeClient fundamentally does not support JS injection into frames other + * than the main frame, so these two properties are mostly here just for parity with iOS & macOS. + */ + protected boolean injectedJavaScriptForMainFrameOnly = true; + protected boolean injectedJavaScriptBeforeContentLoadedForMainFrameOnly = true; + + protected boolean messagingEnabled = false; + protected @Nullable + String messagingModuleName; + protected @Nullable + RNCWebViewClient mRNCWebViewClient; + protected @Nullable + CatalystInstance mCatalystInstance; + protected boolean sendContentSizeChangeEvents = false; + private OnScrollDispatchHelper mOnScrollDispatchHelper; + protected boolean hasScrollEvent = false; + protected boolean nestedScrollEnabled = false; + protected ProgressChangedFilter progressChangedFilter; + + /** + * WebView must be created with an context of the current activity + *

+ * Activity Context is required for creation of dialogs internally by WebView + * Reactive Native needed for access to ReactNative internal system functionality + */ + public RNCWebView(ThemedReactContext reactContext) { + super(reactContext); + this.createCatalystInstance(); + progressChangedFilter = new ProgressChangedFilter(); + } + + public void setIgnoreErrFailedForThisURL(String url) { + mRNCWebViewClient.setIgnoreErrFailedForThisURL(url); + } + + public void setBasicAuthCredential(RNCBasicAuthCredential credential) { + mRNCWebViewClient.setBasicAuthCredential(credential); + } + + public void setSendContentSizeChangeEvents(boolean sendContentSizeChangeEvents) { + this.sendContentSizeChangeEvents = sendContentSizeChangeEvents; + } + + public void setHasScrollEvent(boolean hasScrollEvent) { + this.hasScrollEvent = hasScrollEvent; + } + + public void setNestedScrollEnabled(boolean nestedScrollEnabled) { + this.nestedScrollEnabled = nestedScrollEnabled; + } + + @Override + public void onHostResume() { + // do nothing + } + + @Override + public void onHostPause() { + // do nothing + } + + @Override + public void onHostDestroy() { + cleanupCallbacksAndDestroy(); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (this.nestedScrollEnabled) { + requestDisallowInterceptTouchEvent(true); + } + return super.onTouchEvent(event); + } + + @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 + ) + ); + } + } + + protected @Nullable + List> menuCustomItems; + + public void setMenuCustomItems(List> menuCustomItems) { + this.menuCustomItems = menuCustomItems; + } + + @Override + public ActionMode startActionMode(ActionMode.Callback callback, int type) { + if(menuCustomItems == null ){ + return super.startActionMode(callback, type); + } + + return super.startActionMode(new ActionMode.Callback2() { + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + for (int i = 0; i < menuCustomItems.size(); i++) { + menu.add(Menu.NONE, i, i, (menuCustomItems.get(i)).get("label")); + } + return true; + } + + @Override + public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) { + return false; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + WritableMap wMap = Arguments.createMap(); + RNCWebView.this.evaluateJavascript( + "(function(){return {selection: window.getSelection().toString()} })()", + new ValueCallback() { + @Override + public void onReceiveValue(String selectionJson) { + Map menuItemMap = menuCustomItems.get(item.getItemId()); + wMap.putString("label", menuItemMap.get("label")); + wMap.putString("key", menuItemMap.get("key")); + String selectionText = ""; + try { + selectionText = new JSONObject(selectionJson).getString("selection"); + } catch (JSONException ignored) {} + wMap.putString("selectedText", selectionText); + dispatchEvent(RNCWebView.this, new TopCustomMenuSelectionEvent(RNCWebView.this.getId(), wMap)); + mode.finish(); + } + } + ); + return true; + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + mode = null; + } + + @Override + public void onGetContentRect (ActionMode mode, + View view, + Rect outRect){ + if (callback instanceof ActionMode.Callback2) { + ((ActionMode.Callback2) callback).onGetContentRect(mode, view, outRect); + } else { + super.onGetContentRect(mode, view, outRect); + } + } + }, type); + } + + @Override + public void setWebViewClient(WebViewClient client) { + super.setWebViewClient(client); + if (client instanceof RNCWebViewClient) { + mRNCWebViewClient = (RNCWebViewClient) client; + mRNCWebViewClient.setProgressChangedFilter(progressChangedFilter); + } + } + + WebChromeClient mWebChromeClient; + @Override + public void setWebChromeClient(WebChromeClient client) { + this.mWebChromeClient = client; + super.setWebChromeClient(client); + if (client instanceof RNCWebChromeClient) { + ((RNCWebChromeClient) client).setProgressChangedFilter(progressChangedFilter); + } + } + + public WebChromeClient getWebChromeClient() { + return this.mWebChromeClient; + } + + public @Nullable + RNCWebViewClient getRNCWebViewClient() { + return mRNCWebViewClient; + } + + protected RNCWebViewBridge createRNCWebViewBridge(RNCWebView webView) { + return new RNCWebViewBridge(webView); + } + + protected void createCatalystInstance() { + ThemedReactContext reactContext = (ThemedReactContext) this.getContext(); + + if (reactContext != null) { + mCatalystInstance = reactContext.getCatalystInstance(); + } + } + + @SuppressLint("AddJavascriptInterface") + public void setMessagingEnabled(boolean enabled) { + if (messagingEnabled == enabled) { + return; + } + + messagingEnabled = enabled; + + if (enabled) { + addJavascriptInterface(createRNCWebViewBridge(this), JAVASCRIPT_INTERFACE); + } else { + removeJavascriptInterface(JAVASCRIPT_INTERFACE); + } + } + + protected void evaluateJavascriptWithFallback(String script) { + evaluateJavascript(script, null); + } + + public void callInjectedJavaScript() { + if (getSettings().getJavaScriptEnabled() && + injectedJS != null && + !TextUtils.isEmpty(injectedJS)) { + evaluateJavascriptWithFallback("(function() {\n" + injectedJS + ";\n})();"); + } + } + + public void callInjectedJavaScriptBeforeContentLoaded() { + if (getSettings().getJavaScriptEnabled() && + injectedJSBeforeContentLoaded != null && + !TextUtils.isEmpty(injectedJSBeforeContentLoaded)) { + evaluateJavascriptWithFallback("(function() {\n" + injectedJSBeforeContentLoaded + ";\n})();"); + } + } + + public void onMessage(String message) { + ThemedReactContext reactContext = getThemedReactContext(); + RNCWebView mWebView = this; + + if (mRNCWebViewClient != null) { + WebView webView = this; + webView.post(new Runnable() { + @Override + public void run() { + if (mRNCWebViewClient == null) { + return; + } + WritableMap data = mRNCWebViewClient.createWebViewEvent(webView, webView.getUrl()); + data.putString("data", message); + + if (mCatalystInstance != null) { + mWebView.sendDirectMessage("onMessage", data); + } else { + dispatchEvent(webView, new TopMessageEvent(webView.getId(), data)); + } + } + }); + } else { + WritableMap eventData = Arguments.createMap(); + eventData.putString("data", message); + + if (mCatalystInstance != null) { + this.sendDirectMessage("onMessage", eventData); + } else { + dispatchEvent(this, new TopMessageEvent(this.getId(), eventData)); + } + } + } + + protected void sendDirectMessage(final String method, WritableMap data) { + WritableNativeMap event = new WritableNativeMap(); + event.putMap("nativeEvent", data); + + WritableNativeArray params = new WritableNativeArray(); + params.pushMap(event); + + mCatalystInstance.callFunction(messagingModuleName, method, params); + } + + protected void onScrollChanged(int x, int y, int oldX, int oldY) { + super.onScrollChanged(x, y, oldX, oldY); + + if (!hasScrollEvent) { + return; + } + + if (mOnScrollDispatchHelper == null) { + mOnScrollDispatchHelper = new OnScrollDispatchHelper(); + } + + if (mOnScrollDispatchHelper.onScrollChanged(x, y)) { + ScrollEvent event = ScrollEvent.obtain( + this.getId(), + ScrollEventType.SCROLL, + x, + y, + mOnScrollDispatchHelper.getXFlingVelocity(), + mOnScrollDispatchHelper.getYFlingVelocity(), + this.computeHorizontalScrollRange(), + this.computeVerticalScrollRange(), + this.getWidth(), + this.getHeight()); + + dispatchEvent(this, event); + } + } + + protected void dispatchEvent(WebView webView, Event event) { + ThemedReactContext reactContext = getThemedReactContext(); + int reactTag = webView.getId(); + UIManagerHelper.getEventDispatcherForReactTag(reactContext, reactTag).dispatchEvent(event); + } + + protected void cleanupCallbacksAndDestroy() { + setWebViewClient(null); + destroy(); + } + + @Override + public void destroy() { + if (mWebChromeClient != null) { + mWebChromeClient.onHideCustomView(); + } + super.destroy(); + } + + public ThemedReactContext getThemedReactContext() { + return (ThemedReactContext) this.getContext(); + } + + protected class RNCWebViewBridge { + RNCWebView mWebView; + + RNCWebViewBridge(RNCWebView c) { + mWebView = c; + } + + /** + * This method is called whenever JavaScript running within the web view calls: + * - window[JAVASCRIPT_INTERFACE].postMessage + */ + @JavascriptInterface + public void postMessage(String message) { + mWebView.onMessage(message); + } + } + + + protected static class ProgressChangedFilter { + private boolean waitingForCommandLoadUrl = false; + + public void setWaitingForCommandLoadUrl(boolean isWaiting) { + waitingForCommandLoadUrl = isWaiting; + } + + public boolean isWaitingForCommandLoadUrl() { + return waitingForCommandLoadUrl; + } + } +} \ No newline at end of file diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebViewClient.java b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebViewClient.java new file mode 100644 index 0000000000000..c9f8018759c95 --- /dev/null +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebViewClient.java @@ -0,0 +1,318 @@ +package versioned.host.exp.exponent.modules.api.components.webview; + +import android.annotation.TargetApi; +import android.graphics.Bitmap; +import android.net.http.SslError; +import android.os.Build; +import android.os.SystemClock; +import android.util.Log; +import android.webkit.HttpAuthHandler; +import android.webkit.RenderProcessGoneDetail; +import android.webkit.SslErrorHandler; +import android.webkit.WebResourceRequest; +import android.webkit.WebResourceResponse; +import android.webkit.WebView; +import android.webkit.WebViewClient; + +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.core.util.Pair; + +import com.facebook.common.logging.FLog; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.uimanager.ThemedReactContext; +import com.facebook.react.uimanager.UIManagerHelper; +import versioned.host.exp.exponent.modules.api.components.webview.events.TopHttpErrorEvent; +import versioned.host.exp.exponent.modules.api.components.webview.events.TopLoadingErrorEvent; +import versioned.host.exp.exponent.modules.api.components.webview.events.TopLoadingFinishEvent; +import versioned.host.exp.exponent.modules.api.components.webview.events.TopLoadingStartEvent; +import versioned.host.exp.exponent.modules.api.components.webview.events.TopRenderProcessGoneEvent; +import versioned.host.exp.exponent.modules.api.components.webview.events.TopShouldStartLoadWithRequestEvent; + +import java.util.concurrent.atomic.AtomicReference; + +public class RNCWebViewClient extends WebViewClient { + private static String TAG = "RNCWebViewClient"; + protected static final int SHOULD_OVERRIDE_URL_LOADING_TIMEOUT = 250; + + protected boolean mLastLoadFailed = false; + protected RNCWebView.ProgressChangedFilter progressChangedFilter = null; + protected @Nullable String ignoreErrFailedForThisURL = null; + protected @Nullable RNCBasicAuthCredential basicAuthCredential = null; + + public void setIgnoreErrFailedForThisURL(@Nullable String url) { + ignoreErrFailedForThisURL = url; + } + + public void setBasicAuthCredential(@Nullable RNCBasicAuthCredential credential) { + basicAuthCredential = credential; + } + + @Override + public void onPageFinished(WebView webView, String url) { + super.onPageFinished(webView, url); + + if (!mLastLoadFailed) { + RNCWebView reactWebView = (RNCWebView) webView; + + reactWebView.callInjectedJavaScript(); + + emitFinishEvent(webView, url); + } + } + + @Override + public void doUpdateVisitedHistory (WebView webView, String url, boolean isReload) { + super.doUpdateVisitedHistory(webView, url, isReload); + + ((RNCWebView) webView).dispatchEvent( + webView, + new TopLoadingStartEvent( + webView.getId(), + createWebViewEvent(webView, url))); + } + + @Override + public void onPageStarted(WebView webView, String url, Bitmap favicon) { + super.onPageStarted(webView, url, favicon); + mLastLoadFailed = false; + + RNCWebView reactWebView = (RNCWebView) webView; + reactWebView.callInjectedJavaScriptBeforeContentLoaded(); + } + + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + final RNCWebView rncWebView = (RNCWebView) view; + final boolean isJsDebugging = ((ReactContext) view.getContext()).getJavaScriptContextHolder().get() == 0; + + if (!isJsDebugging && rncWebView.mCatalystInstance != null) { + final Pair> lock = RNCWebViewModuleImpl.shouldOverrideUrlLoadingLock.getNewLock(); + final double lockIdentifier = lock.first; + final AtomicReference lockObject = lock.second; + + final WritableMap event = createWebViewEvent(view, url); + event.putDouble("lockIdentifier", lockIdentifier); + rncWebView.sendDirectMessage("onShouldStartLoadWithRequest", event); + + try { + assert lockObject != null; + synchronized (lockObject) { + final long startTime = SystemClock.elapsedRealtime(); + while (lockObject.get() == RNCWebViewModuleImpl.ShouldOverrideUrlLoadingLock.ShouldOverrideCallbackState.UNDECIDED) { + if (SystemClock.elapsedRealtime() - startTime > SHOULD_OVERRIDE_URL_LOADING_TIMEOUT) { + FLog.w(TAG, "Did not receive response to shouldOverrideUrlLoading in time, defaulting to allow loading."); + RNCWebViewModuleImpl.shouldOverrideUrlLoadingLock.removeLock(lockIdentifier); + return false; + } + lockObject.wait(SHOULD_OVERRIDE_URL_LOADING_TIMEOUT); + } + } + } catch (InterruptedException e) { + FLog.e(TAG, "shouldOverrideUrlLoading was interrupted while waiting for result.", e); + RNCWebViewModuleImpl.shouldOverrideUrlLoadingLock.removeLock(lockIdentifier); + return false; + } + + final boolean shouldOverride = lockObject.get() == RNCWebViewModuleImpl.ShouldOverrideUrlLoadingLock.ShouldOverrideCallbackState.SHOULD_OVERRIDE; + RNCWebViewModuleImpl.shouldOverrideUrlLoadingLock.removeLock(lockIdentifier); + + return shouldOverride; + } else { + FLog.w(TAG, "Couldn't use blocking synchronous call for onShouldStartLoadWithRequest due to debugging or missing Catalyst instance, falling back to old event-and-load."); + progressChangedFilter.setWaitingForCommandLoadUrl(true); + + int reactTag = view.getId(); + UIManagerHelper.getEventDispatcherForReactTag((ReactContext) view.getContext(), reactTag).dispatchEvent(new TopShouldStartLoadWithRequestEvent( + reactTag, + createWebViewEvent(view, url))); + return true; + } + } + + @TargetApi(Build.VERSION_CODES.N) + @Override + public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { + final String url = request.getUrl().toString(); + return this.shouldOverrideUrlLoading(view, url); + } + + @Override + public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) { + if (basicAuthCredential != null) { + handler.proceed(basicAuthCredential.username, basicAuthCredential.password); + return; + } + super.onReceivedHttpAuthRequest(view, handler, host, realm); + } + + @Override + public void onReceivedSslError(final WebView webView, final SslErrorHandler handler, final SslError error) { + // onReceivedSslError is called for most requests, per Android docs: https://developer.android.com/reference/android/webkit/WebViewClient#onReceivedSslError(android.webkit.WebView,%2520android.webkit.SslErrorHandler,%2520android.net.http.SslError) + // WebView.getUrl() will return the top-level window URL. + // If a top-level navigation triggers this error handler, the top-level URL will be the failing URL (not the URL of the currently-rendered page). + // This is desired behavior. We later use these values to determine whether the request is a top-level navigation or a subresource request. + String topWindowUrl = webView.getUrl(); + String failingUrl = error.getUrl(); + + // Cancel request after obtaining top-level URL. + // If request is cancelled before obtaining top-level URL, undesired behavior may occur. + // Undesired behavior: Return value of WebView.getUrl() may be the current URL instead of the failing URL. + handler.cancel(); + + if (!topWindowUrl.equalsIgnoreCase(failingUrl)) { + // If error is not due to top-level navigation, then do not call onReceivedError() + Log.w(TAG, "Resource blocked from loading due to SSL error. Blocked URL: "+failingUrl); + return; + } + + int code = error.getPrimaryError(); + String description = ""; + String descriptionPrefix = "SSL error: "; + + // https://developer.android.com/reference/android/net/http/SslError.html + switch (code) { + case SslError.SSL_DATE_INVALID: + description = "The date of the certificate is invalid"; + break; + case SslError.SSL_EXPIRED: + description = "The certificate has expired"; + break; + case SslError.SSL_IDMISMATCH: + description = "Hostname mismatch"; + break; + case SslError.SSL_INVALID: + description = "A generic error occurred"; + break; + case SslError.SSL_NOTYETVALID: + description = "The certificate is not yet valid"; + break; + case SslError.SSL_UNTRUSTED: + description = "The certificate authority is not trusted"; + break; + default: + description = "Unknown SSL Error"; + break; + } + + description = descriptionPrefix + description; + + this.onReceivedError( + webView, + code, + description, + failingUrl + ); + } + + @Override + public void onReceivedError( + WebView webView, + int errorCode, + String description, + String failingUrl) { + + if (ignoreErrFailedForThisURL != null + && failingUrl.equals(ignoreErrFailedForThisURL) + && errorCode == -1 + && description.equals("net::ERR_FAILED")) { + + // This is a workaround for a bug in the WebView. + // See these chromium issues for more context: + // https://bugs.chromium.org/p/chromium/issues/detail?id=1023678 + // https://bugs.chromium.org/p/chromium/issues/detail?id=1050635 + // This entire commit should be reverted once this bug is resolved in chromium. + setIgnoreErrFailedForThisURL(null); + return; + } + + super.onReceivedError(webView, errorCode, description, failingUrl); + mLastLoadFailed = true; + + // In case of an error JS side expect to get a finish event first, and then get an error event + // Android WebView does it in the opposite way, so we need to simulate that behavior + emitFinishEvent(webView, failingUrl); + + WritableMap eventData = createWebViewEvent(webView, failingUrl); + eventData.putDouble("code", errorCode); + eventData.putString("description", description); + + int reactTag = webView.getId(); + UIManagerHelper.getEventDispatcherForReactTag((ReactContext) webView.getContext(), reactTag).dispatchEvent(new TopLoadingErrorEvent(webView.getId(), eventData)); + } + + @RequiresApi(api = Build.VERSION_CODES.M) + @Override + public void onReceivedHttpError( + WebView webView, + WebResourceRequest request, + WebResourceResponse errorResponse) { + super.onReceivedHttpError(webView, request, errorResponse); + + if (request.isForMainFrame()) { + WritableMap eventData = createWebViewEvent(webView, request.getUrl().toString()); + eventData.putInt("statusCode", errorResponse.getStatusCode()); + eventData.putString("description", errorResponse.getReasonPhrase()); + + int reactTag = webView.getId(); + UIManagerHelper.getEventDispatcherForReactTag((ReactContext) webView.getContext(), reactTag).dispatchEvent(new TopHttpErrorEvent(webView.getId(), eventData)); + } + } + + @TargetApi(Build.VERSION_CODES.O) + @Override + public boolean onRenderProcessGone(WebView webView, RenderProcessGoneDetail detail) { + // WebViewClient.onRenderProcessGone was added in O. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return false; + } + super.onRenderProcessGone(webView, detail); + + if(detail.didCrash()){ + Log.e(TAG, "The WebView rendering process crashed."); + } + else{ + Log.w(TAG, "The WebView rendering process was killed by the system."); + } + + // if webView is null, we cannot return any event + // since the view is already dead/disposed + // still prevent the app crash by returning true. + if(webView == null){ + return true; + } + + WritableMap event = createWebViewEvent(webView, webView.getUrl()); + event.putBoolean("didCrash", detail.didCrash()); + int reactTag = webView.getId(); + UIManagerHelper.getEventDispatcherForReactTag((ReactContext) webView.getContext(), reactTag).dispatchEvent(new TopRenderProcessGoneEvent(webView.getId(), event)); + + // returning false would crash the app. + return true; + } + + protected void emitFinishEvent(WebView webView, String url) { + int reactTag = webView.getId(); + UIManagerHelper.getEventDispatcherForReactTag((ReactContext) webView.getContext(), reactTag).dispatchEvent(new TopLoadingFinishEvent(webView.getId(), createWebViewEvent(webView, url))); + } + + protected WritableMap createWebViewEvent(WebView webView, String url) { + WritableMap event = Arguments.createMap(); + event.putDouble("target", webView.getId()); + // Don't use webView.getUrl() here, the URL isn't updated to the new value yet in callbacks + // like onPageFinished + event.putString("url", url); + event.putBoolean("loading", !mLastLoadFailed && webView.getProgress() != 100); + event.putString("title", webView.getTitle()); + event.putBoolean("canGoBack", webView.canGoBack()); + event.putBoolean("canGoForward", webView.canGoForward()); + return event; + } + + public void setProgressChangedFilter(RNCWebView.ProgressChangedFilter filter) { + progressChangedFilter = filter; + } +} \ No newline at end of file diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/WebViewConfig.java b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebViewConfig.java similarity index 79% rename from android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/WebViewConfig.java rename to android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebViewConfig.java index b983561058340..2dfc317729363 100644 --- a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/WebViewConfig.java +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebViewConfig.java @@ -6,7 +6,6 @@ * Implement this interface in order to config your {@link WebView}. An instance of that * implementation will have to be given as a constructor argument to {@link RNCWebViewManager}. */ -public interface WebViewConfig { - - void configWebView(WebView webView); -} +public interface RNCWebViewConfig { + void configWebView(WebView webView); +} \ No newline at end of file diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebViewFileProvider.java b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebViewFileProvider.java index 239d0c43df91d..a3e9d7105d8d7 100644 --- a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebViewFileProvider.java +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebViewFileProvider.java @@ -11,4 +11,4 @@ public class RNCWebViewFileProvider extends FileProvider { // This class intentionally left blank. -} +} \ No newline at end of file diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebViewManager.java b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebViewManager.java index 2c8ba7750765e..2a7b8e53db94b 100644 --- a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebViewManager.java +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebViewManager.java @@ -1,1853 +1,318 @@ package versioned.host.exp.exponent.modules.api.components.webview; -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.app.Activity; -import android.app.DownloadManager; -import android.content.Context; -import android.content.pm.ActivityInfo; -import android.content.pm.PackageManager; -import android.graphics.Bitmap; -import android.graphics.Color; -import android.Manifest; -import android.net.http.SslError; -import android.net.Uri; -import android.os.Build; -import android.os.Environment; -import android.os.Message; -import android.os.SystemClock; -import android.text.TextUtils; -import android.util.Log; -import android.view.Gravity; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewGroup.LayoutParams; -import android.view.WindowManager; -import android.webkit.ConsoleMessage; -import android.webkit.CookieManager; -import android.webkit.DownloadListener; -import android.webkit.GeolocationPermissions; -import android.webkit.HttpAuthHandler; -import android.webkit.JavascriptInterface; -import android.webkit.RenderProcessGoneDetail; -import android.webkit.SslErrorHandler; -import android.webkit.PermissionRequest; -import android.webkit.ValueCallback; -import android.webkit.WebChromeClient; -import android.webkit.WebResourceRequest; -import android.webkit.WebResourceResponse; -import android.webkit.WebSettings; -import android.webkit.WebView; -import android.webkit.WebViewClient; -import android.widget.FrameLayout; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; -import androidx.core.content.ContextCompat; -import androidx.core.util.Pair; -import androidx.webkit.WebSettingsCompat; -import androidx.webkit.WebViewFeature; -import com.facebook.common.logging.FLog; -import com.facebook.react.modules.core.PermissionAwareActivity; -import com.facebook.react.modules.core.PermissionListener; -import com.facebook.react.views.scroll.ScrollEvent; -import com.facebook.react.views.scroll.ScrollEventType; -import com.facebook.react.views.scroll.OnScrollDispatchHelper; -import com.facebook.react.bridge.Arguments; -import com.facebook.react.bridge.CatalystInstance; -import com.facebook.react.bridge.LifecycleEventListener; -import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.bridge.ReadableMapKeySetIterator; -import com.facebook.react.bridge.WritableMap; -import com.facebook.react.bridge.WritableNativeArray; -import com.facebook.react.bridge.WritableNativeMap; import com.facebook.react.common.MapBuilder; -import com.facebook.react.common.build.ReactBuildConfig; import com.facebook.react.module.annotations.ReactModule; import com.facebook.react.uimanager.SimpleViewManager; import com.facebook.react.uimanager.ThemedReactContext; -import com.facebook.react.uimanager.UIManagerModule; 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 versioned.host.exp.exponent.modules.api.components.webview.RNCWebViewModule.ShouldOverrideUrlLoadingLock.ShouldOverrideCallbackState; -import versioned.host.exp.exponent.modules.api.components.webview.events.TopLoadingErrorEvent; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.views.scroll.ScrollEventType; +import versioned.host.exp.exponent.modules.api.components.webview.events.TopCustomMenuSelectionEvent; import versioned.host.exp.exponent.modules.api.components.webview.events.TopHttpErrorEvent; +import versioned.host.exp.exponent.modules.api.components.webview.events.TopLoadingErrorEvent; import versioned.host.exp.exponent.modules.api.components.webview.events.TopLoadingFinishEvent; import versioned.host.exp.exponent.modules.api.components.webview.events.TopLoadingProgressEvent; import versioned.host.exp.exponent.modules.api.components.webview.events.TopLoadingStartEvent; import versioned.host.exp.exponent.modules.api.components.webview.events.TopMessageEvent; -import versioned.host.exp.exponent.modules.api.components.webview.events.TopShouldStartLoadWithRequestEvent; import versioned.host.exp.exponent.modules.api.components.webview.events.TopRenderProcessGoneEvent; +import versioned.host.exp.exponent.modules.api.components.webview.events.TopShouldStartLoadWithRequestEvent; + +import android.graphics.Color; import org.json.JSONException; import org.json.JSONObject; -import java.io.UnsupportedEncodingException; -import java.lang.IllegalArgumentException; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLEncoder; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; - -/** - * Manages instances of {@link WebView} - *

- * Can accept following commands: - * - 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 - * - url - url set for the webview - * - loading - whether webview is in a loading state - * - title - title of the current page - * - canGoBack - boolean, whether there is anything on a history stack to go back - * - canGoForward - boolean, whether it is possible to request GO_FORWARD command - */ -@ReactModule(name = RNCWebViewManager.REACT_CLASS) -public class RNCWebViewManager extends SimpleViewManager { - private static final String TAG = "RNCWebViewManager"; - - public static final int COMMAND_GO_BACK = 1; - public static final int COMMAND_GO_FORWARD = 2; - public static final int COMMAND_RELOAD = 3; - 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; - public static final int COMMAND_FOCUS = 8; - - // android commands - public static final int COMMAND_CLEAR_FORM_DATA = 1000; - public static final int COMMAND_CLEAR_CACHE = 1001; - public static final int COMMAND_CLEAR_HISTORY = 1002; - - protected static final String REACT_CLASS = "RNCWebView"; - protected static final String HTML_ENCODING = "UTF-8"; - protected static final String HTML_MIME_TYPE = "text/html"; - protected static final String JAVASCRIPT_INTERFACE = "ReactNativeWebView"; - protected static final String HTTP_METHOD_POST = "POST"; - // 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 static final int SHOULD_OVERRIDE_URL_LOADING_TIMEOUT = 250; - protected static final String DEFAULT_DOWNLOADING_MESSAGE = "Downloading"; - protected static final String DEFAULT_LACK_PERMISSION_TO_DOWNLOAD_MESSAGE = - "Cannot download files as permission was denied. Please provide permission to write to storage, in order to download files."; - protected WebViewConfig mWebViewConfig; - - protected RNCWebChromeClient mWebChromeClient = null; - protected boolean mAllowsFullscreenVideo = false; - protected boolean mAllowsProtectedMedia = false; - protected @Nullable String mUserAgent = null; - protected @Nullable String mUserAgentWithApplicationName = null; - protected @Nullable String mDownloadingMessage = null; - protected @Nullable String mLackPermissionToDownloadMessage = null; - - public RNCWebViewManager() { - mWebViewConfig = new WebViewConfig() { - public void configWebView(WebView webView) { - } - }; - } - - public RNCWebViewManager(WebViewConfig webViewConfig) { - mWebViewConfig = webViewConfig; - } - - @Override - public String getName() { - return REACT_CLASS; - } - - protected RNCWebView createRNCWebViewInstance(ThemedReactContext reactContext) { - return new RNCWebView(reactContext); - } - - @Override - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - protected WebView createViewInstance(ThemedReactContext reactContext) { - RNCWebView webView = createRNCWebViewInstance(reactContext); - setupWebChromeClient(reactContext, webView); - reactContext.addLifecycleEventListener(webView); - mWebViewConfig.configWebView(webView); - WebSettings settings = webView.getSettings(); - settings.setBuiltInZoomControls(true); - settings.setDisplayZoomControls(false); - settings.setDomStorageEnabled(true); - settings.setSupportMultipleWindows(true); - - settings.setAllowFileAccess(false); - settings.setAllowContentAccess(false); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - settings.setAllowFileAccessFromFileURLs(false); - setAllowUniversalAccessFromFileURLs(webView, false); - } - setMixedContentMode(webView, "never"); - - // Fixes broken full-screen modals/galleries due to body height being 0. - webView.setLayoutParams( - new LayoutParams(LayoutParams.MATCH_PARENT, - LayoutParams.MATCH_PARENT)); - - if (ReactBuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - WebView.setWebContentsDebuggingEnabled(true); - } - - webView.setDownloadListener(new DownloadListener() { - public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) { - webView.setIgnoreErrFailedForThisURL(url); - - RNCWebViewModule module = getModule(reactContext); - - DownloadManager.Request request; - try { - request = new DownloadManager.Request(Uri.parse(url)); - } catch (IllegalArgumentException e) { - Log.w(TAG, "Unsupported URI, aborting download", e); - return; - } - - String 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); - } catch (MalformedURLException e) { - Log.w(TAG, "Error getting cookie for DownloadManager", e); - } - - //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(getDownloadingMessage(), getLackPermissionToDownloadMessage())) { - module.downloadFile(getDownloadingMessage()); - } - } - }); - - return webView; - } - - private String getDownloadingMessage() { - return mDownloadingMessage == null ? DEFAULT_DOWNLOADING_MESSAGE : mDownloadingMessage; - } - - private String getLackPermissionToDownloadMessage() { - return mDownloadingMessage == null ? DEFAULT_LACK_PERMISSION_TO_DOWNLOAD_MESSAGE : mLackPermissionToDownloadMessage; - } - - @ReactProp(name = "javaScriptEnabled") - public void setJavaScriptEnabled(WebView view, boolean enabled) { - view.getSettings().setJavaScriptEnabled(enabled); - } - - @ReactProp(name = "setBuiltInZoomControls") - public void setBuiltInZoomControls(WebView view, boolean enabled) { - view.getSettings().setBuiltInZoomControls(enabled); - } - - @ReactProp(name = "setDisplayZoomControls") - public void setDisplayZoomControls(WebView view, boolean enabled) { - view.getSettings().setDisplayZoomControls(enabled); - } - - @ReactProp(name = "setSupportMultipleWindows") - public void setSupportMultipleWindows(WebView view, boolean enabled){ - view.getSettings().setSupportMultipleWindows(enabled); - } - - @ReactProp(name = "showsHorizontalScrollIndicator") - public void setShowsHorizontalScrollIndicator(WebView view, boolean enabled) { - view.setHorizontalScrollBarEnabled(enabled); - } - - @ReactProp(name = "showsVerticalScrollIndicator") - public void setShowsVerticalScrollIndicator(WebView view, boolean enabled) { - view.setVerticalScrollBarEnabled(enabled); - } - - @ReactProp(name = "downloadingMessage") - public void setDownloadingMessage(WebView view, String message) { - mDownloadingMessage = message; - } - - @ReactProp(name = "lackPermissionToDownloadMessage") - public void setLackPermissionToDownlaodMessage(WebView view, String message) { - mLackPermissionToDownloadMessage = message; - } - - @ReactProp(name = "cacheEnabled") - public void setCacheEnabled(WebView view, boolean enabled) { - view.getSettings().setCacheMode(enabled ? WebSettings.LOAD_DEFAULT : WebSettings.LOAD_NO_CACHE); - } - - @ReactProp(name = "cacheMode") - public void setCacheMode(WebView view, String cacheModeString) { - Integer cacheMode; - switch (cacheModeString) { - case "LOAD_CACHE_ONLY": - cacheMode = WebSettings.LOAD_CACHE_ONLY; - break; - case "LOAD_CACHE_ELSE_NETWORK": - cacheMode = WebSettings.LOAD_CACHE_ELSE_NETWORK; - break; - case "LOAD_NO_CACHE": - cacheMode = WebSettings.LOAD_NO_CACHE; - break; - case "LOAD_DEFAULT": - default: - cacheMode = WebSettings.LOAD_DEFAULT; - break; - } - view.getSettings().setCacheMode(cacheMode); - } - - @ReactProp(name = "androidHardwareAccelerationDisabled") - public void setHardwareAccelerationDisabled(WebView view, boolean disabled) { - if (disabled) { - view.setLayerType(View.LAYER_TYPE_SOFTWARE, null); - } - } - - @ReactProp(name = "androidLayerType") - public void setLayerType(WebView view, String layerTypeString) { - int layerType = View.LAYER_TYPE_NONE; - switch (layerTypeString) { - case "hardware": - layerType = View.LAYER_TYPE_HARDWARE; - break; - case "software": - layerType = View.LAYER_TYPE_SOFTWARE; - break; - } - view.setLayerType(layerType, null); - } - - - @ReactProp(name = "overScrollMode") - public void setOverScrollMode(WebView view, String overScrollModeString) { - Integer overScrollMode; - switch (overScrollModeString) { - case "never": - overScrollMode = View.OVER_SCROLL_NEVER; - break; - case "content": - overScrollMode = View.OVER_SCROLL_IF_CONTENT_SCROLLS; - break; - case "always": - default: - overScrollMode = View.OVER_SCROLL_ALWAYS; - break; - } - view.setOverScrollMode(overScrollMode); - } - - @ReactProp(name = "nestedScrollEnabled") - public void setNestedScrollEnabled(WebView view, boolean enabled) { - ((RNCWebView) view).setNestedScrollEnabled(enabled); - } - - @ReactProp(name = "thirdPartyCookiesEnabled") - public void setThirdPartyCookiesEnabled(WebView view, boolean enabled) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - CookieManager.getInstance().setAcceptThirdPartyCookies(view, enabled); - } - } - - @ReactProp(name = "textZoom") - public void setTextZoom(WebView view, int value) { - view.getSettings().setTextZoom(value); - } - - @ReactProp(name = "scalesPageToFit") - public void setScalesPageToFit(WebView view, boolean enabled) { - view.getSettings().setLoadWithOverviewMode(enabled); - view.getSettings().setUseWideViewPort(enabled); - } - - @ReactProp(name = "domStorageEnabled") - public void setDomStorageEnabled(WebView view, boolean enabled) { - view.getSettings().setDomStorageEnabled(enabled); - } - - @ReactProp(name = "userAgent") - public void setUserAgent(WebView view, @Nullable String userAgent) { - if (userAgent != null) { - mUserAgent = userAgent; - } else { - mUserAgent = null; - } - this.setUserAgentString(view); - } - - @ReactProp(name = "applicationNameForUserAgent") - public void setApplicationNameForUserAgent(WebView view, @Nullable String applicationName) { - if(applicationName != null) { - if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - String defaultUserAgent = WebSettings.getDefaultUserAgent(view.getContext()); - mUserAgentWithApplicationName = defaultUserAgent + " " + applicationName; - } - } else { - mUserAgentWithApplicationName = null; - } - this.setUserAgentString(view); - } - - protected void setUserAgentString(WebView view) { - if(mUserAgent != null) { - view.getSettings().setUserAgentString(mUserAgent); - } else if(mUserAgentWithApplicationName != null) { - view.getSettings().setUserAgentString(mUserAgentWithApplicationName); - } else if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - // handle unsets of `userAgent` prop as long as device is >= API 17 - view.getSettings().setUserAgentString(WebSettings.getDefaultUserAgent(view.getContext())); - } - } - - @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) - @ReactProp(name = "mediaPlaybackRequiresUserAction") - public void setMediaPlaybackRequiresUserAction(WebView view, boolean requires) { - view.getSettings().setMediaPlaybackRequiresUserGesture(requires); - } - - @ReactProp(name = "javaScriptCanOpenWindowsAutomatically") - public void setJavaScriptCanOpenWindowsAutomatically(WebView view, boolean enabled) { - view.getSettings().setJavaScriptCanOpenWindowsAutomatically(enabled); - } - - @ReactProp(name = "allowFileAccessFromFileURLs") - public void setAllowFileAccessFromFileURLs(WebView view, boolean allow) { - view.getSettings().setAllowFileAccessFromFileURLs(allow); - } - - @ReactProp(name = "allowUniversalAccessFromFileURLs") - public void setAllowUniversalAccessFromFileURLs(WebView view, boolean allow) { - view.getSettings().setAllowUniversalAccessFromFileURLs(allow); - } - - @ReactProp(name = "saveFormDataDisabled") - public void setSaveFormDataDisabled(WebView view, boolean disable) { - view.getSettings().setSaveFormData(!disable); - } - - @ReactProp(name = "injectedJavaScript") - public void setInjectedJavaScript(WebView view, @Nullable String injectedJavaScript) { - ((RNCWebView) view).setInjectedJavaScript(injectedJavaScript); - } - - @ReactProp(name = "injectedJavaScriptBeforeContentLoaded") - public void setInjectedJavaScriptBeforeContentLoaded(WebView view, @Nullable String injectedJavaScriptBeforeContentLoaded) { - ((RNCWebView) view).setInjectedJavaScriptBeforeContentLoaded(injectedJavaScriptBeforeContentLoaded); - } - - @ReactProp(name = "injectedJavaScriptForMainFrameOnly") - public void setInjectedJavaScriptForMainFrameOnly(WebView view, boolean enabled) { - ((RNCWebView) view).setInjectedJavaScriptForMainFrameOnly(enabled); - } - - @ReactProp(name = "injectedJavaScriptBeforeContentLoadedForMainFrameOnly") - public void setInjectedJavaScriptBeforeContentLoadedForMainFrameOnly(WebView view, boolean enabled) { - ((RNCWebView) view).setInjectedJavaScriptBeforeContentLoadedForMainFrameOnly(enabled); - } - - @ReactProp(name = "messagingEnabled") - public void setMessagingEnabled(WebView view, boolean enabled) { - ((RNCWebView) view).setMessagingEnabled(enabled); - } +import java.util.HashMap; - @ReactProp(name = "messagingModuleName") - public void setMessagingModuleName(WebView view, String moduleName) { - ((RNCWebView) view).setMessagingModuleName(moduleName); - } +public class RNCWebViewManager extends SimpleViewManager { - @ReactProp(name = "incognito") - public void setIncognito(WebView view, boolean enabled) { - // Don't do anything when incognito is disabled - if (!enabled) { - return; - } + private final RNCWebViewManagerImpl mRNCWebViewManagerImpl; - // Remove all previous cookies - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - CookieManager.getInstance().removeAllCookies(null); - } else { - CookieManager.getInstance().removeAllCookie(); + public RNCWebViewManager() { + mRNCWebViewManagerImpl = new RNCWebViewManagerImpl(); } - // Disable caching - view.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE); - view.clearHistory(); - view.clearCache(true); - - // No form data or autofill enabled - view.clearFormData(); - view.getSettings().setSavePassword(false); - view.getSettings().setSaveFormData(false); - } - - @ReactProp(name = "source") - public void setSource(WebView view, @Nullable ReadableMap source) { - if (source != null) { - if (source.hasKey("html")) { - String html = source.getString("html"); - String baseUrl = source.hasKey("baseUrl") ? source.getString("baseUrl") : ""; - view.loadDataWithBaseURL(baseUrl, html, HTML_MIME_TYPE, HTML_ENCODING, null); - return; - } - if (source.hasKey("uri")) { - String url = source.getString("uri"); - String previousUrl = view.getUrl(); - if (previousUrl != null && previousUrl.equals(url)) { - return; - } - if (source.hasKey("method")) { - String method = source.getString("method"); - if (method.equalsIgnoreCase(HTTP_METHOD_POST)) { - byte[] postData = null; - if (source.hasKey("body")) { - String body = source.getString("body"); - try { - postData = body.getBytes("UTF-8"); - } catch (UnsupportedEncodingException e) { - postData = body.getBytes(); - } - } - if (postData == null) { - postData = new byte[0]; - } - view.postUrl(url, postData); - return; - } - } - HashMap headerMap = new HashMap<>(); - if (source.hasKey("headers")) { - ReadableMap headers = source.getMap("headers"); - ReadableMapKeySetIterator iter = headers.keySetIterator(); - while (iter.hasNextKey()) { - String key = iter.nextKey(); - if ("user-agent".equals(key.toLowerCase(Locale.ENGLISH))) { - if (view.getSettings() != null) { - view.getSettings().setUserAgentString(headers.getString(key)); - } - } else { - headerMap.put(key, headers.getString(key)); - } - } - } - view.loadUrl(url, headerMap); - return; - } + @Override + public String getName() { + return RNCWebViewManagerImpl.NAME; } - view.loadUrl(BLANK_URL); - } - @ReactProp(name = "basicAuthCredential") - public void setBasicAuthCredential(WebView view, @Nullable ReadableMap credential) { - @Nullable BasicAuthCredential basicAuthCredential = null; - if (credential != null) { - if (credential.hasKey("username") && credential.hasKey("password")) { - String username = credential.getString("username"); - String password = credential.getString("password"); - basicAuthCredential = new BasicAuthCredential(username, password); - } + @Override + public RNCWebView createViewInstance(ThemedReactContext context) { + return mRNCWebViewManagerImpl.createViewInstance(context); } - ((RNCWebView) view).setBasicAuthCredential(basicAuthCredential); - } - - @ReactProp(name = "onContentSizeChange") - public void setOnContentSizeChange(WebView view, boolean sendContentSizeChangeEvents) { - ((RNCWebView) view).setSendContentSizeChangeEvents(sendContentSizeChangeEvents); - } - @ReactProp(name = "mixedContentMode") - public void setMixedContentMode(WebView view, @Nullable String mixedContentMode) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - if (mixedContentMode == null || "never".equals(mixedContentMode)) { - view.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW); - } else if ("always".equals(mixedContentMode)) { - view.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW); - } else if ("compatibility".equals(mixedContentMode)) { - view.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE); - } + public RNCWebView createViewInstance(ThemedReactContext context, RNCWebView webView) { + return mRNCWebViewManagerImpl.createViewInstance(context, webView); } - } - @ReactProp(name = "urlPrefixesForDefaultIntent") - public void setUrlPrefixesForDefaultIntent( - WebView view, - @Nullable ReadableArray urlPrefixesForDefaultIntent) { - RNCWebViewClient client = ((RNCWebView) view).getRNCWebViewClient(); - if (client != null && urlPrefixesForDefaultIntent != null) { - client.setUrlPrefixesForDefaultIntent(urlPrefixesForDefaultIntent); + @ReactProp(name = "allowFileAccess") + public void setAllowFileAccess(RNCWebView view, boolean value) { + mRNCWebViewManagerImpl.setAllowFileAccess(view, value); } - } - - @ReactProp(name = "allowsFullscreenVideo") - public void setAllowsFullscreenVideo( - WebView view, - @Nullable Boolean allowsFullscreenVideo) { - mAllowsFullscreenVideo = allowsFullscreenVideo != null && allowsFullscreenVideo; - setupWebChromeClient((ReactContext)view.getContext(), view); - } - - @ReactProp(name = "allowFileAccess") - public void setAllowFileAccess( - WebView view, - @Nullable Boolean allowFileAccess) { - view.getSettings().setAllowFileAccess(allowFileAccess != null && allowFileAccess); - } - @ReactProp(name = "geolocationEnabled") - public void setGeolocationEnabled( - WebView view, - @Nullable Boolean isGeolocationEnabled) { - view.getSettings().setGeolocationEnabled(isGeolocationEnabled != null && isGeolocationEnabled); - } + @ReactProp(name = "allowFileAccessFromFileURLs") + public void setAllowFileAccessFromFileURLs(RNCWebView view, boolean value) { + mRNCWebViewManagerImpl.setAllowFileAccessFromFileURLs(view, value); - @ReactProp(name = "onScroll") - public void setOnScroll(WebView view, boolean hasScrollEvent) { - ((RNCWebView) view).setHasScrollEvent(hasScrollEvent); - } - - @ReactProp(name = "forceDarkOn") - public void setForceDarkOn(WebView view, boolean enabled) { - // Only Android 10+ support dark mode - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { - // Switch WebView dark mode - if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) { - int forceDarkMode = enabled ? WebSettingsCompat.FORCE_DARK_ON : WebSettingsCompat.FORCE_DARK_OFF; - WebSettingsCompat.setForceDark(view.getSettings(), forceDarkMode); - } - - // Set how WebView content should be darkened. - // PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING: checks for the "color-scheme" tag. - // If present, it uses media queries. If absent, it applies user-agent (automatic) - // More information about Force Dark Strategy can be found here: - // https://developer.android.com/reference/androidx/webkit/WebSettingsCompat#setForceDarkStrategy(android.webkit.WebSettings) - if (enabled && WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK_STRATEGY)) { - WebSettingsCompat.setForceDarkStrategy(view.getSettings(), WebSettingsCompat.DARK_STRATEGY_PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING); - } } - } - - @ReactProp(name = "minimumFontSize") - public void setMinimumFontSize(WebView view, int fontSize) { - view.getSettings().setMinimumFontSize(fontSize); - } - @ReactProp(name = "allowsProtectedMedia") - public void setAllowsProtectedMedia(WebView view, boolean enabled) { - // This variable is used to keep consistency - // in case a new WebChromeClient is created - // (eg. when mAllowsFullScreenVideo changes) - mAllowsProtectedMedia = enabled; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - WebChromeClient client = view.getWebChromeClient(); - if (client != null && client instanceof RNCWebChromeClient) { - ((RNCWebChromeClient) client).setAllowsProtectedMedia(enabled); - } + @ReactProp(name = "allowUniversalAccessFromFileURLs") + public void setAllowUniversalAccessFromFileURLs(RNCWebView view, boolean value) { + mRNCWebViewManagerImpl.setAllowUniversalAccessFromFileURLs(view, value); } - } - - @Override - protected void addEventEmitters(ThemedReactContext reactContext, WebView view) { - // Do not register default touch emitter and let WebView implementation handle touches - view.setWebViewClient(new RNCWebViewClient()); - } - @Override - public Map getExportedCustomDirectEventTypeConstants() { - Map export = super.getExportedCustomDirectEventTypeConstants(); - if (export == null) { - export = MapBuilder.newHashMap(); + @ReactProp(name = "allowsFullscreenVideo") + public void setAllowsFullscreenVideo(RNCWebView view, boolean value) { + mRNCWebViewManagerImpl.setAllowsFullscreenVideo(view, value); } - // Default events but adding them here explicitly for clarity - export.put(TopLoadingStartEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoadingStart")); - export.put(TopLoadingFinishEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoadingFinish")); - export.put(TopLoadingErrorEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoadingError")); - export.put(TopMessageEvent.EVENT_NAME, MapBuilder.of("registrationName", "onMessage")); - // !Default events but adding them here explicitly for clarity - export.put(TopLoadingProgressEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoadingProgress")); - export.put(TopShouldStartLoadWithRequestEvent.EVENT_NAME, MapBuilder.of("registrationName", "onShouldStartLoadWithRequest")); - export.put(ScrollEventType.getJSEventName(ScrollEventType.SCROLL), MapBuilder.of("registrationName", "onScroll")); - export.put(TopHttpErrorEvent.EVENT_NAME, MapBuilder.of("registrationName", "onHttpError")); - export.put(TopRenderProcessGoneEvent.EVENT_NAME, MapBuilder.of("registrationName", "onRenderProcessGone")); - return export; - } - - @Override - public @Nullable - Map getCommandsMap() { - return MapBuilder.builder() - .put("goBack", COMMAND_GO_BACK) - .put("goForward", COMMAND_GO_FORWARD) - .put("reload", COMMAND_RELOAD) - .put("stopLoading", COMMAND_STOP_LOADING) - .put("postMessage", COMMAND_POST_MESSAGE) - .put("injectJavaScript", COMMAND_INJECT_JAVASCRIPT) - .put("loadUrl", COMMAND_LOAD_URL) - .put("requestFocus", COMMAND_FOCUS) - .put("clearFormData", COMMAND_CLEAR_FORM_DATA) - .put("clearCache", COMMAND_CLEAR_CACHE) - .put("clearHistory", COMMAND_CLEAR_HISTORY) - .build(); - } - - @Override - public void receiveCommand(@NonNull WebView root, String commandId, @Nullable ReadableArray args) { - switch (commandId) { - case "goBack": - root.goBack(); - break; - case "goForward": - root.goForward(); - break; - case "reload": - root.reload(); - break; - case "stopLoading": - root.stopLoading(); - break; - case "postMessage": - try { - RNCWebView reactWebView = (RNCWebView) root; - JSONObject eventInitDict = new JSONObject(); - eventInitDict.put("data", args.getString(0)); - reactWebView.evaluateJavascriptWithFallback("(function () {" + - "var event;" + - "var data = " + eventInitDict.toString() + ";" + - "try {" + - "event = new MessageEvent('message', data);" + - "} catch (e) {" + - "event = document.createEvent('MessageEvent');" + - "event.initMessageEvent('message', true, true, data.data, data.origin, data.lastEventId, data.source);" + - "}" + - "document.dispatchEvent(event);" + - "})();"); - } catch (JSONException e) { - throw new RuntimeException(e); - } - break; - case "injectJavaScript": - RNCWebView reactWebView = (RNCWebView) root; - reactWebView.evaluateJavascriptWithFallback(args.getString(0)); - break; - case "loadUrl": - if (args == null) { - throw new RuntimeException("Arguments for loading an url are null!"); - } - ((RNCWebView) root).progressChangedFilter.setWaitingForCommandLoadUrl(false); - root.loadUrl(args.getString(0)); - break; - case "requestFocus": - root.requestFocus(); - break; - case "clearFormData": - root.clearFormData(); - break; - case "clearCache": - boolean includeDiskFiles = args != null && args.getBoolean(0); - root.clearCache(includeDiskFiles); - break; - case "clearHistory": - root.clearHistory(); - break; + @ReactProp(name = "allowsProtectedMedia") + public void setAllowsProtectedMedia(RNCWebView view, boolean value) { + mRNCWebViewManagerImpl.setAllowsProtectedMedia(view, value); } - super.receiveCommand(root, commandId, args); - } - - @Override - public void onDropViewInstance(WebView webView) { - super.onDropViewInstance(webView); - ((ThemedReactContext) webView.getContext()).removeLifecycleEventListener((RNCWebView) webView); - ((RNCWebView) webView).cleanupCallbacksAndDestroy(); - mWebChromeClient = null; - } - - public static RNCWebViewModule getModule(ReactContext reactContext) { - return reactContext.getNativeModule(RNCWebViewModule.class); - } - - protected void setupWebChromeClient(ReactContext reactContext, WebView webView) { - Activity activity = reactContext.getCurrentActivity(); - - if (mAllowsFullscreenVideo && activity != null) { - int initialRequestedOrientation = activity.getRequestedOrientation(); - - mWebChromeClient = new RNCWebChromeClient(reactContext, webView) { - @Override - public Bitmap getDefaultVideoPoster() { - return Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888); - } - - @Override - public void onShowCustomView(View view, CustomViewCallback callback) { - if (mVideoView != null) { - callback.onCustomViewHidden(); - return; - } - - mVideoView = view; - mCustomViewCallback = callback; - - activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - mVideoView.setSystemUiVisibility(FULLSCREEN_SYSTEM_UI_VISIBILITY); - activity.getWindow().setFlags( - WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, - WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS - ); - } - - mVideoView.setBackgroundColor(Color.BLACK); - - // Since RN's Modals interfere with the View hierarchy - // we will decide which View to hide if the hierarchy - // does not match (i.e., the WebView is within a Modal) - // NOTE: We could use `mWebView.getRootView()` instead of `getRootView()` - // but that breaks the Modal's styles and layout, so we need this to render - // in the main View hierarchy regardless - ViewGroup rootView = getRootView(); - rootView.addView(mVideoView, FULLSCREEN_LAYOUT_PARAMS); - - // Different root views, we are in a Modal - if (rootView.getRootView() != mWebView.getRootView()) { - mWebView.getRootView().setVisibility(View.GONE); - } else { - // Same view hierarchy (no Modal), just hide the WebView then - mWebView.setVisibility(View.GONE); - } - - mReactContext.addLifecycleEventListener(this); - } - @Override - public void onHideCustomView() { - if (mVideoView == null) { - return; - } - - // Same logic as above - ViewGroup rootView = getRootView(); - - if (rootView.getRootView() != mWebView.getRootView()) { - mWebView.getRootView().setVisibility(View.VISIBLE); - } else { - // Same view hierarchy (no Modal) - mWebView.setVisibility(View.VISIBLE); - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); - } - - rootView.removeView(mVideoView); - mCustomViewCallback.onCustomViewHidden(); - - mVideoView = null; - mCustomViewCallback = null; - - activity.setRequestedOrientation(initialRequestedOrientation); - - mReactContext.removeLifecycleEventListener(this); - } - }; - } else { - if (mWebChromeClient != null) { - mWebChromeClient.onHideCustomView(); - } - - mWebChromeClient = new RNCWebChromeClient(reactContext, webView) { - @Override - public Bitmap getDefaultVideoPoster() { - return Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888); - } - }; - } - mWebChromeClient.setAllowsProtectedMedia(mAllowsProtectedMedia); - webView.setWebChromeClient(mWebChromeClient); - } - - protected static class RNCWebViewClient extends WebViewClient { - - protected boolean mLastLoadFailed = false; - protected @Nullable - ReadableArray mUrlPrefixesForDefaultIntent; - protected RNCWebView.ProgressChangedFilter progressChangedFilter = null; - protected @Nullable String ignoreErrFailedForThisURL = null; - protected @Nullable BasicAuthCredential basicAuthCredential = null; - - public void setIgnoreErrFailedForThisURL(@Nullable String url) { - ignoreErrFailedForThisURL = url; - } - - public void setBasicAuthCredential(@Nullable BasicAuthCredential credential) { - basicAuthCredential = credential; - } - - @Override - public void onPageFinished(WebView webView, String url) { - super.onPageFinished(webView, url); - - if (!mLastLoadFailed) { - RNCWebView reactWebView = (RNCWebView) webView; - - reactWebView.callInjectedJavaScript(); - - emitFinishEvent(webView, url); - } + @ReactProp(name = "androidLayerType") + public void setAndroidLayerType(RNCWebView view, @Nullable String value) { + mRNCWebViewManagerImpl.setAndroidLayerType(view, value); } - @Override - public void onPageStarted(WebView webView, String url, Bitmap favicon) { - super.onPageStarted(webView, url, favicon); - mLastLoadFailed = false; - - RNCWebView reactWebView = (RNCWebView) webView; - reactWebView.callInjectedJavaScriptBeforeContentLoaded(); - - ((RNCWebView) webView).dispatchEvent( - webView, - new TopLoadingStartEvent( - webView.getId(), - createWebViewEvent(webView, url))); + @ReactProp(name = "applicationNameForUserAgent") + public void setApplicationNameForUserAgent(RNCWebView view, @Nullable String value) { + mRNCWebViewManagerImpl.setApplicationNameForUserAgent(view, value); } - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - final RNCWebView rncWebView = (RNCWebView) view; - final boolean isJsDebugging = ((ReactContext) view.getContext()).getJavaScriptContextHolder().get() == 0; - - if (!isJsDebugging && rncWebView.mCatalystInstance != null) { - final Pair> lock = RNCWebViewModule.shouldOverrideUrlLoadingLock.getNewLock(); - final int lockIdentifier = lock.first; - final AtomicReference lockObject = lock.second; - - final WritableMap event = createWebViewEvent(view, url); - event.putInt("lockIdentifier", lockIdentifier); - rncWebView.sendDirectMessage("onShouldStartLoadWithRequest", event); - - try { - assert lockObject != null; - synchronized (lockObject) { - final long startTime = SystemClock.elapsedRealtime(); - while (lockObject.get() == ShouldOverrideCallbackState.UNDECIDED) { - if (SystemClock.elapsedRealtime() - startTime > SHOULD_OVERRIDE_URL_LOADING_TIMEOUT) { - FLog.w(TAG, "Did not receive response to shouldOverrideUrlLoading in time, defaulting to allow loading."); - RNCWebViewModule.shouldOverrideUrlLoadingLock.removeLock(lockIdentifier); - return false; - } - lockObject.wait(SHOULD_OVERRIDE_URL_LOADING_TIMEOUT); - } - } - } catch (InterruptedException e) { - FLog.e(TAG, "shouldOverrideUrlLoading was interrupted while waiting for result.", e); - RNCWebViewModule.shouldOverrideUrlLoadingLock.removeLock(lockIdentifier); - return false; - } - - final boolean shouldOverride = lockObject.get() == ShouldOverrideCallbackState.SHOULD_OVERRIDE; - RNCWebViewModule.shouldOverrideUrlLoadingLock.removeLock(lockIdentifier); - - return shouldOverride; - } else { - FLog.w(TAG, "Couldn't use blocking synchronous call for onShouldStartLoadWithRequest due to debugging or missing Catalyst instance, falling back to old event-and-load."); - progressChangedFilter.setWaitingForCommandLoadUrl(true); - ((RNCWebView) view).dispatchEvent( - view, - new TopShouldStartLoadWithRequestEvent( - view.getId(), - createWebViewEvent(view, url))); - return true; - } + @ReactProp(name = "basicAuthCredential") + public void setBasicAuthCredential(RNCWebView view, @Nullable ReadableMap value) { + mRNCWebViewManagerImpl.setBasicAuthCredential(view, value); } - @TargetApi(Build.VERSION_CODES.N) - @Override - public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { - final String url = request.getUrl().toString(); - return this.shouldOverrideUrlLoading(view, url); + @ReactProp(name = "cacheEnabled") + public void setCacheEnabled(RNCWebView view, boolean value) { + mRNCWebViewManagerImpl.setCacheEnabled(view, value); } - @Override - public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) { - if (basicAuthCredential != null) { - handler.proceed(basicAuthCredential.username, basicAuthCredential.password); - return; - } - super.onReceivedHttpAuthRequest(view, handler, host, realm); + @ReactProp(name = "cacheMode") + public void setCacheMode(RNCWebView view, @Nullable String value) { + mRNCWebViewManagerImpl.setCacheMode(view, value); } - @Override - public void onReceivedSslError(final WebView webView, final SslErrorHandler handler, final SslError error) { - // onReceivedSslError is called for most requests, per Android docs: https://developer.android.com/reference/android/webkit/WebViewClient#onReceivedSslError(android.webkit.WebView,%2520android.webkit.SslErrorHandler,%2520android.net.http.SslError) - // WebView.getUrl() will return the top-level window URL. - // If a top-level navigation triggers this error handler, the top-level URL will be the failing URL (not the URL of the currently-rendered page). - // This is desired behavior. We later use these values to determine whether the request is a top-level navigation or a subresource request. - String topWindowUrl = webView.getUrl(); - String failingUrl = error.getUrl(); - - // Cancel request after obtaining top-level URL. - // If request is cancelled before obtaining top-level URL, undesired behavior may occur. - // Undesired behavior: Return value of WebView.getUrl() may be the current URL instead of the failing URL. - handler.cancel(); - - if (!topWindowUrl.equalsIgnoreCase(failingUrl)) { - // If error is not due to top-level navigation, then do not call onReceivedError() - Log.w(TAG, "Resource blocked from loading due to SSL error. Blocked URL: "+failingUrl); - return; - } - - int code = error.getPrimaryError(); - String description = ""; - String descriptionPrefix = "SSL error: "; - - // https://developer.android.com/reference/android/net/http/SslError.html - switch (code) { - case SslError.SSL_DATE_INVALID: - description = "The date of the certificate is invalid"; - break; - case SslError.SSL_EXPIRED: - description = "The certificate has expired"; - break; - case SslError.SSL_IDMISMATCH: - description = "Hostname mismatch"; - break; - case SslError.SSL_INVALID: - description = "A generic error occurred"; - break; - case SslError.SSL_NOTYETVALID: - description = "The certificate is not yet valid"; - break; - case SslError.SSL_UNTRUSTED: - description = "The certificate authority is not trusted"; - break; - default: - description = "Unknown SSL Error"; - break; - } - - description = descriptionPrefix + description; - - this.onReceivedError( - webView, - code, - description, - failingUrl - ); + @ReactProp(name = "domStorageEnabled") + public void setDomStorageEnabled(RNCWebView view, boolean value) { + mRNCWebViewManagerImpl.setDomStorageEnabled(view, value); } - @Override - public void onReceivedError( - WebView webView, - int errorCode, - String description, - String failingUrl) { - - if (ignoreErrFailedForThisURL != null - && failingUrl.equals(ignoreErrFailedForThisURL) - && errorCode == -1 - && description.equals("net::ERR_FAILED")) { - - // This is a workaround for a bug in the WebView. - // See these chromium issues for more context: - // https://bugs.chromium.org/p/chromium/issues/detail?id=1023678 - // https://bugs.chromium.org/p/chromium/issues/detail?id=1050635 - // This entire commit should be reverted once this bug is resolved in chromium. - setIgnoreErrFailedForThisURL(null); - return; - } - - super.onReceivedError(webView, errorCode, description, failingUrl); - mLastLoadFailed = true; - - // In case of an error JS side expect to get a finish event first, and then get an error event - // Android WebView does it in the opposite way, so we need to simulate that behavior - emitFinishEvent(webView, failingUrl); - - WritableMap eventData = createWebViewEvent(webView, failingUrl); - eventData.putDouble("code", errorCode); - eventData.putString("description", description); - - ((RNCWebView) webView).dispatchEvent( - webView, - new TopLoadingErrorEvent(webView.getId(), eventData)); + @ReactProp(name = "downloadingMessage") + public void setDownloadingMessage(RNCWebView view, @Nullable String value) { + mRNCWebViewManagerImpl.setDownloadingMessage(value); } - @RequiresApi(api = Build.VERSION_CODES.M) - @Override - public void onReceivedHttpError( - WebView webView, - WebResourceRequest request, - WebResourceResponse errorResponse) { - super.onReceivedHttpError(webView, request, errorResponse); - - if (request.isForMainFrame()) { - WritableMap eventData = createWebViewEvent(webView, request.getUrl().toString()); - eventData.putInt("statusCode", errorResponse.getStatusCode()); - eventData.putString("description", errorResponse.getReasonPhrase()); - - ((RNCWebView) webView).dispatchEvent( - webView, - new TopHttpErrorEvent(webView.getId(), eventData)); - } + @ReactProp(name = "forceDarkOn") + public void setForceDarkOn(RNCWebView view, boolean value) { + mRNCWebViewManagerImpl.setForceDarkOn(view, value); } - @TargetApi(Build.VERSION_CODES.O) - @Override - public boolean onRenderProcessGone(WebView webView, RenderProcessGoneDetail detail) { - // WebViewClient.onRenderProcessGone was added in O. - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - return false; - } - super.onRenderProcessGone(webView, detail); - - if(detail.didCrash()){ - Log.e(TAG, "The WebView rendering process crashed."); - } - else{ - Log.w(TAG, "The WebView rendering process was killed by the system."); - } - - // if webView is null, we cannot return any event - // since the view is already dead/disposed - // still prevent the app crash by returning true. - if(webView == null){ - return true; - } - - WritableMap event = createWebViewEvent(webView, webView.getUrl()); - event.putBoolean("didCrash", detail.didCrash()); - - ((RNCWebView) webView).dispatchEvent( - webView, - new TopRenderProcessGoneEvent(webView.getId(), event) - ); - - // returning false would crash the app. - return true; + @ReactProp(name = "geolocationEnabled") + public void setGeolocationEnabled(RNCWebView view, boolean value) { + mRNCWebViewManagerImpl.setGeolocationEnabled(view, value); } - protected void emitFinishEvent(WebView webView, String url) { - ((RNCWebView) webView).dispatchEvent( - webView, - new TopLoadingFinishEvent( - webView.getId(), - createWebViewEvent(webView, url))); + @ReactProp(name = "hasOnScroll") + public void setHasOnScroll(RNCWebView view, boolean hasScrollEvent) { + mRNCWebViewManagerImpl.setHasOnScroll(view, hasScrollEvent); } - protected WritableMap createWebViewEvent(WebView webView, String url) { - WritableMap event = Arguments.createMap(); - event.putDouble("target", webView.getId()); - // Don't use webView.getUrl() here, the URL isn't updated to the new value yet in callbacks - // like onPageFinished - event.putString("url", url); - event.putBoolean("loading", !mLastLoadFailed && webView.getProgress() != 100); - event.putString("title", webView.getTitle()); - event.putBoolean("canGoBack", webView.canGoBack()); - event.putBoolean("canGoForward", webView.canGoForward()); - return event; + @ReactProp(name = "incognito") + public void setIncognito(RNCWebView view, boolean value) { + mRNCWebViewManagerImpl.setIncognito(view, value); } - public void setUrlPrefixesForDefaultIntent(ReadableArray specialUrls) { - mUrlPrefixesForDefaultIntent = specialUrls; + @ReactProp(name = "injectedJavaScript") + public void setInjectedJavaScript(RNCWebView view, @Nullable String value) { + mRNCWebViewManagerImpl.setInjectedJavaScript(view, value); } - public void setProgressChangedFilter(RNCWebView.ProgressChangedFilter filter) { - progressChangedFilter = filter; + @ReactProp(name = "injectedJavaScriptBeforeContentLoaded") + public void setInjectedJavaScriptBeforeContentLoaded(RNCWebView view, @Nullable String value) { + mRNCWebViewManagerImpl.setInjectedJavaScriptBeforeContentLoaded(view, value); } - } - - protected static class RNCWebChromeClient extends WebChromeClient implements LifecycleEventListener { - protected static final FrameLayout.LayoutParams FULLSCREEN_LAYOUT_PARAMS = new FrameLayout.LayoutParams( - LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT, Gravity.CENTER); - - @RequiresApi(api = Build.VERSION_CODES.KITKAT) - protected static final int FULLSCREEN_SYSTEM_UI_VISIBILITY = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | - View.SYSTEM_UI_FLAG_LAYOUT_STABLE | - View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | - View.SYSTEM_UI_FLAG_FULLSCREEN | - View.SYSTEM_UI_FLAG_IMMERSIVE | - View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; - - protected static final int COMMON_PERMISSION_REQUEST = 3; - - protected ReactContext mReactContext; - protected View mWebView; - - protected View mVideoView; - protected WebChromeClient.CustomViewCallback mCustomViewCallback; - /* - * - Permissions - - * As native permissions are asynchronously handled by the PermissionListener, many fields have - * to be stored to send permissions results to the webview - */ + @ReactProp(name = "injectedJavaScriptForMainFrameOnly") + public void setInjectedJavaScriptForMainFrameOnly(RNCWebView view, boolean value) { + mRNCWebViewManagerImpl.setInjectedJavaScriptForMainFrameOnly(view, value); - // Webview camera & audio permission callback - protected PermissionRequest permissionRequest; - // Webview camera & audio permission already granted - protected List grantedPermissions; - - // Webview geolocation permission callback - protected GeolocationPermissions.Callback geolocationPermissionCallback; - // Webview geolocation permission origin callback - protected String geolocationPermissionOrigin; - - // true if native permissions dialog is shown, false otherwise - protected boolean permissionsRequestShown = false; - // Pending Android permissions for the next request - protected List pendingPermissions = new ArrayList<>(); - - protected RNCWebView.ProgressChangedFilter progressChangedFilter = null; - - // True if protected media should be allowed, false otherwise - protected boolean mAllowsProtectedMedia = false; - - public RNCWebChromeClient(ReactContext reactContext, WebView webView) { - this.mReactContext = reactContext; - this.mWebView = webView; } - @Override - public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) { - - final WebView newWebView = new WebView(view.getContext()); - final WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj; - transport.setWebView(newWebView); - resultMsg.sendToTarget(); + @ReactProp(name = "injectedJavaScriptBeforeContentLoadedForMainFrameOnly") + public void setInjectedJavaScriptBeforeContentLoadedForMainFrameOnly(RNCWebView view, boolean value) { + mRNCWebViewManagerImpl.setInjectedJavaScriptBeforeContentLoadedForMainFrameOnly(view, value); - return true; } - @Override - public boolean onConsoleMessage(ConsoleMessage message) { - if (ReactBuildConfig.DEBUG) { - return super.onConsoleMessage(message); - } - // Ignore console logs in non debug builds. - return true; + @ReactProp(name = "javaScriptCanOpenWindowsAutomatically") + public void setJavaScriptCanOpenWindowsAutomatically(RNCWebView view, boolean value) { + mRNCWebViewManagerImpl.setJavaScriptCanOpenWindowsAutomatically(view, value); } - @Override - public void onProgressChanged(WebView webView, int newProgress) { - super.onProgressChanged(webView, newProgress); - final String url = webView.getUrl(); - if (progressChangedFilter.isWaitingForCommandLoadUrl()) { - return; - } - WritableMap event = Arguments.createMap(); - event.putDouble("target", webView.getId()); - event.putString("title", webView.getTitle()); - event.putString("url", url); - event.putBoolean("canGoBack", webView.canGoBack()); - event.putBoolean("canGoForward", webView.canGoForward()); - event.putDouble("progress", (float) newProgress / 100); - ((RNCWebView) webView).dispatchEvent( - webView, - new TopLoadingProgressEvent( - webView.getId(), - event)); + @ReactProp(name = "javaScriptEnabled") + public void setJavaScriptEnabled(RNCWebView view, boolean enabled) { + mRNCWebViewManagerImpl.setJavaScriptEnabled(view, enabled); } - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - @Override - public void onPermissionRequest(final PermissionRequest request) { - - grantedPermissions = new ArrayList<>(); - - ArrayList requestedAndroidPermissions = new ArrayList<>(); - for (String requestedResource : request.getResources()) { - String androidPermission = null; - - if (requestedResource.equals(PermissionRequest.RESOURCE_AUDIO_CAPTURE)) { - androidPermission = Manifest.permission.RECORD_AUDIO; - } else if (requestedResource.equals(PermissionRequest.RESOURCE_VIDEO_CAPTURE)) { - androidPermission = Manifest.permission.CAMERA; - } else if(requestedResource.equals(PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID)) { - if (mAllowsProtectedMedia) { - grantedPermissions.add(requestedResource); - } else { - /** - * Legacy handling (Kept in case it was working under some conditions (given Android version or something)) - * - * Try to ask user to grant permission using Activity.requestPermissions - * - * Find more details here: https://github.com/react-native-webview/react-native-webview/pull/2732 - */ - androidPermission = PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID; - } - } - // TODO: RESOURCE_MIDI_SYSEX. - - if (androidPermission != null) { - if (ContextCompat.checkSelfPermission(mReactContext, androidPermission) == PackageManager.PERMISSION_GRANTED) { - grantedPermissions.add(requestedResource); - } else { - requestedAndroidPermissions.add(androidPermission); - } - } - } - - // If all the permissions are already granted, send the response to the WebView synchronously - if (requestedAndroidPermissions.isEmpty()) { - request.grant(grantedPermissions.toArray(new String[0])); - grantedPermissions = null; - return; - } - - // Otherwise, ask to Android System for native permissions asynchronously - - this.permissionRequest = request; - - requestPermissions(requestedAndroidPermissions); - } - - - @Override - public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) { - - if (ContextCompat.checkSelfPermission(mReactContext, Manifest.permission.ACCESS_FINE_LOCATION) - != PackageManager.PERMISSION_GRANTED) { - - /* - * Keep the trace of callback and origin for the async permission request - */ - geolocationPermissionCallback = callback; - geolocationPermissionOrigin = origin; - - requestPermissions(Collections.singletonList(Manifest.permission.ACCESS_FINE_LOCATION)); - - } else { - callback.invoke(origin, true, false); - } + @ReactProp(name = "lackPermissionToDownloadMessage") + public void setLackPermissionToDownloadMessage(RNCWebView view, @Nullable String value) { + mRNCWebViewManagerImpl.setLackPermissionToDownloadMessage(value); } - private PermissionAwareActivity getPermissionAwareActivity() { - Activity activity = mReactContext.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; + @ReactProp(name = "mediaPlaybackRequiresUserAction") + public void setMediaPlaybackRequiresUserAction(RNCWebView view, boolean value) { + mRNCWebViewManagerImpl.setMediaPlaybackRequiresUserAction(view, value); } - private synchronized void requestPermissions(List permissions) { - - /* - * If permissions request dialog is displayed on the screen and another request is sent to the - * activity, the last permission asked is skipped. As a work-around, we use pendingPermissions - * to store next required permissions. - */ - - if (permissionsRequestShown) { - pendingPermissions.addAll(permissions); - return; - } - - PermissionAwareActivity activity = getPermissionAwareActivity(); - permissionsRequestShown = true; - - activity.requestPermissions( - permissions.toArray(new String[0]), - COMMON_PERMISSION_REQUEST, - webviewPermissionsListener - ); - - // Pending permissions have been sent, the list can be cleared - pendingPermissions.clear(); + @ReactProp(name = "messagingEnabled") + public void setMessagingEnabled(RNCWebView view, boolean value) { + mRNCWebViewManagerImpl.setMessagingEnabled(view, value); } - - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - private PermissionListener webviewPermissionsListener = (requestCode, permissions, grantResults) -> { - - permissionsRequestShown = false; - - /* - * As a "pending requests" approach is used, requestCode cannot help to define if the request - * came from geolocation or camera/audio. This is why shouldAnswerToPermissionRequest is used - */ - boolean shouldAnswerToPermissionRequest = false; - - for (int i = 0; i < permissions.length; i++) { - - String permission = permissions[i]; - boolean granted = grantResults[i] == PackageManager.PERMISSION_GRANTED; - - if (permission.equals(Manifest.permission.ACCESS_FINE_LOCATION) - && geolocationPermissionCallback != null - && geolocationPermissionOrigin != null) { - - if (granted) { - geolocationPermissionCallback.invoke(geolocationPermissionOrigin, true, false); - } else { - geolocationPermissionCallback.invoke(geolocationPermissionOrigin, false, false); - } - - geolocationPermissionCallback = null; - geolocationPermissionOrigin = null; - } - - if (permission.equals(Manifest.permission.RECORD_AUDIO)) { - if (granted && grantedPermissions != null) { - grantedPermissions.add(PermissionRequest.RESOURCE_AUDIO_CAPTURE); - } - shouldAnswerToPermissionRequest = true; - } - - if (permission.equals(Manifest.permission.CAMERA)) { - if (granted && grantedPermissions != null) { - grantedPermissions.add(PermissionRequest.RESOURCE_VIDEO_CAPTURE); - } - shouldAnswerToPermissionRequest = true; - } - - if (permission.equals(PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID)) { - if (granted && grantedPermissions != null) { - grantedPermissions.add(PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID); - } - shouldAnswerToPermissionRequest = true; - } - } - - if (shouldAnswerToPermissionRequest - && permissionRequest != null - && grantedPermissions != null) { - permissionRequest.grant(grantedPermissions.toArray(new String[0])); - permissionRequest = null; - grantedPermissions = null; - } - - if (!pendingPermissions.isEmpty()) { - requestPermissions(pendingPermissions); - return false; - } - - return true; - }; - - protected void openFileChooser(ValueCallback filePathCallback, String acceptType) { - getModule(mReactContext).startPhotoPickerIntent(filePathCallback, acceptType); + @ReactProp(name = "menuItems") + public void setMenuCustomItems(RNCWebView view, @Nullable ReadableArray items) { + mRNCWebViewManagerImpl.setMenuCustomItems(view, items); } - protected void openFileChooser(ValueCallback filePathCallback) { - getModule(mReactContext).startPhotoPickerIntent(filePathCallback, ""); + @ReactProp(name = "messagingModuleName") + public void setMessagingModuleName(RNCWebView view, @Nullable String value) { + mRNCWebViewManagerImpl.setMessagingModuleName(view, value); } - protected void openFileChooser(ValueCallback filePathCallback, String acceptType, String capture) { - getModule(mReactContext).startPhotoPickerIntent(filePathCallback, acceptType); + @ReactProp(name = "minimumFontSize") + public void setMinimumFontSize(RNCWebView view, int value) { + mRNCWebViewManagerImpl.setMinimumFontSize(view, value); } - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - @Override - public boolean onShowFileChooser(WebView webView, ValueCallback filePathCallback, FileChooserParams fileChooserParams) { - String[] acceptTypes = fileChooserParams.getAcceptTypes(); - boolean allowMultiple = fileChooserParams.getMode() == WebChromeClient.FileChooserParams.MODE_OPEN_MULTIPLE; - return getModule(mReactContext).startPhotoPickerIntent(filePathCallback, acceptTypes, allowMultiple); + @ReactProp(name = "mixedContentMode") + public void setMixedContentMode(RNCWebView view, @Nullable String value) { + mRNCWebViewManagerImpl.setMixedContentMode(view, value); } - @Override - public void onHostResume() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && mVideoView != null && mVideoView.getSystemUiVisibility() != FULLSCREEN_SYSTEM_UI_VISIBILITY) { - mVideoView.setSystemUiVisibility(FULLSCREEN_SYSTEM_UI_VISIBILITY); - } + @ReactProp(name = "nestedScrollEnabled") + public void setNestedScrollEnabled(RNCWebView view, boolean value) { + mRNCWebViewManagerImpl.setNestedScrollEnabled(view, value); } - @Override - public void onHostPause() { } - - @Override - public void onHostDestroy() { } - - protected ViewGroup getRootView() { - return (ViewGroup) mReactContext.getCurrentActivity().findViewById(android.R.id.content); + @ReactProp(name = "overScrollMode") + public void setOverScrollMode(RNCWebView view, @Nullable String value) { + mRNCWebViewManagerImpl.setOverScrollMode(view, value); } - public void setProgressChangedFilter(RNCWebView.ProgressChangedFilter filter) { - progressChangedFilter = filter; + @ReactProp(name = "saveFormDataDisabled") + public void setSaveFormDataDisabled(RNCWebView view, boolean value) { + mRNCWebViewManagerImpl.setSaveFormDataDisabled(view, value); } - /** - * Set whether or not protected media should be allowed - * /!\ Setting this to false won't revoke permission already granted to the current webpage. - * In order to do so, you'd need to reload the page /!\ - */ - public void setAllowsProtectedMedia(boolean enabled) { - mAllowsProtectedMedia = enabled; + @ReactProp(name = "scalesPageToFit") + public void setScalesPageToFit(RNCWebView view, boolean value) { + mRNCWebViewManagerImpl.setScalesPageToFit(view, value); } - } - - /** - * Subclass of {@link WebView} that implements {@link LifecycleEventListener} interface in order - * to call {@link WebView#destroy} on activity destroy event and also to clear the client - */ - protected static class RNCWebView extends WebView implements LifecycleEventListener { - protected @Nullable - String injectedJS; - protected @Nullable - String injectedJSBeforeContentLoaded; - - /** - * android.webkit.WebChromeClient fundamentally does not support JS injection into frames other - * than the main frame, so these two properties are mostly here just for parity with iOS & macOS. - */ - protected boolean injectedJavaScriptForMainFrameOnly = true; - protected boolean injectedJavaScriptBeforeContentLoadedForMainFrameOnly = true; - protected boolean messagingEnabled = false; - protected @Nullable - String messagingModuleName; - protected @Nullable - RNCWebViewClient mRNCWebViewClient; - protected @Nullable - CatalystInstance mCatalystInstance; - protected boolean sendContentSizeChangeEvents = false; - private OnScrollDispatchHelper mOnScrollDispatchHelper; - protected boolean hasScrollEvent = false; - protected boolean nestedScrollEnabled = false; - protected ProgressChangedFilter progressChangedFilter; - - /** - * WebView must be created with an context of the current activity - *

- * Activity Context is required for creation of dialogs internally by WebView - * Reactive Native needed for access to ReactNative internal system functionality - */ - public RNCWebView(ThemedReactContext reactContext) { - super(reactContext); - this.createCatalystInstance(); - progressChangedFilter = new ProgressChangedFilter(); + @ReactProp(name = "setBuiltInZoomControls") + public void setSetBuiltInZoomControls(RNCWebView view, boolean value) { + mRNCWebViewManagerImpl.setSetBuiltInZoomControls(view, value); } - public void setIgnoreErrFailedForThisURL(String url) { - mRNCWebViewClient.setIgnoreErrFailedForThisURL(url); + @ReactProp(name = "setDisplayZoomControls") + public void setSetDisplayZoomControls(RNCWebView view, boolean value) { + mRNCWebViewManagerImpl.setSetDisplayZoomControls(view, value); } - public void setBasicAuthCredential(BasicAuthCredential credential) { - mRNCWebViewClient.setBasicAuthCredential(credential); + @ReactProp(name = "setSupportMultipleWindows") + public void setSetSupportMultipleWindows(RNCWebView view, boolean value) { + mRNCWebViewManagerImpl.setSetSupportMultipleWindows(view, value); } - public void setSendContentSizeChangeEvents(boolean sendContentSizeChangeEvents) { - this.sendContentSizeChangeEvents = sendContentSizeChangeEvents; + @ReactProp(name = "showsHorizontalScrollIndicator") + public void setShowsHorizontalScrollIndicator(RNCWebView view, boolean value) { + mRNCWebViewManagerImpl.setShowsHorizontalScrollIndicator(view, value); } - public void setHasScrollEvent(boolean hasScrollEvent) { - this.hasScrollEvent = hasScrollEvent; + @ReactProp(name = "showsVerticalScrollIndicator") + public void setShowsVerticalScrollIndicator(RNCWebView view, boolean value) { + mRNCWebViewManagerImpl.setShowsVerticalScrollIndicator(view, value); } - public void setNestedScrollEnabled(boolean nestedScrollEnabled) { - this.nestedScrollEnabled = nestedScrollEnabled; + @ReactProp(name = "source") + public void setSource(RNCWebView view, @Nullable ReadableMap value) { + mRNCWebViewManagerImpl.setSource(view, value, false); } - @Override - public void onHostResume() { - // do nothing + @ReactProp(name = "textZoom") + public void setTextZoom(RNCWebView view, int value) { + mRNCWebViewManagerImpl.setTextZoom(view, value); } - @Override - public void onHostPause() { - // do nothing + @ReactProp(name = "thirdPartyCookiesEnabled") + public void setThirdPartyCookiesEnabled(RNCWebView view, boolean value) { + mRNCWebViewManagerImpl.setThirdPartyCookiesEnabled(view, value); } - @Override - public void onHostDestroy() { - cleanupCallbacksAndDestroy(); + @ReactProp(name = "webviewDebuggingEnabled") + public void setWebviewDebuggingEnabled(RNCWebView view, boolean value) { + mRNCWebViewManagerImpl.setWebviewDebuggingEnabled(view, value); } - @Override - public boolean onTouchEvent(MotionEvent event) { - if (this.nestedScrollEnabled) { - requestDisallowInterceptTouchEvent(true); - } - return super.onTouchEvent(event); + @ReactProp(name = "userAgent") + public void setUserAgent(RNCWebView view, @Nullable String value) { + mRNCWebViewManagerImpl.setUserAgent(view, value); } @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 - ) - ); - } + protected void addEventEmitters(@NonNull ThemedReactContext reactContext, RNCWebView view) { + // Do not register default touch emitter and let WebView implementation handle touches + view.setWebViewClient(new RNCWebViewClient()); } @Override - public void setWebViewClient(WebViewClient client) { - super.setWebViewClient(client); - if (client instanceof RNCWebViewClient) { - mRNCWebViewClient = (RNCWebViewClient) client; - mRNCWebViewClient.setProgressChangedFilter(progressChangedFilter); - } + public Map getExportedCustomDirectEventTypeConstants() { + Map export = super.getExportedCustomDirectEventTypeConstants(); + if (export == null) { + export = MapBuilder.newHashMap(); + } + // Default events but adding them here explicitly for clarity + export.put(TopLoadingStartEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoadingStart")); + export.put(TopLoadingFinishEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoadingFinish")); + export.put(TopLoadingErrorEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoadingError")); + export.put(TopMessageEvent.EVENT_NAME, MapBuilder.of("registrationName", "onMessage")); + // !Default events but adding them here explicitly for clarity + + export.put(TopLoadingProgressEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoadingProgress")); + export.put(TopShouldStartLoadWithRequestEvent.EVENT_NAME, MapBuilder.of("registrationName", "onShouldStartLoadWithRequest")); + export.put(ScrollEventType.getJSEventName(ScrollEventType.SCROLL), MapBuilder.of("registrationName", "onScroll")); + export.put(TopHttpErrorEvent.EVENT_NAME, MapBuilder.of("registrationName", "onHttpError")); + export.put(TopRenderProcessGoneEvent.EVENT_NAME, MapBuilder.of("registrationName", "onRenderProcessGone")); + export.put(TopCustomMenuSelectionEvent.EVENT_NAME, MapBuilder.of("registrationName", "onCustomMenuSelection")); + return export; } - WebChromeClient mWebChromeClient; @Override - public void setWebChromeClient(WebChromeClient client) { - this.mWebChromeClient = client; - super.setWebChromeClient(client); - if (client instanceof RNCWebChromeClient) { - ((RNCWebChromeClient) client).setProgressChangedFilter(progressChangedFilter); - } - } - public @Nullable - RNCWebViewClient getRNCWebViewClient() { - return mRNCWebViewClient; - } - - public void setInjectedJavaScript(@Nullable String js) { - injectedJS = js; - } - - public void setInjectedJavaScriptBeforeContentLoaded(@Nullable String js) { - injectedJSBeforeContentLoaded = js; - } - - public void setInjectedJavaScriptForMainFrameOnly(boolean enabled) { - injectedJavaScriptForMainFrameOnly = enabled; - } - - public void setInjectedJavaScriptBeforeContentLoadedForMainFrameOnly(boolean enabled) { - injectedJavaScriptBeforeContentLoadedForMainFrameOnly = enabled; - } - - protected RNCWebViewBridge createRNCWebViewBridge(RNCWebView webView) { - return new RNCWebViewBridge(webView); - } - - protected void createCatalystInstance() { - ReactContext reactContext = (ReactContext) this.getContext(); - - if (reactContext != null) { - mCatalystInstance = reactContext.getCatalystInstance(); - } - } - - @SuppressLint("AddJavascriptInterface") - public void setMessagingEnabled(boolean enabled) { - if (messagingEnabled == enabled) { - return; - } - - messagingEnabled = enabled; - - if (enabled) { - addJavascriptInterface(createRNCWebViewBridge(this), JAVASCRIPT_INTERFACE); - } else { - removeJavascriptInterface(JAVASCRIPT_INTERFACE); - } - } - - public void setMessagingModuleName(String moduleName) { - messagingModuleName = moduleName; - } - - protected void evaluateJavascriptWithFallback(String script) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - evaluateJavascript(script, null); - return; - } - - try { - loadUrl("javascript:" + URLEncoder.encode(script, "UTF-8")); - } catch (UnsupportedEncodingException e) { - // UTF-8 should always be supported - throw new RuntimeException(e); - } - } - - public void callInjectedJavaScript() { - if (getSettings().getJavaScriptEnabled() && - injectedJS != null && - !TextUtils.isEmpty(injectedJS)) { - evaluateJavascriptWithFallback("(function() {\n" + injectedJS + ";\n})();"); - } - } - - public void callInjectedJavaScriptBeforeContentLoaded() { - if (getSettings().getJavaScriptEnabled() && - injectedJSBeforeContentLoaded != null && - !TextUtils.isEmpty(injectedJSBeforeContentLoaded)) { - evaluateJavascriptWithFallback("(function() {\n" + injectedJSBeforeContentLoaded + ";\n})();"); - } - } - - public void onMessage(String message) { - ReactContext reactContext = (ReactContext) this.getContext(); - RNCWebView mContext = this; - - if (mRNCWebViewClient != null) { - WebView webView = this; - webView.post(new Runnable() { - @Override - public void run() { - if (mRNCWebViewClient == null) { - return; - } - WritableMap data = mRNCWebViewClient.createWebViewEvent(webView, webView.getUrl()); - data.putString("data", message); - - if (mCatalystInstance != null) { - mContext.sendDirectMessage("onMessage", data); - } else { - dispatchEvent(webView, new TopMessageEvent(webView.getId(), data)); - } - } - }); - } else { - WritableMap eventData = Arguments.createMap(); - eventData.putString("data", message); - - if (mCatalystInstance != null) { - this.sendDirectMessage("onMessage", eventData); - } else { - dispatchEvent(this, new TopMessageEvent(this.getId(), eventData)); - } - } - } - - protected void sendDirectMessage(final String method, WritableMap data) { - WritableNativeMap event = new WritableNativeMap(); - event.putMap("nativeEvent", data); - - WritableNativeArray params = new WritableNativeArray(); - params.pushMap(event); - - mCatalystInstance.callFunction(messagingModuleName, method, params); - } - - protected void onScrollChanged(int x, int y, int oldX, int oldY) { - super.onScrollChanged(x, y, oldX, oldY); - - if (!hasScrollEvent) { - return; - } - - if (mOnScrollDispatchHelper == null) { - mOnScrollDispatchHelper = new OnScrollDispatchHelper(); - } - - if (mOnScrollDispatchHelper.onScrollChanged(x, y)) { - ScrollEvent event = ScrollEvent.obtain( - this.getId(), - ScrollEventType.SCROLL, - x, - y, - mOnScrollDispatchHelper.getXFlingVelocity(), - mOnScrollDispatchHelper.getYFlingVelocity(), - this.computeHorizontalScrollRange(), - this.computeVerticalScrollRange(), - this.getWidth(), - this.getHeight()); - - dispatchEvent(this, event); - } - } - - protected void dispatchEvent(WebView webView, Event event) { - ReactContext reactContext = (ReactContext) webView.getContext(); - EventDispatcher eventDispatcher = - reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher(); - eventDispatcher.dispatchEvent(event); - } - - protected void cleanupCallbacksAndDestroy() { - setWebViewClient(null); - destroy(); + Map getCommandsMap() { + return mRNCWebViewManagerImpl.getCommandsMap(); } @Override - public void destroy() { - if (mWebChromeClient != null) { - mWebChromeClient.onHideCustomView(); - } - super.destroy(); + public void receiveCommand(@NonNull RNCWebView reactWebView, String commandId, @Nullable ReadableArray args) { + mRNCWebViewManagerImpl.receiveCommand(reactWebView, commandId, args); + super.receiveCommand(reactWebView, commandId, args); } - protected class RNCWebViewBridge { - RNCWebView mContext; - - RNCWebViewBridge(RNCWebView c) { - mContext = c; - } - - /** - * This method is called whenever JavaScript running within the web view calls: - * - window[JAVASCRIPT_INTERFACE].postMessage - */ - @JavascriptInterface - public void postMessage(String message) { - mContext.onMessage(message); - } - } - - protected static class ProgressChangedFilter { - private boolean waitingForCommandLoadUrl = false; - - public void setWaitingForCommandLoadUrl(boolean isWaiting) { - waitingForCommandLoadUrl = isWaiting; - } - - public boolean isWaitingForCommandLoadUrl() { - return waitingForCommandLoadUrl; - } + @Override + public void onDropViewInstance(@NonNull RNCWebView view) { + mRNCWebViewManagerImpl.onDropViewInstance(view); + super.onDropViewInstance(view); } - } -} - -class BasicAuthCredential { - String username; - String password; - - BasicAuthCredential(String username, String password) { - this.username = username; - this.password = password; - } -} +} \ No newline at end of file diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebViewManagerImpl.kt b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebViewManagerImpl.kt new file mode 100644 index 0000000000000..e85b9f5b2ed79 --- /dev/null +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebViewManagerImpl.kt @@ -0,0 +1,652 @@ +package versioned.host.exp.exponent.modules.api.components.webview + +import android.app.DownloadManager +import android.content.pm.ActivityInfo +import android.graphics.Bitmap +import android.graphics.Color +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.util.Log +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import android.webkit.CookieManager +import android.webkit.DownloadListener +import android.webkit.WebSettings +import android.webkit.WebView +import androidx.webkit.WebSettingsCompat +import androidx.webkit.WebViewFeature +import com.facebook.react.bridge.ReadableArray +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.common.MapBuilder +import com.facebook.react.common.build.ReactBuildConfig +import com.facebook.react.uimanager.ThemedReactContext +import org.json.JSONException +import org.json.JSONObject +import java.io.UnsupportedEncodingException +import java.net.MalformedURLException +import java.net.URL +import java.util.* + +val invalidCharRegex = "[\\\\/%\"]".toRegex() + +class RNCWebViewManagerImpl { + companion object { + const val NAME = "RNCWebView" + } + + private val TAG = "RNCWebViewManagerImpl" + private var mWebViewConfig: RNCWebViewConfig = RNCWebViewConfig { webView: WebView? -> } + private var mAllowsFullscreenVideo = false + private var mAllowsProtectedMedia = false + private var mDownloadingMessage: String? = null + private var mLackPermissionToDownloadMessage: String? = null + + private var mUserAgent: String? = null + private var mUserAgentWithApplicationName: String? = null + private val HTML_ENCODING = "UTF-8" + private val HTML_MIME_TYPE = "text/html" + private val HTTP_METHOD_POST = "POST" + + // Use `webView.loadUrl("about:blank")` to reliably reset the view + // state and release page resources (including any running JavaScript). + private val BLANK_URL = "about:blank" + + private val DEFAULT_DOWNLOADING_MESSAGE = "Downloading" + private val DEFAULT_LACK_PERMISSION_TO_DOWNLOAD_MESSAGE = + "Cannot download files as permission was denied. Please provide permission to write to storage, in order to download files." + + fun createRNCWebViewInstance(context: ThemedReactContext): RNCWebView { + return RNCWebView(context) + } + + fun createViewInstance(context: ThemedReactContext): RNCWebView { + val webView = createRNCWebViewInstance(context) + return createViewInstance(context, webView); + } + + fun createViewInstance(context: ThemedReactContext, webView: RNCWebView): RNCWebView { + setupWebChromeClient(webView) + context.addLifecycleEventListener(webView) + mWebViewConfig.configWebView(webView) + val settings = webView.settings + settings.builtInZoomControls = true + settings.displayZoomControls = false + settings.domStorageEnabled = true + settings.setSupportMultipleWindows(true) + settings.allowFileAccess = false + settings.allowContentAccess = false + settings.allowFileAccessFromFileURLs = false + setAllowUniversalAccessFromFileURLs(webView, false) + setMixedContentMode(webView, "never") + + // Fixes broken full-screen modals/galleries due to body height being 0. + webView.layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + if (ReactBuildConfig.DEBUG) { + WebView.setWebContentsDebuggingEnabled(true) + } + webView.setDownloadListener(DownloadListener { url, userAgent, contentDisposition, mimetype, contentLength -> + webView.setIgnoreErrFailedForThisURL(url) + val module = webView.themedReactContext.getNativeModule(RNCWebViewModule::class.java) ?: return@DownloadListener + val request: DownloadManager.Request = try { + DownloadManager.Request(Uri.parse(url)) + } catch (e: IllegalArgumentException) { + Log.w(TAG, "Unsupported URI, aborting download", e) + return@DownloadListener + } + var fileName = URLUtil.guessFileName(url, contentDisposition, mimetype) + + // Sanitize filename by replacing invalid characters with "_" + fileName = fileName.replace(invalidCharRegex, "_") + + val downloadMessage = "Downloading $fileName" + + //Attempt to add cookie, if it exists + var urlObj: URL? = null + try { + urlObj = URL(url) + val baseUrl = urlObj.protocol + "://" + urlObj.host + val cookie = CookieManager.getInstance().getCookie(baseUrl) + request.addRequestHeader("Cookie", cookie) + } catch (e: MalformedURLException) { + Log.w(TAG, "Error getting cookie for DownloadManager", e) + } + + //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( + getDownloadingMessageOrDefault(), + getLackPermissionToDownloadMessageOrDefault() + ) + ) { + module.downloadFile( + getDownloadingMessageOrDefault() + ) + } + }) + return webView + } + + private fun setupWebChromeClient( + webView: RNCWebView, + ) { + val activity = webView.themedReactContext.currentActivity + if (mAllowsFullscreenVideo && activity != null) { + val initialRequestedOrientation = activity.requestedOrientation + val webChromeClient: RNCWebChromeClient = + object : RNCWebChromeClient(webView) { + override fun getDefaultVideoPoster(): Bitmap? { + return Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888) + } + + override fun onShowCustomView(view: View, callback: CustomViewCallback) { + if (mVideoView != null) { + callback.onCustomViewHidden() + return + } + mVideoView = view + mCustomViewCallback = callback + activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED + mVideoView.systemUiVisibility = FULLSCREEN_SYSTEM_UI_VISIBILITY + activity.window.setFlags( + WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, + WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS + ) + mVideoView.setBackgroundColor(Color.BLACK) + + // Since RN's Modals interfere with the View hierarchy + // we will decide which View to hide if the hierarchy + // does not match (i.e., the WebView is within a Modal) + // NOTE: We could use `mWebView.getRootView()` instead of `getRootView()` + // but that breaks the Modal's styles and layout, so we need this to render + // in the main View hierarchy regardless + val rootView = rootView + rootView.addView(mVideoView, FULLSCREEN_LAYOUT_PARAMS) + + // Different root views, we are in a Modal + if (rootView.rootView !== mWebView.rootView) { + mWebView.rootView.visibility = View.GONE + } else { + // Same view hierarchy (no Modal), just hide the WebView then + mWebView.visibility = View.GONE + } + mWebView.themedReactContext.addLifecycleEventListener(this) + } + + override fun onHideCustomView() { + if (mVideoView == null) { + return + } + + // Same logic as above + val rootView = rootView + if (rootView.rootView !== mWebView.rootView) { + mWebView.rootView.visibility = View.VISIBLE + } else { + // Same view hierarchy (no Modal) + mWebView.visibility = View.VISIBLE + } + activity.window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS) + rootView.removeView(mVideoView) + mCustomViewCallback.onCustomViewHidden() + mVideoView = null + mCustomViewCallback = null + activity.requestedOrientation = initialRequestedOrientation + mWebView.themedReactContext.removeLifecycleEventListener(this) + } + } + webChromeClient.setAllowsProtectedMedia(mAllowsProtectedMedia); + webView.webChromeClient = webChromeClient + } else { + var webChromeClient = webView.webChromeClient as RNCWebChromeClient? + webChromeClient?.onHideCustomView() + webChromeClient = object : RNCWebChromeClient(webView) { + override fun getDefaultVideoPoster(): Bitmap? { + return Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888) + } + } + webChromeClient.setAllowsProtectedMedia(mAllowsProtectedMedia); + webView.webChromeClient = webChromeClient + } + } + + fun setUserAgent(view: WebView, userAgent: String?) { + mUserAgent = userAgent + setUserAgentString(view) + } + + fun setApplicationNameForUserAgent(view: WebView, applicationName: String?) { + when { + applicationName != null -> { + val defaultUserAgent = WebSettings.getDefaultUserAgent(view.context) + mUserAgentWithApplicationName = "$defaultUserAgent $applicationName" + } + else -> { + mUserAgentWithApplicationName = null + } + } + setUserAgentString(view) + } + + private fun setUserAgentString(view: WebView) { + when { + mUserAgent != null -> { + view.settings.userAgentString = mUserAgent + } + mUserAgentWithApplicationName != null -> { + view.settings.userAgentString = mUserAgentWithApplicationName + } + else -> { + view.settings.userAgentString = WebSettings.getDefaultUserAgent(view.context) + } + } + } + + fun setBasicAuthCredential(view: WebView, credential: ReadableMap?) { + var basicAuthCredential: RNCBasicAuthCredential? = null + if (credential != null) { + if (credential.hasKey("username") && credential.hasKey("password")) { + val username = credential.getString("username") + val password = credential.getString("password") + basicAuthCredential = RNCBasicAuthCredential(username, password) + } + } + (view as RNCWebView).setBasicAuthCredential(basicAuthCredential) + } + + fun onDropViewInstance(webView: RNCWebView) { + webView.themedReactContext.removeLifecycleEventListener(webView) + webView.cleanupCallbacksAndDestroy() + webView.mWebChromeClient = null + } + + val COMMAND_GO_BACK = 1 + val COMMAND_GO_FORWARD = 2 + val COMMAND_RELOAD = 3 + val COMMAND_STOP_LOADING = 4 + val COMMAND_POST_MESSAGE = 5 + val COMMAND_INJECT_JAVASCRIPT = 6 + val COMMAND_LOAD_URL = 7 + val COMMAND_FOCUS = 8 + + // android commands + val COMMAND_CLEAR_FORM_DATA = 1000 + val COMMAND_CLEAR_CACHE = 1001 + val COMMAND_CLEAR_HISTORY = 1002 + + fun getCommandsMap(): Map? { + return MapBuilder.builder() + .put("goBack", COMMAND_GO_BACK) + .put("goForward", COMMAND_GO_FORWARD) + .put("reload", COMMAND_RELOAD) + .put("stopLoading", COMMAND_STOP_LOADING) + .put("postMessage", COMMAND_POST_MESSAGE) + .put("injectJavaScript", COMMAND_INJECT_JAVASCRIPT) + .put("loadUrl", COMMAND_LOAD_URL) + .put("requestFocus", COMMAND_FOCUS) + .put("clearFormData", COMMAND_CLEAR_FORM_DATA) + .put("clearCache", COMMAND_CLEAR_CACHE) + .put("clearHistory", COMMAND_CLEAR_HISTORY) + .build() + } + + fun receiveCommand(webView: RNCWebView, commandId: String, args: ReadableArray) { + when (commandId) { + "goBack" -> webView.goBack() + "goForward" -> webView.goForward() + "reload" -> webView.reload() + "stopLoading" -> webView.stopLoading() + "postMessage" -> try { + val eventInitDict = JSONObject() + eventInitDict.put("data", args.getString(0)) + webView.evaluateJavascriptWithFallback( + "(function () {" + + "var event;" + + "var data = " + eventInitDict.toString() + ";" + + "try {" + + "event = new MessageEvent('message', data);" + + "} catch (e) {" + + "event = document.createEvent('MessageEvent');" + + "event.initMessageEvent('message', true, true, data.data, data.origin, data.lastEventId, data.source);" + + "}" + + "document.dispatchEvent(event);" + + "})();" + ) + } catch (e: JSONException) { + throw RuntimeException(e) + } + "injectJavaScript" -> webView.evaluateJavascriptWithFallback(args.getString(0)) + "loadUrl" -> { + if (args == null) { + throw RuntimeException("Arguments for loading an url are null!") + } + webView.progressChangedFilter.setWaitingForCommandLoadUrl(false) + webView.loadUrl(args.getString(0)) + } + "requestFocus" -> webView.requestFocus() + "clearFormData" -> webView.clearFormData() + "clearCache" -> { + val includeDiskFiles = args != null && args.getBoolean(0) + webView.clearCache(includeDiskFiles) + } + "clearHistory" -> webView.clearHistory() + } + } + + fun setMixedContentMode(view: WebView, mixedContentMode: String?) { + if (mixedContentMode == null || "never" == mixedContentMode) { + view.settings.mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW + } else if ("always" == mixedContentMode) { + view.settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW + } else if ("compatibility" == mixedContentMode) { + view.settings.mixedContentMode = WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE + } + } + + fun setAllowUniversalAccessFromFileURLs(view: WebView, allow: Boolean) { + view.settings.allowUniversalAccessFromFileURLs = allow + } + + private fun getDownloadingMessageOrDefault(): String? { + return mDownloadingMessage ?: DEFAULT_DOWNLOADING_MESSAGE + } + + private fun getLackPermissionToDownloadMessageOrDefault(): String? { + return mLackPermissionToDownloadMessage + ?: DEFAULT_LACK_PERMISSION_TO_DOWNLOAD_MESSAGE + } + + fun setSource(view: RNCWebView, source: ReadableMap?, newArch: Boolean = true) { + if (source != null) { + if (source.hasKey("html")) { + val html = source.getString("html") + val baseUrl = if (source.hasKey("baseUrl")) source.getString("baseUrl") else "" + view.loadDataWithBaseURL( + baseUrl, + html!!, + HTML_MIME_TYPE, + HTML_ENCODING, + null + ) + return + } + if (source.hasKey("uri")) { + val url = source.getString("uri") + val previousUrl = view.url + if (previousUrl != null && previousUrl == url) { + return + } + if (source.hasKey("method")) { + val method = source.getString("method") + if (method.equals(HTTP_METHOD_POST, ignoreCase = true)) { + var postData: ByteArray? = null + if (source.hasKey("body")) { + val body = source.getString("body") + postData = try { + body!!.toByteArray(charset("UTF-8")) + } catch (e: UnsupportedEncodingException) { + body!!.toByteArray() + } + } + if (postData == null) { + postData = ByteArray(0) + } + view.postUrl(url!!, postData) + return + } + } + val headerMap = HashMap() + if (source.hasKey("headers")) { + if (newArch) { + val headerArray = source.getArray("headers"); + for (header in headerArray!!.toArrayList()) { + val headerCasted = header as HashMap + val name = headerCasted.get("name") ?: "" + val value = headerCasted.get("value") ?: "" + if ("user-agent" == name.lowercase(Locale.ENGLISH)) { + view.settings.userAgentString = value + } else { + headerMap[name] = value + } + } + } else { + val headers = source.getMap("headers") + val iter = headers!!.keySetIterator() + while (iter.hasNextKey()) { + val key = iter.nextKey() + if ("user-agent" == key.lowercase(Locale.ENGLISH)) { + view.settings.userAgentString = headers.getString(key) + } else { + headerMap[key] = headers.getString(key) + } + } + } + } + view.loadUrl(url!!, headerMap) + return + } + } + view.loadUrl(BLANK_URL) + } + + fun setMessagingModuleName(view: RNCWebView, value: String?) { + view.messagingModuleName = value + } + + fun setCacheEnabled(view: RNCWebView, enabled: Boolean) { + view.settings.cacheMode = if (enabled) WebSettings.LOAD_DEFAULT else WebSettings.LOAD_NO_CACHE + } + + fun setIncognito(view: RNCWebView, enabled: Boolean) { + // Don't do anything when incognito is disabled + if (!enabled) { + return; + } + + // Remove all previous cookies + CookieManager.getInstance().removeAllCookies(null); + + // Disable caching + view.settings.cacheMode = WebSettings.LOAD_NO_CACHE + view.clearHistory(); + view.clearCache(true); + + // No form data or autofill enabled + view.clearFormData(); + view.settings.savePassword = false; + view.settings.saveFormData = false; + } + + fun setInjectedJavaScript(view: RNCWebView, injectedJavaScript: String?) { + view.injectedJS = injectedJavaScript + } + + fun setInjectedJavaScriptBeforeContentLoaded(view: RNCWebView, value: String?) { + view.injectedJSBeforeContentLoaded = value + } + + fun setInjectedJavaScriptForMainFrameOnly(view: RNCWebView, value: Boolean) { + view.injectedJavaScriptForMainFrameOnly = value + } + + fun setInjectedJavaScriptBeforeContentLoadedForMainFrameOnly(view: RNCWebView, value: Boolean) { + view.injectedJavaScriptBeforeContentLoadedForMainFrameOnly = value + } + + fun setJavaScriptCanOpenWindowsAutomatically(view: RNCWebView, value: Boolean) { + view.settings.javaScriptCanOpenWindowsAutomatically = value + } + + fun setShowsVerticalScrollIndicator(view: RNCWebView, value: Boolean) { + view.isVerticalScrollBarEnabled = value + } + + fun setShowsHorizontalScrollIndicator(view: RNCWebView, value: Boolean) { + view.isHorizontalScrollBarEnabled = value + } + + fun setMessagingEnabled(view: RNCWebView, value: Boolean) { + view.setMessagingEnabled(value) + } + + fun setMediaPlaybackRequiresUserAction(view: RNCWebView, value: Boolean) { + view.settings.mediaPlaybackRequiresUserGesture = value + } + + fun setHasOnScroll(view: RNCWebView, value: Boolean) { + view.setHasScrollEvent(value) + } + + fun setJavaScriptEnabled(view: RNCWebView, enabled: Boolean) { + view.settings.javaScriptEnabled = enabled + } + + fun setAllowFileAccess(view: RNCWebView, allowFileAccess: Boolean) { + view.settings.allowFileAccess = allowFileAccess; + } + + fun setAllowFileAccessFromFileURLs(view: RNCWebView, value: Boolean) { + view.settings.allowFileAccessFromFileURLs = value; + } + + fun setAllowsFullscreenVideo(view: RNCWebView, value: Boolean) { + mAllowsFullscreenVideo = value + setupWebChromeClient(view) + } + + fun setAndroidLayerType(view: RNCWebView, layerTypeString: String?) { + val layerType = when (layerTypeString) { + "hardware" -> View.LAYER_TYPE_HARDWARE + "software" -> View.LAYER_TYPE_SOFTWARE + else -> View.LAYER_TYPE_NONE + } + view.setLayerType(layerType, null) + } + + fun setCacheMode(view: RNCWebView, cacheModeString: String?) { + view.settings.cacheMode = when (cacheModeString) { + "LOAD_CACHE_ONLY" -> WebSettings.LOAD_CACHE_ONLY + "LOAD_CACHE_ELSE_NETWORK" -> WebSettings.LOAD_CACHE_ELSE_NETWORK + "LOAD_NO_CACHE" -> WebSettings.LOAD_NO_CACHE + "LOAD_DEFAULT" -> WebSettings.LOAD_DEFAULT + else -> WebSettings.LOAD_DEFAULT + } + } + + fun setDomStorageEnabled(view: RNCWebView, value: Boolean) { + view.settings.domStorageEnabled = value + } + + fun setDownloadingMessage(value: String?) { + mDownloadingMessage = value + } + + fun setForceDarkOn(view: RNCWebView, enabled: Boolean) { + // Only Android 10+ support dark mode + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { + if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) { + val forceDarkMode = + if (enabled) WebSettingsCompat.FORCE_DARK_ON else WebSettingsCompat.FORCE_DARK_OFF + WebSettingsCompat.setForceDark(view.settings, forceDarkMode) + } + + // Set how WebView content should be darkened. + // PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING: checks for the "color-scheme" tag. + // If present, it uses media queries. If absent, it applies user-agent (automatic) + // More information about Force Dark Strategy can be found here: + // https://developer.android.com/reference/androidx/webkit/WebSettingsCompat#setForceDarkStrategy(android.webkit.WebSettings) + if (enabled && WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK_STRATEGY)) { + WebSettingsCompat.setForceDarkStrategy( + view.settings, + WebSettingsCompat.DARK_STRATEGY_PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING + ) + } + } + } + + fun setGeolocationEnabled(view: RNCWebView, value: Boolean) { + view.settings.setGeolocationEnabled(value) + } + + fun setLackPermissionToDownloadMessage(value: String?) { + mLackPermissionToDownloadMessage = value + } + + fun setMinimumFontSize(view: RNCWebView, value: Int) { + view.settings.minimumFontSize = value + } + + fun setAllowsProtectedMedia(view: RNCWebView, enabled: Boolean) { + // This variable is used to keep consistency + // in case a new WebChromeClient is created + // (eg. when mAllowsFullScreenVideo changes) + mAllowsProtectedMedia = enabled + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val client = view.webChromeClient + if (client != null && client is RNCWebChromeClient) { + client.setAllowsProtectedMedia(enabled) + } + } + } + + fun setMenuCustomItems(view: RNCWebView, value: ReadableArray) { + view.setMenuCustomItems(value.toArrayList() as List>) + } + + fun setNestedScrollEnabled(view: RNCWebView, value: Boolean) { + view.nestedScrollEnabled = value + } + + fun setOverScrollMode(view: RNCWebView, overScrollModeString: String?) { + view.overScrollMode = when (overScrollModeString) { + "never" -> View.OVER_SCROLL_NEVER + "content" -> View.OVER_SCROLL_IF_CONTENT_SCROLLS + "always" -> View.OVER_SCROLL_ALWAYS + else -> View.OVER_SCROLL_ALWAYS + } + } + + fun setSaveFormDataDisabled(view: RNCWebView, disabled: Boolean) { + view.settings.saveFormData = !disabled + } + + fun setScalesPageToFit(view: RNCWebView, value: Boolean) { + view.settings.loadWithOverviewMode = value + view.settings.useWideViewPort = value + } + + fun setSetBuiltInZoomControls(view: RNCWebView, value: Boolean) { + view.settings.builtInZoomControls = value + } + + fun setSetDisplayZoomControls(view: RNCWebView, value: Boolean) { + view.settings.displayZoomControls = value + + } + + fun setSetSupportMultipleWindows(view: RNCWebView, value: Boolean) { + view.settings.setSupportMultipleWindows(value) + } + + fun setTextZoom(view: RNCWebView, value: Int) { + view.settings.textZoom = value + } + + fun setThirdPartyCookiesEnabled(view: RNCWebView, enabled: Boolean) { + CookieManager.getInstance().setAcceptThirdPartyCookies(view, enabled) + } + + fun setWebviewDebuggingEnabled(view: RNCWebView, enabled: Boolean) { + RNCWebView.setWebContentsDebuggingEnabled(enabled) + } +} \ No newline at end of file diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebViewModule.java b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebViewModule.java index e1919417a8bd5..31b7c82fe855c 100644 --- a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebViewModule.java +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebViewModule.java @@ -1,550 +1,59 @@ package versioned.host.exp.exponent.modules.api.components.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 androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; -import androidx.core.content.ContextCompat; -import androidx.core.content.FileProvider; -import androidx.core.util.Pair; - -import android.util.Log; -import android.webkit.MimeTypeMap; +import androidx.annotation.NonNull; 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.module.annotations.ReactModule; -import com.facebook.react.modules.core.PermissionAwareActivity; -import com.facebook.react.modules.core.PermissionListener; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.concurrent.atomic.AtomicReference; - -import static android.app.Activity.RESULT_OK; - -@ReactModule(name = RNCWebViewModule.MODULE_NAME) -public class RNCWebViewModule extends ReactContextBaseJavaModule implements ActivityEventListener { - public static final String MODULE_NAME = "RNCWebView"; - private static final int PICKER = 1; - private static final int PICKER_LEGACY = 3; - private static final int FILE_DOWNLOAD_PERMISSION_REQUEST = 1; - private ValueCallback filePathCallbackLegacy; - private ValueCallback filePathCallback; - private File outputImage; - private File outputVideo; - private DownloadManager.Request downloadRequest; - - protected static class ShouldOverrideUrlLoadingLock { - protected enum ShouldOverrideCallbackState { - UNDECIDED, - SHOULD_OVERRIDE, - DO_NOT_OVERRIDE, - } - - private int nextLockIdentifier = 1; - private final HashMap> shouldOverrideLocks = new HashMap<>(); - - public synchronized Pair> getNewLock() { - final int lockIdentifier = nextLockIdentifier++; - final AtomicReference shouldOverride = new AtomicReference<>(ShouldOverrideCallbackState.UNDECIDED); - shouldOverrideLocks.put(lockIdentifier, shouldOverride); - return new Pair<>(lockIdentifier, shouldOverride); - } - - @Nullable - public synchronized AtomicReference getLock(Integer lockIdentifier) { - return shouldOverrideLocks.get(lockIdentifier); - } - - public synchronized void removeLock(Integer lockIdentifier) { - shouldOverrideLocks.remove(lockIdentifier); - } - } - protected static final ShouldOverrideUrlLoadingLock shouldOverrideUrlLoadingLock = new ShouldOverrideUrlLoadingLock(); +@ReactModule(name = RNCWebViewModuleImpl.NAME) +public class RNCWebViewModule extends ReactContextBaseJavaModule { + final private RNCWebViewModuleImpl mRNCWebViewModuleImpl; - private enum MimeType { - DEFAULT("*/*"), - IMAGE("image"), - VIDEO("video"); - - private final String value; - - MimeType(String value) { - this.value = value; + public RNCWebViewModule(ReactApplicationContext reactContext) { + super(reactContext); + mRNCWebViewModuleImpl = new RNCWebViewModuleImpl(reactContext); } - } - - private PermissionListener getWebviewFileDownloaderPermissionListener(String downloadingMessage, String lackPermissionToDownloadMessage) { - return 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(downloadingMessage); - } - } else { - Toast.makeText(getCurrentActivity().getApplicationContext(), lackPermissionToDownloadMessage, Toast.LENGTH_LONG).show(); - } - return true; - } - } - return false; - } - }; - } - - public RNCWebViewModule(ReactApplicationContext reactContext) { - super(reactContext); - reactContext.addActivityEventListener(this); - } - @Override - public String getName() { - return MODULE_NAME; - } - - @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; + @ReactMethod + public void isFileUploadSupported(final Promise promise) { + promise.resolve(mRNCWebViewModuleImpl.isFileUploadSupported()); } - promise.resolve(result); - } - @ReactMethod(isBlockingSynchronousMethod = true) - public void onShouldStartLoadWithRequestCallback(final boolean shouldStart, final int lockIdentifier) { - final AtomicReference lockObject = shouldOverrideUrlLoadingLock.getLock(lockIdentifier); - if (lockObject != null) { - synchronized (lockObject) { - lockObject.set(shouldStart ? ShouldOverrideUrlLoadingLock.ShouldOverrideCallbackState.DO_NOT_OVERRIDE : ShouldOverrideUrlLoadingLock.ShouldOverrideCallbackState.SHOULD_OVERRIDE); - lockObject.notify(); - } + @ReactMethod + public void shouldStartLoadWithLockIdentifier(boolean shouldStart, double lockIdentifier) { + mRNCWebViewModuleImpl.shouldStartLoadWithLockIdentifier(shouldStart, lockIdentifier); } - } - - public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { - if (filePathCallback == null && filePathCallbackLegacy == null) { - return; + public void startPhotoPickerIntent(ValueCallback filePathCallback, String acceptType) { + mRNCWebViewModuleImpl.startPhotoPickerIntent(acceptType, filePathCallback); } - boolean imageTaken = false; - boolean videoTaken = false; - - if (outputImage != null && outputImage.length() > 0) { - imageTaken = true; - } - if (outputVideo != null && outputVideo.length() > 0) { - videoTaken = true; + public boolean startPhotoPickerIntent(final ValueCallback callback, final String[] acceptTypes, final boolean allowMultiple, final boolean isCaptureEnabled) { + return mRNCWebViewModuleImpl.startPhotoPickerIntent(acceptTypes, allowMultiple, callback, isCaptureEnabled); } - // 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 { - if (imageTaken) { - filePathCallback.onReceiveValue(new Uri[]{getOutputUri(outputImage)}); - } else if (videoTaken) { - filePathCallback.onReceiveValue(new Uri[]{getOutputUri(outputVideo)}); - } else { - filePathCallback.onReceiveValue(this.getSelectedFiles(data, resultCode)); - } - } - break; - case PICKER_LEGACY: - if (resultCode != RESULT_OK) { - filePathCallbackLegacy.onReceiveValue(null); - } else { - if (imageTaken) { - filePathCallbackLegacy.onReceiveValue(getOutputUri(outputImage)); - } else if (videoTaken) { - filePathCallbackLegacy.onReceiveValue(getOutputUri(outputVideo)); - } else { - filePathCallbackLegacy.onReceiveValue(data.getData()); - } - } - break; - + public void setDownloadRequest(DownloadManager.Request request) { + mRNCWebViewModuleImpl.setDownloadRequest(request); } - if (outputImage != null && !imageTaken) { - outputImage.delete(); + public void downloadFile(String downloadingMessage) { + mRNCWebViewModuleImpl.downloadFile(downloadingMessage); } - if (outputVideo != null && !videoTaken) { - outputVideo.delete(); - } - - filePathCallback = null; - filePathCallbackLegacy = null; - outputImage = null; - outputVideo = null; - } - - public void onNewIntent(Intent intent) { - } - private Uri[] getSelectedFiles(Intent data, int resultCode) { - if (data == null) { - return null; + public boolean grantFileDownloaderPermissions(String downloadingMessage, String lackPermissionToDownloadMessage) { + return mRNCWebViewModuleImpl.grantFileDownloaderPermissions(downloadingMessage, lackPermissionToDownloadMessage); } - // 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; - } - - // we have one file selected - if (data.getData() != null && resultCode == RESULT_OK && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - return WebChromeClient.FileChooserParams.parseResult(resultCode, data); - } - - return null; - } - - public void startPhotoPickerIntent(ValueCallback filePathCallback, String acceptType) { - filePathCallbackLegacy = filePathCallback; - - Intent fileChooserIntent = getFileChooserIntent(acceptType); - Intent chooserIntent = Intent.createChooser(fileChooserIntent, ""); - - ArrayList extraIntents = new ArrayList<>(); - if (acceptsImages(acceptType)) { - Intent photoIntent = getPhotoIntent(); - if (photoIntent != null) { - extraIntents.add(photoIntent); - } - } - if (acceptsVideo(acceptType)) { - Intent videoIntent = getVideoIntent(); - if (videoIntent != null) { - extraIntents.add(videoIntent); - } - } - 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 callback, final String[] acceptTypes, final boolean allowMultiple) { - filePathCallback = callback; - - ArrayList extraIntents = new ArrayList<>(); - if (!needsCameraPermission()) { - if (acceptsImages(acceptTypes)) { - Intent photoIntent = getPhotoIntent(); - if (photoIntent != null) { - extraIntents.add(photoIntent); - } - } - if (acceptsVideo(acceptTypes)) { - Intent videoIntent = getVideoIntent(); - if (videoIntent != null) { - extraIntents.add(videoIntent); - } - } - } - - 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(String downloadingMessage) { - DownloadManager dm = (DownloadManager) getCurrentActivity().getBaseContext().getSystemService(Context.DOWNLOAD_SERVICE); - - try { - dm.enqueue(this.downloadRequest); - } catch (IllegalArgumentException e) { - Log.w("RNCWebViewModule", "Unsupported URI, aborting download", e); - return; - } - - Toast.makeText(getCurrentActivity().getApplicationContext(), downloadingMessage, Toast.LENGTH_LONG).show(); - } - - public boolean grantFileDownloaderPermissions(String downloadingMessage, String lackPermissionToDownloadMessage) { - // Permission not required for Android Q and above - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { - return true; - } - - boolean result = ContextCompat.checkSelfPermission(getCurrentActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; - if (!result && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - PermissionAwareActivity activity = getPermissionAwareActivity(); - activity.requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, FILE_DOWNLOAD_PERMISSION_REQUEST, getWebviewFileDownloaderPermissionListener(downloadingMessage, lackPermissionToDownloadMessage)); - } - - return result; - } - - protected boolean needsCameraPermission() { - boolean needed = false; - - PackageManager packageManager = getCurrentActivity().getPackageManager(); - try { - String[] requestedPermissions = packageManager.getPackageInfo(getReactApplicationContext().getPackageName(), PackageManager.GET_PERMISSIONS).requestedPermissions; - if (Arrays.asList(requestedPermissions).contains(Manifest.permission.CAMERA) - && ContextCompat.checkSelfPermission(getCurrentActivity(), Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { - needed = true; - } - } catch (PackageManager.NameNotFoundException e) { - needed = true; - } - - return needed; - } - - private Intent getPhotoIntent() { - Intent intent = null; - - try { - outputImage = getCapturedFile(MimeType.IMAGE); - Uri outputImageUri = getOutputUri(outputImage); - intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); - intent.putExtra(MediaStore.EXTRA_OUTPUT, outputImageUri); - } catch (IOException | IllegalArgumentException e) { - Log.e("CREATE FILE", "Error occurred while creating the File", e); - e.printStackTrace(); - } - - return intent; - } - - private Intent getVideoIntent() { - Intent intent = null; - - try { - outputVideo = getCapturedFile(MimeType.VIDEO); - Uri outputVideoUri = getOutputUri(outputVideo); - intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); - intent.putExtra(MediaStore.EXTRA_OUTPUT, outputVideoUri); - } catch (IOException | IllegalArgumentException e) { - Log.e("CREATE FILE", "Error occurred while creating the File", e); - e.printStackTrace(); - } - - return intent; - } - - private Intent getFileChooserIntent(String acceptTypes) { - String _acceptTypes = acceptTypes; - if (acceptTypes.isEmpty()) { - _acceptTypes = MimeType.DEFAULT.value; - } - if (acceptTypes.matches("\\.\\w+")) { - _acceptTypes = getMimeTypeFromExtension(acceptTypes.replace(".", "")); - } - 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(MimeType.DEFAULT.value); - intent.putExtra(Intent.EXTRA_MIME_TYPES, getAcceptedMimeType(acceptTypes)); - intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple); - return intent; - } - - private Boolean acceptsImages(String types) { - String mimeType = types; - if (types.matches("\\.\\w+")) { - mimeType = getMimeTypeFromExtension(types.replace(".", "")); - } - return mimeType.isEmpty() || mimeType.toLowerCase().contains(MimeType.IMAGE.value); - } - - private Boolean acceptsImages(String[] types) { - String[] mimeTypes = getAcceptedMimeType(types); - return arrayContainsString(mimeTypes, MimeType.DEFAULT.value) || arrayContainsString(mimeTypes, MimeType.IMAGE.value); - } - - private Boolean acceptsVideo(String types) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - return false; - } - - String mimeType = types; - if (types.matches("\\.\\w+")) { - mimeType = getMimeTypeFromExtension(types.replace(".", "")); - } - return mimeType.isEmpty() || mimeType.toLowerCase().contains(MimeType.VIDEO.value); - } - - private Boolean acceptsVideo(String[] types) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - return false; - } - - String[] mimeTypes = getAcceptedMimeType(types); - return arrayContainsString(mimeTypes, MimeType.DEFAULT.value) || arrayContainsString(mimeTypes, MimeType.VIDEO.value); - } - - 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 (noAcceptTypesSet(types)) { - return new String[]{MimeType.DEFAULT.value}; - } - String[] mimeTypes = new String[types.length]; - for (int i = 0; i < types.length; i++) { - String t = types[i]; - // convert file extensions to mime types - if (t.matches("\\.\\w+")) { - String mimeType = getMimeTypeFromExtension(t.replace(".", "")); - if(mimeType != null) { - mimeTypes[i] = mimeType; - } else { - mimeTypes[i] = t; - } - } else { - mimeTypes[i] = t; - } - } - return mimeTypes; - } - - private String getMimeTypeFromExtension(String extension) { - String type = null; - if (extension != null) { - type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); - } - return type; - } - - private Uri getOutputUri(File capturedFile) { - // 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(MimeType mimeType) throws IOException { - String prefix = ""; - String suffix = ""; - String dir = ""; - - switch (mimeType) { - case IMAGE: - prefix = "image-"; - suffix = ".jpg"; - dir = Environment.DIRECTORY_PICTURES; - break; - case VIDEO: - prefix = "video-"; - suffix = ".mp4"; - dir = Environment.DIRECTORY_MOVIES; - break; - - default: - break; - } - - String filename = prefix + String.valueOf(System.currentTimeMillis()) + suffix; - File outputFile = null; - - // 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); - outputFile = new File(storageDir, filename); - } else { - File storageDir = getReactApplicationContext().getExternalFilesDir(null); - outputFile = File.createTempFile(prefix, suffix, storageDir); - } - - return outputFile; - } - - private Boolean noAcceptTypesSet(String[] types) { - // when our array returned from getAcceptTypes() has no values set from the webview - // i.e. , without any "accept" attr - // will be an array with one empty string element, afaik - - return types.length == 0 || (types.length == 1 && types[0] != null && types[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."); + @NonNull + @Override + public String getName() { + return RNCWebViewModuleImpl.NAME; } - return (PermissionAwareActivity) activity; - } -} +} \ No newline at end of file diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebViewModuleImpl.java b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebViewModuleImpl.java new file mode 100644 index 0000000000000..85916f086c513 --- /dev/null +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebViewModuleImpl.java @@ -0,0 +1,554 @@ +package versioned.host.exp.exponent.modules.api.components.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 androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.core.content.ContextCompat; +import androidx.core.content.FileProvider; +import androidx.core.util.Pair; + +import android.util.Log; +import android.webkit.MimeTypeMap; +import android.webkit.ValueCallback; +import android.webkit.WebChromeClient; +import android.widget.Toast; + +import com.facebook.common.activitylistener.ActivityListenerManager; +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.module.annotations.ReactModule; +import com.facebook.react.modules.core.PermissionAwareActivity; +import com.facebook.react.modules.core.PermissionListener; + +import java.io.File; +import java.io.IOException; +import java.lang.SecurityException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.concurrent.atomic.AtomicReference; + +import static android.app.Activity.RESULT_OK; + +public class RNCWebViewModuleImpl implements ActivityEventListener { + public static final String NAME = "RNCWebView"; + + public static final int PICKER = 1; + public static final int PICKER_LEGACY = 3; + public static final int FILE_DOWNLOAD_PERMISSION_REQUEST = 1; + + final private ReactApplicationContext mContext; + + private DownloadManager.Request mDownloadRequest; + + private ValueCallback mFilePathCallbackLegacy; + private ValueCallback mFilePathCallback; + private File mOutputImage; + private File mOutputVideo; + + public RNCWebViewModuleImpl(ReactApplicationContext context) { + mContext = context; + context.addActivityEventListener(this); + } + + @Override + public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { + if (mFilePathCallback == null && mFilePathCallbackLegacy == null) { + return; + } + + boolean imageTaken = false; + boolean videoTaken = false; + + if (mOutputImage != null && mOutputImage.length() > 0) { + imageTaken = true; + } + if (mOutputVideo != null && mOutputVideo.length() > 0) { + videoTaken = true; + } + + // 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 RNCWebViewModuleImpl.PICKER: + if (resultCode != RESULT_OK) { + if (mFilePathCallback != null) { + mFilePathCallback.onReceiveValue(null); + } + } else { + if (imageTaken) { + mFilePathCallback.onReceiveValue(new Uri[]{getOutputUri(mOutputImage)}); + } else if (videoTaken) { + mFilePathCallback.onReceiveValue(new Uri[]{getOutputUri(mOutputVideo)}); + } else { + mFilePathCallback.onReceiveValue(getSelectedFiles(data, resultCode)); + } + } + break; + case RNCWebViewModuleImpl.PICKER_LEGACY: + if (resultCode != RESULT_OK) { + mFilePathCallbackLegacy.onReceiveValue(null); + } else { + if (imageTaken) { + mFilePathCallbackLegacy.onReceiveValue(getOutputUri(mOutputImage)); + } else if (videoTaken) { + mFilePathCallbackLegacy.onReceiveValue(getOutputUri(mOutputVideo)); + } else { + mFilePathCallbackLegacy.onReceiveValue(data.getData()); + } + } + break; + + } + + if (mOutputImage != null && !imageTaken) { + mOutputImage.delete(); + } + if (mOutputVideo != null && !videoTaken) { + mOutputVideo.delete(); + } + + mFilePathCallback = null; + mFilePathCallbackLegacy = null; + mOutputImage = null; + mOutputVideo = null; + } + + @Override + public void onNewIntent(Intent intent) { + + } + + protected static class ShouldOverrideUrlLoadingLock { + protected enum ShouldOverrideCallbackState { + UNDECIDED, + SHOULD_OVERRIDE, + DO_NOT_OVERRIDE, + } + + private double nextLockIdentifier = 1; + private final HashMap> shouldOverrideLocks = new HashMap<>(); + + public synchronized Pair> getNewLock() { + final double lockIdentifier = nextLockIdentifier++; + final AtomicReference shouldOverride = new AtomicReference<>(ShouldOverrideCallbackState.UNDECIDED); + shouldOverrideLocks.put(lockIdentifier, shouldOverride); + return new Pair<>(lockIdentifier, shouldOverride); + } + + @Nullable + public synchronized AtomicReference getLock(Double lockIdentifier) { + return shouldOverrideLocks.get(lockIdentifier); + } + + public synchronized void removeLock(Double lockIdentifier) { + shouldOverrideLocks.remove(lockIdentifier); + } + } + + protected static final ShouldOverrideUrlLoadingLock shouldOverrideUrlLoadingLock = new ShouldOverrideUrlLoadingLock(); + + private enum MimeType { + DEFAULT("*/*"), + IMAGE("image"), + VIDEO("video"); + + private final String value; + + MimeType(String value) { + this.value = value; + } + } + + private PermissionListener getWebviewFileDownloaderPermissionListener(String downloadingMessage, String lackPermissionToDownloadMessage) { + return 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 (mDownloadRequest != null) { + downloadFile(downloadingMessage); + } + } else { + Toast.makeText(mContext, lackPermissionToDownloadMessage, Toast.LENGTH_LONG).show(); + } + return true; + } + } + return false; + } + }; + } + + public boolean isFileUploadSupported() { + return true; + } + + public void shouldStartLoadWithLockIdentifier(boolean shouldStart, double lockIdentifier) { + final AtomicReference lockObject = shouldOverrideUrlLoadingLock.getLock(lockIdentifier); + if (lockObject != null) { + synchronized (lockObject) { + lockObject.set(shouldStart ? ShouldOverrideUrlLoadingLock.ShouldOverrideCallbackState.DO_NOT_OVERRIDE : ShouldOverrideUrlLoadingLock.ShouldOverrideCallbackState.SHOULD_OVERRIDE); + lockObject.notify(); + } + } + } + + public Uri[] getSelectedFiles(Intent data, int resultCode) { + if (data == null) { + 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; + } + + // we have one file selected + if (data.getData() != null && resultCode == RESULT_OK && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + return WebChromeClient.FileChooserParams.parseResult(resultCode, data); + } + + return null; + } + + public void startPhotoPickerIntent(String acceptType, ValueCallback callback) { + mFilePathCallbackLegacy = callback; + Activity activity = mContext.getCurrentActivity(); + Intent fileChooserIntent = getFileChooserIntent(acceptType); + Intent chooserIntent = Intent.createChooser(fileChooserIntent, ""); + + ArrayList extraIntents = new ArrayList<>(); + if (acceptsImages(acceptType)) { + Intent photoIntent = getPhotoIntent(); + if (photoIntent != null) { + extraIntents.add(photoIntent); + } + } + if (acceptsVideo(acceptType)) { + Intent videoIntent = getVideoIntent(); + if (videoIntent != null) { + extraIntents.add(videoIntent); + } + } + chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Parcelable[]{})); + + if (chooserIntent.resolveActivity(activity.getPackageManager()) != null) { + activity.startActivityForResult(chooserIntent, PICKER_LEGACY); + } else { + Log.w("RNCWebViewModule", "there is no Activity to handle this Intent"); + } + } + + public boolean startPhotoPickerIntent(final String[] acceptTypes, final boolean allowMultiple, final ValueCallback callback, final boolean isCaptureEnabled) { + mFilePathCallback = callback; + Activity activity = mContext.getCurrentActivity(); + + ArrayList extraIntents = new ArrayList<>(); + Intent photoIntent = null; + if (!needsCameraPermission()) { + if (acceptsImages(acceptTypes)) { + photoIntent = getPhotoIntent(); + if (photoIntent != null) { + extraIntents.add(photoIntent); + } + } + if (acceptsVideo(acceptTypes)) { + Intent videoIntent = getVideoIntent(); + if (videoIntent != null) { + extraIntents.add(videoIntent); + } + } + } + + Intent chooserIntent = new Intent(Intent.ACTION_CHOOSER); + if (isCaptureEnabled) { + chooserIntent = photoIntent; + } else { + Intent fileSelectionIntent = getFileChooserIntent(acceptTypes, allowMultiple); + + chooserIntent.putExtra(Intent.EXTRA_INTENT, fileSelectionIntent); + chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Parcelable[]{})); + } + + if (chooserIntent != null) { + if (chooserIntent.resolveActivity(activity.getPackageManager()) != null) { + activity.startActivityForResult(chooserIntent, PICKER); + } else { + Log.w("RNCWebViewModule", "there is no Activity to handle this Intent"); + } + } else { + Log.w("RNCWebViewModule", "there is no Camera permission"); + } + + return true; + } + + public void setDownloadRequest(DownloadManager.Request request) { + mDownloadRequest = request; + } + + public void downloadFile(String downloadingMessage) { + DownloadManager dm = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE); + + try { + dm.enqueue(mDownloadRequest); + } catch (IllegalArgumentException | SecurityException e) { + Log.w("RNCWebViewModule", "Unsupported URI, aborting download", e); + return; + } + + Toast.makeText(mContext, downloadingMessage, Toast.LENGTH_LONG).show(); + } + + public boolean grantFileDownloaderPermissions(String downloadingMessage, String lackPermissionToDownloadMessage) { + Activity activity = mContext.getCurrentActivity(); + // Permission not required for Android Q and above + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { + return true; + } + + boolean result = ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; + if (!result && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + PermissionAwareActivity PAactivity = getPermissionAwareActivity(); + PAactivity.requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, FILE_DOWNLOAD_PERMISSION_REQUEST, getWebviewFileDownloaderPermissionListener(downloadingMessage, lackPermissionToDownloadMessage)); + } + + return result; + } + + protected boolean needsCameraPermission() { + Activity activity = mContext.getCurrentActivity(); + boolean needed = false; + + PackageManager packageManager = activity.getPackageManager(); + try { + String[] requestedPermissions = packageManager.getPackageInfo(activity.getApplicationContext().getPackageName(), PackageManager.GET_PERMISSIONS).requestedPermissions; + if (Arrays.asList(requestedPermissions).contains(Manifest.permission.CAMERA) + && ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { + needed = true; + } + } catch (PackageManager.NameNotFoundException e) { + needed = true; + } + + return needed; + } + + public Intent getPhotoIntent() { + Intent intent = null; + + try { + mOutputImage = getCapturedFile(MimeType.IMAGE); + Uri outputImageUri = getOutputUri(mOutputImage); + intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + intent.putExtra(MediaStore.EXTRA_OUTPUT, outputImageUri); + } catch (IOException | IllegalArgumentException e) { + Log.e("CREATE FILE", "Error occurred while creating the File", e); + e.printStackTrace(); + } + + return intent; + } + + public Intent getVideoIntent() { + Intent intent = null; + + try { + mOutputVideo = getCapturedFile(MimeType.VIDEO); + Uri outputVideoUri = getOutputUri(mOutputVideo); + intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); + intent.putExtra(MediaStore.EXTRA_OUTPUT, outputVideoUri); + } catch (IOException | IllegalArgumentException e) { + Log.e("CREATE FILE", "Error occurred while creating the File", e); + e.printStackTrace(); + } + + return intent; + } + + private Intent getFileChooserIntent(String acceptTypes) { + String _acceptTypes = acceptTypes; + if (acceptTypes.isEmpty()) { + _acceptTypes = MimeType.DEFAULT.value; + } + if (acceptTypes.matches("\\.\\w+")) { + _acceptTypes = getMimeTypeFromExtension(acceptTypes.replace(".", "")); + } + 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(MimeType.DEFAULT.value); + intent.putExtra(Intent.EXTRA_MIME_TYPES, getAcceptedMimeType(acceptTypes)); + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple); + return intent; + } + + private Boolean acceptsImages(String types) { + String mimeType = types; + if (types.matches("\\.\\w+")) { + mimeType = getMimeTypeFromExtension(types.replace(".", "")); + } + return mimeType.isEmpty() || mimeType.toLowerCase().contains(MimeType.IMAGE.value); + } + + private Boolean acceptsImages(String[] types) { + String[] mimeTypes = getAcceptedMimeType(types); + return arrayContainsString(mimeTypes, MimeType.DEFAULT.value) || arrayContainsString(mimeTypes, MimeType.IMAGE.value); + } + + private Boolean acceptsVideo(String types) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return false; + } + + String mimeType = types; + if (types.matches("\\.\\w+")) { + mimeType = getMimeTypeFromExtension(types.replace(".", "")); + } + return mimeType.isEmpty() || mimeType.toLowerCase().contains(MimeType.VIDEO.value); + } + + private Boolean acceptsVideo(String[] types) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return false; + } + + String[] mimeTypes = getAcceptedMimeType(types); + return arrayContainsString(mimeTypes, MimeType.DEFAULT.value) || arrayContainsString(mimeTypes, MimeType.VIDEO.value); + } + + 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 (noAcceptTypesSet(types)) { + return new String[]{MimeType.DEFAULT.value}; + } + String[] mimeTypes = new String[types.length]; + for (int i = 0; i < types.length; i++) { + String t = types[i]; + // convert file extensions to mime types + if (t.matches("\\.\\w+")) { + String mimeType = getMimeTypeFromExtension(t.replace(".", "")); + if(mimeType != null) { + mimeTypes[i] = mimeType; + } else { + mimeTypes[i] = t; + } + } else { + mimeTypes[i] = t; + } + } + return mimeTypes; + } + + private String getMimeTypeFromExtension(String extension) { + String type = null; + if (extension != null) { + type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + } + return type; + } + + public Uri getOutputUri(File capturedFile) { + // 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 = mContext.getPackageName(); + return FileProvider.getUriForFile(mContext, packageName + ".fileprovider", capturedFile); + } + + public File getCapturedFile(MimeType mimeType) throws IOException { + String prefix = ""; + String suffix = ""; + String dir = ""; + + switch (mimeType) { + case IMAGE: + prefix = "image-"; + suffix = ".jpg"; + dir = Environment.DIRECTORY_PICTURES; + break; + case VIDEO: + prefix = "video-"; + suffix = ".mp4"; + dir = Environment.DIRECTORY_MOVIES; + break; + + default: + break; + } + + String filename = prefix + String.valueOf(System.currentTimeMillis()) + suffix; + File outputFile = null; + + // 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); + outputFile = new File(storageDir, filename); + } else { + File storageDir = mContext.getExternalFilesDir(null); + outputFile = File.createTempFile(prefix, suffix, storageDir); + } + + return outputFile; + } + + private Boolean noAcceptTypesSet(String[] types) { + // when our array returned from getAcceptTypes() has no values set from the webview + // i.e. , without any "accept" attr + // will be an array with one empty string element, afaik + + return types.length == 0 || (types.length == 1 && types[0] != null && types[0].length() == 0); + } + + private PermissionAwareActivity getPermissionAwareActivity() { + Activity activity = mContext.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; + } +} \ No newline at end of file diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebViewPackage.java b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebViewPackage.java new file mode 100644 index 0000000000000..5939a6ed02654 --- /dev/null +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebViewPackage.java @@ -0,0 +1,57 @@ +package versioned.host.exp.exponent.modules.api.components.webview; +import host.exp.expoview.BuildConfig; + +import androidx.annotation.Nullable; + +import com.facebook.react.TurboReactPackage; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.module.model.ReactModuleInfo; +import com.facebook.react.module.model.ReactModuleInfoProvider; +import com.facebook.react.uimanager.ViewManager; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class RNCWebViewPackage extends TurboReactPackage { + + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + List viewManagers = new ArrayList<>(); + viewManagers.add(new RNCWebViewManager()); + return viewManagers; + } + + @Override + public ReactModuleInfoProvider getReactModuleInfoProvider() { + return () -> { + final Map moduleInfos = new HashMap<>(); + boolean isTurboModule = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED; + moduleInfos.put( + RNCWebViewModuleImpl.NAME, + new ReactModuleInfo( + RNCWebViewModuleImpl.NAME, + RNCWebViewModuleImpl.NAME, + false, // canOverrideExistingModule + false, // needsEagerInit + true, // hasConstants + false, // isCxxModule + isTurboModule // isTurboModule + )); + return moduleInfos; + }; + } + + @Nullable + @Override + public NativeModule getModule(String name, ReactApplicationContext reactContext) { + if (name.equals(RNCWebViewModuleImpl.NAME)) { + return new RNCWebViewModule(reactContext); + } else { + return null; + } + } + +} \ No newline at end of file diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebViewPackage.kt b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebViewPackage.kt deleted file mode 100644 index f8ce21d6a9b54..0000000000000 --- a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/RNCWebViewPackage.kt +++ /dev/null @@ -1,15 +0,0 @@ -package versioned.host.exp.exponent.modules.api.components.webview - -import com.facebook.react.ReactPackage -import com.facebook.react.bridge.ReactApplicationContext - - -class RNCWebViewPackage: ReactPackage { - override fun createNativeModules(reactContext: ReactApplicationContext) = listOf( - RNCWebViewModule(reactContext) - ) - - override fun createViewManagers(reactContext: ReactApplicationContext) = listOf( - RNCWebViewManager() - ) -} diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/events/TopCustomMenuSelectionEvent.kt b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/events/TopCustomMenuSelectionEvent.kt new file mode 100644 index 0000000000000..8dc2558b0efcb --- /dev/null +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/webview/events/TopCustomMenuSelectionEvent.kt @@ -0,0 +1,24 @@ +package versioned.host.exp.exponent.modules.api.components.webview.events + +import com.facebook.react.bridge.WritableMap +import com.facebook.react.uimanager.events.Event +import com.facebook.react.uimanager.events.RCTEventEmitter + +/** + * Event emitted when there is a loading progress event. + */ +class TopCustomMenuSelectionEvent(viewId: Int, private val mEventData: WritableMap) : + Event(viewId) { + companion object { + const val EVENT_NAME = "topCustomMenuSelection" + } + + override fun getEventName(): String = EVENT_NAME + + override fun canCoalesce(): Boolean = false + + override fun getCoalescingKey(): Short = 0 + + override fun dispatch(rctEventEmitter: RCTEventEmitter) = + rctEventEmitter.receiveEvent(viewTag, eventName, mEventData) +} diff --git a/apps/bare-expo/package.json b/apps/bare-expo/package.json index 54c00cd31a25b..e1a60d73fcd4a 100644 --- a/apps/bare-expo/package.json +++ b/apps/bare-expo/package.json @@ -89,7 +89,7 @@ "react-native-shared-element": "0.8.8", "react-native-svg": "13.9.0", "react-native-view-shot": "3.6.0", - "react-native-webview": "11.26.0", + "react-native-webview": "13.2.2", "test-suite": "*" }, "devDependencies": { diff --git a/apps/native-component-list/package.json b/apps/native-component-list/package.json index ec440358d83be..d735058f47c78 100644 --- a/apps/native-component-list/package.json +++ b/apps/native-component-list/package.json @@ -154,7 +154,7 @@ "react-native-svg": "13.9.0", "react-native-view-shot": "3.6.0", "react-native-web": "~0.18.10", - "react-native-webview": "11.26.0", + "react-native-webview": "13.2.2", "react-navigation": "^4.4.0", "react-navigation-shared-element": "^3.1.2", "regl": "^1.3.0", diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 1685f4551ff0f..b80e8b3a0d954 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2120,7 +2120,7 @@ PODS: - React-Core - react-native-slider (4.4.2): - React-Core - - react-native-webview (11.26.0): + - react-native-webview (13.2.2): - React-Core - React-NativeModulesApple (0.72.0-rc.5): - hermes-engine @@ -3780,7 +3780,7 @@ SPEC CHECKSUMS: react-native-segmented-control: 06607462630512ff8eef652ec560e6235a30cc3e react-native-skia: ff2265fb802b2a3e1bf4a3c5a86d46936dd20354 react-native-slider: 33b8d190b59d4f67a541061bb91775d53d617d9d - react-native-webview: 994b9f8fbb504d6314dc40d83f94f27c6831b3bf + react-native-webview: b8ec89966713985111a14d6e4bf98d8b54bced0d React-NativeModulesApple: ae27f95a2a84a877785a20ce4c689995dd5f4a64 React-perflogger: fdd8c2969761105b1c85432fecdfff0616100cc6 React-RCTActionSheet: 5aaa270460794991553f80d393bcdcb97a372273 diff --git a/ios/vendored/unversioned/react-native-webview/apple/RNCWebView.h b/ios/vendored/unversioned/react-native-webview/apple/RNCWebView.h index 4fc4fbedb02a9..d359365e0f292 100644 --- a/ios/vendored/unversioned/react-native-webview/apple/RNCWebView.h +++ b/ios/vendored/unversioned/react-native-webview/apple/RNCWebView.h @@ -1,119 +1,29 @@ -/** - * 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. - */ +// This guard prevent this file to be compiled in the old architecture. +#ifdef RCT_NEW_ARCH_ENABLED +#import +#import +#import +#import +#import -#import -#import -#import +#ifndef NativeComponentExampleComponentView_h +#define NativeComponentExampleComponentView_h -typedef enum RNCWebViewPermissionGrantType : NSUInteger { - RNCWebViewPermissionGrantType_GrantIfSameHost_ElsePrompt, - RNCWebViewPermissionGrantType_GrantIfSameHost_ElseDeny, - RNCWebViewPermissionGrantType_Deny, - RNCWebViewPermissionGrantType_Grant, - RNCWebViewPermissionGrantType_Prompt -} RNCWebViewPermissionGrantType; - -@class RNCWebView; - -@protocol RNCWebViewDelegate - -- (BOOL)webView:(RNCWebView *_Nonnull)webView -shouldStartLoadForRequest:(NSMutableDictionary *_Nonnull)request - withCallback:(RCTDirectEventBlock _Nonnull)callback; - -@end - -@interface RNCWeakScriptMessageDelegate : NSObject - -@property (nonatomic, weak, nullable) id scriptDelegate; - -- (nullable instancetype)initWithDelegate:(id _Nullable)scriptDelegate; +NS_ASSUME_NONNULL_BEGIN +@interface RNCWebView : RCTViewComponentView @end -@interface RNCWebView : RCTView -@property (nonatomic, strong) NSString *scopeKey; - -@property (nonatomic, weak) id _Nullable delegate; -@property (nonatomic, copy) NSDictionary * _Nullable source; -@property (nonatomic, assign) BOOL messagingEnabled; -@property (nonatomic, copy) NSString * _Nullable injectedJavaScript; -@property (nonatomic, copy) NSString * _Nullable injectedJavaScriptBeforeContentLoaded; -@property (nonatomic, assign) BOOL injectedJavaScriptForMainFrameOnly; -@property (nonatomic, assign) BOOL injectedJavaScriptBeforeContentLoadedForMainFrameOnly; -@property (nonatomic, assign) BOOL scrollEnabled; -@property (nonatomic, assign) BOOL sharedCookiesEnabled; -@property (nonatomic, assign) BOOL autoManageStatusBarEnabled; -@property (nonatomic, assign) BOOL pagingEnabled; -@property (nonatomic, assign) CGFloat decelerationRate; -@property (nonatomic, assign) BOOL allowsInlineMediaPlayback; -@property (nonatomic, assign) BOOL allowsAirPlayForMediaPlayback; -@property (nonatomic, assign) BOOL bounces; -@property (nonatomic, assign) BOOL mediaPlaybackRequiresUserAction; -#if WEBKIT_IOS_10_APIS_AVAILABLE -@property (nonatomic, assign) WKDataDetectorTypes dataDetectorTypes; -#endif -@property (nonatomic, assign) UIEdgeInsets contentInset; -@property (nonatomic, assign) BOOL automaticallyAdjustContentInsets; -@property (nonatomic, assign) BOOL keyboardDisplayRequiresUserAction; -@property (nonatomic, assign) BOOL hideKeyboardAccessoryView; -@property (nonatomic, assign) BOOL allowsBackForwardNavigationGestures; -@property (nonatomic, assign) BOOL incognito; -@property (nonatomic, assign) BOOL useSharedProcessPool; -@property (nonatomic, copy) NSString * _Nullable userAgent; -@property (nonatomic, copy) NSString * _Nullable applicationNameForUserAgent; -@property (nonatomic, assign) BOOL cacheEnabled; -@property (nonatomic, assign) BOOL javaScriptEnabled; -@property (nonatomic, assign) BOOL javaScriptCanOpenWindowsAutomatically; -@property (nonatomic, assign) BOOL allowFileAccessFromFileURLs; -@property (nonatomic, assign) BOOL allowUniversalAccessFromFileURLs; -@property (nonatomic, assign) BOOL allowsLinkPreview; -@property (nonatomic, assign) BOOL showsHorizontalScrollIndicator; -@property (nonatomic, assign) BOOL showsVerticalScrollIndicator; -@property (nonatomic, assign) BOOL directionalLockEnabled; -@property (nonatomic, assign) BOOL ignoreSilentHardwareSwitch; -@property (nonatomic, copy) NSString * _Nullable allowingReadAccessToURL; -@property (nonatomic, copy) NSDictionary * _Nullable basicAuthCredential; -@property (nonatomic, assign) BOOL pullToRefreshEnabled; -@property (nonatomic, assign) BOOL enableApplePay; -@property (nonatomic, copy) NSArray * _Nullable menuItems; -@property (nonatomic, copy) RCTDirectEventBlock onCustomMenuSelection; -#if !TARGET_OS_OSX -@property (nonatomic, weak) UIRefreshControl * _Nullable refreshControl; -#endif - -#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 /* iOS 13 */ -@property (nonatomic, assign) WKContentMode contentMode; -#endif - -#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000 /* iOS 14 */ -@property (nonatomic, assign) BOOL limitsNavigationsToAppBoundDomains; -#endif +namespace facebook { +namespace react { + bool operator==(const RNCWebViewMenuItemsStruct& a, const RNCWebViewMenuItemsStruct& b) + { + return b.key == a.key && b.label == a.label; + } +} +} -#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 140500 /* iOS 14.5 */ -@property (nonatomic, assign) BOOL textInteractionEnabled; -#endif +NS_ASSUME_NONNULL_END -#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 150000 /* iOS 15 */ -@property (nonatomic, assign) RNCWebViewPermissionGrantType mediaCapturePermissionGrantType; -#endif - -+ (void)setClientAuthenticationCredential:(nullable NSURLCredential*)credential; -+ (void)setCustomCertificatesForHost:(nullable NSDictionary *)certificates; -- (void)postMessage:(NSString *_Nullable)message; -- (void)injectJavaScript:(NSString *_Nullable)script; -- (void)goForward; -- (void)goBack; -- (void)reload; -- (void)stopLoading; -- (void)requestFocus; -#if !TARGET_OS_OSX -- (void)addPullToRefreshControl; -- (void)pullToRefresh:(UIRefreshControl *_Nonnull)refreshControl; -#endif - -@end +#endif /* NativeComponentExampleComponentView_h */ +#endif /* RCT_NEW_ARCH_ENABLED */ diff --git a/ios/vendored/unversioned/react-native-webview/apple/RNCWebView.mm b/ios/vendored/unversioned/react-native-webview/apple/RNCWebView.mm new file mode 100644 index 0000000000000..8141432af444f --- /dev/null +++ b/ios/vendored/unversioned/react-native-webview/apple/RNCWebView.mm @@ -0,0 +1,504 @@ +// This guard prevent the code from being compiled in the old architecture +#ifdef RCT_NEW_ARCH_ENABLED +#import "RNCWebView.h" +#import "RNCWebViewImpl.h" + +#import +#import +#import +#import + +#import "RCTFabricComponentsPlugins.h" + +using namespace facebook::react; + +auto stringToOnShouldStartLoadWithRequestNavigationTypeEnum(std::string value) { + if (value == "click") return RNCWebViewEventEmitter::OnShouldStartLoadWithRequestNavigationType::Click; + if (value == "formsubmit") return RNCWebViewEventEmitter::OnShouldStartLoadWithRequestNavigationType::Formsubmit; + if (value == "backforward") return RNCWebViewEventEmitter::OnShouldStartLoadWithRequestNavigationType::Backforward; + if (value == "reload") return RNCWebViewEventEmitter::OnShouldStartLoadWithRequestNavigationType::Reload; + if (value == "formresubmit") return RNCWebViewEventEmitter::OnShouldStartLoadWithRequestNavigationType::Formresubmit; + return RNCWebViewEventEmitter::OnShouldStartLoadWithRequestNavigationType::Other; +} + +auto stringToOnLoadingStartNavigationTypeEnum(std::string value) { + if (value == "click") return RNCWebViewEventEmitter::OnLoadingStartNavigationType::Click; + if (value == "formsubmit") return RNCWebViewEventEmitter::OnLoadingStartNavigationType::Formsubmit; + if (value == "backforward") return RNCWebViewEventEmitter::OnLoadingStartNavigationType::Backforward; + if (value == "reload") return RNCWebViewEventEmitter::OnLoadingStartNavigationType::Reload; + if (value == "formresubmit") return RNCWebViewEventEmitter::OnLoadingStartNavigationType::Formresubmit; + return RNCWebViewEventEmitter::OnLoadingStartNavigationType::Other; +} + +auto stringToOnLoadingFinishNavigationTypeEnum(std::string value) { + if (value == "click") return RNCWebViewEventEmitter::OnLoadingFinishNavigationType::Click; + if (value == "formsubmit") return RNCWebViewEventEmitter::OnLoadingFinishNavigationType::Formsubmit; + if (value == "backforward") return RNCWebViewEventEmitter::OnLoadingFinishNavigationType::Backforward; + if (value == "reload") return RNCWebViewEventEmitter::OnLoadingFinishNavigationType::Reload; + if (value == "formresubmit") return RNCWebViewEventEmitter::OnLoadingFinishNavigationType::Formresubmit; + return RNCWebViewEventEmitter::OnLoadingFinishNavigationType::Other; +} + +@interface RNCWebView () + +@end + +@implementation RNCWebView { + RNCWebViewImpl * _view; +} + ++ (ComponentDescriptorProvider)componentDescriptorProvider +{ + return concreteComponentDescriptorProvider(); +} + +// Reproduce the idea from here: https://github.com/facebook/react-native/blob/8bd3edec88148d0ab1f225d2119435681fbbba33/React/Fabric/Mounting/ComponentViews/InputAccessory/RCTInputAccessoryComponentView.mm#L142 +- (void)prepareForRecycle { + [super prepareForRecycle]; + [_view destroyWebView]; +} + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + static const auto defaultProps = std::make_shared(); + _props = defaultProps; + + _view = [[RNCWebViewImpl alloc] init]; + + _view.onShouldStartLoadWithRequest = [self](NSDictionary* dictionary) { + if (_eventEmitter) { + auto webViewEventEmitter = std::static_pointer_cast(_eventEmitter); + facebook::react::RNCWebViewEventEmitter::OnShouldStartLoadWithRequest data = { + .url = std::string([[dictionary valueForKey:@"url"] UTF8String]), + .lockIdentifier = [[dictionary valueForKey:@"lockIdentifier"] doubleValue], + .title = std::string([[dictionary valueForKey:@"title"] UTF8String]), + .navigationType = stringToOnShouldStartLoadWithRequestNavigationTypeEnum(std::string([[dictionary valueForKey:@"navigationType"] UTF8String])), + .canGoBack = [[dictionary valueForKey:@"canGoBack"] boolValue], + .canGoForward = [[dictionary valueForKey:@"canGoBack"] boolValue], + .isTopFrame = [[dictionary valueForKey:@"isTopFrame"] boolValue], + .loading = [[dictionary valueForKey:@"loading"] boolValue], + .mainDocumentURL = std::string([[dictionary valueForKey:@"mainDocumentURL"] UTF8String]) + }; + webViewEventEmitter->onShouldStartLoadWithRequest(data); + }; + }; + _view.onLoadingStart = [self](NSDictionary* dictionary) { + if (_eventEmitter) { + auto webViewEventEmitter = std::static_pointer_cast(_eventEmitter); + facebook::react::RNCWebViewEventEmitter::OnLoadingStart data = { + .url = std::string([[dictionary valueForKey:@"url"] UTF8String]), + .lockIdentifier = [[dictionary valueForKey:@"lockIdentifier"] doubleValue], + .title = std::string([[dictionary valueForKey:@"title"] UTF8String]), + .navigationType = stringToOnLoadingStartNavigationTypeEnum(std::string([[dictionary valueForKey:@"navigationType"] UTF8String])), + .canGoBack = [[dictionary valueForKey:@"canGoBack"] boolValue], + .canGoForward = [[dictionary valueForKey:@"canGoBack"] boolValue], + .loading = [[dictionary valueForKey:@"loading"] boolValue], + .mainDocumentURL = std::string([[dictionary valueForKey:@"mainDocumentURL"] UTF8String], [[dictionary valueForKey:@"mainDocumentURL"] lengthOfBytesUsingEncoding:NSUTF8StringEncoding]) + }; + webViewEventEmitter->onLoadingStart(data); + } + }; + _view.onLoadingError = [self](NSDictionary* dictionary) { + if (_eventEmitter) { + auto webViewEventEmitter = std::static_pointer_cast(_eventEmitter); + facebook::react::RNCWebViewEventEmitter::OnLoadingError data = { + .url = std::string([[dictionary valueForKey:@"url"] UTF8String]), + .lockIdentifier = [[dictionary valueForKey:@"lockIdentifier"] doubleValue], + .title = std::string([[dictionary valueForKey:@"title"] UTF8String]), + .code = [[dictionary valueForKey:@"code"] intValue], + .description = std::string([[dictionary valueForKey:@"description"] UTF8String]), + .canGoBack = [[dictionary valueForKey:@"canGoBack"] boolValue], + .canGoForward = [[dictionary valueForKey:@"canGoBack"] boolValue], + .loading = [[dictionary valueForKey:@"loading"] boolValue], + .domain = std::string([[dictionary valueForKey:@"domain"] UTF8String]) + }; + webViewEventEmitter->onLoadingError(data); + } + }; + _view.onMessage = [self](NSDictionary* dictionary) { + if (_eventEmitter) { + auto webViewEventEmitter = std::static_pointer_cast(_eventEmitter); + facebook::react::RNCWebViewEventEmitter::OnMessage data = { + .url = std::string([[dictionary valueForKey:@"url"] UTF8String]), + .lockIdentifier = [[dictionary valueForKey:@"lockIdentifier"] doubleValue], + .title = std::string([[dictionary valueForKey:@"title"] UTF8String]), + .canGoBack = [[dictionary valueForKey:@"canGoBack"] boolValue], + .canGoForward = [[dictionary valueForKey:@"canGoBack"] boolValue], + .loading = [[dictionary valueForKey:@"loading"] boolValue], + .data = std::string([[dictionary valueForKey:@"data"] UTF8String]) + }; + webViewEventEmitter->onMessage(data); + } + }; + _view.onLoadingFinish = [self](NSDictionary* dictionary) { + if (_eventEmitter) { + auto webViewEventEmitter = std::static_pointer_cast(_eventEmitter); + facebook::react::RNCWebViewEventEmitter::OnLoadingFinish data = { + .url = std::string([[dictionary valueForKey:@"url"] UTF8String]), + .lockIdentifier = [[dictionary valueForKey:@"lockIdentifier"] doubleValue], + .title = std::string([[dictionary valueForKey:@"title"] UTF8String]), + .navigationType = stringToOnLoadingFinishNavigationTypeEnum(std::string([[dictionary valueForKey:@"navigationType"] UTF8String], [[dictionary valueForKey:@"navigationType"] lengthOfBytesUsingEncoding:NSUTF8StringEncoding])), + .canGoBack = [[dictionary valueForKey:@"canGoBack"] boolValue], + .canGoForward = [[dictionary valueForKey:@"canGoBack"] boolValue], + .loading = [[dictionary valueForKey:@"loading"] boolValue], + .mainDocumentURL = std::string([[dictionary valueForKey:@"mainDocumentURL"] UTF8String], [[dictionary valueForKey:@"mainDocumentURL"] lengthOfBytesUsingEncoding:NSUTF8StringEncoding]) + }; + webViewEventEmitter->onLoadingFinish(data); + } + }; + _view.onLoadingProgress = [self](NSDictionary* dictionary) { + if (_eventEmitter) { + auto webViewEventEmitter = std::static_pointer_cast(_eventEmitter); + facebook::react::RNCWebViewEventEmitter::OnLoadingProgress data = { + .url = std::string([[dictionary valueForKey:@"url"] UTF8String]), + .lockIdentifier = [[dictionary valueForKey:@"lockIdentifier"] doubleValue], + .title = std::string([[dictionary valueForKey:@"title"] UTF8String]), + .canGoBack = [[dictionary valueForKey:@"canGoBack"] boolValue], + .canGoForward = [[dictionary valueForKey:@"canGoBack"] boolValue], + .loading = [[dictionary valueForKey:@"loading"] boolValue], + .progress = [[dictionary valueForKey:@"progress"] doubleValue] + }; + webViewEventEmitter->onLoadingProgress(data); + } + }; + _view.onContentProcessDidTerminate = [self](NSDictionary* dictionary) { + if (_eventEmitter) { + auto webViewEventEmitter = std::static_pointer_cast(_eventEmitter); + facebook::react::RNCWebViewEventEmitter::OnContentProcessDidTerminate data = { + .url = std::string([[dictionary valueForKey:@"url"] UTF8String]), + .lockIdentifier = [[dictionary valueForKey:@"lockIdentifier"] doubleValue], + .title = std::string([[dictionary valueForKey:@"title"] UTF8String]), + .canGoBack = [[dictionary valueForKey:@"canGoBack"] boolValue], + .canGoForward = [[dictionary valueForKey:@"canGoBack"] boolValue], + .loading = [[dictionary valueForKey:@"loading"] boolValue] + }; + webViewEventEmitter->onContentProcessDidTerminate(data); + } + }; + _view.onCustomMenuSelection = [self](NSDictionary* dictionary) { + if (_eventEmitter) { + auto webViewEventEmitter = std::static_pointer_cast(_eventEmitter); + facebook::react::RNCWebViewEventEmitter::OnCustomMenuSelection data = { + .selectedText = std::string([[dictionary valueForKey:@"selectedText"] UTF8String]), + .key = std::string([[dictionary valueForKey:@"key"] UTF8String]), + .label = std::string([[dictionary valueForKey:@"label"] UTF8String]) + + }; + webViewEventEmitter->onCustomMenuSelection(data); + } + }; + _view.onScroll = [self](NSDictionary* dictionary) { + if (_eventEmitter) { + NSDictionary* contentOffset = [dictionary valueForKey:@"contentOffset"]; + NSDictionary* contentInset = [dictionary valueForKey:@"contentInset"]; + NSDictionary* contentSize = [dictionary valueForKey:@"contentSize"]; + NSDictionary* layoutMeasurement = [dictionary valueForKey:@"layoutMeasurement"]; + double zoomScale = [[dictionary valueForKey:@"zoomScale"] doubleValue]; + + auto webViewEventEmitter = std::static_pointer_cast(_eventEmitter); + facebook::react::RNCWebViewEventEmitter::OnScroll data = { + .contentOffset = { + .x = [[contentOffset valueForKey:@"x"] doubleValue], + .y = [[contentOffset valueForKey:@"y"] doubleValue] + }, + .contentInset = { + .left = [[contentInset valueForKey:@"left"] doubleValue], + .right = [[contentInset valueForKey:@"right"] doubleValue], + .top = [[contentInset valueForKey:@"top"] doubleValue], + .bottom = [[contentInset valueForKey:@"bottom"] doubleValue] + }, + .contentSize = { + .width = [[contentSize valueForKey:@"width"] doubleValue], + .height = [[contentSize valueForKey:@"height"] doubleValue] + }, + .layoutMeasurement = { + .width = [[layoutMeasurement valueForKey:@"width"] doubleValue], + .height = [[layoutMeasurement valueForKey:@"height"] doubleValue] }, + .zoomScale = zoomScale + }; + webViewEventEmitter->onScroll(data); + } + }; + _view.onHttpError = [self](NSDictionary* dictionary) { + if (_eventEmitter) { + auto webViewEventEmitter = std::static_pointer_cast(_eventEmitter); + facebook::react::RNCWebViewEventEmitter::OnHttpError data = { + .url = std::string([[dictionary valueForKey:@"url"] UTF8String]), + .lockIdentifier = [[dictionary valueForKey:@"lockIdentifier"] doubleValue], + .title = std::string([[dictionary valueForKey:@"title"] UTF8String]), + .statusCode = [[dictionary valueForKey:@"statusCode"] intValue], + .description = std::string([[dictionary valueForKey:@"description"] UTF8String]), + .canGoBack = [[dictionary valueForKey:@"canGoBack"] boolValue], + .canGoForward = [[dictionary valueForKey:@"canGoBack"] boolValue], + .loading = [[dictionary valueForKey:@"loading"] boolValue] + }; + webViewEventEmitter->onHttpError(data); + } + }; + self.contentView = _view; + } + return self; +} + +- (void)updateEventEmitter:(EventEmitter::Shared const &)eventEmitter +{ + [super updateEventEmitter:eventEmitter]; +} + +- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps +{ + const auto &oldViewProps = *std::static_pointer_cast(_props); + const auto &newViewProps = *std::static_pointer_cast(props); + +#define REMAP_WEBVIEW_PROP(name) \ + if (oldViewProps.name != newViewProps.name) { \ + _view.name = newViewProps.name; \ + } + +#define REMAP_WEBVIEW_STRING_PROP(name) \ + if (oldViewProps.name != newViewProps.name) { \ + _view.name = RCTNSStringFromString(newViewProps.name); \ + } + + REMAP_WEBVIEW_PROP(scrollEnabled) + REMAP_WEBVIEW_STRING_PROP(injectedJavaScript) + REMAP_WEBVIEW_STRING_PROP(injectedJavaScriptBeforeContentLoaded) + REMAP_WEBVIEW_PROP(injectedJavaScriptForMainFrameOnly) + REMAP_WEBVIEW_PROP(injectedJavaScriptBeforeContentLoadedForMainFrameOnly) + REMAP_WEBVIEW_PROP(javaScriptEnabled) + REMAP_WEBVIEW_PROP(javaScriptCanOpenWindowsAutomatically) + REMAP_WEBVIEW_PROP(allowFileAccessFromFileURLs) + REMAP_WEBVIEW_PROP(allowUniversalAccessFromFileURLs) + REMAP_WEBVIEW_PROP(allowsInlineMediaPlayback) + REMAP_WEBVIEW_PROP(webviewDebuggingEnabled) + REMAP_WEBVIEW_PROP(allowsAirPlayForMediaPlayback) + REMAP_WEBVIEW_PROP(mediaPlaybackRequiresUserAction) + REMAP_WEBVIEW_PROP(automaticallyAdjustContentInsets) + REMAP_WEBVIEW_PROP(autoManageStatusBarEnabled) + REMAP_WEBVIEW_PROP(hideKeyboardAccessoryView) + REMAP_WEBVIEW_PROP(allowsBackForwardNavigationGestures) + REMAP_WEBVIEW_PROP(incognito) + REMAP_WEBVIEW_PROP(pagingEnabled) + REMAP_WEBVIEW_STRING_PROP(applicationNameForUserAgent) + REMAP_WEBVIEW_PROP(cacheEnabled) + REMAP_WEBVIEW_PROP(allowsLinkPreview) + REMAP_WEBVIEW_STRING_PROP(allowingReadAccessToURL) + + REMAP_WEBVIEW_PROP(messagingEnabled) + REMAP_WEBVIEW_PROP(fraudulentWebsiteWarningEnabled) + REMAP_WEBVIEW_PROP(enableApplePay) + REMAP_WEBVIEW_PROP(pullToRefreshEnabled) + REMAP_WEBVIEW_PROP(bounces) + REMAP_WEBVIEW_PROP(useSharedProcessPool) + REMAP_WEBVIEW_STRING_PROP(userAgent) + REMAP_WEBVIEW_PROP(sharedCookiesEnabled) + #if !TARGET_OS_OSX + REMAP_WEBVIEW_PROP(decelerationRate) + #endif // !TARGET_OS_OSX + REMAP_WEBVIEW_PROP(directionalLockEnabled) + REMAP_WEBVIEW_PROP(showsHorizontalScrollIndicator) + REMAP_WEBVIEW_PROP(showsVerticalScrollIndicator) + REMAP_WEBVIEW_PROP(keyboardDisplayRequiresUserAction) + +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 /* __IPHONE_13_0 */ + REMAP_WEBVIEW_PROP(automaticallyAdjustContentInsets) +#endif +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000 /* iOS 14 */ + REMAP_WEBVIEW_PROP(limitsNavigationsToAppBoundDomains) +#endif +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 140500 /* iOS 14.5 */ + REMAP_WEBVIEW_PROP(textInteractionEnabled) +#endif + + if (oldViewProps.dataDetectorTypes != newViewProps.dataDetectorTypes) { + WKDataDetectorTypes dataDetectorTypes = WKDataDetectorTypeNone; + if (dataDetectorTypes & RNCWebViewDataDetectorTypes::Address) { + dataDetectorTypes |= WKDataDetectorTypeAddress; + } else if (dataDetectorTypes & RNCWebViewDataDetectorTypes::Link) { + dataDetectorTypes |= WKDataDetectorTypeLink; + } else if (dataDetectorTypes & RNCWebViewDataDetectorTypes::CalendarEvent) { + dataDetectorTypes |= WKDataDetectorTypeCalendarEvent; + } else if (dataDetectorTypes & RNCWebViewDataDetectorTypes::TrackingNumber) { + dataDetectorTypes |= WKDataDetectorTypeTrackingNumber; + } else if (dataDetectorTypes & RNCWebViewDataDetectorTypes::FlightNumber) { + dataDetectorTypes |= WKDataDetectorTypeFlightNumber; + } else if (dataDetectorTypes & RNCWebViewDataDetectorTypes::LookupSuggestion) { + dataDetectorTypes |= WKDataDetectorTypeLookupSuggestion; + } else if (dataDetectorTypes & RNCWebViewDataDetectorTypes::PhoneNumber) { + dataDetectorTypes |= WKDataDetectorTypePhoneNumber; + } else if (dataDetectorTypes & RNCWebViewDataDetectorTypes::All) { + dataDetectorTypes |= WKDataDetectorTypeAll; + } else if (dataDetectorTypes & RNCWebViewDataDetectorTypes::None) { + dataDetectorTypes = WKDataDetectorTypeNone; + } + [_view setDataDetectorTypes:dataDetectorTypes]; + } + if (oldViewProps.contentInset.top != newViewProps.contentInset.top || oldViewProps.contentInset.left != newViewProps.contentInset.left || oldViewProps.contentInset.right != newViewProps.contentInset.right || oldViewProps.contentInset.bottom != newViewProps.contentInset.bottom) { + UIEdgeInsets edgesInsets = { + .top = newViewProps.contentInset.top, + .left = newViewProps.contentInset.left, + .right = newViewProps.contentInset.right, + .bottom = newViewProps.contentInset.bottom + }; + [_view setContentInset: edgesInsets]; + } + + if (oldViewProps.basicAuthCredential.username != newViewProps.basicAuthCredential.username || oldViewProps.basicAuthCredential.password != newViewProps.basicAuthCredential.password) { + [_view setBasicAuthCredential: @{ + @"username": RCTNSStringFromString(newViewProps.basicAuthCredential.username), + @"password": RCTNSStringFromString(newViewProps.basicAuthCredential.password) + }]; + } + if (oldViewProps.contentInsetAdjustmentBehavior != newViewProps.contentInsetAdjustmentBehavior) { + if (newViewProps.contentInsetAdjustmentBehavior == RNCWebViewContentInsetAdjustmentBehavior::Never) { + [_view setContentInsetAdjustmentBehavior: UIScrollViewContentInsetAdjustmentNever]; + } else if (newViewProps.contentInsetAdjustmentBehavior == RNCWebViewContentInsetAdjustmentBehavior::Automatic) { + [_view setContentInsetAdjustmentBehavior: UIScrollViewContentInsetAdjustmentAutomatic]; + } else if (newViewProps.contentInsetAdjustmentBehavior == RNCWebViewContentInsetAdjustmentBehavior::ScrollableAxes) { + [_view setContentInsetAdjustmentBehavior: UIScrollViewContentInsetAdjustmentScrollableAxes]; + } else if (newViewProps.contentInsetAdjustmentBehavior == RNCWebViewContentInsetAdjustmentBehavior::Always) { + [_view setContentInsetAdjustmentBehavior: UIScrollViewContentInsetAdjustmentAlways]; + } + } + + if (oldViewProps.menuItems != newViewProps.menuItems) { + NSMutableArray *newMenuItems = [NSMutableArray array]; + + for (const auto &menuItem: newViewProps.menuItems) { + [newMenuItems addObject:@{ + @"key": RCTNSStringFromString(menuItem.key), + @"label": RCTNSStringFromString(menuItem.label), + }]; + + } + [_view setMenuItems:newMenuItems]; + } + if (oldViewProps.hasOnFileDownload != newViewProps.hasOnFileDownload) { + if (newViewProps.hasOnFileDownload) { + _view.onFileDownload = [self](NSDictionary* dictionary) { + if (_eventEmitter) { + auto webViewEventEmitter = std::static_pointer_cast(_eventEmitter); + facebook::react::RNCWebViewEventEmitter::OnFileDownload data = { + .downloadUrl = std::string([[dictionary valueForKey:@"downloadUrl"] UTF8String]) + }; + webViewEventEmitter->onFileDownload(data); + } + }; + } else { + _view.onFileDownload = nil; + } + } +// +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 /* iOS 13 */ + if (oldViewProps.contentMode != newViewProps.contentMode) { + if (newViewProps.contentMode == RNCWebViewContentMode::Recommended) { + [_view setContentMode: WKContentModeRecommended]; + } else if (newViewProps.contentMode == RNCWebViewContentMode::Mobile) { + [_view setContentMode:WKContentModeMobile]; + } else if (newViewProps.contentMode == RNCWebViewContentMode::Desktop) { + [_view setContentMode:WKContentModeDesktop]; + } + } +#endif + +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 150000 /* iOS 15 */ + if (oldViewProps.mediaCapturePermissionGrantType != newViewProps.mediaCapturePermissionGrantType) { + if (newViewProps.mediaCapturePermissionGrantType == RNCWebViewMediaCapturePermissionGrantType::Prompt) { + [_view setMediaCapturePermissionGrantType:RNCWebViewPermissionGrantType_Prompt]; + } else if (newViewProps.mediaCapturePermissionGrantType == RNCWebViewMediaCapturePermissionGrantType::Grant) { + [_view setMediaCapturePermissionGrantType:RNCWebViewPermissionGrantType_Grant]; + } else if (newViewProps.mediaCapturePermissionGrantType == RNCWebViewMediaCapturePermissionGrantType::Deny) { + [_view setMediaCapturePermissionGrantType:RNCWebViewPermissionGrantType_Deny]; + }else if (newViewProps.mediaCapturePermissionGrantType == RNCWebViewMediaCapturePermissionGrantType::GrantIfSameHostElsePrompt) { + [_view setMediaCapturePermissionGrantType:RNCWebViewPermissionGrantType_GrantIfSameHost_ElsePrompt]; + }else if (newViewProps.mediaCapturePermissionGrantType == RNCWebViewMediaCapturePermissionGrantType::GrantIfSameHostElseDeny) { + [_view setMediaCapturePermissionGrantType:RNCWebViewPermissionGrantType_GrantIfSameHost_ElseDeny]; + } + } +#endif + + NSMutableDictionary* source = [[NSMutableDictionary alloc] init]; + if (!newViewProps.newSource.uri.empty()) { + [source setValue:RCTNSStringFromString(newViewProps.newSource.uri) forKey:@"uri"]; + } + NSMutableDictionary* headers = [[NSMutableDictionary alloc] init]; + for (auto & element : newViewProps.newSource.headers) { + [headers setValue:RCTNSStringFromString(element.value) forKey:RCTNSStringFromString(element.name)]; + } + if (headers.count > 0) { + [source setObject:headers forKey:@"headers"]; + } + if (!newViewProps.newSource.baseUrl.empty()) { + [source setValue:RCTNSStringFromString(newViewProps.newSource.baseUrl) forKey:@"baseUrl"]; + } + if (!newViewProps.newSource.body.empty()) { + [source setValue:RCTNSStringFromString(newViewProps.newSource.body) forKey:@"body"]; + } + if (!newViewProps.newSource.html.empty()) { + [source setValue:RCTNSStringFromString(newViewProps.newSource.html) forKey:@"html"]; + } + if (!newViewProps.newSource.method.empty()) { + [source setValue:RCTNSStringFromString(newViewProps.newSource.method) forKey:@"method"]; + } + [_view setSource:source]; + + [super updateProps:props oldProps:oldProps]; +} + +- (void)handleCommand:(nonnull const NSString *)commandName args:(nonnull const NSArray *)args { + RCTRNCWebViewHandleCommand(self, commandName, args); +} + + +Class RNCWebViewCls(void) +{ + return RNCWebView.class; +} + +- (void)goBack { + [_view goBack]; +} + +- (void)goForward { + [_view goForward]; +} + +- (void)injectJavaScript:(nonnull NSString *)javascript { + [_view injectJavaScript:javascript]; +} + +- (void)loadUrl:(nonnull NSString *)url { + // android only +} + +- (void)postMessage:(nonnull NSString *)data { + [_view postMessage:data]; +} + +- (void)reload { + [_view reload]; +} + +- (void)requestFocus { + [_view requestFocus]; +} + +- (void)stopLoading { + [_view stopLoading]; +} + +- (void)clearFormData { + // android only +} + +- (void)clearCache:(BOOL)includeDiskFiles { + // android only +} + +- (void)clearHistory { + // android only +} + +@end +#endif diff --git a/ios/vendored/unversioned/react-native-webview/apple/RNCWebViewDecisionManager.h b/ios/vendored/unversioned/react-native-webview/apple/RNCWebViewDecisionManager.h new file mode 100644 index 0000000000000..da01fba0b8ce3 --- /dev/null +++ b/ios/vendored/unversioned/react-native-webview/apple/RNCWebViewDecisionManager.h @@ -0,0 +1,20 @@ +#import +#import +#import + +typedef void (^DecisionBlock)(BOOL); + +@interface RNCWebViewDecisionManager : NSObject { + int nextLockIdentifier; + NSMutableDictionary *decisionHandlers; +} + +@property (nonatomic) int nextLockIdentifier; +@property (nonatomic, retain) NSMutableDictionary *decisionHandlers; + ++ (id) getInstance; + +- (int)setDecisionHandler:(DecisionBlock)handler; +- (void) setResult:(BOOL)shouldStart + forLockIdentifier:(int)lockIdentifier; +@end diff --git a/ios/vendored/unversioned/react-native-webview/apple/RNCWebViewDecisionManager.m b/ios/vendored/unversioned/react-native-webview/apple/RNCWebViewDecisionManager.m new file mode 100644 index 0000000000000..aa9054813221c --- /dev/null +++ b/ios/vendored/unversioned/react-native-webview/apple/RNCWebViewDecisionManager.m @@ -0,0 +1,47 @@ +#import "RNCWebViewDecisionManager.h" + + + +@implementation RNCWebViewDecisionManager + +@synthesize nextLockIdentifier; +@synthesize decisionHandlers; + ++ (id)getInstance { + static RNCWebViewDecisionManager *lockManager = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + lockManager = [[self alloc] init]; + }); + return lockManager; +} + +- (int)setDecisionHandler:(DecisionBlock)decisionHandler { + int lockIdentifier = self.nextLockIdentifier++; + + [self.decisionHandlers setObject:decisionHandler forKey:@(lockIdentifier)]; + return lockIdentifier; +} + +- (void) setResult:(BOOL)shouldStart + forLockIdentifier:(int)lockIdentifier { + DecisionBlock handler = [self.decisionHandlers objectForKey:@(lockIdentifier)]; + if (handler == nil) { + RCTLogWarn(@"Lock not found"); + return; + } + handler(shouldStart); + [self.decisionHandlers removeObjectForKey:@(lockIdentifier)]; +} + +- (id)init { + if (self = [super init]) { + self.nextLockIdentifier = 1; + self.decisionHandlers = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (void)dealloc {} + +@end diff --git a/ios/vendored/unversioned/react-native-webview/apple/RNCWebViewImpl.h b/ios/vendored/unversioned/react-native-webview/apple/RNCWebViewImpl.h new file mode 100644 index 0000000000000..9d6da12d0de5b --- /dev/null +++ b/ios/vendored/unversioned/react-native-webview/apple/RNCWebViewImpl.h @@ -0,0 +1,148 @@ +/** + * 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 +#import +#import +#import + +#if !TARGET_OS_OSX +#import +#endif // !TARGET_OS_OSX + +#import "RNCWebViewDecisionManager.h" + +typedef enum RNCWebViewPermissionGrantType : NSUInteger { + RNCWebViewPermissionGrantType_GrantIfSameHost_ElsePrompt, + RNCWebViewPermissionGrantType_GrantIfSameHost_ElseDeny, + RNCWebViewPermissionGrantType_Deny, + RNCWebViewPermissionGrantType_Grant, + RNCWebViewPermissionGrantType_Prompt +} RNCWebViewPermissionGrantType; + +@class RNCWebViewImpl; + +NS_ASSUME_NONNULL_BEGIN + +@protocol RNCWebViewDelegate + +- (BOOL)webView:(RNCWebViewImpl *)webView +shouldStartLoadForRequest:(NSMutableDictionary *)request + withCallback:(RCTDirectEventBlock)callback; + +@end + +@interface RNCWeakScriptMessageDelegate : NSObject + +@property (nonatomic, weak, nullable) id scriptDelegate; + +- (nullable instancetype)initWithDelegate:(id _Nullable)scriptDelegate; + +@end + +@interface RNCWebViewImpl : RCTView +@property (nonatomic, strong) NSString *scopeKey; +@property (nonatomic, copy) RCTDirectEventBlock onFileDownload; +@property (nonatomic, copy) RCTDirectEventBlock onLoadingStart; +@property (nonatomic, copy) RCTDirectEventBlock onLoadingFinish; +@property (nonatomic, copy) RCTDirectEventBlock onLoadingError; +@property (nonatomic, copy) RCTDirectEventBlock onLoadingProgress; +@property (nonatomic, copy) RCTDirectEventBlock onShouldStartLoadWithRequest; +@property (nonatomic, copy) RCTDirectEventBlock onHttpError; +@property (nonatomic, copy) RCTDirectEventBlock onMessage; +@property (nonatomic, copy) RCTDirectEventBlock onScroll; +@property (nonatomic, copy) RCTDirectEventBlock onContentProcessDidTerminate; + + +@property (nonatomic, weak) id _Nullable delegate; +@property (nonatomic, copy) NSDictionary * _Nullable source; +@property (nonatomic, assign) BOOL messagingEnabled; +@property (nonatomic, copy) NSString * _Nullable injectedJavaScript; +@property (nonatomic, copy) NSString * _Nullable injectedJavaScriptBeforeContentLoaded; +@property (nonatomic, assign) BOOL injectedJavaScriptForMainFrameOnly; +@property (nonatomic, assign) BOOL injectedJavaScriptBeforeContentLoadedForMainFrameOnly; +@property (nonatomic, assign) BOOL scrollEnabled; +@property (nonatomic, assign) BOOL sharedCookiesEnabled; +@property (nonatomic, assign) BOOL autoManageStatusBarEnabled; +@property (nonatomic, assign) BOOL pagingEnabled; +@property (nonatomic, assign) CGFloat decelerationRate; +@property (nonatomic, assign) BOOL allowsInlineMediaPlayback; +@property (nonatomic, assign) BOOL webviewDebuggingEnabled; +@property (nonatomic, assign) BOOL allowsAirPlayForMediaPlayback; +@property (nonatomic, assign) BOOL bounces; +@property (nonatomic, assign) BOOL mediaPlaybackRequiresUserAction; +@property (nonatomic, assign) UIEdgeInsets contentInset; +@property (nonatomic, assign) BOOL automaticallyAdjustContentInsets; +@property (nonatomic, assign) BOOL keyboardDisplayRequiresUserAction; +@property (nonatomic, assign) BOOL hideKeyboardAccessoryView; +@property (nonatomic, assign) BOOL allowsBackForwardNavigationGestures; +@property (nonatomic, assign) BOOL incognito; +@property (nonatomic, assign) BOOL useSharedProcessPool; +@property (nonatomic, copy) NSString * _Nullable userAgent; +@property (nonatomic, copy) NSString * _Nullable applicationNameForUserAgent; +@property (nonatomic, assign) BOOL cacheEnabled; +@property (nonatomic, assign) BOOL javaScriptEnabled; +@property (nonatomic, assign) BOOL javaScriptCanOpenWindowsAutomatically; +@property (nonatomic, assign) BOOL allowFileAccessFromFileURLs; +@property (nonatomic, assign) BOOL allowUniversalAccessFromFileURLs; +@property (nonatomic, assign) BOOL allowsLinkPreview; +@property (nonatomic, assign) BOOL showsHorizontalScrollIndicator; +@property (nonatomic, assign) BOOL showsVerticalScrollIndicator; +@property (nonatomic, assign) BOOL directionalLockEnabled; +@property (nonatomic, assign) BOOL ignoreSilentHardwareSwitch; +@property (nonatomic, copy) NSString * _Nullable allowingReadAccessToURL; +@property (nonatomic, copy) NSDictionary * _Nullable basicAuthCredential; +@property (nonatomic, assign) BOOL pullToRefreshEnabled; +@property (nonatomic, assign) BOOL enableApplePay; +@property (nonatomic, copy) NSArray * _Nullable menuItems; +@property (nonatomic, copy) RCTDirectEventBlock onCustomMenuSelection; +#if !TARGET_OS_OSX +@property (nonatomic, assign) WKDataDetectorTypes dataDetectorTypes; +@property (nonatomic, weak) UIRefreshControl * _Nullable refreshControl; +#endif + +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 /* iOS 13 */ +@property (nonatomic, assign) WKContentMode contentMode; +@property (nonatomic, assign) BOOL fraudulentWebsiteWarningEnabled; +#endif + +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000 /* iOS 14 */ +@property (nonatomic, assign) BOOL limitsNavigationsToAppBoundDomains; +#endif + +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 140500 /* iOS 14.5 */ +@property (nonatomic, assign) BOOL textInteractionEnabled; +#endif + +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 150000 /* iOS 15 */ +@property (nonatomic, assign) RNCWebViewPermissionGrantType mediaCapturePermissionGrantType; +#endif + +#if !TARGET_OS_OSX +- (void)setContentInsetAdjustmentBehavior:(UIScrollViewContentInsetAdjustmentBehavior)behavior; +#endif // !TARGET_OS_OSX + ++ (void)setClientAuthenticationCredential:(nullable NSURLCredential*)credential; ++ (void)setCustomCertificatesForHost:(nullable NSDictionary *)certificates; +- (void)postMessage:(NSString *_Nullable)message; +- (void)injectJavaScript:(NSString *_Nullable)script; +- (void)goForward; +- (void)goBack; +- (void)reload; +- (void)stopLoading; +- (void)requestFocus; +#ifdef RCT_NEW_ARCH_ENABLED +- (void)destroyWebView; +#endif +#if !TARGET_OS_OSX +- (void)addPullToRefreshControl; +- (void)pullToRefresh:(UIRefreshControl *)refreshControl; +#endif + +@end + +NS_ASSUME_NONNULL_END diff --git a/ios/vendored/unversioned/react-native-webview/apple/RNCWebView.m b/ios/vendored/unversioned/react-native-webview/apple/RNCWebViewImpl.m similarity index 89% rename from ios/vendored/unversioned/react-native-webview/apple/RNCWebView.m rename to ios/vendored/unversioned/react-native-webview/apple/RNCWebViewImpl.m index bde64f22b3ae3..d5766f3c64622 100644 --- a/ios/vendored/unversioned/react-native-webview/apple/RNCWebView.m +++ b/ios/vendored/unversioned/react-native-webview/apple/RNCWebViewImpl.m @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -#import "RNCWebView.h" +#import "RNCWebViewImpl.h" #import #import #import "RNCWKProcessPoolManager.h" @@ -37,7 +37,7 @@ -(id)inputAccessoryView if (_webView == nil) { return nil; } - + if ([_webView respondsToSelector:@selector(inputAssistantItem)]) { UITextInputAssistantItem *inputAssistantItem = [_webView inputAssistantItem]; inputAssistantItem.leadingBarButtonGroups = @[]; @@ -48,49 +48,56 @@ -(id)inputAccessoryView @end #endif // !TARGET_OS_OSX -#if TARGET_OS_OSX @interface RNCWKWebView : WKWebView +#if !TARGET_OS_OSX +@property (nonatomic, copy) NSArray * _Nullable menuItems; +#endif // !TARGET_OS_OSX @end @implementation RNCWKWebView +#if !TARGET_OS_OSX +- (BOOL)canPerformAction:(SEL)action + withSender:(id)sender{ + + if (!self.menuItems) { + return [super canPerformAction:action withSender:sender]; + } + + return NO; +} +- (void)buildMenuWithBuilder:(id)builder API_AVAILABLE(ios(13.0)) { + if (@available(iOS 16.0, *)) { + if(self.menuItems){ + [builder removeMenuForIdentifier:UIMenuLookup]; + } + } + [super buildMenuWithBuilder:builder]; +} +#else // TARGET_OS_OSX - (void)scrollWheel:(NSEvent *)theEvent { - RNCWebView *rncWebView = (RNCWebView *)[self superview]; - RCTAssert([rncWebView isKindOfClass:[rncWebView class]], @"superview must be an RNCWebView"); + RNCWebViewImpl *rncWebView = (RNCWebViewImpl *)[self superview]; + RCTAssert([rncWebView isKindOfClass:[rncWebView class]], @"superview must be an RNCWebViewImpl"); if (![rncWebView scrollEnabled]) { [[self nextResponder] scrollWheel:theEvent]; return; } [super scrollWheel:theEvent]; } -@end #endif // TARGET_OS_OSX +@end -@interface RNCWebView () -@property (nonatomic, copy) RCTDirectEventBlock onFileDownload; -@property (nonatomic, copy) RCTDirectEventBlock onLoadingStart; -@property (nonatomic, copy) RCTDirectEventBlock onLoadingFinish; -@property (nonatomic, copy) RCTDirectEventBlock onLoadingError; -@property (nonatomic, copy) RCTDirectEventBlock onLoadingProgress; -@property (nonatomic, copy) RCTDirectEventBlock onShouldStartLoadWithRequest; -@property (nonatomic, copy) RCTDirectEventBlock onHttpError; -@property (nonatomic, copy) RCTDirectEventBlock onMessage; -@property (nonatomic, copy) RCTDirectEventBlock onScroll; -@property (nonatomic, copy) RCTDirectEventBlock onContentProcessDidTerminate; -#if !TARGET_OS_OSX -@property (nonatomic, copy) WKWebView *webView; -#else @property (nonatomic, copy) RNCWKWebView *webView; -#endif // !TARGET_OS_OSX @property (nonatomic, strong) WKUserScript *postMessageScript; @property (nonatomic, strong) WKUserScript *atStartScript; @property (nonatomic, strong) WKUserScript *atEndScript; @end -@implementation RNCWebView +@implementation RNCWebViewImpl { #if !TARGET_OS_OSX UIColor * _savedBackgroundColor; @@ -99,7 +106,7 @@ @implementation RNCWebView #endif // !TARGET_OS_OSX BOOL _savedHideKeyboardAccessoryView; BOOL _savedKeyboardDisplayRequiresUserAction; - + // Workaround for StatusBar appearance bug for iOS 12 // https://github.com/react-native-webview/react-native-webview/issues/62 BOOL _isFullScreenVideoOpen; @@ -107,7 +114,7 @@ @implementation RNCWebView UIStatusBarStyle _savedStatusBarStyle; #endif // !TARGET_OS_OSX BOOL _savedStatusBarHidden; - + #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */ UIScrollViewContentInsetAdjustmentBehavior _savedContentInsetAdjustmentBehavior; #endif @@ -141,7 +148,7 @@ - (instancetype)initWithFrame:(CGRect)frame _injectedJavaScriptForMainFrameOnly = YES; _injectedJavaScriptBeforeContentLoaded = nil; _injectedJavaScriptBeforeContentLoadedForMainFrameOnly = YES; - + #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */ _savedContentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; #endif @@ -153,13 +160,13 @@ - (instancetype)initWithFrame:(CGRect)frame _mediaCapturePermissionGrantType = RNCWebViewPermissionGrantType_Prompt; #endif } - + #if !TARGET_OS_OSX [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(appDidBecomeActive) name:UIApplicationDidBecomeActiveNotification object:nil]; - + [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(appWillResignActive) name:UIApplicationWillResignActiveNotification @@ -175,19 +182,19 @@ - (instancetype)initWithFrame:(CGRect)frame addObserver:self selector:@selector(keyboardWillShow) name:UIKeyboardWillShowNotification object:nil]; - + // Workaround for StatusBar appearance bug for iOS 12 // https://github.com/react-native-webview/react-native-webview/issues/62 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(showFullScreenVideoStatusBars) name:UIWindowDidBecomeVisibleNotification object:nil]; - + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(hideFullScreenVideoStatusBars) name:UIWindowDidBecomeHiddenNotification object:nil]; - + } #endif // !TARGET_OS_OSX return self; @@ -206,27 +213,30 @@ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecogni // Listener for long presses - (void)startLongPress:(UILongPressGestureRecognizer *)pressSender { - // When a long press ends, bring up our custom UIMenu - if(pressSender.state == UIGestureRecognizerStateEnded) { - if (!self.menuItems || self.menuItems.count == 0) { - return; + if (pressSender.state != UIGestureRecognizerStateEnded || !self.menuItems) { + return; + } + // When a long press ends, bring up our custom UIMenu if defined + if (self.menuItems.count == 0) { + UIMenuController *menuController = [UIMenuController sharedMenuController]; + menuController.menuItems = nil; + [menuController setMenuVisible:NO animated:YES]; + return; } UIMenuController *menuController = [UIMenuController sharedMenuController]; NSMutableArray *menuControllerItems = [NSMutableArray arrayWithCapacity:self.menuItems.count]; - + for(NSDictionary *menuItem in self.menuItems) { NSString *menuItemLabel = [RCTConvert NSString:menuItem[@"label"]]; NSString *menuItemKey = [RCTConvert NSString:menuItem[@"key"]]; NSString *sel = [NSString stringWithFormat:@"%@%@", CUSTOM_SELECTOR, menuItemKey]; UIMenuItem *item = [[UIMenuItem alloc] initWithTitle: menuItemLabel action: NSSelectorFromString(sel)]; - [menuControllerItems addObject: item]; } - + menuController.menuItems = menuControllerItems; [menuController setMenuVisible:YES animated:YES]; - } } #endif // !TARGET_OS_OSX @@ -300,7 +310,7 @@ - (BOOL)canPerformAction:(SEL)action withSender:(id)sender NSString *sel = NSStringFromSelector(action); // Do any of them have our custom keys? NSRange match = [sel rangeOfString:CUSTOM_SELECTOR]; - + if (match.location == 0) { return YES; } @@ -344,6 +354,14 @@ - (WKWebViewConfiguration *)setUpWkWebViewConfig prefs.javaScriptEnabled = NO; _prefsUsed = YES; } +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 /* iOS 13 */ + if (@available(iOS 13.0, *)) { + if (!_fraudulentWebsiteWarningEnabled) { + prefs.fraudulentWebsiteWarningEnabled = NO; + _prefsUsed = YES; + } + } +#endif if (_allowUniversalAccessFromFileURLs) { [wkWebViewConfig setValue:@TRUE forKey:@"allowUniversalAccessFromFileURLs"]; } @@ -375,7 +393,7 @@ - (WKWebViewConfiguration *)setUpWkWebViewConfig wkWebViewConfig.processPool = [[RNCWKProcessPoolManager sharedManager] sharedProcessPoolForScopeKey:self.scopeKey]; } wkWebViewConfig.userContentController = [WKUserContentController new]; - + #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 /* iOS 13 */ if (@available(iOS 13.0, *)) { WKWebpagePreferences *pagePrefs = [[WKWebpagePreferences alloc]init]; @@ -383,7 +401,7 @@ - (WKWebViewConfiguration *)setUpWkWebViewConfig wkWebViewConfig.defaultWebpagePreferences = pagePrefs; } #endif - + #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000 /* iOS 14 */ if (@available(iOS 14.0, *)) { if ([wkWebViewConfig respondsToSelector:@selector(limitsNavigationsToAppBoundDomains)]) { @@ -393,47 +411,46 @@ - (WKWebViewConfiguration *)setUpWkWebViewConfig } } #endif - + // Shim the HTML5 history API: [wkWebViewConfig.userContentController addScriptMessageHandler:[[RNCWeakScriptMessageDelegate alloc] initWithDelegate:self] name:HistoryShimName]; [self resetupScripts:wkWebViewConfig]; - + if(@available(macos 10.11, ios 9.0, *)) { wkWebViewConfig.allowsAirPlayForMediaPlayback = _allowsAirPlayForMediaPlayback; } - + #if !TARGET_OS_OSX wkWebViewConfig.allowsInlineMediaPlayback = _allowsInlineMediaPlayback; -#if WEBKIT_IOS_10_APIS_AVAILABLE wkWebViewConfig.mediaTypesRequiringUserActionForPlayback = _mediaPlaybackRequiresUserAction ? WKAudiovisualMediaTypeAll : WKAudiovisualMediaTypeNone; wkWebViewConfig.dataDetectorTypes = _dataDetectorTypes; -#else - wkWebViewConfig.mediaPlaybackRequiresUserAction = _mediaPlaybackRequiresUserAction; -#endif #endif // !TARGET_OS_OSX - + if (_applicationNameForUserAgent) { wkWebViewConfig.applicationNameForUserAgent = [NSString stringWithFormat:@"%@ %@", wkWebViewConfig.applicationNameForUserAgent, _applicationNameForUserAgent]; } - + return wkWebViewConfig; } +// react-native-mac os does not support didMoveToSuperView https://github.com/microsoft/react-native-macos/blob/main/React/Base/RCTUIKit.h#L388 +#if !TARGET_OS_OSX +- (void)didMoveToSuperview +{ + if (_webView == nil) { +#else - (void)didMoveToWindow { if (self.window != nil && _webView == nil) { +#endif // !TARGET_OS_OSX WKWebViewConfiguration *wkWebViewConfig = [self setUpWkWebViewConfig]; -#if !TARGET_OS_OSX - _webView = [[WKWebView alloc] initWithFrame:self.bounds configuration: wkWebViewConfig]; -#else _webView = [[RNCWKWebView alloc] initWithFrame:self.bounds configuration: wkWebViewConfig]; -#endif // !TARGET_OS_OSX - [self setBackgroundColor: _savedBackgroundColor]; #if !TARGET_OS_OSX + _webView.menuItems = _menuItems; _webView.scrollView.delegate = self; #endif // !TARGET_OS_OSX _webView.UIDelegate = self; @@ -453,19 +470,28 @@ - (void)didMoveToWindow _webView.allowsLinkPreview = _allowsLinkPreview; [_webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil]; _webView.allowsBackForwardNavigationGestures = _allowsBackForwardNavigationGestures; - + _webView.customUserAgent = _userAgent; -#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */ + +#if !TARGET_OS_OSX if ([_webView.scrollView respondsToSelector:@selector(setContentInsetAdjustmentBehavior:)]) { _webView.scrollView.contentInsetAdjustmentBehavior = _savedContentInsetAdjustmentBehavior; } -#endif +#endif // !TARGET_OS_OSX + #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 /* __IPHONE_13_0 */ if (@available(iOS 13.0, *)) { _webView.scrollView.automaticallyAdjustsScrollIndicatorInsets = _savedAutomaticallyAdjustsScrollIndicatorInsets; } #endif - + +#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 130300 || \ + __IPHONE_OS_VERSION_MAX_ALLOWED >= 160400 || \ + __TV_OS_VERSION_MAX_ALLOWED >= 160400 + if (@available(macOS 13.3, iOS 16.4, tvOS 16.4, *)) + _webView.inspectable = _webviewDebuggingEnabled; +#endif + [self addSubview:_webView]; [self setHideKeyboardAccessoryView: _savedHideKeyboardAccessoryView]; [self setKeyboardDisplayRequiresUserAction: _savedKeyboardDisplayRequiresUserAction]; @@ -476,7 +502,7 @@ - (void)didMoveToWindow if (self.menuItems != nil) { UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(startLongPress:)]; longPress.delegate = self; - + longPress.minimumPressDuration = 0.4f; longPress.numberOfTouchesRequired = 1; longPress.cancelsTouchesInView = YES; @@ -491,7 +517,21 @@ - (void)setAllowsBackForwardNavigationGestures:(BOOL)allowsBackForwardNavigation _webView.allowsBackForwardNavigationGestures = _allowsBackForwardNavigationGestures; } +#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 130300 || \ + __IPHONE_OS_VERSION_MAX_ALLOWED >= 160400 || \ + __TV_OS_VERSION_MAX_ALLOWED >= 160400 +- (void)setWebviewDebuggingEnabled:(BOOL)webviewDebuggingEnabled { + _webviewDebuggingEnabled = webviewDebuggingEnabled; + if (@available(macOS 13.3, iOS 16.4, tvOS 16.4, *)) + _webView.inspectable = _webviewDebuggingEnabled; +} +#endif + +#ifdef RCT_NEW_ARCH_ENABLED +- (void)destroyWebView +#else - (void)removeFromSuperview +#endif { if (_webView) { [_webView.configuration.userContentController removeScriptMessageHandlerForName:HistoryShimName]; @@ -500,6 +540,10 @@ - (void)removeFromSuperview [_webView removeFromSuperview]; #if !TARGET_OS_OSX _webView.scrollView.delegate = nil; + if (_menuItems) { + UIMenuController *menuController = [UIMenuController sharedMenuController]; + menuController.menuItems = nil; + } #endif // !TARGET_OS_OSX _webView = nil; if (_onContentProcessDidTerminate) { @@ -507,8 +551,10 @@ - (void)removeFromSuperview _onContentProcessDidTerminate(event); } } - + +#ifndef RCT_NEW_ARCH_ENABLED [super removeFromSuperview]; +#endif } #if !TARGET_OS_OSX @@ -518,7 +564,7 @@ -(void)showFullScreenVideoStatusBars if (!_autoManageStatusBarEnabled) { return; } - + _isFullScreenVideoOpen = YES; RCTUnsafeExecuteOnMainQueueSync(^{ [RCTSharedApplication() setStatusBarStyle:self->_savedStatusBarStyle animated:YES]; @@ -532,7 +578,7 @@ -(void)hideFullScreenVideoStatusBars if (!_autoManageStatusBarEnabled) { return; } - + _isFullScreenVideoOpen = NO; RCTUnsafeExecuteOnMainQueueSync(^{ [RCTSharedApplication() setStatusBarHidden:self->_savedStatusBarHidden animated:YES]; @@ -591,7 +637,7 @@ - (void)setBackgroundColor:(RCTUIColor *)backgroundColor if (_webView == nil) { return; } - + CGFloat alpha = CGColorGetAlpha(backgroundColor.CGColor); BOOL opaque = (alpha == 1.0); #if !TARGET_OS_OSX @@ -609,21 +655,22 @@ - (void)setBackgroundColor:(RCTUIColor *)backgroundColor #endif // !TARGET_OS_OSX } -#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */ +#if !TARGET_OS_OSX - (void)setContentInsetAdjustmentBehavior:(UIScrollViewContentInsetAdjustmentBehavior)behavior { _savedContentInsetAdjustmentBehavior = behavior; if (_webView == nil) { return; } - + if ([_webView.scrollView respondsToSelector:@selector(setContentInsetAdjustmentBehavior:)]) { CGPoint contentOffset = _webView.scrollView.contentOffset; _webView.scrollView.contentInsetAdjustmentBehavior = behavior; _webView.scrollView.contentOffset = contentOffset; } } -#endif +#endif // !TARGET_OS_OSX + #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 /* __IPHONE_13_0 */ - (void)setAutomaticallyAdjustsScrollIndicatorInsets:(BOOL)automaticallyAdjustsScrollIndicatorInsets{ _savedAutomaticallyAdjustsScrollIndicatorInsets = automaticallyAdjustsScrollIndicatorInsets; @@ -635,6 +682,7 @@ - (void)setAutomaticallyAdjustsScrollIndicatorInsets:(BOOL)automaticallyAdjustsS } } #endif + /** * This method is called whenever JavaScript running within the web view calls: * - window.webkit.messageHandlers[MessageHandlerName].postMessage @@ -661,7 +709,7 @@ - (void)setSource:(NSDictionary *)source { if (![_source isEqualToDictionary:source]) { _source = [source copy]; - + if (_webView != nil) { [self visitSource]; } @@ -672,7 +720,7 @@ - (void)setAllowingReadAccessToURL:(NSString *)allowingReadAccessToURL { if (![_allowingReadAccessToURL isEqualToString:allowingReadAccessToURL]) { _allowingReadAccessToURL = [allowingReadAccessToURL copy]; - + if (_webView != nil) { [self visitSource]; } @@ -716,62 +764,69 @@ - (void)visitSource NSArray *httpCookies = [NSHTTPCookie cookiesWithResponseHeaderFields:headers forURL:urlString]; [self writeCookiesToWebView:httpCookies completion:nil]; } - + NSURLRequest *request = [self requestForSource:_source]; - + __weak WKWebView *webView = _webView; + NSString *allowingReadAccessToURL = _allowingReadAccessToURL; + [self syncCookiesToWebView:^{ // Because of the way React works, as pages redirect, we actually end up // passing the redirect urls back here, so we ignore them if trying to load // the same url. We'll expose a call to 'reload' to allow a user to load // the existing page. - if ([request.URL isEqual:_webView.URL]) { + if ([request.URL isEqual:webView.URL]) { return; } if (!request.URL) { // Clear the webview - [_webView loadHTMLString:@"" baseURL:nil]; + [webView loadHTMLString:@"" baseURL:nil]; return; } if (request.URL.host) { - [_webView loadRequest:request]; + [webView loadRequest:request]; } else { - NSURL* readAccessUrl = _allowingReadAccessToURL ? [RCTConvert NSURL:_allowingReadAccessToURL] : request.URL; - [_webView loadFileURL:request.URL allowingReadAccessToURL:readAccessUrl]; + NSURL* readAccessUrl = allowingReadAccessToURL ? [RCTConvert NSURL:allowingReadAccessToURL] : request.URL; + [webView loadFileURL:request.URL allowingReadAccessToURL:readAccessUrl]; } }]; } #if !TARGET_OS_OSX +-(void)setMenuItems:(NSArray *)menuItems { + _menuItems = menuItems; + _webView.menuItems = menuItems; +} + -(void)setKeyboardDisplayRequiresUserAction:(BOOL)keyboardDisplayRequiresUserAction { if (_webView == nil) { _savedKeyboardDisplayRequiresUserAction = keyboardDisplayRequiresUserAction; return; } - + if (_savedKeyboardDisplayRequiresUserAction == true) { return; } - + UIView* subview; - + for (UIView* view in _webView.scrollView.subviews) { if([[view.class description] hasPrefix:@"WK"]) subview = view; } - + if(subview == nil) return; - + Class class = subview.class; - + NSOperatingSystemVersion iOS_11_3_0 = (NSOperatingSystemVersion){11, 3, 0}; NSOperatingSystemVersion iOS_12_2_0 = (NSOperatingSystemVersion){12, 2, 0}; NSOperatingSystemVersion iOS_13_0_0 = (NSOperatingSystemVersion){13, 0, 0}; - + Method method; IMP override; - + if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion: iOS_13_0_0]) { // iOS 13.0.0 - Future SEL selector = sel_getUid("_elementDidFocus:userIsInteracting:blurPreviousNode:activityStateChanges:userObject:"); @@ -807,7 +862,7 @@ -(void)setKeyboardDisplayRequiresUserAction:(BOOL)keyboardDisplayRequiresUserAct ((void (*)(id, SEL, void*, BOOL, BOOL, id))original)(me, selector, arg0, TRUE, arg2, arg3); }); } - + method_setImplementation(method, override); } @@ -817,34 +872,34 @@ -(void)setHideKeyboardAccessoryView:(BOOL)hideKeyboardAccessoryView _savedHideKeyboardAccessoryView = hideKeyboardAccessoryView; return; } - + if (_savedHideKeyboardAccessoryView == false) { return; } - + UIView* subview; - + for (UIView* view in _webView.scrollView.subviews) { if([[view.class description] hasPrefix:@"WK"]) subview = view; } - + if(subview == nil) return; - + NSString* name = [NSString stringWithFormat:@"%@_SwizzleHelperWK", subview.class.superclass]; Class newClass = NSClassFromString(name); - + if(newClass == nil) { newClass = objc_allocateClassPair(subview.class, [name cStringUsingEncoding:NSASCIIStringEncoding], 0); if(!newClass) return; - + Method method = class_getInstanceMethod([_SwizzleHelperWK class], @selector(inputAccessoryView)); class_addMethod(newClass, @selector(inputAccessoryView), method_getImplementation(method), method_getTypeEncoding(method)); - + objc_registerClassPair(newClass); } - + object_setClass(subview, newClass); } @@ -935,8 +990,8 @@ - (void)postMessage:(NSString *)message - (void)layoutSubviews { [super layoutSubviews]; - - // Ensure webview takes the position and dimensions of RNCWebView + + // Ensure webview takes the position and dimensions of RNCWebViewImpl _webView.frame = self.bounds; #if !TARGET_OS_OSX _webView.scrollView.contentInset = _contentInset; @@ -1077,7 +1132,7 @@ - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSSt #else NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:prompt]; - + const NSRect RCTSingleTextFieldFrame = NSMakeRect(0.0, 0.0, 275.0, 22.0); NSTextField *textField = [[NSTextField alloc] initWithFrame:RCTSingleTextFieldFrame]; textField.cell.scrollable = YES; @@ -1086,7 +1141,7 @@ - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSSt } textField.stringValue = defaultText; [alert setAccessoryView:textField]; - + [alert addButtonWithTitle:NSLocalizedString(@"OK", @"OK button")]; [alert addButtonWithTitle:NSLocalizedString(@"Cancel", @"Cancel button")]; [alert beginSheetModalForWindow:[NSApp keyWindow] completionHandler:^(NSModalResponse response) { @@ -1143,58 +1198,79 @@ - (void) webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { - static NSDictionary *navigationTypes; - static dispatch_once_t onceToken; - - dispatch_once(&onceToken, ^{ - navigationTypes = @{ - @(WKNavigationTypeLinkActivated): @"click", - @(WKNavigationTypeFormSubmitted): @"formsubmit", - @(WKNavigationTypeBackForward): @"backforward", - @(WKNavigationTypeReload): @"reload", - @(WKNavigationTypeFormResubmitted): @"formresubmit", - @(WKNavigationTypeOther): @"other", - }; - }); - - WKNavigationType navigationType = navigationAction.navigationType; - NSURLRequest *request = navigationAction.request; - BOOL isTopFrame = [request.URL isEqual:request.mainDocumentURL]; - - if (_onShouldStartLoadWithRequest) { - NSMutableDictionary *event = [self baseEvent]; - if (request.mainDocumentURL) { - [event addEntriesFromDictionary: @{ - @"mainDocumentURL": (request.mainDocumentURL).absoluteString, - }]; - } - [event addEntriesFromDictionary: @{ - @"url": (request.URL).absoluteString, - @"navigationType": navigationTypes[@(navigationType)], - @"isTopFrame": @(isTopFrame) - }]; - if (![self.delegate webView:self - shouldStartLoadForRequest:event - withCallback:_onShouldStartLoadWithRequest]) { - decisionHandler(WKNavigationActionPolicyCancel); - return; + static NSDictionary *navigationTypes; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + navigationTypes = @{ + @(WKNavigationTypeLinkActivated): @"click", + @(WKNavigationTypeFormSubmitted): @"formsubmit", + @(WKNavigationTypeBackForward): @"backforward", + @(WKNavigationTypeReload): @"reload", + @(WKNavigationTypeFormResubmitted): @"formresubmit", + @(WKNavigationTypeOther): @"other", + }; + }); + + WKNavigationType navigationType = navigationAction.navigationType; + NSURLRequest *request = navigationAction.request; + BOOL isTopFrame = [request.URL isEqual:request.mainDocumentURL]; + + if (_onShouldStartLoadWithRequest) { + NSMutableDictionary *event = [self baseEvent]; + int lockIdentifier = [[RNCWebViewDecisionManager getInstance] setDecisionHandler: ^(BOOL shouldStart){ + dispatch_async(dispatch_get_main_queue(), ^{ + if (!shouldStart) { + decisionHandler(WKNavigationActionPolicyCancel); + return; + } + if (self->_onLoadingStart) { + // We have this check to filter out iframe requests and whatnot + if (isTopFrame) { + NSMutableDictionary *event = [self baseEvent]; + [event addEntriesFromDictionary: @{ + @"url": (request.URL).absoluteString, + @"navigationType": navigationTypes[@(navigationType)] + }]; + self->_onLoadingStart(event); + } + } + + // Allow all navigation by default + decisionHandler(WKNavigationActionPolicyAllow); + }); + + }]; + if (request.mainDocumentURL) { + [event addEntriesFromDictionary: @{ + @"mainDocumentURL": (request.mainDocumentURL).absoluteString, + }]; + } + [event addEntriesFromDictionary: @{ + @"url": (request.URL).absoluteString, + @"navigationType": navigationTypes[@(navigationType)], + @"isTopFrame": @(isTopFrame), + @"lockIdentifier": @(lockIdentifier) + }]; + _onShouldStartLoadWithRequest(event); + // decisionHandler(WKNavigationActionPolicyAllow); + return; } - } - - if (_onLoadingStart) { - // We have this check to filter out iframe requests and whatnot - if (isTopFrame) { - NSMutableDictionary *event = [self baseEvent]; - [event addEntriesFromDictionary: @{ - @"url": (request.URL).absoluteString, - @"navigationType": navigationTypes[@(navigationType)] - }]; - _onLoadingStart(event); + + if (_onLoadingStart) { + // We have this check to filter out iframe requests and whatnot + if (isTopFrame) { + NSMutableDictionary *event = [self baseEvent]; + [event addEntriesFromDictionary: @{ + @"url": (request.URL).absoluteString, + @"navigationType": navigationTypes[@(navigationType)] + }]; + _onLoadingStart(event); + } } - } - - // Allow all navigation by default - decisionHandler(WKNavigationActionPolicyAllow); + + // Allow all navigation by default + decisionHandler(WKNavigationActionPolicyAllow); } /** @@ -1223,26 +1299,26 @@ - (void) webView:(WKWebView *)webView if ([navigationResponse.response isKindOfClass:[NSHTTPURLResponse class]]) { NSHTTPURLResponse *response = (NSHTTPURLResponse *)navigationResponse.response; NSInteger statusCode = response.statusCode; - + if (statusCode >= 400) { NSMutableDictionary *httpErrorEvent = [self baseEvent]; [httpErrorEvent addEntriesFromDictionary: @{ @"url": response.URL.absoluteString, @"statusCode": @(statusCode) }]; - + _onHttpError(httpErrorEvent); } - + NSString *disposition = nil; - if (@available(iOS 13, *)) { + if (@available(iOS 13, macOS 10.15, *)) { disposition = [response valueForHTTPHeaderField:@"Content-Disposition"]; } BOOL isAttachment = disposition != nil && [disposition hasPrefix:@"attachment"]; if (isAttachment || !navigationResponse.canShowMIMEType) { if (_onFileDownload) { policy = WKNavigationResponsePolicyCancel; - + NSMutableDictionary *downloadEvent = [self baseEvent]; [downloadEvent addEntriesFromDictionary: @{ @"downloadUrl": (response.URL).absoluteString, @@ -1252,7 +1328,7 @@ - (void) webView:(WKWebView *)webView } } } - + decisionHandler(policy); } @@ -1272,7 +1348,7 @@ - (void) webView:(WKWebView *)webView // http://stackoverflow.com/questions/1024748/how-do-i-fix-nsurlerrordomain-error-999-in-iphone-3-0-os return; } - + if ([error.domain isEqualToString:@"WebKitErrorDomain"] && (error.code == 102 || error.code == 101)) { // Error code 102 "Frame load interrupted" is raised by the WKWebView @@ -1280,7 +1356,7 @@ - (void) webView:(WKWebView *)webView // implementing OAuth with a WebView. return; } - + NSMutableDictionary *event = [self baseEvent]; [event addEntriesFromDictionary:@{ @"didFailProvisionalNavigation": @YES, @@ -1358,13 +1434,15 @@ - (void)webView:(WKWebView *)webView - (void)cookiesDidChangeInCookieStore:(WKHTTPCookieStore *)cookieStore { - if(_sharedCookiesEnabled && @available(iOS 11.0, *)) { - // Write all cookies from WKWebView back to sharedHTTPCookieStorage - [cookieStore getAllCookies:^(NSArray* cookies) { - for (NSHTTPCookie *cookie in cookies) { - [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie]; - } - }]; + if (@available(iOS 11.0, *)) { + if(_sharedCookiesEnabled) { + // Write all cookies from WKWebView back to sharedHTTPCookieStorage + [cookieStore getAllCookies:^(NSArray* cookies) { + for (NSHTTPCookie *cookie in cookies) { + [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie]; + } + }]; + } } } @@ -1391,7 +1469,7 @@ - (void)reload * manually call [_webView loadRequest:request]. */ NSURLRequest *request = [self requestForSource:self.source]; - + if (request.URL && !_webView.URL.absoluteString.length) { [_webView loadRequest:request]; } else { @@ -1417,13 +1495,13 @@ - (void)pullToRefresh:(UIRefreshControl *)refreshControl - (void)setPullToRefreshEnabled:(BOOL)pullToRefreshEnabled { _pullToRefreshEnabled = pullToRefreshEnabled; - + if (pullToRefreshEnabled) { [self addPullToRefreshControl]; } else { [_refreshControl removeFromSuperview]; } - + [self setBounces:_bounces]; } #endif // !TARGET_OS_OSX @@ -1451,23 +1529,30 @@ - (void)setBounces:(BOOL)bounces - (void)setInjectedJavaScript:(NSString *)source { _injectedJavaScript = source; - + self.atEndScript = source == nil ? nil : [[WKUserScript alloc] initWithSource:source injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:_injectedJavaScriptForMainFrameOnly]; - + if(_webView != nil){ [self resetupScripts:_webView.configuration]; } } +- (void)setEnableApplePay:(BOOL)enableApplePay { + _enableApplePay = enableApplePay; + if(_webView != nil){ + [self resetupScripts:_webView.configuration]; + } +} + - (void)setInjectedJavaScriptBeforeContentLoaded:(NSString *)source { _injectedJavaScriptBeforeContentLoaded = source; - + self.atStartScript = source == nil ? nil : [[WKUserScript alloc] initWithSource:source injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:_injectedJavaScriptBeforeContentLoadedForMainFrameOnly]; - + if(_webView != nil){ [self resetupScripts:_webView.configuration]; } @@ -1485,7 +1570,7 @@ - (void)setInjectedJavaScriptBeforeContentLoadedForMainFrameOnly:(BOOL)mainFrame - (void)setMessagingEnabled:(BOOL)messagingEnabled { _messagingEnabled = messagingEnabled; - + self.postMessageScript = _messagingEnabled ? [ [WKUserScript alloc] @@ -1505,7 +1590,7 @@ - (void)setMessagingEnabled:(BOOL)messagingEnabled { forMainFrameOnly:YES ] : nil; - + if(_webView != nil){ [self resetupScripts:_webView.configuration]; } @@ -1513,22 +1598,28 @@ - (void)setMessagingEnabled:(BOOL)messagingEnabled { - (void)writeCookiesToWebView:(NSArray*)cookies completion:(void (^)(void))completion { // The required cookie APIs only became available on iOS 11 - if(_sharedCookiesEnabled && @available(iOS 11.0, *)) { - dispatch_async(dispatch_get_main_queue(), ^{ - dispatch_group_t group = dispatch_group_create(); - for (NSHTTPCookie *cookie in cookies) { - dispatch_group_enter(group); - [_webView.configuration.websiteDataStore.httpCookieStore setCookie:cookie completionHandler:^{ - dispatch_group_leave(group); - }]; - } - dispatch_group_notify(group, dispatch_get_main_queue(), ^{ - if (completion) { - completion(); + if (@available(iOS 11.0, *)) { + if (_sharedCookiesEnabled) { + __weak WKWebView *webView = _webView; + dispatch_async(dispatch_get_main_queue(), ^{ + dispatch_group_t group = dispatch_group_create(); + for (NSHTTPCookie *cookie in cookies) { + dispatch_group_enter(group); + [webView.configuration.websiteDataStore.httpCookieStore setCookie:cookie completionHandler:^{ + dispatch_group_leave(group); + }]; } + dispatch_group_notify(group, dispatch_get_main_queue(), ^{ + if (completion) { + completion(); + } + }); }); - }); - } else if (completion) { + return; + } + } + + if (completion) { completion(); } } @@ -1548,7 +1639,7 @@ - (void)resetupScripts:(WKWebViewConfiguration *)wkWebViewConfig { } return; } - + NSString *html5HistoryAPIShimSource = [NSString stringWithFormat: @"(function(history) {\n" " function notify(type) {\n" @@ -1571,7 +1662,7 @@ - (void)resetupScripts:(WKWebViewConfiguration *)wkWebViewConfig { ]; WKUserScript *script = [[WKUserScript alloc] initWithSource:html5HistoryAPIShimSource injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES]; [wkWebViewConfig.userContentController addUserScript:script]; - + if(_sharedCookiesEnabled) { // More info to sending cookies with WKWebView // https://stackoverflow.com/questions/26573137/can-i-set-the-cookies-to-be-used-by-a-wkwebview/26577303#26577303 @@ -1587,7 +1678,7 @@ - (void)resetupScripts:(WKWebViewConfiguration *)wkWebViewConfig { }]; } else { NSMutableString *script = [NSMutableString string]; - + // Clear all existing cookies in a direct called function. This ensures that no // javascript error will break the web content javascript. // We keep this code here, if someone requires that Cookies are also removed within the @@ -1605,7 +1696,7 @@ - (void)resetupScripts:(WKWebViewConfiguration *)wkWebViewConfig { [script appendString:@" }\n"]; [script appendString:@"})();\n\n"]; */ - + // Set cookies in a direct called function. This ensures that no // javascript error will break the web content javascript. // Generates JS: document.cookie = "key=value; Path=/; Expires=Thu, 01 Jan 20xx 00:00:01 GMT;" @@ -1626,14 +1717,14 @@ - (void)resetupScripts:(WKWebViewConfiguration *)wkWebViewConfig { [script appendString:@";\n"]; } [script appendString:@"})();\n"]; - + WKUserScript* cookieInScript = [[WKUserScript alloc] initWithSource:script injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES]; [wkWebViewConfig.userContentController addUserScript:cookieInScript]; } } - + if(_messagingEnabled){ if (self.postMessageScript){ [wkWebViewConfig.userContentController addScriptMessageHandler:[[RNCWeakScriptMessageDelegate alloc] initWithDelegate:self] @@ -1652,7 +1743,7 @@ - (void)resetupScripts:(WKWebViewConfiguration *)wkWebViewConfig { - (NSURLRequest *)requestForSource:(id)json { NSURLRequest *request = [RCTConvert NSURLRequest:self.source]; - + // If sharedCookiesEnabled we automatically add all application cookies to the // http request. This is automatically done on iOS 11+ in the WebView constructor. // Se we need to manually add these shared cookies here only for iOS versions < 11. diff --git a/ios/vendored/unversioned/react-native-webview/apple/RNCWebViewManager.h b/ios/vendored/unversioned/react-native-webview/apple/RNCWebViewManager.h index 83ba6eb9e8c57..af000be5c1a70 100644 --- a/ios/vendored/unversioned/react-native-webview/apple/RNCWebViewManager.h +++ b/ios/vendored/unversioned/react-native-webview/apple/RNCWebViewManager.h @@ -1,13 +1,9 @@ -/** - * 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. - */ +#ifndef RNCWebViewManager_h +#define RNCWebViewManager_h #import @interface RNCWebViewManager : RCTViewManager -@property (nonatomic, copy) NSArray * _Nullable menuItems; -@property (nonatomic, copy) RCTDirectEventBlock onCustomMenuSelection; @end + +#endif /* RNCWebViewManager_h */ diff --git a/ios/vendored/unversioned/react-native-webview/apple/RNCWebViewManager.m b/ios/vendored/unversioned/react-native-webview/apple/RNCWebViewManager.mm similarity index 51% rename from ios/vendored/unversioned/react-native-webview/apple/RNCWebViewManager.m rename to ios/vendored/unversioned/react-native-webview/apple/RNCWebViewManager.mm index 6607806ef68ab..709745c677ea4 100644 --- a/ios/vendored/unversioned/react-native-webview/apple/RNCWebViewManager.m +++ b/ios/vendored/unversioned/react-native-webview/apple/RNCWebViewManager.mm @@ -1,18 +1,19 @@ -/** - * 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 #import "RNCWebViewManager.h" +#import "RNCWebViewImpl.h" +#import "RNCWebViewDecisionManager.h" +#ifdef RCT_NEW_ARCH_ENABLED +#import "RNCWebViewSpec/RNCWebViewSpec.h" +#endif -#import -#import -#import "RNCWebView.h" - -@interface RNCWebViewManager () -@end +#if TARGET_OS_OSX +#define RNCView NSView +@class NSView; +#else +#define RNCView UIView +@class UIView; +#endif // TARGET_OS_OSX @implementation RCTConvert (WKWebView) #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 /* iOS 13 */ @@ -34,18 +35,20 @@ @implementation RCTConvert (WKWebView) #endif @end -@implementation RNCWebViewManager -{ - NSString *_scopeKey; - NSConditionLock *_shouldStartLoadLock; - BOOL _shouldStartLoad; + +@implementation RNCWebViewManager { + NSString *_scopeKey; + NSConditionLock *_shouldStartLoadLock; + BOOL _shouldStartLoad; } +RCT_EXPORT_MODULE(RNCWebView) + - (instancetype)initWithExperienceStableLegacyId:(NSString *)experienceStableLegacyId - scopeKey:(NSString *)scopeKey - easProjectId:(NSString *)easProjectId - kernelServiceDelegate:(id)kernelServiceInstance - params:(NSDictionary *)params + scopeKey:(NSString *)scopeKey + easProjectId:(NSString *)easProjectId + kernelServiceDelegate:(id)kernelServiceInstance + params:(NSDictionary *)params { if (self = [super init]) { _scopeKey = scopeKey; @@ -53,19 +56,16 @@ - (instancetype)initWithExperienceStableLegacyId:(NSString *)experienceStableLeg return self; } -#if !TARGET_OS_OSX -- (UIView *)view -#else -- (RCTUIView *)view -#endif // !TARGET_OS_OSX +- (RNCView *)view { - RNCWebView *webView = [RNCWebView new]; - webView.scopeKey = _scopeKey; - webView.delegate = self; - return webView; + RNCWebViewImpl *webview = [[RNCWebViewImpl alloc] init]; + webview.scopeKey = _scopeKey; + return webview; } RCT_EXPORT_VIEW_PROPERTY(source, NSDictionary) +// New arch only +RCT_CUSTOM_VIEW_PROPERTY(newSource, NSDictionary, RNCWebViewImpl) {} RCT_EXPORT_VIEW_PROPERTY(onFileDownload, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onLoadingStart, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onLoadingFinish, RCTDirectEventBlock) @@ -83,11 +83,10 @@ - (RCTUIView *)view RCT_EXPORT_VIEW_PROPERTY(allowFileAccessFromFileURLs, BOOL) RCT_EXPORT_VIEW_PROPERTY(allowUniversalAccessFromFileURLs, BOOL) RCT_EXPORT_VIEW_PROPERTY(allowsInlineMediaPlayback, BOOL) +RCT_EXPORT_VIEW_PROPERTY(webviewDebuggingEnabled, BOOL) RCT_EXPORT_VIEW_PROPERTY(allowsAirPlayForMediaPlayback, BOOL) RCT_EXPORT_VIEW_PROPERTY(mediaPlaybackRequiresUserAction, BOOL) -#if WEBKIT_IOS_10_APIS_AVAILABLE RCT_EXPORT_VIEW_PROPERTY(dataDetectorTypes, WKDataDetectorTypes) -#endif RCT_EXPORT_VIEW_PROPERTY(contentInset, UIEdgeInsets) RCT_EXPORT_VIEW_PROPERTY(automaticallyAdjustContentInsets, BOOL) RCT_EXPORT_VIEW_PROPERTY(autoManageStatusBarEnabled, BOOL) @@ -124,6 +123,10 @@ - (RCTUIView *)view RCT_EXPORT_VIEW_PROPERTY(mediaCapturePermissionGrantType, RNCWebViewPermissionGrantType) #endif +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 /* iOS 13 */ +RCT_EXPORT_VIEW_PROPERTY(fraudulentWebsiteWarningEnabled, BOOL) +#endif + /** * Expose methods to enable messaging the webview. */ @@ -132,170 +135,108 @@ - (RCTUIView *)view RCT_EXPORT_VIEW_PROPERTY(onScroll, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(enableApplePay, BOOL) RCT_EXPORT_VIEW_PROPERTY(menuItems, NSArray); +// New arch only +RCT_CUSTOM_VIEW_PROPERTY(hasOnFileDownload, BOOL, RNCWebViewImpl) {} RCT_EXPORT_VIEW_PROPERTY(onCustomMenuSelection, RCTDirectEventBlock) - -RCT_EXPORT_METHOD(postMessage:(nonnull NSNumber *)reactTag message:(NSString *)message) -{ - [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { - RNCWebView *view = viewRegistry[reactTag]; - if (![view isKindOfClass:[RNCWebView class]]) { - RCTLogError(@"Invalid view returned from registry, expecting RNCWebView, got: %@", view); - } else { - [view postMessage:message]; - } - }]; -} - -RCT_CUSTOM_VIEW_PROPERTY(pullToRefreshEnabled, BOOL, RNCWebView) { +RCT_CUSTOM_VIEW_PROPERTY(pullToRefreshEnabled, BOOL, RNCWebViewImpl) { view.pullToRefreshEnabled = json == nil ? false : [RCTConvert BOOL: json]; } -RCT_CUSTOM_VIEW_PROPERTY(bounces, BOOL, RNCWebView) { +RCT_CUSTOM_VIEW_PROPERTY(bounces, BOOL, RNCWebViewImpl) { view.bounces = json == nil ? true : [RCTConvert BOOL: json]; } -RCT_CUSTOM_VIEW_PROPERTY(useSharedProcessPool, BOOL, RNCWebView) { +RCT_CUSTOM_VIEW_PROPERTY(useSharedProcessPool, BOOL, RNCWebViewImpl) { view.useSharedProcessPool = json == nil ? true : [RCTConvert BOOL: json]; } -RCT_CUSTOM_VIEW_PROPERTY(userAgent, NSString, RNCWebView) { +RCT_CUSTOM_VIEW_PROPERTY(userAgent, NSString, RNCWebViewImpl) { view.userAgent = [RCTConvert NSString: json]; } -RCT_CUSTOM_VIEW_PROPERTY(scrollEnabled, BOOL, RNCWebView) { +RCT_CUSTOM_VIEW_PROPERTY(scrollEnabled, BOOL, RNCWebViewImpl) { view.scrollEnabled = json == nil ? true : [RCTConvert BOOL: json]; } -RCT_CUSTOM_VIEW_PROPERTY(sharedCookiesEnabled, BOOL, RNCWebView) { +RCT_CUSTOM_VIEW_PROPERTY(sharedCookiesEnabled, BOOL, RNCWebViewImpl) { view.sharedCookiesEnabled = json == nil ? false : [RCTConvert BOOL: json]; } #if !TARGET_OS_OSX -RCT_CUSTOM_VIEW_PROPERTY(decelerationRate, CGFloat, RNCWebView) { +RCT_CUSTOM_VIEW_PROPERTY(decelerationRate, CGFloat, RNCWebViewImpl) { view.decelerationRate = json == nil ? UIScrollViewDecelerationRateNormal : [RCTConvert CGFloat: json]; } #endif // !TARGET_OS_OSX -RCT_CUSTOM_VIEW_PROPERTY(directionalLockEnabled, BOOL, RNCWebView) { +RCT_CUSTOM_VIEW_PROPERTY(directionalLockEnabled, BOOL, RNCWebViewImpl) { view.directionalLockEnabled = json == nil ? true : [RCTConvert BOOL: json]; } -RCT_CUSTOM_VIEW_PROPERTY(showsHorizontalScrollIndicator, BOOL, RNCWebView) { +RCT_CUSTOM_VIEW_PROPERTY(showsHorizontalScrollIndicator, BOOL, RNCWebViewImpl) { view.showsHorizontalScrollIndicator = json == nil ? true : [RCTConvert BOOL: json]; } -RCT_CUSTOM_VIEW_PROPERTY(showsVerticalScrollIndicator, BOOL, RNCWebView) { +RCT_CUSTOM_VIEW_PROPERTY(showsVerticalScrollIndicator, BOOL, RNCWebViewImpl) { view.showsVerticalScrollIndicator = json == nil ? true : [RCTConvert BOOL: json]; } -RCT_CUSTOM_VIEW_PROPERTY(keyboardDisplayRequiresUserAction, BOOL, RNCWebView) { +RCT_CUSTOM_VIEW_PROPERTY(keyboardDisplayRequiresUserAction, BOOL, RNCWebViewImpl) { view.keyboardDisplayRequiresUserAction = json == nil ? true : [RCTConvert BOOL: json]; } -RCT_EXPORT_METHOD(injectJavaScript:(nonnull NSNumber *)reactTag script:(NSString *)script) -{ - [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { - RNCWebView *view = viewRegistry[reactTag]; - if (![view isKindOfClass:[RNCWebView class]]) { - RCTLogError(@"Invalid view returned from registry, expecting RNCWebView, got: %@", view); - } else { - [view injectJavaScript:script]; - } - }]; -} - -RCT_EXPORT_METHOD(goBack:(nonnull NSNumber *)reactTag) -{ - [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { - RNCWebView *view = viewRegistry[reactTag]; - if (![view isKindOfClass:[RNCWebView class]]) { - RCTLogError(@"Invalid view returned from registry, expecting RNCWebView, got: %@", view); - } else { - [view goBack]; - } - }]; -} - -RCT_EXPORT_METHOD(goForward:(nonnull NSNumber *)reactTag) -{ - [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { - RNCWebView *view = viewRegistry[reactTag]; - if (![view isKindOfClass:[RNCWebView class]]) { - RCTLogError(@"Invalid view returned from registry, expecting RNCWebView, got: %@", view); - } else { - [view goForward]; - } - }]; -} - -RCT_EXPORT_METHOD(reload:(nonnull NSNumber *)reactTag) -{ - [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { - RNCWebView *view = viewRegistry[reactTag]; - if (![view isKindOfClass:[RNCWebView class]]) { - RCTLogError(@"Invalid view returned from registry, expecting RNCWebView, got: %@", view); - } else { - [view reload]; - } - }]; -} - -RCT_EXPORT_METHOD(stopLoading:(nonnull NSNumber *)reactTag) -{ - [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { - RNCWebView *view = viewRegistry[reactTag]; - if (![view isKindOfClass:[RNCWebView class]]) { - RCTLogError(@"Invalid view returned from registry, expecting RNCWebView, got: %@", view); - } else { - [view stopLoading]; - } - }]; -} - -RCT_EXPORT_METHOD(requestFocus:(nonnull NSNumber *)reactTag) -{ - [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { - RNCWebView *view = viewRegistry[reactTag]; - if (![view isKindOfClass:[RNCWebView class]]) { - RCTLogError(@"Invalid view returned from registry, expecting RNCWebView, got: %@", view); - } else { - [view requestFocus]; - } - }]; -} - -#pragma mark - Exported synchronous methods +#if !TARGET_OS_OSX + #define BASE_VIEW_PER_OS() UIView +#else + #define BASE_VIEW_PER_OS() NSView +#endif -- (BOOL) webView:(RNCWebView *)webView -shouldStartLoadForRequest:(NSMutableDictionary *)request - withCallback:(RCTDirectEventBlock)callback +#define QUICK_RCT_EXPORT_COMMAND_METHOD(name) \ +RCT_EXPORT_METHOD(name:(nonnull NSNumber *)reactTag) \ +{ \ +[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { \ + RNCWebViewImpl *view = (RNCWebViewImpl *)viewRegistry[reactTag]; \ + if (![view isKindOfClass:[RNCWebViewImpl class]]) { \ + RCTLogError(@"Invalid view returned from registry, expecting RNCWebView, got: %@", view); \ + } else { \ + [view name]; \ + } \ + }]; \ +} +#define QUICK_RCT_EXPORT_COMMAND_METHOD_PARAMS(name, in_param, out_param) \ +RCT_EXPORT_METHOD(name:(nonnull NSNumber *)reactTag in_param) \ +{ \ +[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { \ + RNCWebViewImpl *view = (RNCWebViewImpl *)viewRegistry[reactTag]; \ + if (![view isKindOfClass:[RNCWebViewImpl class]]) { \ + RCTLogError(@"Invalid view returned from registry, expecting RNCWebView, got: %@", view); \ + } else { \ + [view name:out_param]; \ + } \ + }]; \ +} + +QUICK_RCT_EXPORT_COMMAND_METHOD(reload) +QUICK_RCT_EXPORT_COMMAND_METHOD(goBack) +QUICK_RCT_EXPORT_COMMAND_METHOD(goForward) +QUICK_RCT_EXPORT_COMMAND_METHOD(stopLoading) +QUICK_RCT_EXPORT_COMMAND_METHOD(requestFocus) + +QUICK_RCT_EXPORT_COMMAND_METHOD_PARAMS(postMessage, message:(NSString *)message, message) +QUICK_RCT_EXPORT_COMMAND_METHOD_PARAMS(injectJavaScript, script:(NSString *)script, script) + +RCT_EXPORT_METHOD(shouldStartLoadWithLockIdentifier:(BOOL)shouldStart + lockIdentifier:(double)lockIdentifier) { - _shouldStartLoadLock = [[NSConditionLock alloc] initWithCondition:arc4random()]; - _shouldStartLoad = YES; - request[@"lockIdentifier"] = @(_shouldStartLoadLock.condition); - callback(request); - - // Block the main thread for a maximum of 250ms until the JS thread returns - if ([_shouldStartLoadLock lockWhenCondition:0 beforeDate:[NSDate dateWithTimeIntervalSinceNow:.25]]) { - BOOL returnValue = _shouldStartLoad; - [_shouldStartLoadLock unlock]; - _shouldStartLoadLock = nil; - return returnValue; - } else { - RCTLogWarn(@"Did not receive response to shouldStartLoad in time, defaulting to YES"); - return YES; - } + [[RNCWebViewDecisionManager getInstance] setResult:shouldStart forLockIdentifier:(int)lockIdentifier]; } -RCT_EXPORT_METHOD(startLoadWithResult:(BOOL)result lockIdentifier:(NSInteger)lockIdentifier) +// Thanks to this guard, we won't compile this code when we build for the old architecture. +#ifdef RCT_NEW_ARCH_ENABLED +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params { - if ([_shouldStartLoadLock tryLockWhenCondition:lockIdentifier]) { - _shouldStartLoad = result; - [_shouldStartLoadLock unlockWithCondition:0]; - } else { - RCTLogWarn(@"startLoadWithResult invoked with invalid lockIdentifier: " - "got %lld, expected %lld", (long long)lockIdentifier, (long long)_shouldStartLoadLock.condition); - } + return std::make_shared(params); } +#endif @end diff --git a/ios/vendored/unversioned/react-native-webview/react-native-webview.podspec.json b/ios/vendored/unversioned/react-native-webview/react-native-webview.podspec.json index 1801d7c49066f..8dccca7edc15c 100644 --- a/ios/vendored/unversioned/react-native-webview/react-native-webview.podspec.json +++ b/ios/vendored/unversioned/react-native-webview/react-native-webview.podspec.json @@ -1,6 +1,6 @@ { "name": "react-native-webview", - "version": "11.26.0", + "version": "13.2.2", "summary": "React Native WebView component for iOS, Android, macOS, and Windows", "license": "MIT", "authors": "Jamon Holmgren ", @@ -11,9 +11,9 @@ }, "source": { "git": "https://github.com/react-native-webview/react-native-webview.git", - "tag": "v11.26.0" + "tag": "v13.2.2" }, - "source_files": "apple/**/*.{h,m}", + "source_files": "apple/**/*.{h,m,mm,swift}", "dependencies": { "React-Core": [] } diff --git a/packages/expo/bundledNativeModules.json b/packages/expo/bundledNativeModules.json index 5b44ea5f786e9..a31476be44d79 100644 --- a/packages/expo/bundledNativeModules.json +++ b/packages/expo/bundledNativeModules.json @@ -95,7 +95,7 @@ "react-native-shared-element": "0.8.8", "react-native-svg": "13.9.0", "react-native-view-shot": "3.6.0", - "react-native-webview": "11.26.0", + "react-native-webview": "13.2.2", "sentry-expo": "~6.2.0", "unimodules-app-loader": "~4.1.2", "unimodules-image-loader-interface": "~6.1.0", diff --git a/tools/src/vendoring/config/expoGoConfig.ts b/tools/src/vendoring/config/expoGoConfig.ts index 778ab5aa59965..9489528ab19cb 100644 --- a/tools/src/vendoring/config/expoGoConfig.ts +++ b/tools/src/vendoring/config/expoGoConfig.ts @@ -287,33 +287,36 @@ const config: VendoringTargetConfig = { `, }, { - paths: 'RNCWebView.h', - find: /@interface RNCWebView : RCTView/, + paths: 'RNCWebViewImpl.h', + find: /@interface RNCWebViewImpl : RCTView/, replaceWith: '$&\n@property (nonatomic, strong) NSString *scopeKey;', }, { - paths: 'RNCWebView.m', + paths: 'RNCWebViewImpl.m', find: /(\[\[RNCWKProcessPoolManager sharedManager\] sharedProcessPool)]/, replaceWith: '$1ForScopeKey:self.scopeKey]', }, { - paths: 'RNCWebViewManager.m', + paths: 'RNCWebViewManager.mm', find: /@implementation RNCWebViewManager\s*{/, - replaceWith: '$&\n NSString *_scopeKey;', + replaceWith: '$&\n NSString *_scopeKey;', }, { - paths: 'RNCWebViewManager.m', - find: '*webView = [RNCWebView new];', - replaceWith: '*webView = [RNCWebView new];\n webView.scopeKey = _scopeKey;', + paths: 'RNCWebViewManager.mm', + find: 'return [[RNCWebViewImpl alloc] init];', + replaceWith: + 'RNCWebViewImpl *webview = [[RNCWebViewImpl alloc] init];\n webview.scopeKey = _scopeKey;\n return webview;', }, { - paths: 'RNCWebViewManager.m', - find: /RCT_EXPORT_MODULE\(\)/, - replaceWith: `- (instancetype)initWithExperienceStableLegacyId:(NSString *)experienceStableLegacyId - scopeKey:(NSString *)scopeKey - easProjectId:(NSString *)easProjectId - kernelServiceDelegate:(id)kernelServiceInstance - params:(NSDictionary *)params + paths: 'RNCWebViewManager.mm', + find: /RCT_EXPORT_MODULE\(RNCWebView\)/, + replaceWith: `RCT_EXPORT_MODULE(RNCWebView) + +- (instancetype)initWithExperienceStableLegacyId:(NSString *)experienceStableLegacyId + scopeKey:(NSString *)scopeKey + easProjectId:(NSString *)easProjectId + kernelServiceDelegate:(id)kernelServiceInstance + params:(NSDictionary *)params { if (self = [super init]) { _scopeKey = scopeKey; diff --git a/tools/src/vendoring/legacy.ts b/tools/src/vendoring/legacy.ts index 3a33370193d04..8b7789a166f94 100644 --- a/tools/src/vendoring/legacy.ts +++ b/tools/src/vendoring/legacy.ts @@ -590,6 +590,24 @@ const vendoredModulesConfig: { [key: string]: VendoredModuleConfig } = { sourceAndroidPackage: 'com.reactnativecommunity.webview', targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.components.webview', }, + { + sourceAndroidPath: 'android/src/oldarch/com/reactnativecommunity/webview', + cleanupTargetPath: false, + targetAndroidPath: 'modules/api/components/webview', + sourceAndroidPackage: 'com.reactnativecommunity.webview', + targetAndroidPackage: 'versioned.host.exp.exponent.modules.api.components.webview', + onDidVendorAndroidFile: async (file: string) => { + const fileName = path.basename(file); + if (fileName === 'RNCWebViewPackage.java') { + let content = await fs.readFile(file, 'utf8'); + content = content.replace( + /^(package .+)$/gm, + '$1\nimport host.exp.expoview.BuildConfig;' + ); + await fs.writeFile(file, content, 'utf8'); + } + }, + }, ], }, 'react-native-safe-area-context': { diff --git a/yarn.lock b/yarn.lock index f2815945b530a..50d9b483d4893 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15984,10 +15984,10 @@ react-native-web@~0.18.10: postcss-value-parser "^4.2.0" styleq "^0.1.2" -react-native-webview@11.26.0: - version "11.26.0" - resolved "https://registry.yarnpkg.com/react-native-webview/-/react-native-webview-11.26.0.tgz#e524992876fe4a79e69905f0fab8949b470e9f16" - integrity sha512-4T4CKRm8xlaQDz9h/bCMPGAvtkesrhkRWqCX9FDJEzBToaVUIsV0ZOqtC4w/JSnCtFKKYiaC1ReJtCGv+4mFeQ== +react-native-webview@13.2.1: + version "13.2.1" + resolved "https://registry.yarnpkg.com/react-native-webview/-/react-native-webview-13.2.1.tgz#e2974339adc76450a2be686cdea1042fd025c2f0" + integrity sha512-2U5XEBg8nUlGBsFkG1QgLUVN+CJJxNvZfprXlgPUFsU2UhYiVs7GM1sJez1YcnZ2TWKMCd6nVrLRsPNiLhjqVg== dependencies: escape-string-regexp "2.0.0" invariant "2.2.4"