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