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