From 418d08f49bcd2ed6f7481099f54b2c55f3cfde6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Chmiela?= Date: Tue, 7 May 2019 22:32:30 +0200 Subject: [PATCH 1/4] [android] Update react-native-webview to 5.8.1 --- .../api/components/webview/RNCWebViewManager.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) 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 e6091178a3d5b..fa21932217842 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 @@ -319,6 +319,11 @@ public void setThirdPartyCookiesEnabled(WebView view, boolean 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); @@ -369,12 +374,8 @@ public void setSource(WebView view, @Nullable ReadableMap source) { if (source != null) { if (source.hasKey("html")) { String html = source.getString("html"); - if (source.hasKey("baseUrl")) { - view.loadDataWithBaseURL( - source.getString("baseUrl"), html, HTML_MIME_TYPE, HTML_ENCODING, null); - } else { - view.loadData(html, HTML_MIME_TYPE + "; charset=" + HTML_ENCODING, null); - } + String baseUrl = source.hasKey("baseUrl") ? source.getString("baseUrl") : ""; + view.loadDataWithBaseURL(baseUrl, html, HTML_MIME_TYPE, HTML_ENCODING, null); return; } if (source.hasKey("uri")) { From 25a7199d4250ab0492a5ade8033b422348c0f794 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Chmiela?= Date: Tue, 7 May 2019 22:46:33 +0200 Subject: [PATCH 2/4] [ios] Upgrade react-native-webview to 5.8.1 --- .../Api/Components/WebView/RNCWKWebView.h | 2 + .../Api/Components/WebView/RNCWKWebView.m | 239 +++++++++++++++--- .../Components/WebView/RNCWKWebViewManager.m | 8 + 3 files changed, 219 insertions(+), 30 deletions(-) diff --git a/ios/Exponent/Versioned/Core/Api/Components/WebView/RNCWKWebView.h b/ios/Exponent/Versioned/Core/Api/Components/WebView/RNCWKWebView.h index 806c154bdddcd..fa427212d575a 100644 --- a/ios/Exponent/Versioned/Core/Api/Components/WebView/RNCWKWebView.h +++ b/ios/Exponent/Versioned/Core/Api/Components/WebView/RNCWKWebView.h @@ -28,6 +28,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; @@ -38,6 +39,7 @@ #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; diff --git a/ios/Exponent/Versioned/Core/Api/Components/WebView/RNCWKWebView.m b/ios/Exponent/Versioned/Core/Api/Components/WebView/RNCWKWebView.m index 3154f61721e7f..13568b7d12b4c 100644 --- a/ios/Exponent/Versioned/Core/Api/Components/WebView/RNCWKWebView.m +++ b/ios/Exponent/Versioned/Core/Api/Components/WebView/RNCWKWebView.m @@ -41,6 +41,13 @@ @implementation RNCWKWebView { UIColor * _savedBackgroundColor; BOOL _savedHideKeyboardAccessoryView; + BOOL _savedKeyboardDisplayRequiresUserAction; + + // Workaround for StatusBar appearance bug for iOS 12 + // https://github.com/react-native-community/react-native-webview/issues/62 + BOOL _isFullScreenVideoOpen; + UIStatusBarStyle _savedStatusBarStyle; + BOOL _savedStatusBarHidden; } - (instancetype)initWithFrame:(CGRect)frame @@ -54,11 +61,14 @@ - (instancetype)initWithFrame:(CGRect)frame _directionalLockEnabled = YES; _automaticallyAdjustContentInsets = YES; _contentInset = UIEdgeInsetsZero; + _savedKeyboardDisplayRequiresUserAction = YES; + _savedStatusBarStyle = RCTSharedApplication().statusBarStyle; + _savedStatusBarHidden = RCTSharedApplication().statusBarHidden; } - // Workaround for a keyboard dismissal bug present in iOS 12 - // https://openradar.appspot.com/radar?id=5018321736957952 if (@available(iOS 12.0, *)) { + // Workaround for a keyboard dismissal bug present in iOS 12 + // https://openradar.appspot.com/radar?id=5018321736957952 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide) @@ -67,8 +77,12 @@ - (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-community/react-native-webview/issues/62 + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(toggleFullScreenVideoStatusBars) name:@"_MRMediaRemotePlayerSupportedCommandsDidChangeNotification" object:nil]; } - + return self; } @@ -127,6 +141,68 @@ - (void)didMoveToWindow 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; @@ -152,6 +228,7 @@ - (void)didMoveToWindow [self addSubview:_webView]; [self setHideKeyboardAccessoryView: _savedHideKeyboardAccessoryView]; + [self setKeyboardDisplayRequiresUserAction: _savedKeyboardDisplayRequiresUserAction]; [self visitSource]; } } @@ -176,6 +253,24 @@ - (void)removeFromSuperview [super removeFromSuperview]; } +-(void)toggleFullScreenVideoStatusBars +{ +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + if (!_isFullScreenVideoOpen) { + _isFullScreenVideoOpen = YES; + RCTUnsafeExecuteOnMainQueueSync(^{ + [RCTSharedApplication() setStatusBarStyle:UIStatusBarStyleLightContent animated:YES]; + }); + } else { + _isFullScreenVideoOpen = NO; + RCTUnsafeExecuteOnMainQueueSync(^{ + [RCTSharedApplication() setStatusBarHidden:_savedStatusBarHidden animated:YES]; + [RCTSharedApplication() setStatusBarStyle:_savedStatusBarStyle animated:YES]; + }); + } +#pragma clang diagnostic pop +} + -(void)keyboardWillHide { keyboardTimer = [NSTimer scheduledTimerWithTimeInterval:0 target:self selector:@selector(keyboardDisplacementFix) userInfo:nil repeats:false]; @@ -270,31 +365,94 @@ - (void)refreshContentInset - (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; - } - [_webView loadRequest:request]; + 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)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}; + + Method method; + IMP override; + + if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion: iOS_12_2_0]) { + // iOS 12.2.0 - Future + SEL selector = sel_getUid("_elementDidFocus:userIsInteracting:blurPreviousNode:changingActivityState:userObject:"); + method = class_getInstanceMethod(class, selector); + IMP original = method_getImplementation(method); + override = imp_implementationWithBlock(^void(id me, void* arg0, BOOL arg1, BOOL arg2, BOOL arg3, id arg4) { + ((void (*)(id, SEL, void*, BOOL, BOOL, BOOL, id))original)(me, selector, arg0, TRUE, arg2, arg3, arg4); + }); + } + else if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion: iOS_11_3_0]) { + // iOS 11.3.0 - 12.2.0 + SEL selector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:changingActivityState:userObject:"); + method = class_getInstanceMethod(class, selector); + IMP original = method_getImplementation(method); + override = imp_implementationWithBlock(^void(id me, void* arg0, BOOL arg1, BOOL arg2, BOOL arg3, id arg4) { + ((void (*)(id, SEL, void*, BOOL, BOOL, BOOL, id))original)(me, selector, arg0, TRUE, arg2, arg3, arg4); + }); + } else { + // iOS 9.0 - 11.3.0 + SEL selector = sel_getUid("_startAssistingNode:userIsInteracting:blurPreviousNode:userObject:"); + method = class_getInstanceMethod(class, selector); + IMP original = method_getImplementation(method); + override = imp_implementationWithBlock(^void(id me, void* arg0, BOOL arg1, BOOL arg2, id arg3) { + ((void (*)(id, SEL, void*, BOOL, BOOL, id))original)(me, selector, arg0, TRUE, arg2, arg3); + }); + } + + method_setImplementation(method, override); } -(void)setHideKeyboardAccessoryView:(BOOL)hideKeyboardAccessoryView @@ -393,7 +551,7 @@ - (void)layoutSubviews { NSDictionary *event = @{ @"url": _webView.URL.absoluteString ?: @"", - @"title": _webView.title, + @"title": _webView.title ?: @"", @"loading" : @(_webView.loading), @"canGoBack": @(_webView.canGoBack), @"canGoForward" : @(_webView.canGoForward) @@ -656,11 +814,11 @@ - (void)reload * [_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]; } } @@ -675,4 +833,25 @@ - (void)setBounces:(BOOL)bounces _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/Exponent/Versioned/Core/Api/Components/WebView/RNCWKWebViewManager.m b/ios/Exponent/Versioned/Core/Api/Components/WebView/RNCWKWebViewManager.m index 5963f33147571..a43b5ec97c86b 100644 --- a/ios/Exponent/Versioned/Core/Api/Components/WebView/RNCWKWebViewManager.m +++ b/ios/Exponent/Versioned/Core/Api/Components/WebView/RNCWKWebViewManager.m @@ -95,6 +95,10 @@ - (UIView *)view 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]; } @@ -111,6 +115,10 @@ - (UIView *)view view.showsVerticalScrollIndicator = json == nil ? true : [RCTConvert BOOL: json]; } +RCT_CUSTOM_VIEW_PROPERTY(keyboardDisplayRequiresUserAction, BOOL, RNCWKWebView) { + 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) { From f99539973f9b9482f59fa26f2999137debb61868 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Chmiela?= Date: Tue, 7 May 2019 22:49:01 +0200 Subject: [PATCH 3/4] [expo] Upgrade react-native-webview to 5.8.1 --- packages/expo/bundledNativeModules.json | 4 ++-- packages/expo/package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/expo/bundledNativeModules.json b/packages/expo/bundledNativeModules.json index b8b881c612b94..009ffcb1fedee 100644 --- a/packages/expo/bundledNativeModules.json +++ b/packages/expo/bundledNativeModules.json @@ -58,7 +58,7 @@ "react-native-screens": "1.0.0-alpha.22", "react-native-svg": "9.3.6", "react-native-view-shot": "2.5.0", - "react-native-webview": "5.4.6", + "react-native-webview": "~5.8.1", "unimodules-barcode-scanner-interface": "~2.0.0-rc.0", "unimodules-camera-interface": "~2.0.0-rc.0", "unimodules-constants-interface": "~2.0.0-rc.0", @@ -69,4 +69,4 @@ "unimodules-permissions-interface": "~2.0.0-rc.0", "unimodules-sensors-interface": "~2.0.0-rc.0", "unimodules-task-manager-interface": "~2.0.0-rc.0" -} \ No newline at end of file +} diff --git a/packages/expo/package.json b/packages/expo/package.json index 70384de6faa83..443654d307483 100644 --- a/packages/expo/package.json +++ b/packages/expo/package.json @@ -131,7 +131,7 @@ "react-native-screens": "1.0.0-alpha.22", "react-native-svg": "9.3.6", "react-native-view-shot": "2.5.0", - "react-native-webview": "5.4.6", + "react-native-webview": "5.8.1", "serialize-error": "^2.1.0", "unimodules-barcode-scanner-interface": "^2.0.0-rc.0", "unimodules-camera-interface": "^2.0.0-rc.0", diff --git a/yarn.lock b/yarn.lock index 1362138d4b23b..e6efbe85471ad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13579,10 +13579,10 @@ react-native-web@^0.11.0: prop-types "^15.6.0" react-timer-mixin "^0.13.4" -react-native-webview@5.4.6: - version "5.4.6" - resolved "https://registry.yarnpkg.com/react-native-webview/-/react-native-webview-5.4.6.tgz#18f33d52ae97cdb2a2eda5483f7d4c010134cce3" - integrity sha512-xh0kv/fNT9Uh6gJSBE8qutktb8YvgwWGMA2DJCKfkl7Px1ZSE/a/sT1wyD0G8rhFH5wDv3jmhazNeIt8xTeDtA== +react-native-webview@5.8.1: + version "5.8.1" + resolved "https://registry.yarnpkg.com/react-native-webview/-/react-native-webview-5.8.1.tgz#6f5a83dec55bbc02700155b1a16a668870f14de0" + integrity sha512-b6pSvmjoiWtcz6YspggW02X+BRXJWuquHwkh37BRx1NMW1iwMZA31SnFQvTpPzWYYIb9WF/mRsy2nGtt9C6NIg== dependencies: escape-string-regexp "1.0.5" invariant "2.2.4" From fb40fd5b784288e3e5e63d51f69c29479cb5a967 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Chmiela?= Date: Tue, 7 May 2019 22:59:50 +0200 Subject: [PATCH 4/4] [changelog] Add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7108d35f379e..0a601f76e235a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ This is the log of notable changes to the Expo client that are developer-facing. - added `Location.enableNetworkProviderAsync` method to ask the user to turn on high accuracy location services by [@tsapeta](https://github.com/tsapeta) ([#3273](https://github.com/expo/expo/pull/3273)) - upgraded Facebook Audience Network SDK dependency to 5.1.1 by [@sjchmiela](https://github.com/sjchmiela) ([#3394](https://github.com/expo/expo/pull/3394)) - upgraded Facebook Core- and LoginKit dependency to 4.40.0 by [@sjchmiela](https://github.com/sjchmiela) ([#3394](https://github.com/expo/expo/pull/3394)) +- upgraded `react-native-webview` to `5.8.1` by [@sjchmiela](https://github.com/sjchmiela) ([#4146](https://github.com/expo/expo/pull/4146)) - upgrade `react-native-maps` to `0.23.0` by [@sjchmiela](https://github.com/sjchmiela) ([#3389](https://github.com/expo/expo/pull/3389)) - added Firebase integration to `expo-analytics-segment` by [@sjchmiela](https://github.com/sjchmiela) ([#3615](https://github.com/expo/expo/pull/3615)) - added support for new arguments in `WebBrowser.openBrowserAsync` as described in [the documentation](https://docs.expo.io/versions/latest/sdk/webbrowser/) by [@mczernek](https://github.com/mczernek) ([#3691](https://github.com/expo/expo/pull/3691))