Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ If set to true, links with `target="_blank"` or `window.open` will be opened in

Set `sendCookies` to true to copy cookies from `sharedHTTPCookieStorage` when calling loadRequest. This emulates the behavior of react-native's `WebView` component. You can set cookies using `react-native-cookies` Default is false.

- **useWKCookieStore**

Set `useWKCookieStore` to true to use the webView's `WKHTTPCookieStorage`. All Cookies from `sharedHTTPCookieStorage` will be copied to it.

- **source={{file: '', allowingReadAccessToURL: '' }}**

This allows WKWebView loads a local HTML file. Please note the underlying API is only introduced in iOS 9+. So in iOS 8, it will simple ignores these two properties.
Expand Down
7 changes: 6 additions & 1 deletion WKWebView.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,10 @@ class WKWebView extends React.Component {
* Set this to true to emulate behavior of WebView component.
*/
sendCookies: PropTypes.bool,
/**
* Initializes the webView's WKHTTPCookieStorage and copies all cookies from sharedHTTPCookieStorage
*/
useWKCookieStore: PropTypes.bool,
/**
* If set to true, target="_blank" or window.open will be opened in WebView, instead
* of new window. Default is false to be backward compatible.
Expand Down Expand Up @@ -316,7 +320,8 @@ class WKWebView extends React.Component {
if (this.props.source && typeof this.props.source === 'object') {
source = Object.assign({}, this.props.source, {
sendCookies: this.props.sendCookies,
customUserAgent: this.props.customUserAgent || this.props.userAgent
customUserAgent: this.props.customUserAgent || this.props.userAgent,
useWKCookieStore: this.props.useWKCookieStore
});

if (this.props.html) {
Expand Down
156 changes: 113 additions & 43 deletions ios/RCTWKWebView/RCTWKWebView.m
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ @interface RCTWKWebView () <WKNavigationDelegate, RCTAutoInsetsProtocol, WKScrip
@property (nonatomic, copy) RCTDirectEventBlock onScroll;
@property (nonatomic, copy) RCTDirectEventBlock onNavigationResponse;
@property (assign) BOOL sendCookies;
@property (assign) BOOL useWKCookieStore;
@property (nonatomic, strong) WKUserScript *atStartScript;
@property (nonatomic, strong) WKUserScript *atEndScript;

Expand Down Expand Up @@ -63,18 +64,18 @@ - (instancetype)initWithProcessPool:(WKProcessPool *)processPool
super.backgroundColor = [UIColor clearColor];
_automaticallyAdjustContentInsets = YES;
_contentInset = UIEdgeInsetsZero;

WKWebViewConfiguration* config = [[WKWebViewConfiguration alloc] init];
config.processPool = processPool;
WKUserContentController* userController = [[WKUserContentController alloc]init];
[userController addScriptMessageHandler:[[WeakScriptMessageDelegate alloc] initWithDelegate:self] name:@"reactNative"];
config.userContentController = userController;

_webView = [[WKWebView alloc] initWithFrame:self.bounds configuration:config];
_webView.UIDelegate = self;
_webView.navigationDelegate = self;
_webView.scrollView.delegate = self;

#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */
// `contentInsetAdjustmentBehavior` is only available since iOS 11.
// We set the default behavior to "never" so that iOS
Expand Down Expand Up @@ -163,7 +164,7 @@ - (void)loadRequest:(NSURLRequest *)request
request = mutableRequest;
}
}

[_webView loadRequest:request];
}

Expand All @@ -179,29 +180,29 @@ -(void)setHideKeyboardAccessoryView:(BOOL)hideKeyboardAccessoryView
if (!hideKeyboardAccessoryView) {
return;
}

UIView* subview;
for (UIView* view in _webView.scrollView.subviews) {
if([[view.class description] hasPrefix:@"WKContent"])
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);
}

Expand All @@ -212,7 +213,7 @@ -(void)setKeyboardDisplayRequiresUserAction:(BOOL)keyboardDisplayRequiresUserAct
if (!keyboardDisplayRequiresUserAction) {
Class class = NSClassFromString(@"WKContentView");
NSOperatingSystemVersion iOS_11_3_0 = (NSOperatingSystemVersion){11, 3, 0};

if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion: iOS_11_3_0]) {
SEL selector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:changingActivityState:userObject:");
Method method = class_getInstanceMethod(class, selector);
Expand Down Expand Up @@ -306,28 +307,88 @@ - (void)stopLoading
[_webView stopLoading];
}

