diff --git a/React/Base/RCTConvert.h b/React/Base/RCTConvert.h index b3ef27cdb4cedf..c6a737754d7e6a 100644 --- a/React/Base/RCTConvert.h +++ b/React/Base/RCTConvert.h @@ -9,6 +9,9 @@ #import #import +#if !TARGET_OS_TV +#import +#endif #import #import @@ -70,6 +73,7 @@ typedef NSURL RCTFileURL; + (UIReturnKeyType)UIReturnKeyType:(id)json; #if !TARGET_OS_TV + (UIDataDetectorTypes)UIDataDetectorTypes:(id)json; ++ (WKDataDetectorTypes)WKDataDetectorTypes:(id)json; #endif + (UIViewContentMode)UIViewContentMode:(id)json; diff --git a/React/Base/RCTConvert.m b/React/Base/RCTConvert.m index 6be303046efcc1..0d3a4aacf10493 100644 --- a/React/Base/RCTConvert.m +++ b/React/Base/RCTConvert.m @@ -363,6 +363,19 @@ + (NSLocale *)NSLocale:(id)json @"none": @(UIDataDetectorTypeNone), @"all": @(UIDataDetectorTypeAll), }), UIDataDetectorTypePhoneNumber, unsignedLongLongValue) + +RCT_MULTI_ENUM_CONVERTER(WKDataDetectorTypes, (@{ + @"phoneNumber": @(WKDataDetectorTypePhoneNumber), + @"link": @(WKDataDetectorTypeLink), + @"address": @(WKDataDetectorTypeAddress), + @"calendarEvent": @(WKDataDetectorTypeCalendarEvent), + @"trackingNumber": @(WKDataDetectorTypeTrackingNumber), + @"flightNumber": @(WKDataDetectorTypeFlightNumber), + @"lookupSuggestion": @(WKDataDetectorTypeLookupSuggestion), + @"none": @(WKDataDetectorTypeNone), + @"all": @(WKDataDetectorTypeAll), +}), WKDataDetectorTypePhoneNumber, unsignedLongLongValue) + #endif RCT_ENUM_CONVERTER(UIKeyboardAppearance, (@{ diff --git a/React/Views/RCTWebView.h b/React/Views/RCTWebView.h index 92e9c6bb7ea791..32e20c7d1e50bd 100644 --- a/React/Views/RCTWebView.h +++ b/React/Views/RCTWebView.h @@ -7,6 +7,8 @@ * of patent rights can be found in the PATENTS file in the same directory. */ +#import + #import @class RCTWebView; @@ -37,6 +39,9 @@ shouldStartLoadForRequest:(NSMutableDictionary *)request @property (nonatomic, assign) BOOL messagingEnabled; @property (nonatomic, copy) NSString *injectedJavaScript; @property (nonatomic, assign) BOOL scalesPageToFit; +@property (nonatomic, assign) BOOL allowsInlineMediaPlayback; +@property (nonatomic, assign) BOOL mediaPlaybackRequiresUserAction; +@property (nonatomic, assign) WKDataDetectorTypes dataDetectorTypes; - (void)goForward; - (void)goBack; diff --git a/React/Views/RCTWebView.m b/React/Views/RCTWebView.m index 596f87e9680330..fa88700a302e9d 100644 --- a/React/Views/RCTWebView.m +++ b/React/Views/RCTWebView.m @@ -9,7 +9,7 @@ #import "RCTWebView.h" -#import +#import #import "RCTAutoInsetsProtocol.h" #import "RCTConvert.h" @@ -19,11 +19,38 @@ #import "RCTView.h" #import "UIView+React.h" +NSString *const RCTJSScriptMessageName = @"react-message"; NSString *const RCTJSNavigationScheme = @"react-js-navigation"; - static NSString *const kPostMessageHost = @"postMessage"; +static NSString *const kCookieHeaderName = @"Cookie"; // https://www.ietf.org/rfc/rfc2109.txt + +/** + * A simple weak wrapper to provide a passthrough scriptDelegate. The issue is that the WKUserContentController + * retains a strong reference to it's delegate, which causes a retain cycle between this viewController and the + * WKUserContentController. + * For reference: https://stackoverflow.com/a/26383032 + */ +@interface RCTWeakScriptMessageDelegate : NSObject +@property (nonatomic, weak) id scriptDelegate; +- (instancetype)initWithDelegate:(id)scriptDelegate; +@end -@interface RCTWebView () +@implementation RCTWeakScriptMessageDelegate +- (instancetype)initWithDelegate:(id)scriptDelegate { + self = [super init]; + if (self) { + _scriptDelegate = scriptDelegate; + } + return self; +} + +- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { + [self.scriptDelegate userContentController:userContentController didReceiveScriptMessage:message]; +} +@end + + +@interface RCTWebView () @property (nonatomic, copy) RCTDirectEventBlock onLoadingStart; @property (nonatomic, copy) RCTDirectEventBlock onLoadingFinish; @@ -35,13 +62,61 @@ @interface RCTWebView () @implementation RCTWebView { - UIWebView *_webView; + WKWebView *_webView; NSString *_injectedJavaScript; } - (void)dealloc { - _webView.delegate = nil; + _webView.navigationDelegate = nil; + + if ([_webView.configuration.websiteDataStore respondsToSelector:@selector(httpCookieStore)]) { + [_webView.configuration.websiteDataStore.httpCookieStore removeObserver:self]; + } +} + +- (WKWebView *)buildWebViewWithDataDetectorTypes:(WKDataDetectorTypes)dataDetectorTypes + allowsInlineMediaPlayback:(BOOL)allowsInlineMediaPlayback + mediaPlaybackRequiresUserAction:(BOOL)mediaPlaybackRequiresUserAction +{ + WKWebViewConfiguration* config = [[WKWebViewConfiguration alloc] init]; + WKUserContentController* userContentController = [[WKUserContentController alloc] init]; + [userContentController addScriptMessageHandler:[[RCTWeakScriptMessageDelegate alloc] initWithDelegate:self] name:RCTJSScriptMessageName]; + + if (_scalesPageToFit) { + [userContentController addUserScript:[RCTWebView scalesPageToFitUserScript]]; + } + + if (_messagingEnabled) { + [userContentController addUserScript:[RCTWebView messageUserScript]]; + } + + if (_injectedJavaScript != nil) { + WKUserScript *messageUserScript = [[WKUserScript alloc] initWithSource:_injectedJavaScript injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES]; + [userContentController addUserScript:messageUserScript]; + } + + config.userContentController = userContentController; + + if ([_webView.configuration.websiteDataStore respondsToSelector:@selector(httpCookieStore)]) { + [_webView.configuration.websiteDataStore.httpCookieStore removeObserver:self]; + } + + if ([config.websiteDataStore respondsToSelector:@selector(httpCookieStore)]) { + [config.websiteDataStore.httpCookieStore addObserver:self]; + } + + config.allowsInlineMediaPlayback = allowsInlineMediaPlayback; + config.mediaPlaybackRequiresUserAction = mediaPlaybackRequiresUserAction; + config.dataDetectorTypes = dataDetectorTypes; + WKWebView *webView = [[WKWebView alloc] initWithFrame:self.bounds + configuration:config]; + webView.navigationDelegate = self; + webView.UIDelegate = self; + if ([webView.scrollView respondsToSelector:@selector(setContentInsetAdjustmentBehavior:)]) { + webView.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; + } + return webView; } - (instancetype)initWithFrame:(CGRect)frame @@ -50,13 +125,9 @@ - (instancetype)initWithFrame:(CGRect)frame super.backgroundColor = [UIColor clearColor]; _automaticallyAdjustContentInsets = YES; _contentInset = UIEdgeInsetsZero; - _webView = [[UIWebView alloc] initWithFrame:self.bounds]; - _webView.delegate = self; -#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */ - if ([_webView.scrollView respondsToSelector:@selector(setContentInsetAdjustmentBehavior:)]) { - _webView.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; - } -#endif + _webView = [self buildWebViewWithDataDetectorTypes:WKDataDetectorTypeNone + allowsInlineMediaPlayback:NO + mediaPlaybackRequiresUserAction:YES]; [self addSubview:_webView]; } return self; @@ -77,7 +148,7 @@ - (void)goBack - (void)reload { NSURLRequest *request = [RCTConvert NSURLRequest:self.source]; - if (request.URL && !_webView.request.URL.absoluteString.length) { + if (request.URL && !_webView.URL.absoluteString.length) { [_webView loadRequest:request]; } else { @@ -93,18 +164,22 @@ - (void)stopLoading - (void)postMessage:(NSString *)message { NSDictionary *eventInitDict = @{ - @"data": message, - }; + @"data": message, + }; NSString *source = [NSString - stringWithFormat:@"document.dispatchEvent(new MessageEvent('message', %@));", - RCTJSONStringify(eventInitDict, NULL) - ]; - [_webView stringByEvaluatingJavaScriptFromString:source]; + stringWithFormat:@"document.dispatchEvent(new MessageEvent('message', %@));", + RCTJSONStringify(eventInitDict, NULL) + ]; + [self injectJavaScript:source]; } - (void)injectJavaScript:(NSString *)script { - [_webView stringByEvaluatingJavaScriptFromString:script]; + [_webView evaluateJavaScript:script completionHandler:^(__unused id result, NSError *error) { + if (error) { + RCTLogError(@"Failed to evaluate JavaScript: \"%@\" with error: %@", script, error); + } + }]; } - (void)setSource:(NSDictionary *)source @@ -119,7 +194,13 @@ - (void)setSource:(NSDictionary *)source if (!baseURL) { baseURL = [NSURL URLWithString:@"about:blank"]; } - [_webView loadHTMLString:html baseURL:baseURL]; + if (@available(macOS 10.13, iOS 11.0, *)) { + [self syncCookiesForURL:baseURL completionHandler:^{ + [_webView loadHTMLString:html baseURL:baseURL]; + }]; + } else { + [_webView loadHTMLString:html baseURL:baseURL]; + } return; } @@ -128,7 +209,7 @@ - (void)setSource:(NSDictionary *)source // 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.request.URL]) { + if ([request.URL isEqual:_webView.URL]) { return; } if (!request.URL) { @@ -136,8 +217,52 @@ - (void)setSource:(NSDictionary *)source [_webView loadHTMLString:@"" baseURL:nil]; return; } - [_webView loadRequest:request]; + + if (@available(macOS 10.13, iOS 11.0, *)) { + [self syncCookiesForURL:request.URL completionHandler:^{ + [_webView loadRequest:request]; + }]; + } else { + NSURLRequest *requestWithCookies = [self attachCookiesToRequest:request]; + [_webView loadRequest:requestWithCookies]; + } + } +} + +/** + * Copies the cookies from NSHTTPCookiesStorage.sharedHTTPCookieStorage to WKHTTPCookieStore for the URL. + * + * iOS 11 and later. + */ +- (void)syncCookiesForURL:(NSURL*)url completionHandler:(void (^)())completionHandler NS_AVAILABLE_IOS(11_0) +{ + dispatch_group_t serviceGroup = dispatch_group_create(); + + NSArray *cookies = [NSHTTPCookieStorage.sharedHTTPCookieStorage cookiesForURL:url]; + for (NSHTTPCookie *cookie in cookies) { + dispatch_group_enter(serviceGroup); + [_webView.configuration.websiteDataStore.httpCookieStore setCookie:cookie completionHandler:^{ + dispatch_group_leave(serviceGroup); + }]; } + + dispatch_group_notify(serviceGroup, dispatch_get_main_queue(), ^{ + completionHandler(); + }); +} + +- (NSURLRequest*)attachCookiesToRequest:(NSURLRequest*)request +{ + if (!request.URL) { + return request; + } + + NSMutableURLRequest* result = [request mutableCopy]; + NSArray* cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:request.URL]; + NSDictionary* cookiesHeaders = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies]; + [result setValue:cookiesHeaders[kCookieHeaderName] forHTTPHeaderField:kCookieHeaderName]; + + return result; } - (void)layoutSubviews @@ -156,15 +281,35 @@ - (void)setContentInset:(UIEdgeInsets)contentInset - (void)setScalesPageToFit:(BOOL)scalesPageToFit { - if (_webView.scalesPageToFit != scalesPageToFit) { - _webView.scalesPageToFit = scalesPageToFit; - [_webView reload]; + if (_scalesPageToFit != scalesPageToFit) { + _scalesPageToFit = scalesPageToFit; + WKWebView *webView = [self buildWebViewWithDataDetectorTypes:self.dataDetectorTypes + allowsInlineMediaPlayback:self.allowsInlineMediaPlayback + mediaPlaybackRequiresUserAction:self.mediaPlaybackRequiresUserAction]; + [self updateWebView:webView]; + } +} + +- (void)setInjectedJavaScript:(NSString *)injectedJavaScript +{ + if (![_injectedJavaScript isEqualToString:injectedJavaScript]) { + _injectedJavaScript = injectedJavaScript; + WKWebView *webView = [self buildWebViewWithDataDetectorTypes:self.dataDetectorTypes + allowsInlineMediaPlayback:self.allowsInlineMediaPlayback + mediaPlaybackRequiresUserAction:self.mediaPlaybackRequiresUserAction]; + [self updateWebView:webView]; } } -- (BOOL)scalesPageToFit +- (void)setMessagingEnabled:(BOOL)messagingEnabled { - return _webView.scalesPageToFit; + if (_messagingEnabled != messagingEnabled) { + _messagingEnabled = messagingEnabled; + WKWebView *webView = [self buildWebViewWithDataDetectorTypes:self.dataDetectorTypes + allowsInlineMediaPlayback:self.allowsInlineMediaPlayback + mediaPlaybackRequiresUserAction:self.mediaPlaybackRequiresUserAction]; + [self updateWebView:webView]; + } } - (void)setBackgroundColor:(UIColor *)backgroundColor @@ -179,17 +324,49 @@ - (UIColor *)backgroundColor return _webView.backgroundColor; } -- (NSMutableDictionary *)baseEvent +- (void)setAllowsInlineMediaPlayback:(BOOL)allowsInlineMediaPlayback { - NSMutableDictionary *event = [[NSMutableDictionary alloc] initWithDictionary:@{ - @"url": _webView.request.URL.absoluteString ?: @"", - @"loading" : @(_webView.loading), - @"title": [_webView stringByEvaluatingJavaScriptFromString:@"document.title"], - @"canGoBack": @(_webView.canGoBack), - @"canGoForward" : @(_webView.canGoForward), - }]; + if (self.allowsInlineMediaPlayback != allowsInlineMediaPlayback) { + WKWebView *webView = [self buildWebViewWithDataDetectorTypes:self.dataDetectorTypes + allowsInlineMediaPlayback:allowsInlineMediaPlayback + mediaPlaybackRequiresUserAction:self.mediaPlaybackRequiresUserAction]; + [self updateWebView:webView]; + } +} - return event; +- (BOOL)allowsInlineMediaPlayback +{ + return _webView.configuration.allowsInlineMediaPlayback; +} + +- (void)setMediaPlaybackRequiresUserAction:(BOOL)mediaPlaybackRequiresUserAction +{ + if (self.mediaPlaybackRequiresUserAction != mediaPlaybackRequiresUserAction) { + WKWebView *webView = [self buildWebViewWithDataDetectorTypes:self.dataDetectorTypes + allowsInlineMediaPlayback:self.allowsInlineMediaPlayback + mediaPlaybackRequiresUserAction:mediaPlaybackRequiresUserAction]; + [self updateWebView:webView]; + } +} + +- (BOOL)mediaPlaybackRequiresUserAction +{ + return _webView.configuration.mediaPlaybackRequiresUserAction; +} + +- (void)setDataDetectorTypes:(WKDataDetectorTypes)dataDetectorTypes +{ + if (self.dataDetectorTypes != dataDetectorTypes) { + WKWebView *webView = [self buildWebViewWithDataDetectorTypes:dataDetectorTypes + allowsInlineMediaPlayback:self.allowsInlineMediaPlayback + mediaPlaybackRequiresUserAction:self.mediaPlaybackRequiresUserAction]; + [self updateWebView:webView]; + } +} + +- (WKDataDetectorTypes)dataDetectorTypes +{ + return _webView.configuration.dataDetectorTypes; } - (void)refreshContentInset @@ -199,76 +376,101 @@ - (void)refreshContentInset updateOffset:YES]; } -#pragma mark - UIWebViewDelegate methods +- (NSMutableDictionary *)baseEvent +{ + NSMutableDictionary *event = [[NSMutableDictionary alloc] initWithDictionary:@{ + @"url": _webView.URL.absoluteString ?: @"", + @"loading": @(_webView.loading), + @"title": _webView.title, + @"canGoBack": @(_webView.canGoBack), + @"canGoForward": @(_webView.canGoForward), + }]; + + return event; +} -- (BOOL)webView:(__unused UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request - navigationType:(UIWebViewNavigationType)navigationType +- (void)updateWebView:(WKWebView *)webView { - BOOL isJSNavigation = [request.URL.scheme isEqualToString:RCTJSNavigationScheme]; + if (webView != nil) { + [_webView removeFromSuperview]; + _webView.navigationDelegate = nil; + _webView.UIDelegate = nil; + _webView = webView; + [self addSubview:_webView]; + [self reload]; + } +} + +#pragma mark - WKNavigationDelegate methods + +- (void)webView:(__unused WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { + BOOL isJSNavigation = [navigationAction.request.URL.scheme isEqualToString:RCTJSNavigationScheme]; static NSDictionary *navigationTypes; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ navigationTypes = @{ - @(UIWebViewNavigationTypeLinkClicked): @"click", - @(UIWebViewNavigationTypeFormSubmitted): @"formsubmit", - @(UIWebViewNavigationTypeBackForward): @"backforward", - @(UIWebViewNavigationTypeReload): @"reload", - @(UIWebViewNavigationTypeFormResubmitted): @"formresubmit", - @(UIWebViewNavigationTypeOther): @"other", - }; + @(WKNavigationTypeLinkActivated): @"click", + @(WKNavigationTypeFormSubmitted): @"formsubmit", + @(WKNavigationTypeBackForward): @"backforward", + @(WKNavigationTypeReload): @"reload", + @(WKNavigationTypeFormResubmitted): @"formresubmit", + @(WKNavigationTypeOther): @"other", + }; }); // skip this for the JS Navigation handler if (!isJSNavigation && _onShouldStartLoadWithRequest) { NSMutableDictionary *event = [self baseEvent]; [event addEntriesFromDictionary: @{ - @"url": (request.URL).absoluteString, - @"navigationType": navigationTypes[@(navigationType)] - }]; + @"url": navigationAction.request.URL.absoluteString ?: @"", + @"navigationType": navigationTypes[@(navigationAction.navigationType)] + }]; if (![self.delegate webView:self shouldStartLoadForRequest:event withCallback:_onShouldStartLoadWithRequest]) { - return NO; + decisionHandler(WKNavigationActionPolicyCancel); + return; } } if (_onLoadingStart) { // We have this check to filter out iframe requests and whatnot - BOOL isTopFrame = [request.URL isEqual:request.mainDocumentURL]; + BOOL isTopFrame = [navigationAction.request.URL isEqual:navigationAction.request.mainDocumentURL]; if (isTopFrame) { NSMutableDictionary *event = [self baseEvent]; [event addEntriesFromDictionary: @{ - @"url": (request.URL).absoluteString, - @"navigationType": navigationTypes[@(navigationType)] - }]; + @"url": navigationAction.request.URL.absoluteString ?: @"", + @"navigationType": navigationTypes[@(navigationAction.navigationType)] + }]; _onLoadingStart(event); } } - if (isJSNavigation && [request.URL.host isEqualToString:kPostMessageHost]) { - NSString *data = request.URL.query; + if (isJSNavigation && [navigationAction.request.URL.host isEqualToString:kPostMessageHost]) { + NSString *data = navigationAction.request.URL.query; data = [data stringByReplacingOccurrencesOfString:@"+" withString:@" "]; data = [data stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; NSMutableDictionary *event = [self baseEvent]; [event addEntriesFromDictionary: @{ - @"data": data, - }]; + @"data": data, + }]; NSString *source = @"document.dispatchEvent(new MessageEvent('message:received'));"; - - [_webView stringByEvaluatingJavaScriptFromString:source]; - - _onMessage(event); + [_webView evaluateJavaScript:source completionHandler:^(__unused id result, NSError *error) { + if (error) { + RCTLogError(@"Failed to evaluate JavaScript: \"%@\" with error: %@", source, error); + } + _onMessage(event); + }]; } // JS Navigation handler - return !isJSNavigation; + decisionHandler(isJSNavigation ? WKNavigationActionPolicyCancel : WKNavigationActionPolicyAllow); } -- (void)webView:(__unused UIWebView *)webView didFailLoadWithError:(NSError *)error -{ +- (void)webView:(__unused WKWebView *)webView didFailNavigation:(__unused WKNavigation *)navigation withError:(NSError *)error { if (_onLoadingError) { if ([error.domain isEqualToString:NSURLErrorDomain] && error.code == NSURLErrorCancelled) { // NSURLErrorCancelled is reported when a page has a redirect OR if you load @@ -288,66 +490,166 @@ - (void)webView:(__unused UIWebView *)webView didFailLoadWithError:(NSError *)er NSMutableDictionary *event = [self baseEvent]; [event addEntriesFromDictionary:@{ - @"domain": error.domain, - @"code": @(error.code), - @"description": error.localizedDescription, - }]; + @"domain": error.domain, + @"code": @(error.code), + @"description": error.localizedDescription, + }]; _onLoadingError(event); } } -- (void)webViewDidFinishLoad:(UIWebView *)webView +- (void)webView:(__unused WKWebView *)webView didFinishNavigation:(__unused WKNavigation *)navigation { +#if RCT_DEV if (_messagingEnabled) { - #if RCT_DEV // See isNative in lodash NSString *testPostMessageNative = @"String(window.postMessage) === String(Object.hasOwnProperty).replace('hasOwnProperty', 'postMessage')"; - BOOL postMessageIsNative = [ - [webView stringByEvaluatingJavaScriptFromString:testPostMessageNative] - isEqualToString:@"true" - ]; - if (!postMessageIsNative) { - RCTLogError(@"Setting onMessage on a WebView overrides existing values of window.postMessage, but a previous value was defined"); - } - #endif - NSString *source = [NSString stringWithFormat: - @"(function() {" - "window.originalPostMessage = window.postMessage;" - - "var messageQueue = [];" - "var messagePending = false;" - - "function processQueue() {" - "if (!messageQueue.length || messagePending) return;" - "messagePending = true;" - "window.location = '%@://%@?' + encodeURIComponent(messageQueue.shift());" - "}" - - "window.postMessage = function(data) {" - "messageQueue.push(String(data));" - "processQueue();" - "};" - - "document.addEventListener('message:received', function(e) {" - "messagePending = false;" - "processQueue();" - "});" - "})();", RCTJSNavigationScheme, kPostMessageHost - ]; - [webView stringByEvaluatingJavaScriptFromString:source]; + [_webView evaluateJavaScript:testPostMessageNative completionHandler:^(id result, NSError *error) { + if (error) { + RCTLogError(@"Failed to evaluate JavaScript: \"%@\" with error: %@", testPostMessageNative, error); + } else { + NSString *resultString = [NSString stringWithFormat:@"%@", result]; + if (![resultString isEqualToString:@"true"]) { + RCTLogError(@"Setting onMessage on a WebView overrides existing values of window.postMessage, but a previous value was defined"); + } + } + }]; } - if (_injectedJavaScript != nil) { - NSString *jsEvaluationValue = [webView stringByEvaluatingJavaScriptFromString:_injectedJavaScript]; - - NSMutableDictionary *event = [self baseEvent]; - event[@"jsEvaluationValue"] = jsEvaluationValue; +#endif - _onLoadingFinish(event); - } // we only need the final 'finishLoad' call so only fire the event when we're actually done loading. - else if (_onLoadingFinish && !webView.loading && ![webView.request.URL.absoluteString isEqualToString:@"about:blank"]) { + if (_onLoadingFinish && !webView.loading && ![webView.URL.absoluteString isEqualToString:@"about:blank"]) { _onLoadingFinish([self baseEvent]); } } +#pragma mark - WKUIDelegate + +- (WKWebView *)webView:(WKWebView *)webView +createWebViewWithConfiguration:(__unused WKWebViewConfiguration *)configuration + forNavigationAction:(WKNavigationAction *)navigationAction + windowFeatures:(__unused WKWindowFeatures *)windowFeatures +{ + // Override the action if opening a new webView, signaled by the navigationAction having a nil + // or non-mainFrame targetFrame. + if (!navigationAction.targetFrame.isMainFrame) { + [webView loadRequest:navigationAction.request]; + } + + return nil; +} + +- (void)webView:(__unused WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(__unused WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler +{ + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:nil message:message preferredStyle:UIAlertControllerStyleAlert]; + [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"OK", nil) style:UIAlertActionStyleDefault handler:^(__unused UIAlertAction *action) { + completionHandler(); + }]]; + UIViewController *presentedController = RCTPresentedViewController(); + [presentedController presentViewController:alertController animated:YES completion:nil]; +} + +- (void)webView:(__unused WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(__unused WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler +{ + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" message:message preferredStyle:UIAlertControllerStyleAlert]; + [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", nil) style:UIAlertActionStyleDefault handler:^(__unused UIAlertAction *action) { + completionHandler(NO); + }]]; + [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"OK", nil) style:UIAlertActionStyleDefault handler:^(__unused UIAlertAction *action) { + completionHandler(YES); + }]]; + UIViewController *presentedController = RCTPresentedViewController(); + [presentedController presentViewController:alertController animated:YES completion:nil]; +} + +- (void)webView:(__unused WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(__unused WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable))completionHandler +{ + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" message:prompt preferredStyle:UIAlertControllerStyleAlert]; + [alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) { + textField.text = defaultText; + }]; + [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", nil) style:UIAlertActionStyleCancel handler:^(__unused UIAlertAction *action) { + completionHandler(nil); + }]]; + [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"OK", nil) style:UIAlertActionStyleDefault handler:^(__unused UIAlertAction *action) { + NSString *input = ((UITextField *)alertController.textFields.firstObject).text; + completionHandler(input); + }]]; + UIViewController *presentedController = RCTPresentedViewController(); + [presentedController presentViewController:alertController animated:YES completion:nil]; +} + +#pragma mark - WKScriptMessageHandler + +- (void)userContentController:(nonnull __unused WKUserContentController *)userContentController didReceiveScriptMessage:(nonnull WKScriptMessage *)message +{ + if(_onMessage) { + NSMutableDictionary *event = [self baseEvent]; + [event addEntriesFromDictionary:@{ + @"name" : message.name, + @"body" : message.body + }]; + _onMessage(event); + } +} + +#pragma mark - WKHTTPCookieStoreObserver + +- (void)cookiesDidChangeInCookieStore:(WKHTTPCookieStore *)cookieStore NS_AVAILABLE_IOS(11_0) +{ + [cookieStore getAllCookies:^(NSArray *cookies) { + for (NSHTTPCookie *cookie in cookies) { + [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie]; + } + }]; +} + +#pragma mark - static WKUserScripts + ++ (WKUserScript *) scalesPageToFitUserScript +{ + static WKUserScript *script; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSString *source = @"var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', 'width=device-width'); document.getElementsByTagName('head')[0].appendChild(meta);"; + script = [[WKUserScript alloc] initWithSource:source injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES]; + }); + return script; +} + ++ (WKUserScript *) messageUserScript +{ + static WKUserScript *script; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSString *source = [NSString stringWithFormat: + @"(function() {" + "window.originalPostMessage = window.postMessage;" + + "var messageQueue = [];" + "var messagePending = false;" + + "function processQueue() {" + "if (!messageQueue.length || messagePending) return;" + "messagePending = true;" + "window.location = '%@://%@?' + encodeURIComponent(messageQueue.shift());" + "}" + + "window.postMessage = function(data) {" + "messageQueue.push(String(data));" + "processQueue();" + "};" + + "document.addEventListener('message:received', function(e) {" + "messagePending = false;" + "processQueue();" + "});" + "})();", RCTJSNavigationScheme, kPostMessageHost + ]; + script = [[WKUserScript alloc] initWithSource:source injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES]; + }); + return script; +} + @end + diff --git a/React/Views/RCTWebViewManager.m b/React/Views/RCTWebViewManager.m index ec6192ef5672b1..8b6a38f104789b 100644 --- a/React/Views/RCTWebViewManager.m +++ b/React/Views/RCTWebViewManager.m @@ -47,9 +47,9 @@ - (UIView *)view RCT_EXPORT_VIEW_PROPERTY(onLoadingError, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onMessage, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onShouldStartLoadWithRequest, RCTDirectEventBlock) -RCT_REMAP_VIEW_PROPERTY(allowsInlineMediaPlayback, _webView.allowsInlineMediaPlayback, BOOL) -RCT_REMAP_VIEW_PROPERTY(mediaPlaybackRequiresUserAction, _webView.mediaPlaybackRequiresUserAction, BOOL) -RCT_REMAP_VIEW_PROPERTY(dataDetectorTypes, _webView.dataDetectorTypes, UIDataDetectorTypes) +RCT_EXPORT_VIEW_PROPERTY(allowsInlineMediaPlayback, BOOL) +RCT_EXPORT_VIEW_PROPERTY(mediaPlaybackRequiresUserAction, BOOL) +RCT_EXPORT_VIEW_PROPERTY(dataDetectorTypes, WKDataDetectorTypes) RCT_EXPORT_METHOD(goBack:(nonnull NSNumber *)reactTag) { @@ -65,8 +65,8 @@ - (UIView *)view RCT_EXPORT_METHOD(goForward:(nonnull NSNumber *)reactTag) { - [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { - id view = viewRegistry[reactTag]; + [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { + RCTWebView *view = viewRegistry[reactTag]; if (![view isKindOfClass:[RCTWebView class]]) { RCTLogError(@"Invalid view returned from registry, expecting RCTWebView, got: %@", view); } else {