xref: /expo/ios/Exponent/ExpoKit/ExpoKit.m (revision 2fde1d7b)
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