diff --git a/.changeset/big-files-greet.md b/.changeset/big-files-greet.md new file mode 100644 index 000000000..0fe75fcaf --- /dev/null +++ b/.changeset/big-files-greet.md @@ -0,0 +1,12 @@ +--- +'@phantom/react-native-webview': minor +--- + +- Merged 16 commits from upstream/master +- Upgraded androidx.webkit:webkit from 1.4.0 to 1.14.0 +- Added SSL error handling for sub-resources +- Added Payment Request API support (disabled downloads for security) +- Preserved Phantom's custom changes: + - Package name and version (1.0.2) + - Download blocking with toast message + - All existing security configurations diff --git a/.github/workflows/ios-ci.yml b/.github/workflows/ios-ci.yml index b8461672a..46ae130e7 100644 --- a/.github/workflows/ios-ci.yml +++ b/.github/workflows/ios-ci.yml @@ -25,7 +25,7 @@ jobs: working-directory: example/ios - name: Build iOS test app run: | - device_name='iPhone 15' + device_name='iPhone 16' device=$(xcrun simctl list devices "${device_name}" available | grep "${device_name} (") re='\(([-0-9A-Fa-f]+)\)' [[ $device =~ $re ]] || exit 1 diff --git a/android/gradle.properties b/android/gradle.properties index 4d9af0821..6bbf414e0 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,5 +1,5 @@ ReactNativeWebView_kotlinVersion=1.6.0 -ReactNativeWebView_webkitVersion=1.4.0 +ReactNativeWebView_webkitVersion=1.14.0 ReactNativeWebView_compileSdkVersion=31 ReactNativeWebView_targetSdkVersion=31 ReactNativeWebView_minSdkVersion=24 diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index b8f945d14..d9fdd5cc7 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,6 +1,18 @@ + + + + + + + + + + + + + + + + + + + + + + + + + - webView.setIgnoreErrFailedForThisURL(url) android.widget.Toast.makeText(context, "File downloads are not supported", android.widget.Toast.LENGTH_SHORT).show(); return@DownloadListener; }) @@ -675,4 +674,11 @@ class RNCWebViewManagerImpl(private val newArch: Boolean = false) { fun setWebviewDebuggingEnabled(viewWrapper: RNCWebViewWrapper, enabled: Boolean) { RNCWebView.setWebContentsDebuggingEnabled(enabled) } + + fun setPaymentRequestEnabled(viewWrapper: RNCWebViewWrapper, enabled: Boolean) { + val view = viewWrapper.webView + if (WebViewFeature.isFeatureSupported(WebViewFeature.PAYMENT_REQUEST)) { + WebSettingsCompat.setPaymentRequestEnabled(view.settings, enabled) + } + } } diff --git a/android/src/main/java/com/reactnativecommunity/webview/events/SubResourceErrorEvent.kt b/android/src/main/java/com/reactnativecommunity/webview/events/SubResourceErrorEvent.kt new file mode 100644 index 000000000..24ac2f319 --- /dev/null +++ b/android/src/main/java/com/reactnativecommunity/webview/events/SubResourceErrorEvent.kt @@ -0,0 +1,25 @@ +package com.reactnativecommunity.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 an error in loading a subresource + */ +class SubResourceErrorEvent(viewId: Int, private val mEventData: WritableMap) : + Event(viewId) { + companion object { + const val EVENT_NAME = "topLoadingSubResourceError" + } + + 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/android/src/newarch/com/reactnativecommunity/webview/RNCWebViewManager.java b/android/src/newarch/com/reactnativecommunity/webview/RNCWebViewManager.java index ff2cba5f7..4709e2822 100644 --- a/android/src/newarch/com/reactnativecommunity/webview/RNCWebViewManager.java +++ b/android/src/newarch/com/reactnativecommunity/webview/RNCWebViewManager.java @@ -15,6 +15,7 @@ import com.facebook.react.viewmanagers.RNCWebViewManagerInterface; import com.facebook.react.views.scroll.ScrollEventType; import com.reactnativecommunity.webview.events.TopCustomMenuSelectionEvent; +import com.reactnativecommunity.webview.events.SubResourceErrorEvent; import com.reactnativecommunity.webview.events.TopHttpErrorEvent; import com.reactnativecommunity.webview.events.TopLoadingErrorEvent; import com.reactnativecommunity.webview.events.TopLoadingFinishEvent; @@ -226,7 +227,7 @@ public void setMenuItems(RNCWebViewWrapper view, @Nullable ReadableArray items) } @Override - @ReactProp(name = "suppressMenuItems ") + @ReactProp(name = "suppressMenuItems") public void setSuppressMenuItems(RNCWebViewWrapper view, @Nullable ReadableArray items) {} @Override @@ -331,6 +332,12 @@ public void setWebviewDebuggingEnabled(RNCWebViewWrapper view, boolean value) { mRNCWebViewManagerImpl.setWebviewDebuggingEnabled(view, value); } + @Override + @ReactProp(name = "paymentRequestEnabled") + public void setPaymentRequestEnabled(RNCWebViewWrapper view, boolean value) { + mRNCWebViewManagerImpl.setPaymentRequestEnabled(view, value); + } + /* iOS PROPS - no implemented here */ @Override public void setAllowingReadAccessToURL(RNCWebViewWrapper view, @Nullable String value) {} @@ -395,6 +402,9 @@ public void setPullToRefreshEnabled(RNCWebViewWrapper view, boolean value) {} @Override public void setRefreshControlLightMode(RNCWebViewWrapper view, boolean value) {} + @Override + public void setIndicatorStyle(RNCWebViewWrapper view, @Nullable String value) {} + @Override public void setScrollEnabled(RNCWebViewWrapper view, boolean value) {} @@ -515,6 +525,7 @@ public Map getExportedCustomDirectEventTypeConstants() { 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(SubResourceErrorEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoadingSubResourceError")); export.put(TopMessageEvent.EVENT_NAME, MapBuilder.of("registrationName", "onMessage")); // !Default events but adding them here explicitly for clarity diff --git a/android/src/oldarch/com/reactnativecommunity/webview/RNCWebViewManager.java b/android/src/oldarch/com/reactnativecommunity/webview/RNCWebViewManager.java index 78fba18a6..974337f41 100644 --- a/android/src/oldarch/com/reactnativecommunity/webview/RNCWebViewManager.java +++ b/android/src/oldarch/com/reactnativecommunity/webview/RNCWebViewManager.java @@ -11,6 +11,7 @@ import com.facebook.react.uimanager.annotations.ReactProp; import com.facebook.react.views.scroll.ScrollEventType; import com.reactnativecommunity.webview.events.TopCustomMenuSelectionEvent; +import com.reactnativecommunity.webview.events.SubResourceErrorEvent; import com.reactnativecommunity.webview.events.TopHttpErrorEvent; import com.reactnativecommunity.webview.events.TopLoadingErrorEvent; import com.reactnativecommunity.webview.events.TopLoadingFinishEvent; @@ -273,6 +274,11 @@ public void setUserAgent(RNCWebViewWrapper view, @Nullable String value) { mRNCWebViewManagerImpl.setUserAgent(view, value); } + @ReactProp(name = "paymentRequestEnabled") + public void setPaymentRequestEnabled(RNCWebViewWrapper view, boolean value) { + mRNCWebViewManagerImpl.setPaymentRequestEnabled(view, value); + } + @Override protected void addEventEmitters(@NonNull ThemedReactContext reactContext, RNCWebViewWrapper viewWrapper) { // Do not register default touch emitter and let WebView implementation handle touches @@ -289,6 +295,7 @@ public Map getExportedCustomDirectEventTypeConstants() { 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(SubResourceErrorEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoadingSubResourceError")); export.put(TopMessageEvent.EVENT_NAME, MapBuilder.of("registrationName", "onMessage")); // !Default events but adding them here explicitly for clarity diff --git a/apple/RNCWebView.h b/apple/RNCWebView.h index 659334e21..ad0b778d7 100644 --- a/apple/RNCWebView.h +++ b/apple/RNCWebView.h @@ -22,7 +22,7 @@ NS_ASSUME_NONNULL_BEGIN namespace facebook { namespace react { - bool operator==(const RNCWebViewMenuItemsStruct& a, const RNCWebViewMenuItemsStruct& b) + inline bool operator==(const RNCWebViewMenuItemsStruct& a, const RNCWebViewMenuItemsStruct& b) { return b.key == a.key && b.label == a.label; } diff --git a/apple/RNCWebView.mm b/apple/RNCWebView.mm index f9d080e3d..95c0fcc91 100644 --- a/apple/RNCWebView.mm +++ b/apple/RNCWebView.mm @@ -71,9 +71,9 @@ - (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); @@ -191,7 +191,7 @@ - (instancetype)initWithFrame:(CGRect)frame .selectedText = std::string([[dictionary valueForKey:@"selectedText"] UTF8String]), .key = std::string([[dictionary valueForKey:@"key"] UTF8String]), .label = std::string([[dictionary valueForKey:@"label"] UTF8String]) - + }; webViewEventEmitter->onCustomMenuSelection(data); } @@ -312,7 +312,7 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & 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 @@ -398,7 +398,7 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & for (const auto &menuItem: newViewProps.suppressMenuItems) { [suppressMenuItems addObject: RCTNSStringFromString(menuItem)]; } - + [_view setSuppressMenuItems:suppressMenuItems]; } if (oldViewProps.hasOnFileDownload != newViewProps.hasOnFileDownload) { @@ -410,10 +410,10 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & .downloadUrl = std::string([[dictionary valueForKey:@"downloadUrl"] UTF8String]) }; webViewEventEmitter->onFileDownload(data); - } + } }; } else { - _view.onFileDownload = nil; + _view.onFileDownload = nil; } } if (oldViewProps.hasOnOpenWindowEvent != newViewProps.hasOnOpenWindowEvent) { @@ -459,7 +459,16 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & } } #endif - + if (oldViewProps.indicatorStyle != newViewProps.indicatorStyle) { + if (newViewProps.indicatorStyle == RNCWebViewIndicatorStyle::Black) { + [_view setIndicatorStyle:@"black"]; + } else if (newViewProps.indicatorStyle == RNCWebViewIndicatorStyle::White) { + [_view setIndicatorStyle:@"white"]; + } else { + [_view setIndicatorStyle:@"default"]; + } + } + NSMutableDictionary* source = [[NSMutableDictionary alloc] init]; if (!newViewProps.newSource.uri.empty()) { [source setValue:RCTNSStringFromString(newViewProps.newSource.uri) forKey:@"uri"]; @@ -484,7 +493,7 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & [source setValue:RCTNSStringFromString(newViewProps.newSource.method) forKey:@"method"]; } [_view setSource:source]; - + [super updateProps:props oldProps:oldProps]; } diff --git a/apple/RNCWebViewImpl.h b/apple/RNCWebViewImpl.h index 6246c45ef..1f6bbfd69 100644 --- a/apple/RNCWebViewImpl.h +++ b/apple/RNCWebViewImpl.h @@ -102,6 +102,7 @@ shouldStartLoadForRequest:(NSMutableDictionary *)request @property (nonatomic, assign) BOOL allowsLinkPreview; @property (nonatomic, assign) BOOL showsHorizontalScrollIndicator; @property (nonatomic, assign) BOOL showsVerticalScrollIndicator; +@property (nonatomic, copy) NSString * _Nullable indicatorStyle; @property (nonatomic, assign) BOOL directionalLockEnabled; @property (nonatomic, assign) BOOL ignoreSilentHardwareSwitch; @property (nonatomic, copy) NSString * _Nullable allowingReadAccessToURL; diff --git a/apple/RNCWebViewImpl.m b/apple/RNCWebViewImpl.m index af8c1d347..7733e1b14 100644 --- a/apple/RNCWebViewImpl.m +++ b/apple/RNCWebViewImpl.m @@ -74,7 +74,7 @@ - (NSString *)stringFromAction:(SEL) action { @"toggleUnderline:": @"underline", @"_share:": @"share", }; - + return map[sel] ?: sel; } @@ -86,7 +86,7 @@ - (BOOL)canPerformAction:(SEL)action return NO; } } - + if (!self.menuItems) { return [super canPerformAction:action withSender:sender]; } @@ -276,7 +276,7 @@ - (void)startLongPress:(UILongPressGestureRecognizer *)pressSender 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"]]; @@ -535,6 +535,15 @@ - (void)didMoveToWindow _webView.scrollView.bounces = _pullToRefreshEnabled || _bounces; _webView.scrollView.showsHorizontalScrollIndicator = _showsHorizontalScrollIndicator; _webView.scrollView.showsVerticalScrollIndicator = _showsVerticalScrollIndicator; + + if ([_indicatorStyle isEqualToString:@"black"]) { + _webView.scrollView.indicatorStyle = UIScrollViewIndicatorStyleBlack; + } else if ([_indicatorStyle isEqualToString:@"white"]) { + _webView.scrollView.indicatorStyle = UIScrollViewIndicatorStyleWhite; + } else { + _webView.scrollView.indicatorStyle = UIScrollViewIndicatorStyleDefault; + } + _webView.scrollView.directionalLockEnabled = _directionalLockEnabled; #endif // !TARGET_OS_OSX _webView.allowsLinkPreview = _allowsLinkPreview; @@ -854,7 +863,7 @@ - (void)visitSource [self syncCookiesToWebView:^{ // Add observer to sync cookies from webview to sharedHTTPCookieStorage [webView.configuration.websiteDataStore.httpCookieStore addObserver:self]; - + // 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 @@ -1086,6 +1095,19 @@ - (void)setShowsVerticalScrollIndicator:(BOOL)showsVerticalScrollIndicator _showsVerticalScrollIndicator = showsVerticalScrollIndicator; _webView.scrollView.showsVerticalScrollIndicator = showsVerticalScrollIndicator; } + +- (void)setIndicatorStyle:(NSString *)indicatorStyle +{ + _indicatorStyle = indicatorStyle; + + if ([indicatorStyle isEqualToString:@"black"]) { + _webView.scrollView.indicatorStyle = UIScrollViewIndicatorStyleBlack; + } else if ([indicatorStyle isEqualToString:@"white"]) { + _webView.scrollView.indicatorStyle = UIScrollViewIndicatorStyleWhite; + } else { + _webView.scrollView.indicatorStyle = UIScrollViewIndicatorStyleDefault; + } +} #endif // !TARGET_OS_OSX - (void)postMessage:(NSString *)message @@ -1401,7 +1423,7 @@ - (void) webView:(WKWebView *)webView } /** - * Called when the web view’s content process is terminated. + * Called when the web view's content process is terminated. * @see https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455639-webviewwebcontentprocessdidtermi?language=objc */ - (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView diff --git a/apple/RNCWebViewManager.mm b/apple/RNCWebViewManager.mm index a0e0d0063..f8f375f23 100644 --- a/apple/RNCWebViewManager.mm +++ b/apple/RNCWebViewManager.mm @@ -167,6 +167,10 @@ - (RNCView *)view view.showsVerticalScrollIndicator = json == nil ? true : [RCTConvert BOOL: json]; } +RCT_CUSTOM_VIEW_PROPERTY(indicatorStyle, NSString, RNCWebViewImpl) { + view.indicatorStyle = [RCTConvert NSString: json]; +} + RCT_CUSTOM_VIEW_PROPERTY(keyboardDisplayRequiresUserAction, BOOL, RNCWebViewImpl) { view.keyboardDisplayRequiresUserAction = json == nil ? true : [RCTConvert BOOL: json]; } diff --git a/docs/Reference.md b/docs/Reference.md index 897cada86..6d0b99acc 100644 --- a/docs/Reference.md +++ b/docs/Reference.md @@ -53,6 +53,7 @@ This document lays out the current public properties and methods for the React N - [`contentInsetAdjustmentBehavior`](Reference.md#contentInsetAdjustmentBehavior) - [`contentMode`](Reference.md#contentMode) - [`dataDetectorTypes`](Reference.md#datadetectortypes) +- [`indicatorStyle`](Reference.md#indicatorStyle) - [`scrollEnabled`](Reference.md#scrollenabled) - [`nestedScrollEnabled`](Reference.md#nestedscrollenabled) - [`setBuiltInZoomControls`](Reference.md#setBuiltInZoomControls) @@ -93,6 +94,7 @@ This document lays out the current public properties and methods for the React N - [`lackPermissionToDownloadMessage`](Reference.md#lackPermissionToDownloadMessage) - [`allowsProtectedMedia`](Reference.md#allowsProtectedMedia) - [`webviewDebuggingEnabled`](Reference.md#webviewDebuggingEnabled) +- [`paymentRequestEnabled`](Reference.md#paymentRequestEnabled) ## Methods Index @@ -897,7 +899,7 @@ A floating-point number that determines how quickly the scroll view decelerates ### `domStorageEnabled`[⬆](#props-index) -Boolean value to control whether DOM Storage is enabled. Used only in Android. +Boolean value to control whether DOM Storage is enabled. Used only in Android. The default value is `true`. | Type | Required | Platform | | ---- | -------- | -------- | @@ -1026,8 +1028,8 @@ Boolean value that indicates whether HTML5 videos can play Picture in Picture. T > **NOTE** > -> In order to restrict playing video in picture in picture mode this props need to be set to `false` -. +> In order to restrict playing video in picture in picture mode this props need to be set to `false`. + | Type | Required | Platform | | ---- | -------- | -------- | | bool | No | iOS | @@ -1149,6 +1151,16 @@ Boolean value that determines whether scrolling is enabled in the `WebView`. The --- +### `indicatorStyle`[⬆](#props-index) + +The colorstyle of the scroll indicator. The default value is `default`. + +| Type | Required | Platform | +| ------ | -------- | -------- | +| string | No | iOS | + +--- + ### `nestedScrollEnabled`[⬆](#props-index) Boolean value that determines whether scrolling is possible in the `WebView` when used inside a `ScrollView` on Android. The default value is `false`. @@ -1718,6 +1730,15 @@ Default is `false`. Supported on iOS as of 16.4, previous versions always allow | ------- | -------- | -------- | | boolean | No | iOS & Android | +### `paymentRequestEnabled`[⬆](#props-index) + +Whether or not the webview has the Payment Request API enabled. Default is `false`. +This is needed for Google Pay to work within the WebView. + +| Type | Required | Platform | +| ------- | -------- | -------- | +| boolean | No | Android | + ## Methods ### `goForward()`[⬆](#methods-index) diff --git a/example/App.tsx b/example/App.tsx index b2dd6fafc..0bfc50cbe 100644 --- a/example/App.tsx +++ b/example/App.tsx @@ -21,10 +21,12 @@ import Messaging from './examples/Messaging'; import MultiMessaging from './examples/MultiMessaging'; import NativeWebpage from './examples/NativeWebpage'; import ApplePay from './examples/ApplePay'; +import GooglePay from './examples/GooglePay'; import CustomMenu from './examples/CustomMenu'; import OpenWindow from './examples/OpenWindow'; import SuppressMenuItems from './examples/Suppress'; import ClearData from './examples/ClearData'; +import SslError from './examples/SslError'; const TESTS = { Messaging: { @@ -123,6 +125,14 @@ const TESTS = { return ; }, }, + GooglePay: { + title: 'Google Pay ', + testId: 'GooglePay', + description: 'Test to open a Google Pay supported page', + render() { + return ; + }, + }, CustomMenu: { title: 'Custom Menu', testId: 'CustomMenu', @@ -147,6 +157,14 @@ const TESTS = { return ; }, }, + SslError: { + title: 'SslError', + testId: 'SslError', + description: 'SSL error test', + render() { + return ; + }, + }, }; interface Props {} @@ -250,6 +268,13 @@ export default class App extends Component { onPress={() => this._changeTest('ApplePay')} /> )} + {Platform.OS === 'android' && ( +