- (NSString *) cookieDescription:(NSHTTPCookie *)cookie {

NSMutableString *cDesc = [[NSMutableString alloc] init];
[cDesc appendFormat:@"%@=%@;",
[[cookie name] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding],
[[cookie value] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
if ([cookie.domain length] > 0)
[cDesc appendFormat:@"domain=%@;", [cookie domain]];
if ([cookie.path length] > 0)
[cDesc appendFormat:@"path=%@;", [cookie path]];
if (cookie.expiresDate != nil)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its missing HttpOnly, secure and sessionOnly property. Is this something you would be able to add?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for the HttpOnly: I can do that,
for the sessionOnly there ist no such thing, If there is no expire date than it should be a session cookie, otherwise it will be stored until the expire date

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://developer.apple.com/documentation/foundation/nshttpcookie/1392991-sessiononly?language=objc

I was referencing that boolean to indicate its a session only cookie

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And Thanks for fixing this issue!

[cDesc appendFormat:@"expiresDate=%@;", [cookie expiresDate]];
if (cookie.HTTPOnly == YES)
[cDesc appendString:@"HttpOnly;"];
if (cookie.secure == YES)
[cDesc appendString:@"Secure;"];


return cDesc;
}

- (void) copyCookies {

NSHTTPCookieStorage* storage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
NSArray* array = [storage cookies];


if (@available(ios 11,*)) {

// The webView websiteDataStore only gets initialized, when needed. Setting cookies on the dataStore's
// httpCookieStore doesn't seem to initialize it. That's why fetchDataRecordsOfTypes is called.
// All the cookies of the sharedHttpCookieStorage, which is used in react-native-cookie,
// are copied to the webSiteDataStore's httpCookieStore.
// https://bugs.webkit.org/show_bug.cgi?id=185483
[_webView.configuration.websiteDataStore fetchDataRecordsOfTypes:[NSSet<NSString *> setWithObject:WKWebsiteDataTypeCookies] completionHandler:^(NSArray<WKWebsiteDataRecord *> *records) {
for (NSHTTPCookie* cookie in array) {
[_webView.configuration.websiteDataStore.httpCookieStore setCookie:cookie completionHandler:nil];
}
}];
} else {
// Create WKUserScript for each cookie
// Cookies are injected with Javascript AtDocumentStart
for (NSHTTPCookie* cookie in array){
NSString* cookieSource = [NSString stringWithFormat:@"document.cookie = '%@'", [self cookieDescription:cookie]];
WKUserScript* cookieScript = [[WKUserScript alloc]
initWithSource:cookieSource
injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];


[_webView.configuration.userContentController addUserScript:cookieScript];
}
}
}

- (void)setSource:(NSDictionary *)source
{
if (![_source isEqualToDictionary:source]) {
_source = [source copy];
_sendCookies = [source[@"sendCookies"] boolValue];
_useWKCookieStore = [source[@"useWKCookieStore"] boolValue];

if (_useWKCookieStore) {
[self copyCookies];
}

if ([source[@"customUserAgent"] length] != 0 && [_webView respondsToSelector:@selector(setCustomUserAgent:)]) {
[_webView setCustomUserAgent:source[@"customUserAgent"]];
}

// Allow loading local files:
// <WKWebView source={{ file: RNFS.MainBundlePath + '/data/index.html', allowingReadAccessToURL: RNFS.MainBundlePath }} />
// Only works for iOS 9+. So iOS 8 will simply ignore those two values
NSString *file = [RCTConvert NSString:source[@"file"]];
NSString *allowingReadAccessToURL = [RCTConvert NSString:source[@"allowingReadAccessToURL"]];

if (file && [_webView respondsToSelector:@selector(loadFileURL:allowingReadAccessToURL:)]) {
NSURL *fileURL = [RCTConvert NSURL:file];
NSURL *baseURL = [RCTConvert NSURL:allowingReadAccessToURL];
[_webView loadFileURL:fileURL allowingReadAccessToURL:baseURL];
return;
}

// Check for a static html source first
NSString *html = [RCTConvert NSString:source[@"html"]];
if (html) {
Expand All @@ -338,7 +399,7 @@ - (void)setSource:(NSDictionary *)source
[_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
Expand Down Expand Up @@ -391,7 +452,7 @@ - (UIColor *)backgroundColor
@"canGoBack": @(_webView.canGoBack),
@"canGoForward" : @(_webView.canGoForward),
}];

return event;
}

Expand Down Expand Up @@ -455,6 +516,30 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView

#pragma mark - WKNavigationDelegate methods

- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there are duplicate methods with this same name, can you please fix this? This is breaking my build with compilation issue

if (_sendCookies) {
NSHTTPURLResponse *response = (NSHTTPURLResponse *)navigationResponse.response;
NSArray *cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:[response allHeaderFields] forURL:response.URL];
for (NSHTTPCookie *cookie in cookies) {
[[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
}
}

if (_onNavigationResponse) {
NSDictionary *headers = ((NSHTTPURLResponse *)navigationResponse.response).allHeaderFields;
NSInteger statusCode = ((NSHTTPURLResponse *)navigationResponse.response).statusCode;
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
[event addEntriesFromDictionary:@{
@"headers": headers,
@"status": [NSHTTPURLResponse localizedStringForStatusCode:statusCode],
@"statusCode": @(statusCode),
}];
_onNavigationResponse(event);
}

decisionHandler(WKNavigationResponsePolicyAllow);
}

#if DEBUG
- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler {
NSURLCredential * credential = [[NSURLCredential alloc] initWithTrust:[challenge protectionSpace].serverTrust];
Expand All @@ -468,9 +553,9 @@ - (void)webView:(__unused WKWebView *)webView decidePolicyForNavigationAction:(W
NSURLRequest *request = navigationAction.request;
NSURL* url = request.URL;
NSString* scheme = url.scheme;

BOOL isJSNavigation = [scheme isEqualToString:RCTJSNavigationScheme];

// handle mailto and tel schemes
if ([scheme isEqualToString:@"mailto"] || [scheme isEqualToString:@"tel"]) {
if ([app canOpenURL:url]) {
Expand All @@ -479,7 +564,7 @@ - (void)webView:(__unused WKWebView *)webView decidePolicyForNavigationAction:(W
return;
}
}

// skip this for the JS Navigation handler
if (!isJSNavigation && _onShouldStartLoadWithRequest) {
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
Expand All @@ -493,7 +578,7 @@ - (void)webView:(__unused WKWebView *)webView decidePolicyForNavigationAction:(W
return decisionHandler(WKNavigationActionPolicyCancel);
}
}

if (_onLoadingStart) {
// We have this check to filter out iframe requests and whatnot
BOOL isTopFrame = [url isEqual:request.mainDocumentURL];
Expand All @@ -506,7 +591,7 @@ - (void)webView:(__unused WKWebView *)webView decidePolicyForNavigationAction:(W
_onLoadingStart(event);
}
}

if (isJSNavigation) {
decisionHandler(WKNavigationActionPolicyCancel);
}
Expand All @@ -525,7 +610,7 @@ - (void)webView:(__unused WKWebView *)webView didFailProvisionalNavigation:(__un
// http://stackoverflow.com/questions/1024748/how-do-i-fix-nsurlerrordomain-error-999-in-iphone-3-0-os
return;
}

NSMutableDictionary<NSString *, id> *event = [self baseEvent];
[event addEntriesFromDictionary:@{
@"domain": error.domain,
Expand All @@ -548,7 +633,7 @@ - (void)webView:(WKWebView *)webView didFinishNavigation:(__unused WKNavigation

- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler {
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:message message:nil preferredStyle:UIAlertControllerStyleAlert];

[alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
completionHandler();
}]];
Expand All @@ -557,7 +642,7 @@ - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSStrin
}

- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler {

// TODO We have to think message to confirm "YES"
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:message message:nil preferredStyle:UIAlertControllerStyleAlert];
[alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"OK", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
Expand All @@ -571,17 +656,17 @@ - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSStr
}

- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *))completionHandler {

UIAlertController *alertController = [UIAlertController alertControllerWithTitle:prompt message:nil preferredStyle:UIAlertControllerStyleAlert];
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
textField.text = defaultText;
}];

[alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"OK", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
NSString *input = ((UITextField *)alertController.textFields.firstObject).text;
completionHandler(input);
}]];

[alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
completionHandler(nil);
}]];
Expand Down Expand Up @@ -609,20 +694,5 @@ - (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView
RCTLogWarn(@"Webview Process Terminated");
}

- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the quick response. Appreciate it

if (_onNavigationResponse) {
NSDictionary *headers = ((NSHTTPURLResponse *)navigationResponse.response).allHeaderFields;
NSInteger statusCode = ((NSHTTPURLResponse *)navigationResponse.response).statusCode;
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
[event addEntriesFromDictionary:@{
@"headers": headers,
@"status": [NSHTTPURLResponse localizedStringForStatusCode:statusCode],
@"statusCode": @(statusCode),
}];
_onNavigationResponse(event);
}

decisionHandler(WKNavigationResponsePolicyAllow);
}

@end