// Copyright 2015-present 650 Industries. All rights reserved.
#import <SafariServices/SafariServices.h>
#import <EXWebBrowser/EXWebBrowser.h>
#import <UMCore/UMUtilities.h>
static NSString* const WebBrowserErrorCode = @"WebBrowser";
static NSString* const WebBrowserControlsColorKey = @"controlsColor";
static NSString* const WebBrowserToolbarColorKey = @"toolbarColor";
@interface EXWebBrowser () <SFSafariViewControllerDelegate>
@property (nonatomic, copy) UMPromiseResolveBlock redirectResolve;
@property (nonatomic, copy) UMPromiseRejectBlock redirectReject;
@property (nonatomic, weak) UMModuleRegistry *moduleRegistry;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wpartial-availability"
@property (nonatomic, strong) SFAuthenticationSession *authSession;
#pragma clang diagnostic pop
@end
@implementation EXWebBrowser
{
UIStatusBarStyle _initialStatusBarStyle;
}
UM_EXPORT_MODULE(ExpoWebBrowser)
- (dispatch_queue_t)methodQueue
{
return dispatch_get_main_queue();
}
UM_EXPORT_METHOD_AS(openAuthSessionAsync,
openAuthSessionAsync:(NSString *)authURL
redirectURL:(NSString *)redirectURL
resolver:(UMPromiseResolveBlock)resolve
rejecter:(UMPromiseRejectBlock)reject)
{
[self initializeWebBrowserWithResolver:resolve andRejecter:reject];
if (@available(iOS 11, *)) {
NSURL *url = [[NSURL alloc] initWithString: authURL];
__weak typeof(self) weakSelf = self;
void (^completionHandler)(NSURL * _Nullable, NSError *_Nullable) = ^(NSURL* _Nullable callbackURL, NSError* _Nullable error) {
__strong typeof(weakSelf) strongSelf = weakSelf;
if (strongSelf) {
if (!error) {
NSString *url = callbackURL.absoluteString;
strongSelf->_redirectResolve(@{
@"type" : @"success",
@"url" : url,
});
} else {
strongSelf->_redirectResolve(@{
@"type" : @"cancel",
});
}
[strongSelf flowDidFinish];
}
};
_authSession = [[SFAuthenticationSession alloc]
initWithURL:url
callbackURLScheme:redirectURL
completionHandler:completionHandler];
[_authSession start];
} else {
resolve(@{
@"type" : @"cancel",
@"message" : @"openAuthSessionAsync requires iOS 11 or greater"
});
[self flowDidFinish];
}
}
UM_EXPORT_METHOD_AS(openBrowserAsync,
openBrowserAsync:(NSString *)authURL
withArguments:(NSDictionary *)arguments
resolver:(UMPromiseResolveBlock)resolve
rejecter:(UMPromiseRejectBlock)reject)
{
if (![self initializeWebBrowserWithResolver:resolve andRejecter:reject]) {
return;
}
NSURL *url = [[NSURL alloc] initWithString:authURL];
SFSafariViewController *safariVC = nil;
if (@available(iOS 11, *)) {
SFSafariViewControllerConfiguration *config = [[SFSafariViewControllerConfiguration alloc] init];
config.barCollapsingEnabled = [arguments[@"enableBarCollapsing"] boolValue];
safariVC = [[SFSafariViewController alloc] initWithURL:url configuration:config];
} else {
safariVC = [[SFSafariViewController alloc] initWithURL:url];
}
if([[arguments allKeys] containsObject:WebBrowserToolbarColorKey]) {
safariVC.preferredBarTintColor = [EXWebBrowser convertHexColorString:arguments[WebBrowserToolbarColorKey]];
}
if([[arguments allKeys] containsObject:WebBrowserControlsColorKey]) {
safariVC.preferredControlTintColor = [EXWebBrowser convertHexColorString:arguments[WebBrowserControlsColorKey]];
}
safariVC.delegate = self;
// By setting the modal presentation style to OverFullScreen, we disable the "Swipe to dismiss"
// gesture that is causing a bug where sometimes `safariViewControllerDidFinish` is not called.
// There are bugs filed already about it on OpenRadar.
[safariVC setModalPresentationStyle: UIModalPresentationOverFullScreen];
// This is a hack to present the SafariViewController modally
UINavigationController *safariHackVC = [[UINavigationController alloc] initWithRootViewController:safariVC];
[safariHackVC setNavigationBarHidden:true animated:false];
UIViewController *currentViewController = [UIApplication sharedApplication].keyWindow.rootViewController;
while (currentViewController.presentedViewController) {
currentViewController = currentViewController.presentedViewController;
}
[currentViewController presentViewController:safariHackVC animated:true completion:nil];
}
UM_EXPORT_METHOD_AS(dismissBrowser,
dismissBrowserWithResolver:(UMPromiseResolveBlock)resolve
rejecter:(UMPromiseRejectBlock)reject)
{
__weak typeof(self) weakSelf = self;
UIViewController *currentViewController = [UIApplication sharedApplication].keyWindow.rootViewController;
while (currentViewController.presentedViewController) {
currentViewController = currentViewController.presentedViewController;
}
[currentViewController dismissViewControllerAnimated:YES completion:^{
resolve(nil);
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf) {
if (strongSelf.redirectResolve) {
strongSelf.redirectResolve(@{
@"type": @"dismiss",
});
}
[strongSelf flowDidFinish];
}
}];
}
UM_EXPORT_METHOD_AS(dismissAuthSession,
dismissAuthSessionWithResolver:(UMPromiseResolveBlock)resolve
rejecter:(UMPromiseRejectBlock)reject)
{
if (@available(iOS 11, *)) {
[_authSession cancel];
resolve(nil);
if (_redirectResolve) {
_redirectResolve(@{
@"type": @"dismiss"
});
[self flowDidFinish];
}
} else {
[self dismissAuthSessionWithResolver:resolve rejecter:reject];
}
}
UM_EXPORT_METHOD_AS(warmUpAsync,
warmUpAsyncWithPackage:(NSString*)browserPackage
resolver:(UMPromiseResolveBlock)resolve
rejecter:(UMPromiseRejectBlock)reject)
{
// stub for jest-expo-mock-generator
}
UM_EXPORT_METHOD_AS(coolDownAsync,
coolDownAsyncWithPackage:(NSString*)browserPackage
resolver:(UMPromiseResolveBlock)resolve
rejecter:(UMPromiseRejectBlock)reject)
{
// stub for jest-expo-mock-generator
}
UM_EXPORT_METHOD_AS(getCustomTabsSupportingBrowsers,
getCustomTabsSupportingBrowsersWithPackage:(UMPromiseResolveBlock)resolve
rejecter:(UMPromiseRejectBlock)reject)
{
// stub for jest-expo-mock-generator
}
UM_EXPORT_METHOD_AS(mayInitWithUrlAsync,
warmUpAsyncWithUrl:(NSString*)url
browserPackage:(NSString*)package
resolver:(UMPromiseResolveBlock)resolve
rejecter:(UMPromiseRejectBlock)reject)
{
// stub for jest-expo-mock-generator
}
/**
* Helper that is used in openBrowserAsync and openAuthSessionAsync
*/
- (BOOL)initializeWebBrowserWithResolver:(UMPromiseResolveBlock)resolve andRejecter:(UMPromiseRejectBlock)reject {
if (_redirectResolve) {
reject(WebBrowserErrorCode, @"Another WebBrowser is already being presented.", nil);
return NO;
}
_redirectReject = reject;
_redirectResolve = resolve;
_initialStatusBarStyle = [UIApplication sharedApplication].statusBarStyle;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleDefault
animated:YES];
#pragma clang diagnostic pop
return YES;
}
/**
* Called when the user dismisses the SFVC without logging in.
*/
- (void)safariViewControllerDidFinish:(SFSafariViewController *)controller
{
_redirectResolve(@{
@"type": @"cancel",
});
[self flowDidFinish];
}
-(void)flowDidFinish
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[[UIApplication sharedApplication] setStatusBarStyle:_initialStatusBarStyle animated:YES];
#pragma clang diagnostic pop
_redirectResolve = nil;
_redirectReject = nil;
}
- (void)setModuleRegistry:(UMModuleRegistry *)moduleRegistry
{
_moduleRegistry = moduleRegistry;
}
+ (UIColor *)convertHexColorString:(NSString *)stringToConvert {
NSString *strippedString = [stringToConvert stringByReplacingOccurrencesOfString:@"#" withString:@""];
NSScanner *scanner = [NSScanner scannerWithString:strippedString];
unsigned hexNum;
if (![scanner scanHexInt:&hexNum]) return nil;
return [EXWebBrowser colorWithRGBHex:hexNum];
}
+ (UIColor *)colorWithRGBHex:(UInt32)hex {
int r = (hex >> 16) & 0xFF;
int g = (hex >> 8) & 0xFF;
int b = (hex) & 0xFF;
return [UIColor colorWithRed:r / 255.0f
green:g / 255.0f
blue:b / 255.0f
alpha:1.0f];
}
@end