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