From cdbfc19cd20a0d96c9cbd13fcb8a32fcde77943b Mon Sep 17 00:00:00 2001 From: SebiVPS <42858722+SebiVPS@users.noreply.github.com> Date: Mon, 15 Apr 2019 10:19:10 +0200 Subject: [PATCH] feat(iOS cookies): implement sharedCookiesEnabled prop for iOS RNCWKWebView (#175) We had the problem on iOS WebViews that local cookies (stored in local HTTPCookieStorage, set with [react-native-cookie](https://github.com/shimohq/react-native-cookie) ) were not added in loadRequests. On Android the local stored cookies were sent like expected. This kinda "hacky" solution is the only way we found, that works for us. The stack overview link is in the code below. If someone finds a better solution we would very much like to accept that. --- docs/Reference.md | 11 +++ ios/RNCWKWebView.h | 1 + ios/RNCWKWebView.m | 146 +++++++++++++++++++++++++++++--------- ios/RNCWKWebViewManager.m | 4 ++ src/WebViewTypes.ts | 8 +++ 5 files changed, 138 insertions(+), 32 deletions(-) diff --git a/docs/Reference.md b/docs/Reference.md index a7af6d4..e3ece1b 100644 --- a/docs/Reference.md +++ b/docs/Reference.md @@ -50,6 +50,7 @@ This document lays out the current public properties and methods for the React N - [`cacheEnabled`](Reference.md#cacheEnabled) - [`pagingEnabled`](Reference.md#pagingEnabled) - [`allowsLinkPreview`](Reference.md#allowsLinkPreview) +- [`sharedCookiesEnabled`](Reference.md#sharedCookiesEnabled) ## Methods Index @@ -833,6 +834,16 @@ A Boolean value that determines whether pressing on a link displays a preview of | ------- | -------- | -------- | | boolean | No | iOS | +--- + +### `sharedCookiesEnabled` + +Set `true` if shared cookies from `[NSHTTPCookieStorage sharedHTTPCookieStorage]` should used for every load request in the `RNCWKWebView`. The default value is `false`. + +| Type | Required | Platform | +| ------- | -------- | -------- | +| boolean | No | iOS | + ## Methods ### `extraNativeComponentConfig()` diff --git a/ios/RNCWKWebView.h b/ios/RNCWKWebView.h index 437d9bd..7cea86b 100644 --- a/ios/RNCWKWebView.h +++ b/ios/RNCWKWebView.h @@ -26,6 +26,7 @@ @property (nonatomic, assign) BOOL messagingEnabled; @property (nonatomic, copy) NSString *injectedJavaScript; @property (nonatomic, assign) BOOL scrollEnabled; +@property (nonatomic, assign) BOOL sharedCookiesEnabled; @property (nonatomic, assign) BOOL pagingEnabled; @property (nonatomic, assign) CGFloat decelerationRate; @property (nonatomic, assign) BOOL allowsInlineMediaPlayback; diff --git a/ios/RNCWKWebView.m b/ios/RNCWKWebView.m index 5307ede..2e5ad05 100644 --- a/ios/RNCWKWebView.m +++ b/ios/RNCWKWebView.m @@ -127,6 +127,68 @@ static NSURLCredential* clientAuthenticationCredential; wkWebViewConfig.mediaPlaybackRequiresUserAction = _mediaPlaybackRequiresUserAction; #endif + 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 + if (@available(iOS 11.0, *)) { + // Set Cookies in iOS 11 and above, initialize websiteDataStore before setting cookies + // See also https://forums.developer.apple.com/thread/97194 + // check if websiteDataStore has not been initialized before + if(!_incognito && !_cacheEnabled) { + wkWebViewConfig.websiteDataStore = [WKWebsiteDataStore nonPersistentDataStore]; + } + for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]) { + [wkWebViewConfig.websiteDataStore.httpCookieStore setCookie:cookie completionHandler:nil]; + } + } 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 + // the WebView and want to extends the current sharedCookiesEnabled option with an + // additional property. + // Generates JS: document.cookie = "key=; Expires=Thu, 01 Jan 1970 00:00:01 GMT;" + // for each cookie which is already available in the WebView context. + /* + [script appendString:@"(function () {\n"]; + [script appendString:@" var cookies = document.cookie.split('; ');\n"]; + [script appendString:@" for (var i = 0; i < cookies.length; i++) {\n"]; + [script appendString:@" if (cookies[i].indexOf('=') !== -1) {\n"]; + [script appendString:@" document.cookie = cookies[i].split('=')[0] + '=; Expires=Thu, 01 Jan 1970 00:00:01 GMT';\n"]; + [script appendString:@" }\n"]; + [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;" + // for each cookie which is available in the application context. + [script appendString:@"(function () {\n"]; + for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]) { + [script appendFormat:@"document.cookie = %@ + '=' + %@", + RCTJSONStringify(cookie.name, NULL), + RCTJSONStringify(cookie.value, NULL)]; + if (cookie.path) { + [script appendFormat:@" + '; Path=' + %@", RCTJSONStringify(cookie.path, NULL)]; + } + if (cookie.expiresDate) { + [script appendFormat:@" + '; Expires=' + new Date(%f).toUTCString()", + cookie.expiresDate.timeIntervalSince1970 * 1000 + ]; + } + [script appendString:@";\n"]; + } + [script appendString:@"})();\n"]; + + WKUserScript* cookieInScript = [[WKUserScript alloc] initWithSource:script + injectionTime:WKUserScriptInjectionTimeAtDocumentStart + forMainFrameOnly:YES]; + [wkWebViewConfig.userContentController addUserScript:cookieInScript]; + } + } + _webView = [[WKWebView alloc] initWithFrame:self.bounds configuration: wkWebViewConfig]; _webView.scrollView.delegate = self; _webView.UIDelegate = self; @@ -270,37 +332,36 @@ static NSURLCredential* clientAuthenticationCredential; - (void)visitSource { - // Check for a static html source first - NSString *html = [RCTConvert NSString:_source[@"html"]]; - if (html) { - NSURL *baseURL = [RCTConvert NSURL:_source[@"baseUrl"]]; - if (!baseURL) { - baseURL = [NSURL URLWithString:@"about:blank"]; + // Check for a static html source first + NSString *html = [RCTConvert NSString:_source[@"html"]]; + if (html) { + NSURL *baseURL = [RCTConvert NSURL:_source[@"baseUrl"]]; + if (!baseURL) { + baseURL = [NSURL URLWithString:@"about:blank"]; + } + [_webView loadHTMLString:html baseURL:baseURL]; + return; } - [_webView loadHTMLString:html baseURL:baseURL]; - return; - } - - NSURLRequest *request = [RCTConvert NSURLRequest:_source]; - // 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]) { - return; - } - if (!request.URL) { - // Clear the webview - [_webView loadHTMLString:@"" baseURL:nil]; - return; - } - if (request.URL.host) { - [_webView loadRequest:request]; - } - else { - [_webView loadFileURL:request.URL allowingReadAccessToURL:request.URL]; - } + NSURLRequest *request = [self requestForSource:_source]; + // 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]) { + return; + } + if (!request.URL) { + // Clear the webview + [_webView loadHTMLString:@"" baseURL:nil]; + return; + } + if (request.URL.host) { + [_webView loadRequest:request]; + } + else { + [_webView loadFileURL:request.URL allowingReadAccessToURL:request.URL]; + } } -(void)setHideKeyboardAccessoryView:(BOOL)hideKeyboardAccessoryView @@ -662,11 +723,11 @@ static NSURLCredential* clientAuthenticationCredential; * [_webView reload] doesn't reload the webpage. Therefore, we must * manually call [_webView loadRequest:request]. */ - NSURLRequest *request = [RCTConvert NSURLRequest:self.source]; + NSURLRequest *request = [self requestForSource:self.source]; + if (request.URL && !_webView.URL.absoluteString.length) { [_webView loadRequest:request]; - } - else { + } else { [_webView reload]; } } @@ -681,4 +742,25 @@ static NSURLCredential* clientAuthenticationCredential; _bounces = bounces; _webView.scrollView.bounces = bounces; } + +- (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. + if (_sharedCookiesEnabled) { + if (@available(iOS 11.0, *)) { + // see WKWebView initialization for added cookies + } else { + NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:request.URL]; + NSDictionary *cookieHeader = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies]; + NSMutableURLRequest *mutableRequest = [request mutableCopy]; + [mutableRequest setAllHTTPHeaderFields:cookieHeader]; + return mutableRequest; + } + } + return request; +} + @end diff --git a/ios/RNCWKWebViewManager.m b/ios/RNCWKWebViewManager.m index bc58bd2..79c4f00 100644 --- a/ios/RNCWKWebViewManager.m +++ b/ios/RNCWKWebViewManager.m @@ -81,6 +81,10 @@ RCT_CUSTOM_VIEW_PROPERTY(scrollEnabled, BOOL, RNCWKWebView) { view.scrollEnabled = json == nil ? true : [RCTConvert BOOL: json]; } +RCT_CUSTOM_VIEW_PROPERTY(sharedCookiesEnabled, BOOL, RNCWKWebView) { + view.sharedCookiesEnabled = json == nil ? false : [RCTConvert BOOL: json]; +} + RCT_CUSTOM_VIEW_PROPERTY(decelerationRate, CGFloat, RNCWKWebView) { view.decelerationRate = json == nil ? UIScrollViewDecelerationRateNormal : [RCTConvert CGFloat: json]; } diff --git a/src/WebViewTypes.ts b/src/WebViewTypes.ts index 49bab67..5b16b0c 100644 --- a/src/WebViewTypes.ts +++ b/src/WebViewTypes.ts @@ -387,12 +387,20 @@ export interface IOSWebViewProps extends WebViewSharedProps { */ allowsLinkPreview?: boolean; + /** + * Set true if shared cookies from HTTPCookieStorage should used for every load request in the + * `RNCWKWebView`. The default value is `false`. + * @platform ios + */ + sharedCookiesEnabled?: boolean; + /** * A Boolean value that determines whether scrolling is disabled in a particular direction. * The default value is `true`. * @platform ios */ directionalLockEnabled?: boolean; + } export interface AndroidWebViewProps extends WebViewSharedProps {