1// Copyright 2015-present 650 Industries. All rights reserved. 2 3#import "ExpoKit.h" 4#import "EXAnalytics.h" 5#import "EXBuildConstants.h" 6#import "EXFacebook.h" 7#import "EXFatalHandler.h" 8#import "EXGoogleAuthManager.h" 9#import "EXKernel.h" 10#import "EXKernelUtil.h" 11#import "EXKernelLinkingManager.h" 12#import "EXRemoteNotificationManager.h" 13#import "EXLocalNotificationManager.h" 14#import "EXViewController.h" 15#import "EXBranchManager.h" 16#import "EXShellManager.h" 17 18#import <Crashlytics/Crashlytics.h> 19#import <FBSDKCoreKit/FBSDKCoreKit.h> 20#import <GoogleMaps/GoogleMaps.h> 21 22NSString * const EXAppDidRegisterForRemoteNotificationsNotification = @"EXAppDidRegisterForRemoteNotificationsNotification"; 23 24@interface ExpoKit () <CrashlyticsDelegate> 25{ 26 Class _rootViewControllerClass; 27 BOOL _hasConsumedLaunchNotification; 28} 29 30@property (nonatomic, nullable, strong) EXViewController *rootViewController; 31 32@end 33 34@implementation ExpoKit 35 36+ (nonnull instancetype)sharedInstance 37{ 38 static ExpoKit *theExpoKit = nil; 39 static dispatch_once_t once; 40 dispatch_once(&once, ^{ 41 if (!theExpoKit) { 42 theExpoKit = [[ExpoKit alloc] init]; 43 } 44 }); 45 return theExpoKit; 46} 47 48- (instancetype)init 49{ 50 if (self = [super init]) { 51 _rootViewControllerClass = [EXViewController class]; 52 _hasConsumedLaunchNotification = NO; 53 54 [[NSNotificationCenter defaultCenter] addObserver:self 55 selector:@selector(_onKernelJSLoaded) 56 name:kEXKernelJSIsLoadedNotification 57 object:nil]; 58 [[NSNotificationCenter defaultCenter] addObserver:self 59 selector:@selector(_onKernelAppDidDisplay) 60 name:kEXKernelAppDidDisplay 61 object:nil]; 62 [self _initDefaultKeys]; 63 } 64 return self; 65} 66 67- (void)dealloc 68{ 69 [[NSNotificationCenter defaultCenter] removeObserver:self]; 70} 71 72- (void)registerRootViewControllerClass:(Class)rootViewControllerClass 73{ 74 NSAssert([rootViewControllerClass isSubclassOfClass:[EXViewController class]], @"ExpoKit root view controller class must subclass EXViewController."); 75 _rootViewControllerClass = rootViewControllerClass; 76} 77 78- (EXViewController *)rootViewController 79{ 80 if (!_rootViewController) { 81 _rootViewController = [[_rootViewControllerClass alloc] initWithLaunchOptions:@{}]; 82 } 83 return _rootViewController; 84} 85 86#pragma mark - misc AppDelegate hooks 87 88- (void)setLaunchOptions:(NSDictionary *)launchOptions 89{ 90 self.rootViewController.appManager.launchOptions = launchOptions; 91} 92 93- (void)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 94{ 95 96 [DDLog addLogger:[DDASLLogger sharedInstance]]; 97 [DDLog addLogger:[DDTTYLogger sharedInstance]]; 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 [self setLaunchOptions:launchOptions]; 125} 126 127#pragma mark - handling JS loads 128 129- (void)_onKernelJSLoaded 130{ 131 if (![EXShellManager sharedInstance].isShell) { 132 // see complementary call in _onKernelAppDidDisplay. 133 [self _sendRemoteOrLocalNotificationFromLaunch]; 134 } 135} 136 137- (void)_onKernelAppDidDisplay 138{ 139 if ([EXShellManager sharedInstance].isShell) { 140 // see complementary call in _onKernelJSLoaded. 141 [self _sendRemoteOrLocalNotificationFromLaunch]; 142 } 143} 144 145- (void)_sendRemoteOrLocalNotificationFromLaunch 146{ 147 if (!_hasConsumedLaunchNotification) { 148 _hasConsumedLaunchNotification = YES; 149 NSDictionary *launchOptions = self.rootViewController.launchOptions; 150 NSDictionary *remoteNotification = [launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey]; 151 152 if (remoteNotification && ![EXShellManager sharedInstance].isDetached) { 153 [[EXKernel sharedInstance].serviceRegistry.remoteNotificationManager handleRemoteNotification:remoteNotification fromBackground:YES]; 154 } 155 156 UILocalNotification *localNotification = [launchOptions objectForKey:UIApplicationLaunchOptionsLocalNotificationKey]; 157 if (localNotification) { 158 [[EXLocalNotificationManager sharedInstance] handleLocalNotification:localNotification fromBackground:YES]; 159 } 160 } 161} 162 163#pragma mark - Crash handling 164 165- (void)crashlyticsDidDetectReportForLastExecution:(CLSReport *)report 166{ 167 // set a persistent flag because we may not get a chance to take any action until a future execution of the app. 168 [[NSUserDefaults standardUserDefaults] setBool:YES forKey:kEXKernelClearJSCacheUserDefaultsKey]; 169 170 // block to ensure we save this key (in case the app crashes again) 171 [[NSUserDefaults standardUserDefaults] synchronize]; 172} 173 174#pragma mark - APNS hooks 175 176- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)token 177{ 178 [[EXKernel sharedInstance].serviceRegistry.remoteNotificationManager registerAPNSToken:token]; 179 [[NSNotificationCenter defaultCenter] postNotificationName:EXAppDidRegisterForRemoteNotificationsNotification object:nil]; 180} 181 182- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)err 183{ 184 DDLogWarn(@"Failed to register for remote notifs: %@", err); 185 [[EXKernel sharedInstance].serviceRegistry.remoteNotificationManager registerAPNSToken:nil]; 186 187 // Post this even in the failure case -- up to subscribers to subsequently read the system permission state 188 [[NSNotificationCenter defaultCenter] postNotificationName:EXAppDidRegisterForRemoteNotificationsNotification object:nil]; 189} 190 191- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)notification 192{ 193 BOOL isFromBackground = !(application.applicationState == UIApplicationStateActive); 194 [[EXKernel sharedInstance].serviceRegistry.remoteNotificationManager handleRemoteNotification:notification fromBackground:isFromBackground]; 195} 196 197- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification 198{ 199 BOOL isFromBackground = !(application.applicationState == UIApplicationStateActive); 200 [[EXLocalNotificationManager sharedInstance] handleLocalNotification:notification fromBackground:isFromBackground]; 201} 202 203#pragma mark - deep linking hooks 204 205- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(nullable NSString *)sourceApplication annotation:(id)annotation 206{ 207 if ([[EXKernel sharedInstance].serviceRegistry.googleAuthManager 208 application:application openURL:url sourceApplication:sourceApplication annotation:annotation]) { 209 return YES; 210 } 211 212 if ([EXFacebook facebookAppIdFromNSBundle]) { 213 if ([[FBSDKApplicationDelegate sharedInstance] application:application 214 openURL:url 215 sourceApplication:sourceApplication 216 annotation:annotation]) { 217 return YES; 218 } 219 } 220 221 if ([[EXKernel sharedInstance].serviceRegistry.branchManager 222 application:application 223 openURL:url 224 sourceApplication:sourceApplication 225 annotation:annotation]) { 226 return YES; 227 } 228 229 // TODO: don't want to launch more bridges when in detached state. 230 return [EXKernelLinkingManager application:application openURL:url sourceApplication:sourceApplication annotation:annotation]; 231} 232 233- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray * _Nullable))restorationHandler 234{ 235 if ([[EXKernel sharedInstance].serviceRegistry.branchManager 236 application:application 237 continueUserActivity:userActivity 238 restorationHandler:restorationHandler]) { 239 return YES; 240 } 241 242 if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) { 243 NSURL *webpageURL = userActivity.webpageURL; 244 if ([EXShellManager sharedInstance].isShell) { 245 return [EXKernelLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler]; 246 } else { 247 NSString *path = [webpageURL path]; 248 249 // Filter out URLs that don't match experience URLs since the AASA pattern's grammar is not as 250 // expressive as we'd like and matches profile URLs too 251 NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"^/@[a-z0-9_-]+/.+$" 252 options:NSRegularExpressionCaseInsensitive 253 error:nil]; 254 NSUInteger matchCount = [regex numberOfMatchesInString:path options:0 range:NSMakeRange(0, path.length)]; 255 256 if (matchCount > 0) { 257 // TODO: don't want to launch more bridges when in detached state. 258 [EXKernelLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler]; 259 return YES; 260 } else { 261 [application openURL:webpageURL]; 262 return YES; 263 } 264 } 265 } 266 267 return NO; 268} 269 270#pragma mark - internal 271 272- (void)_initDefaultKeys 273{ 274 // these are provided in the expo/expo open source repo as defaults; they can all be overridden by setting 275 // the `applicationKeys` property on ExpoKit. 276 if ([EXBuildConstants sharedInstance].defaultApiKeys) { 277 self.applicationKeys = [EXBuildConstants sharedInstance].defaultApiKeys; 278 } 279} 280 281@end 282