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