1// Copyright 2015-present 650 Industries. All rights reserved. 2 3#import "ExpoKit.h" 4#import "EXViewController.h" 5#import "EXAnalytics.h" 6#import "EXBuildConstants.h" 7#import "EXEnvironment.h" 8#import "EXFacebook.h" 9#import "EXGoogleAuthManager.h" 10#import "EXKernel.h" 11#import "EXKernelUtil.h" 12#import "EXKernelLinkingManager.h" 13#import "EXReactAppExceptionHandler.h" 14#import "EXRemoteNotificationManager.h" 15#import "EXLocalNotificationManager.h" 16#import "EXBranchManager.h" 17 18#import <Crashlytics/Crashlytics.h> 19#import <FBSDKCoreKit/FBSDKCoreKit.h> 20#import <GoogleMaps/GoogleMaps.h> 21 22NSString * const EXAppDidRegisterForRemoteNotificationsNotification = @"EXAppDidRegisterForRemoteNotificationsNotification"; 23NSString * const EXAppDidRegisterUserNotificationSettingsNotification = @"EXAppDidRegisterUserNotificationSettingsNotification"; 24 25@interface ExpoKit () <CrashlyticsDelegate> 26{ 27 Class _rootViewControllerClass; 28} 29 30@property (nonatomic, nullable, strong) EXViewController *rootViewController; 31@property (nonatomic, strong) NSDictionary *launchOptions; 32 33@end 34 35@implementation ExpoKit 36 37+ (nonnull instancetype)sharedInstance 38{ 39 static ExpoKit *theExpoKit = nil; 40 static dispatch_once_t once; 41 dispatch_once(&once, ^{ 42 if (!theExpoKit) { 43 theExpoKit = [[ExpoKit alloc] init]; 44 } 45 }); 46 return theExpoKit; 47} 48 49- (instancetype)init 50{ 51 if (self = [super init]) { 52 _rootViewControllerClass = [EXViewController class]; 53 [self _initDefaultKeys]; 54 } 55 return self; 56} 57 58- (void)dealloc 59{ 60 [[NSNotificationCenter defaultCenter] removeObserver:self]; 61} 62 63- (void)registerRootViewControllerClass:(Class)rootViewControllerClass 64{ 65 NSAssert([rootViewControllerClass isSubclassOfClass:[EXViewController class]], @"ExpoKit root view controller class must subclass EXViewController."); 66 _rootViewControllerClass = rootViewControllerClass; 67} 68 69- (EXViewController *)rootViewController 70{ 71 if (!_rootViewController) { 72 _rootViewController = [[_rootViewControllerClass alloc] init]; 73 _rootViewController.delegate = [EXKernel sharedInstance]; 74 } 75 return _rootViewController; 76} 77 78- (UIViewController *)currentViewController 79{ 80 EXViewController *rootViewController = [self rootViewController]; 81 UIViewController *controller = [rootViewController contentViewController]; 82 while (controller.presentedViewController != nil) { 83 controller = controller.presentedViewController; 84 } 85 return controller; 86} 87 88#pragma mark - misc AppDelegate hooks 89 90- (void)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 91{ 92 if (@available(iOS 10, *)) { 93 [DDLog addLogger:[DDOSLogger sharedInstance]]; 94 } else { 95 [DDLog addLogger:[DDASLLogger sharedInstance]]; 96 [DDLog addLogger:[DDTTYLogger sharedInstance]]; 97 } 98 99 RCTSetFatalHandler(handleFatalReactError); 100 101 if ([EXFacebook facebookAppIdFromNSBundle]) { 102 [[FBSDKApplicationDelegate sharedInstance] application:application 103 didFinishLaunchingWithOptions:launchOptions]; 104 } 105 106 // init analytics 107 [EXAnalytics sharedInstance]; 108 109 NSString *standaloneGMSKey = [[NSBundle mainBundle].infoDictionary objectForKey:@"GMSApiKey"]; 110 if (standaloneGMSKey && standaloneGMSKey.length) { 111 [GMSServices provideAPIKey:standaloneGMSKey]; 112 } else { 113 if (_applicationKeys[@"GOOGLE_MAPS_IOS_API_KEY"]) {// we may define this as empty 114 if ([_applicationKeys[@"GOOGLE_MAPS_IOS_API_KEY"] length]) { 115 [GMSServices provideAPIKey:_applicationKeys[@"GOOGLE_MAPS_IOS_API_KEY"]]; 116 } 117 } 118 } 119 120 // This is safe to call; if the app doesn't have permission to display user-facing notifications 121 // then registering for a push token is a no-op 122 [[EXKernel sharedInstance].serviceRegistry.remoteNotificationManager registerForRemoteNotifications]; 123 [[EXKernel sharedInstance].serviceRegistry.branchManager application:application didFinishLaunchingWithOptions:launchOptions]; 124 _launchOptions = launchOptions; 125} 126 127#pragma mark - Crash handling 128 129- (void)crashlyticsDidDetectReportForLastExecution:(CLSReport *)report 130{ 131 // set a persistent flag because we may not get a chance to take any action until a future execution of the app. 132 [[NSUserDefaults standardUserDefaults] setBool:YES forKey:kEXKernelClearJSCacheUserDefaultsKey]; 133 134 // block to ensure we save this key (in case the app crashes again) 135 [[NSUserDefaults standardUserDefaults] synchronize]; 136} 137 138#pragma mark - APNS hooks 139 140- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)token 141{ 142 [[EXKernel sharedInstance].serviceRegistry.remoteNotificationManager registerAPNSToken:token registrationError:nil]; 143 [[NSNotificationCenter defaultCenter] postNotificationName:EXAppDidRegisterForRemoteNotificationsNotification object:nil userInfo:@{ @"token": token }]; 144} 145 146- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)err 147{ 148 DDLogWarn(@"Failed to register for remote notifs: %@", err); 149 [[EXKernel sharedInstance].serviceRegistry.remoteNotificationManager registerAPNSToken:nil registrationError:err]; 150 151 // Post this even in the failure case -- up to subscribers to subsequently read the system permission state 152 [[NSNotificationCenter defaultCenter] postNotificationName:EXAppDidRegisterForRemoteNotificationsNotification object:nil]; 153} 154 155- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)notification 156{ 157 BOOL isFromBackground = !(application.applicationState == UIApplicationStateActive); 158 [[EXKernel sharedInstance].serviceRegistry.remoteNotificationManager handleRemoteNotification:notification fromBackground:isFromBackground]; 159} 160 161- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification 162{ 163 BOOL isFromBackground = !(application.applicationState == UIApplicationStateActive); 164 [[EXLocalNotificationManager sharedInstance] handleLocalNotification:notification fromBackground:isFromBackground]; 165} 166 167- (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(nonnull UIUserNotificationSettings *)notificationSettings 168{ 169 [[NSNotificationCenter defaultCenter] postNotificationName:EXAppDidRegisterUserNotificationSettingsNotification object:nil]; 170} 171 172#pragma mark - deep linking hooks 173 174- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(nullable NSString *)sourceApplication annotation:(id)annotation 175{ 176 if ([[EXKernel sharedInstance].serviceRegistry.googleAuthManager 177 application:application openURL:url sourceApplication:sourceApplication annotation:annotation]) { 178 return YES; 179 } 180 181 if ([EXFacebook facebookAppIdFromNSBundle]) { 182 if ([[FBSDKApplicationDelegate sharedInstance] application:application 183 openURL:url 184 sourceApplication:sourceApplication 185 annotation:annotation]) { 186 return YES; 187 } 188 } 189 190 if ([[EXKernel sharedInstance].serviceRegistry.branchManager 191 application:application 192 openURL:url 193 sourceApplication:sourceApplication 194 annotation:annotation]) { 195 return YES; 196 } 197 198 return [EXKernelLinkingManager application:application openURL:url sourceApplication:sourceApplication annotation:annotation]; 199} 200 201- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray * _Nullable))restorationHandler 202{ 203 if ([[EXKernel sharedInstance].serviceRegistry.branchManager 204 application:application 205 continueUserActivity:userActivity 206 restorationHandler:restorationHandler]) { 207 return YES; 208 } 209 210 if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) { 211 NSURL *webpageURL = userActivity.webpageURL; 212 if ([EXEnvironment sharedEnvironment].isDetached) { 213 return [EXKernelLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler]; 214 } else { 215 NSString *path = [webpageURL path]; 216 217 // Filter out URLs that don't match experience URLs since the AASA pattern's grammar is not as 218 // expressive as we'd like and matches profile URLs too 219 NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"^/@[a-z0-9_-]+/.+$" 220 options:NSRegularExpressionCaseInsensitive 221 error:nil]; 222 NSUInteger matchCount = [regex numberOfMatchesInString:path options:0 range:NSMakeRange(0, path.length)]; 223 224 if (matchCount > 0) { 225 [EXKernelLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler]; 226 return YES; 227 } else { 228 [application openURL:webpageURL]; 229 return YES; 230 } 231 } 232 } 233 234 return NO; 235} 236 237#pragma mark - internal 238 239- (void)_initDefaultKeys 240{ 241 // these are provided in the expo/expo open source repo as defaults; they can all be overridden by setting 242 // the `applicationKeys` property on ExpoKit. 243 if ([EXBuildConstants sharedInstance].defaultApiKeys) { 244 self.applicationKeys = [EXBuildConstants sharedInstance].defaultApiKeys; 245 } 246} 247 248@end 249