xref: /expo/ios/Exponent/Kernel/Core/EXKernel.m (revision 7629aae1)
1// Copyright 2015-present 650 Industries. All rights reserved.
2
3#import "EXAnalytics.h"
4#import "EXAppState.h"
5#import "EXBuildConstants.h"
6#import "EXFrame.h"
7#import "EXFrameReactAppManager.h"
8#import "EXKernel.h"
9#import "EXKernelAppRecord.h"
10#import "EXKernelModule.h"
11#import "EXKernelLinkingManager.h"
12#import "EXLinkingManager.h"
13#import "EXVersions.h"
14#import "EXViewController.h"
15
16#import <React/RCTBridge+Private.h>
17#import <React/RCTEventDispatcher.h>
18#import <React/RCTModuleData.h>
19#import <React/RCTUtils.h>
20
21NS_ASSUME_NONNULL_BEGIN
22
23NSString *kEXKernelErrorDomain = @"EXKernelErrorDomain";
24NSNotificationName kEXKernelJSIsLoadedNotification = @"EXKernelJSIsLoadedNotification";
25NSNotificationName kEXKernelAppDidDisplay = @"EXKernelAppDidDisplay";
26NSString *kEXKernelShouldForegroundTaskEvent = @"foregroundTask";
27NSString * const kEXDeviceInstallUUIDKey = @"EXDeviceInstallUUIDKey";
28NSString * const kEXKernelClearJSCacheUserDefaultsKey = @"EXKernelClearJSCacheUserDefaultsKey";
29NSString * const EXKernelDisableNuxDefaultsKey = @"EXKernelDisableNuxDefaultsKey";
30
31@interface EXKernel () <EXKernelAppRegistryDelegate>
32
33@property (nonatomic, weak) EXViewController *vcExponentRoot;
34
35@end
36
37@implementation EXKernel
38
39+ (instancetype)sharedInstance
40{
41  static EXKernel *theKernel;
42  static dispatch_once_t once;
43  dispatch_once(&once, ^{
44    if (!theKernel) {
45      theKernel = [[EXKernel alloc] init];
46    }
47  });
48  return theKernel;
49}
50
51- (instancetype)init
52{
53  if (self = [super init]) {
54    // init app registry: keep track of RN bridges we are running
55    _appRegistry = [[EXKernelAppRegistry alloc] init];
56    _appRegistry.delegate = self;
57
58    // init service registry: classes which manage shared resources among all bridges
59    _serviceRegistry = [[EXKernelServiceRegistry alloc] init];
60
61    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_onKernelJSLoaded) name:kEXKernelJSIsLoadedNotification object:nil];
62    for (NSString *name in @[UIApplicationDidBecomeActiveNotification,
63                             UIApplicationDidEnterBackgroundNotification,
64                             UIApplicationDidFinishLaunchingNotification,
65                             UIApplicationWillResignActiveNotification,
66                             UIApplicationWillEnterForegroundNotification]) {
67
68      [[NSNotificationCenter defaultCenter] addObserver:self
69                                               selector:@selector(_handleAppStateDidChange:)
70                                                   name:name
71                                                 object:nil];
72    }
73    NSLog(@"Expo iOS Runtime Version %@", [EXBuildConstants sharedInstance].expoRuntimeVersion);
74  }
75  return self;
76}
77
78- (void)dealloc
79{
80  [[NSNotificationCenter defaultCenter] removeObserver:self];
81}
82
83- (void)registerRootExponentViewController:(EXViewController *)exponentViewController
84{
85  _vcExponentRoot = exponentViewController;
86}
87
88- (EXViewController *)rootViewController
89{
90  return _vcExponentRoot;
91}
92
93- (void)_onKernelJSLoaded
94{
95  // used by appetize: optionally disable nux
96  BOOL disableNuxDefaultsValue = [[NSUserDefaults standardUserDefaults] boolForKey:EXKernelDisableNuxDefaultsKey];
97  if (disableNuxDefaultsValue) {
98    [self dispatchKernelJSEvent:@"resetNuxState" body:@{ @"isNuxCompleted": @YES } onSuccess:nil onFailure:nil];
99    [[NSUserDefaults standardUserDefaults] removeObjectForKey:EXKernelDisableNuxDefaultsKey];
100  }
101}
102
103#pragma mark - Misc
104
105- (void)openUrl:(NSString *)urlString onAppManager:(EXReactAppManager *)appManager
106{
107  // fire a Linking url event on this (possibly versioned) bridge
108  id linkingModule = [self nativeModuleForAppManager:appManager named:@"LinkingManager"];
109  if (!linkingModule) {
110    DDLogError(@"Could not find the Linking module to open URL (%@)", urlString);
111  } else if ([linkingModule respondsToSelector:@selector(dispatchOpenUrlEvent:)]) {
112    [linkingModule dispatchOpenUrlEvent:[NSURL URLWithString:urlString]];
113  } else {
114    DDLogError(@"Linking module doesn't support the API we use to open URL (%@)", urlString);
115  }
116  [self _moveAppManagerToForeground:appManager];
117}
118
119+ (NSString *)deviceInstallUUID
120{
121  NSString *uuid = [[NSUserDefaults standardUserDefaults] stringForKey:kEXDeviceInstallUUIDKey];
122  if (!uuid) {
123    uuid = [[NSUUID UUID] UUIDString];
124    [[NSUserDefaults standardUserDefaults] setObject:uuid forKey:kEXDeviceInstallUUIDKey];
125    [[NSUserDefaults standardUserDefaults] synchronize];
126  }
127  return uuid;
128}
129
130#pragma mark - bridge registry delegate
131
132- (void)appRegistry:(EXKernelAppRegistry *)registry didRegisterAppRecord:(EXKernelAppRecord *)appRecord
133{
134  // forward to service registry
135  [_serviceRegistry appRegistry:registry didRegisterAppRecord:appRecord];
136}
137
138- (void)appRegistry:(EXKernelAppRegistry *)registry willUnregisterAppRecord:(EXKernelAppRecord *)appRecord
139{
140  // forward to service registry
141  [_serviceRegistry appRegistry:registry willUnregisterAppRecord:appRecord];
142}
143
144#pragma mark - interfacing with app managers
145
146- (void)dispatchKernelJSEvent:(NSString *)eventName body:(NSDictionary *)eventBody onSuccess:(void (^_Nullable)(NSDictionary * _Nullable))success onFailure:(void (^_Nullable)(NSString * _Nullable))failure
147{
148  EXKernelModule *kernelModule = [self nativeModuleForAppManager:_appRegistry.kernelAppManager named:@"ExponentKernel"];
149  if (kernelModule) {
150    [kernelModule dispatchJSEvent:eventName body:eventBody onSuccess:success onFailure:failure];
151  }
152}
153
154- (void)_dispatchJSEvent:(NSString *)eventName body:(NSDictionary *)eventBody onAppManager:(EXReactAppManager *)appManager
155{
156  [appManager.reactBridge enqueueJSCall:@"RCTDeviceEventEmitter.emit"
157                                   args:eventBody ? @[eventName, eventBody] : @[eventName]];
158}
159
160- (id)nativeModuleForAppManager:(EXReactAppManager *)appManager named:(NSString *)moduleName
161{
162  id destinationBridge = appManager.reactBridge;
163
164  if ([destinationBridge respondsToSelector:@selector(batchedBridge)]) {
165    id batchedBridge = [destinationBridge batchedBridge];
166    id moduleData = [batchedBridge moduleDataForName:moduleName];
167
168    // React Native before SDK 11 didn't strip the "RCT" prefix from module names
169    if (!moduleData && ![moduleName hasPrefix:@"RCT"]) {
170      moduleData = [batchedBridge moduleDataForName:[@"RCT" stringByAppendingString:moduleName]];
171    }
172
173    if (moduleData) {
174      return [moduleData instance];
175    }
176  } else {
177    DDLogError(@"Bridge does not support the API we use to get its underlying batched bridge");
178  }
179  return nil;
180}
181
182/**
183 *  If the bridge has a batchedBridge or parentBridge selector, posts the notification on that object as well.
184 */
185- (void)_postNotificationName: (NSNotificationName)name onAbstractBridge: (id)bridge
186{
187  [[NSNotificationCenter defaultCenter] postNotificationName:name object:bridge];
188  if ([bridge respondsToSelector:@selector(batchedBridge)]) {
189    [[NSNotificationCenter defaultCenter] postNotificationName:name object:[bridge batchedBridge]];
190  } else if ([bridge respondsToSelector:@selector(parentBridge)]) {
191    [[NSNotificationCenter defaultCenter] postNotificationName:name object:[bridge parentBridge]];
192  }
193}
194
195- (void)sendNotification:(NSDictionary *)notifBody
196      toExperienceWithId:(NSString *)destinationExperienceId
197          fromBackground:(BOOL)isFromBackground
198                isRemote:(BOOL)isRemote
199{
200  EXReactAppManager *destinationAppManager = _appRegistry.kernelAppManager;
201  EXKernelAppRecord *recordWithExperienceId = [_appRegistry newestRecordWithExperienceId:destinationExperienceId];
202  if (recordWithExperienceId && recordWithExperienceId.appManager) {
203    destinationAppManager = recordWithExperienceId.appManager;
204  }
205  // if the notification came from the background, in most but not all cases, this means the user acted on an iOS notification
206  // and caused the app to launch.
207  // From SO:
208  // > Note that "App opened from Notification" will be a false positive if the notification is sent while the user is on a different
209  // > screen (for example, if they pull down the status bar and then receive a notification from your app).
210  NSDictionary *bodyWithOrigin = @{
211                                   @"origin": (isFromBackground) ? @"selected" : @"received",
212                                   @"remote": @(isRemote),
213                                   @"data": notifBody,
214                                   };
215  if (destinationAppManager) {
216    if (destinationAppManager == _appRegistry.kernelAppManager) {
217      // send both the body and the experience id, so we can open a new experience from the kernel
218      [self _dispatchJSEvent:@"Exponent.notification"
219                        body:@{
220                               @"body": bodyWithOrigin,
221                               @"experienceId": destinationExperienceId,
222                               }
223                onAppManager:_appRegistry.kernelAppManager];
224    } else {
225      // send the body to the already-open experience
226      [self _dispatchJSEvent:@"Exponent.notification" body:bodyWithOrigin onAppManager:destinationAppManager];
227      [self _moveAppManagerToForeground:destinationAppManager];
228    }
229  }
230}
231
232#pragma mark - App State
233
234- (void)handleJSTaskDidForegroundWithType:(NSInteger)type params:(NSDictionary *)params
235{
236  EXKernelRoute routetype = (EXKernelRoute)type;
237  [[EXAnalytics sharedInstance] logForegroundEventForRoute:routetype fromJS:YES];
238
239  NSString *urlToForeground, *urlToBackground;
240  if (params) {
241    urlToForeground = RCTNilIfNull(params[@"url"]);
242    urlToBackground = RCTNilIfNull(params[@"urlToBackground"]);
243  }
244
245  EXReactAppManager *appManagerToForeground = nil;
246  EXReactAppManager *appManagerToBackground = nil;
247
248  if (routetype == kEXKernelRouteHome) {
249    appManagerToForeground = _appRegistry.kernelAppManager;
250  }
251  if (routetype == kEXKernelRouteBrowser && !urlToBackground) {
252    appManagerToBackground = _appRegistry.kernelAppManager;
253  }
254
255  for (NSString *recordId in [_appRegistry appEnumerator]) {
256    EXKernelAppRecord *appRecord = [_appRegistry recordForId:recordId];
257    if (!appRecord || appRecord.status != EXKernelAppRecordStatusRunning) {
258      continue;
259    }
260    if (urlToForeground && appRecord.appManager && [appRecord.appManager.frame.initialUri.absoluteString isEqualToString:urlToForeground]) {
261      appManagerToForeground = appRecord.appManager;
262    } else if (urlToBackground && appRecord.appManager && [appRecord.appManager.frame.initialUri.absoluteString isEqualToString:urlToBackground]) {
263      appManagerToBackground = appRecord.appManager;
264    }
265  }
266
267  if ([_serviceRegistry.linkingManager isRefreshExpectedForAppManager:appManagerToForeground]) {
268    // shell app foregrounded the same bridge as before.
269    // this would be a no-op, so we force a reload on the existing frame.
270    // this is usually triggered by calling Util.reload() when no new JS bundle is available.
271    [((EXFrameReactAppManager *)_appRegistry.lastKnownForegroundAppManager).frame reload];
272  } else {
273    if (appManagerToBackground) {
274      [self _postNotificationName:kEXKernelBridgeDidBackgroundNotification onAbstractBridge:appManagerToBackground.reactBridge];
275      id appStateModule = [self nativeModuleForAppManager:appManagerToBackground named:@"AppState"];
276      if ([appStateModule respondsToSelector:@selector(setState:)]) {
277        [appStateModule setState:@"background"];
278      }
279    }
280    if (appManagerToForeground) {
281      [self _postNotificationName:kEXKernelBridgeDidForegroundNotification onAbstractBridge:appManagerToForeground.reactBridge];
282      id appStateModule = [self nativeModuleForAppManager:appManagerToForeground named:@"AppState"];
283      if ([appStateModule respondsToSelector:@selector(setState:)]) {
284        [appStateModule setState:@"active"];
285      }
286      _appRegistry.lastKnownForegroundAppManager = appManagerToForeground;
287    } else {
288      _appRegistry.lastKnownForegroundAppManager = nil;
289    }
290  }
291}
292
293- (void)_handleAppStateDidChange:(NSNotification *)notification
294{
295  NSString *newState;
296
297  if ([notification.name isEqualToString:UIApplicationWillResignActiveNotification]) {
298    newState = @"inactive";
299  } else if ([notification.name isEqualToString:UIApplicationWillEnterForegroundNotification]) {
300    newState = @"background";
301  } else {
302    switch (RCTSharedApplication().applicationState) {
303      case UIApplicationStateActive:
304        newState = @"active";
305        break;
306      case UIApplicationStateBackground: {
307        newState = @"background";
308        break;
309      }
310      default: {
311        newState = @"unknown";
312        break;
313      }
314    }
315  }
316
317  if (_appRegistry.lastKnownForegroundAppManager) {
318    EXReactAppManager *appManager = [_appRegistry lastKnownForegroundAppManager];
319    id appStateModule = [self nativeModuleForAppManager:appManager named:@"AppState"];
320    NSString *lastKnownState;
321    if ([appStateModule respondsToSelector:@selector(lastKnownState)]) {
322      lastKnownState = [appStateModule lastKnownState];
323    }
324    if ([appStateModule respondsToSelector:@selector(setState:)]) {
325      [appStateModule setState:newState];
326    }
327    if (!lastKnownState || ![newState isEqualToString:lastKnownState]) {
328      if ([newState isEqualToString:@"active"]) {
329        [self _postNotificationName:kEXKernelBridgeDidForegroundNotification onAbstractBridge:_appRegistry.lastKnownForegroundAppManager.reactBridge];
330      } else if ([newState isEqualToString:@"background"]) {
331        [self _postNotificationName:kEXKernelBridgeDidBackgroundNotification onAbstractBridge:_appRegistry.lastKnownForegroundAppManager.reactBridge];
332      }
333    }
334  }
335}
336
337- (void)_moveAppManagerToForeground: (EXReactAppManager *)appManager
338{
339  if (appManager != _appRegistry.kernelAppManager) {
340    EXFrameReactAppManager *frameAppManager = (EXFrameReactAppManager *)appManager;
341    // kernel JS needs to bring the relevant frame/bridge to visibility.
342    NSURL *frameUrlToForeground = frameAppManager.frame.initialUri;
343    [self dispatchKernelJSEvent:kEXKernelShouldForegroundTaskEvent body:@{ @"taskUrl":frameUrlToForeground.absoluteString } onSuccess:nil onFailure:nil];
344  }
345}
346
347@end
348
349NS_ASSUME_NONNULL_END
350