xref: /expo/ios/Exponent/ExpoKit/ExpoKit.m (revision cb0e1ce3)
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 = (id<UNUserNotificationCenterDelegate>) [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// TODO: Remove once SDK31 is phased out
153#pragma clang diagnostic push
154#pragma clang diagnostic ignored "-Wdeprecated-declarations"
155- (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(nonnull UIUserNotificationSettings *)notificationSettings
156{
157  [[NSNotificationCenter defaultCenter] postNotificationName:EXAppDidRegisterUserNotificationSettingsNotification object:nil];
158}
159#pragma clang diagnostic pop
160
161#pragma mark - deep linking hooks
162
163- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(nullable NSString *)sourceApplication annotation:(id)annotation
164{
165  if ([[EXKernel sharedInstance].serviceRegistry.googleAuthManager
166       application:application openURL:url sourceApplication:sourceApplication annotation:annotation]) {
167    return YES;
168  }
169
170  if ([[EXKernel sharedInstance].serviceRegistry.branchManager
171       application:application
172       openURL:url
173       sourceApplication:sourceApplication
174       annotation:annotation]) {
175    return YES;
176  }
177
178  return [EXKernelLinkingManager application:application openURL:url sourceApplication:sourceApplication annotation:annotation];
179}
180
181- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray * _Nullable))restorationHandler
182{
183  if ([[EXKernel sharedInstance].serviceRegistry.branchManager
184       application:application
185       continueUserActivity:userActivity
186       restorationHandler:restorationHandler]) {
187    return YES;
188  }
189
190  if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) {
191    NSURL *webpageURL = userActivity.webpageURL;
192    if ([EXEnvironment sharedEnvironment].isDetached) {
193      return [EXKernelLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler];
194    } else {
195      NSString *path = [webpageURL path];
196
197      // Filter out URLs that don't match experience URLs since the AASA pattern's grammar is not as
198      // expressive as we'd like and matches profile URLs too
199      NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"^/@[a-z0-9_-]+/.+$"
200                                                                             options:NSRegularExpressionCaseInsensitive
201                                                                               error:nil];
202      NSUInteger matchCount = [regex numberOfMatchesInString:path options:0 range:NSMakeRange(0, path.length)];
203
204      if (matchCount > 0) {
205        [EXKernelLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler];
206        return YES;
207      } else {
208        [application openURL:webpageURL options:@{} completionHandler:nil];
209        return YES;
210      }
211    }
212  }
213
214  return NO;
215}
216
217#pragma mark - internal
218
219- (void)_initDefaultKeys
220{
221  // these are provided in the expo/expo open source repo as defaults; they can all be overridden by setting
222  // the `applicationKeys` property on ExpoKit.
223  if ([EXBuildConstants sharedInstance].defaultApiKeys) {
224    self.applicationKeys = [EXBuildConstants sharedInstance].defaultApiKeys;
225  }
226}
227
228@end
229