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