1#import "EXApiUtil.h" 2#import "EXBuildConstants.h" 3#import "EXEnvironment.h" 4#import "EXErrorRecoveryManager.h" 5#import "EXUserNotificationManager.h" 6#import "EXKernel.h" 7#import "EXAbstractLoader.h" 8#import "EXKernelLinkingManager.h" 9#import "EXKernelServiceRegistry.h" 10#import "EXKernelUtil.h" 11#import "EXLog.h" 12#import "ExpoKit.h" 13#import "EXReactAppManager.h" 14#import "EXReactAppManager+Private.h" 15#import "EXVersionManager.h" 16#import "EXVersions.h" 17#import "EXAppViewController.h" 18#import <ExpoModulesCore/EXModuleRegistryProvider.h> 19#import <EXConstants/EXConstantsService.h> 20#import <EXSplashScreen/EXSplashScreenService.h> 21#import <EXManifests/EXManifests-Swift.h> 22 23#import <React/RCTBridge.h> 24#import <React/RCTCxxBridgeDelegate.h> 25#import <React/JSCExecutorFactory.h> 26#import <React/RCTRootView.h> 27 28@implementation RCTSource (EXReactAppManager) 29 30- (instancetype)initWithURL:(nonnull NSURL *)url data:(nonnull NSData *)data 31{ 32 if (self = [super init]) { 33 // Use KVO since RN publicly declares these properties as readonly and privately defines the 34 // ivars 35 [self setValue:url forKey:@"url"]; 36 [self setValue:data forKey:@"data"]; 37 [self setValue:@(data.length) forKey:@"length"]; 38 [self setValue:@(RCTSourceFilesChangedCountNotBuiltByBundler) forKey:@"filesChangedCount"]; 39 } 40 return self; 41} 42 43@end 44 45@interface EXReactAppManager () <RCTBridgeDelegate, RCTCxxBridgeDelegate> 46 47@property (nonatomic, strong) UIView * __nullable reactRootView; 48@property (nonatomic, copy) RCTSourceLoadBlock loadCallback; 49@property (nonatomic, strong) NSDictionary *initialProps; 50@property (nonatomic, strong) NSTimer *viewTestTimer; 51 52@end 53 54@implementation EXReactAppManager 55 56- (instancetype)initWithAppRecord:(EXKernelAppRecord *)record initialProps:(NSDictionary *)initialProps 57{ 58 if (self = [super init]) { 59 _appRecord = record; 60 _initialProps = initialProps; 61 _isHeadless = NO; 62 _exceptionHandler = [[EXReactAppExceptionHandler alloc] initWithAppRecord:_appRecord]; 63 } 64 return self; 65} 66 67- (void)setAppRecord:(EXKernelAppRecord *)appRecord 68{ 69 _appRecord = appRecord; 70 _exceptionHandler = [[EXReactAppExceptionHandler alloc] initWithAppRecord:appRecord]; 71} 72 73- (EXReactAppManagerStatus)status 74{ 75 if (!_appRecord) { 76 return kEXReactAppManagerStatusError; 77 } 78 if (_loadCallback) { 79 // we have a RCTBridge load callback so we're ready to receive load events 80 return kEXReactAppManagerStatusBridgeLoading; 81 } 82 if (_isBridgeRunning) { 83 return kEXReactAppManagerStatusRunning; 84 } 85 return kEXReactAppManagerStatusNew; 86} 87 88- (UIView *)rootView 89{ 90 return _reactRootView; 91} 92 93- (void)rebuildBridge 94{ 95 EXAssertMainThread(); 96 NSAssert((_delegate != nil), @"Cannot init react app without EXReactAppManagerDelegate"); 97 98 [self _invalidateAndClearDelegate:NO]; 99 [self computeVersionSymbolPrefix]; 100 101 // Assert early so we can catch the error before instantiating the bridge, otherwise we would be passing a 102 // nullish scope key to the scoped modules. 103 // Alternatively we could skip instantiating the scoped modules but then singletons like the one used in 104 // expo-updates would be loaded as bare modules. In the case of expo-updates, this would throw a fatal error 105 // because Expo.plist is not available in the Expo Go app. 106 NSAssert(_appRecord.scopeKey, @"Experience scope key should be nonnull when getting initial properties for root view. This can occur when the manifest JSON, loaded from the server, is missing keys."); 107 108 109 if ([self isReadyToLoad]) { 110 Class versionManagerClass = [self versionedClassFromString:@"EXVersionManager"]; 111 Class bridgeClass = [self versionedClassFromString:@"RCTBridge"]; 112 Class rootViewClass = [self versionedClassFromString:@"RCTRootView"]; 113 114 _versionManager = [[versionManagerClass alloc] initWithParams:[self extraParams] 115 manifest:_appRecord.appLoader.manifest 116 fatalHandler:handleFatalReactError 117 logFunction:[self logFunction] 118 logThreshold:[self logLevel]]; 119 _reactBridge = [[bridgeClass alloc] initWithDelegate:self launchOptions:[self launchOptionsForBridge]]; 120 121 if (!_isHeadless) { 122 // We don't want to run the whole JS app if app launches in the background, 123 // so we're omitting creation of RCTRootView that triggers runApplication and sets up React view hierarchy. 124 _reactRootView = [[rootViewClass alloc] initWithBridge:_reactBridge 125 moduleName:[self applicationKeyForRootView] 126 initialProperties:[self initialPropertiesForRootView]]; 127 } 128 129 [self setupWebSocketControls]; 130 [_delegate reactAppManagerIsReadyForLoad:self]; 131 132 NSAssert([_reactBridge isLoading], @"React bridge should be loading once initialized"); 133 [_versionManager bridgeWillStartLoading:_reactBridge]; 134 } 135} 136 137- (NSDictionary *)extraParams 138{ 139 // we allow the vanilla RN dev menu in some circumstances. 140 BOOL isStandardDevMenuAllowed = [EXEnvironment sharedEnvironment].isDetached; 141 NSMutableDictionary *params = [NSMutableDictionary dictionaryWithDictionary:@{ 142 @"manifest": _appRecord.appLoader.manifest.rawManifestJSON, 143 @"constants": @{ 144 @"linkingUri": RCTNullIfNil([EXKernelLinkingManager linkingUriForExperienceUri:_appRecord.appLoader.manifestUrl useLegacy:[self _compareVersionTo:27] == NSOrderedAscending]), 145 @"experienceUrl": RCTNullIfNil(_appRecord.appLoader.manifestUrl? _appRecord.appLoader.manifestUrl.absoluteString: nil), 146 @"expoRuntimeVersion": [EXBuildConstants sharedInstance].expoRuntimeVersion, 147 @"manifest": _appRecord.appLoader.manifest.rawManifestJSON, 148 @"executionEnvironment": [self _executionEnvironment], 149 @"appOwnership": [self _appOwnership], 150 @"isHeadless": @(_isHeadless), 151 @"supportedExpoSdks": [EXVersions sharedInstance].versions[@"sdkVersions"], 152 }, 153 @"exceptionsManagerDelegate": _exceptionHandler, 154 @"initialUri": RCTNullIfNil([EXKernelLinkingManager initialUriWithManifestUrl:_appRecord.appLoader.manifestUrl]), 155 @"isDeveloper": @([self enablesDeveloperTools]), 156 @"isStandardDevMenuAllowed": @(isStandardDevMenuAllowed), 157 @"testEnvironment": @([EXEnvironment sharedEnvironment].testEnvironment), 158 @"services": [EXKernel sharedInstance].serviceRegistry.allServices, 159 @"singletonModules": [EXModuleRegistryProvider singletonModules], 160 @"moduleRegistryDelegateClass": RCTNullIfNil([self moduleRegistryDelegateClass]), 161 }]; 162 if ([@"expo" isEqualToString:[self _appOwnership]]) { 163 [params addEntriesFromDictionary:@{ 164 @"fileSystemDirectories": @{ 165 @"documentDirectory": [self scopedDocumentDirectory], 166 @"cachesDirectory": [self scopedCachesDirectory] 167 } 168 }]; 169 } 170 return params; 171} 172 173- (void)invalidate 174{ 175 [self _invalidateAndClearDelegate:YES]; 176} 177 178- (void)_invalidateAndClearDelegate:(BOOL)clearDelegate 179{ 180 [self _stopObservingBridgeNotifications]; 181 if (_viewTestTimer) { 182 [_viewTestTimer invalidate]; 183 _viewTestTimer = nil; 184 } 185 if (_versionManager) { 186 [_versionManager invalidate]; 187 _versionManager = nil; 188 } 189 if (_reactRootView) { 190 [_reactRootView removeFromSuperview]; 191 _reactRootView = nil; 192 } 193 if (_reactBridge) { 194 [_reactBridge invalidate]; 195 _reactBridge = nil; 196 if (_delegate) { 197 [_delegate reactAppManagerDidInvalidate:self]; 198 if (clearDelegate) { 199 _delegate = nil; 200 } 201 } 202 } 203 _isBridgeRunning = NO; 204 [self _invalidateVersionState]; 205} 206 207- (void)computeVersionSymbolPrefix 208{ 209 // TODO: ben: kernel checks detached versions here 210 _validatedVersion = [[EXVersions sharedInstance] availableSdkVersionForManifest:_appRecord.appLoader.manifest]; 211 _versionSymbolPrefix = [[EXVersions sharedInstance] symbolPrefixForSdkVersion:self.validatedVersion isKernel:NO]; 212} 213 214- (void)_invalidateVersionState 215{ 216 _versionSymbolPrefix = @""; 217 _validatedVersion = nil; 218} 219 220- (Class)versionedClassFromString: (NSString *)classString 221{ 222 return NSClassFromString([self versionedString:classString]); 223} 224 225- (NSString *)versionedString: (NSString *)string 226{ 227 return [EXVersions versionedString:string withPrefix:_versionSymbolPrefix]; 228} 229 230- (NSString *)escapedResourceName:(NSString *)string 231{ 232 NSString *charactersToEscape = @"!*'();:@&=+$,/?%#[]"; 233 NSCharacterSet *allowedCharacters = [[NSCharacterSet characterSetWithCharactersInString:charactersToEscape] invertedSet]; 234 return [string stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacters]; 235} 236 237- (BOOL)isReadyToLoad 238{ 239 if (_appRecord) { 240 return (_appRecord.appLoader.status == kEXAppLoaderStatusHasManifest || _appRecord.appLoader.status == kEXAppLoaderStatusHasManifestAndBundle); 241 } 242 return NO; 243} 244 245- (NSURL *)bundleUrl 246{ 247 return [EXApiUtil bundleUrlFromManifest:_appRecord.appLoader.manifest]; 248} 249 250#pragma mark - EXAppFetcherDataSource 251 252- (NSString *)bundleResourceNameForAppFetcher:(EXAppFetcher *)appFetcher withManifest:(nonnull EXManifestsManifest *)manifest 253{ 254 if ([EXEnvironment sharedEnvironment].isDetached) { 255 NSLog(@"Standalone bundle remote url is %@", [EXEnvironment sharedEnvironment].standaloneManifestUrl); 256 return kEXEmbeddedBundleResourceName; 257 } else { 258 return manifest.legacyId; 259 } 260} 261 262- (BOOL)appFetcherShouldInvalidateBundleCache:(EXAppFetcher *)appFetcher 263{ 264 return NO; 265} 266 267#pragma mark - RCTBridgeDelegate 268 269- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 270{ 271 return [self bundleUrl]; 272} 273 274- (void)loadSourceForBridge:(RCTBridge *)bridge withBlock:(RCTSourceLoadBlock)loadCallback 275{ 276 // clear any potentially old loading state 277 if (_appRecord.scopeKey) { 278 [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager setError:nil forScopeKey:_appRecord.scopeKey]; 279 } 280 [self _stopObservingBridgeNotifications]; 281 [self _startObservingBridgeNotificationsForBridge:bridge]; 282 283 if ([self enablesDeveloperTools]) { 284 if ([_appRecord.appLoader supportsBundleReload]) { 285 [_appRecord.appLoader forceBundleReload]; 286 } else { 287 NSAssert(_appRecord.scopeKey, @"EXKernelAppRecord.scopeKey should be nonnull if we have a manifest with developer tools enabled"); 288 [[EXKernel sharedInstance] reloadAppWithScopeKey:_appRecord.scopeKey]; 289 } 290 } 291 292 _loadCallback = loadCallback; 293 if (_appRecord.appLoader.status == kEXAppLoaderStatusHasManifestAndBundle) { 294 // finish loading immediately (app loader won't call this since it's already done) 295 [self appLoaderFinished]; 296 } else { 297 // wait for something else to call `appLoaderFinished` or `appLoaderFailed` later. 298 } 299} 300 301- (NSArray *)extraModulesForBridge:(RCTBridge *)bridge 302{ 303 return [self.versionManager extraModulesForBridge:bridge]; 304} 305 306- (void)appLoaderFinished 307{ 308 NSData *data = _appRecord.appLoader.bundle; 309 if (_loadCallback) { 310 _loadCallback(nil, [[RCTSource alloc] initWithURL:[self bundleUrl] data:data]); 311 _loadCallback = nil; 312 } 313} 314 315- (void)appLoaderFailedWithError:(NSError *)error 316{ 317 // RN is going to call RCTFatal() on this error, so keep a reference to it for later 318 // so we can distinguish this non-fatal error from actual fatal cases. 319 if (_appRecord.scopeKey) { 320 [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager setError:error forScopeKey:_appRecord.scopeKey]; 321 } 322 323 // react won't post this for us 324 [[NSNotificationCenter defaultCenter] postNotificationName:[self versionedString:RCTJavaScriptDidFailToLoadNotification] object:error]; 325 326 if (_loadCallback) { 327 _loadCallback(error, nil); 328 _loadCallback = nil; 329 } 330} 331 332#pragma mark - JavaScript loading 333 334- (void)_startObservingBridgeNotificationsForBridge:(RCTBridge *)bridge 335{ 336 NSAssert(bridge, @"Must subscribe to loading notifs for a non-null bridge"); 337 338 [[NSNotificationCenter defaultCenter] addObserver:self 339 selector:@selector(_handleJavaScriptStartLoadingEvent:) 340 name:[self versionedString:RCTJavaScriptWillStartLoadingNotification] 341 object:bridge]; 342 [[NSNotificationCenter defaultCenter] addObserver:self 343 selector:@selector(_handleJavaScriptLoadEvent:) 344 name:[self versionedString:RCTJavaScriptDidLoadNotification] 345 object:bridge]; 346 [[NSNotificationCenter defaultCenter] addObserver:self 347 selector:@selector(_handleJavaScriptLoadEvent:) 348 name:[self versionedString:RCTJavaScriptDidFailToLoadNotification] 349 object:bridge]; 350 [[NSNotificationCenter defaultCenter] addObserver:self 351 selector:@selector(_handleReactContentEvent:) 352 name:[self versionedString:RCTContentDidAppearNotification] 353 object:nil]; 354 [[NSNotificationCenter defaultCenter] addObserver:self 355 selector:@selector(_handleBridgeEvent:) 356 name:[self versionedString:RCTBridgeWillReloadNotification] 357 object:bridge]; 358} 359 360- (void)_stopObservingBridgeNotifications 361{ 362 [[NSNotificationCenter defaultCenter] removeObserver:self name:[self versionedString:RCTJavaScriptWillStartLoadingNotification] object:_reactBridge]; 363 [[NSNotificationCenter defaultCenter] removeObserver:self name:[self versionedString:RCTJavaScriptDidLoadNotification] object:_reactBridge]; 364 [[NSNotificationCenter defaultCenter] removeObserver:self name:[self versionedString:RCTJavaScriptDidFailToLoadNotification] object:_reactBridge]; 365 [[NSNotificationCenter defaultCenter] removeObserver:self name:[self versionedString:RCTContentDidAppearNotification] object:_reactBridge]; 366 [[NSNotificationCenter defaultCenter] removeObserver:self name:[self versionedString:RCTBridgeWillReloadNotification] object:_reactBridge]; 367} 368 369- (void)_handleJavaScriptStartLoadingEvent:(NSNotification *)notification 370{ 371 __weak __typeof(self) weakSelf = self; 372 dispatch_async(dispatch_get_main_queue(), ^{ 373 __strong __typeof(self) strongSelf = weakSelf; 374 if (strongSelf) { 375 [strongSelf.delegate reactAppManagerStartedLoadingJavaScript:strongSelf]; 376 } 377 }); 378} 379 380- (void)_handleJavaScriptLoadEvent:(NSNotification *)notification 381{ 382 if ([notification.name isEqualToString:[self versionedString:RCTJavaScriptDidLoadNotification]]) { 383 _isBridgeRunning = YES; 384 _hasBridgeEverLoaded = YES; 385 [_versionManager bridgeFinishedLoading:_reactBridge]; 386 387 // TODO: temporary solution for hiding LoadingProgressWindow 388 if (_appRecord.viewController) { 389 [_appRecord.viewController hideLoadingProgressWindow]; 390 } 391 } else if ([notification.name isEqualToString:[self versionedString:RCTJavaScriptDidFailToLoadNotification]]) { 392 NSError *error = (notification.userInfo) ? notification.userInfo[@"error"] : nil; 393 if (_appRecord.scopeKey) { 394 [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager setError:error forScopeKey:_appRecord.scopeKey]; 395 } 396 397 EX_WEAKIFY(self); 398 dispatch_async(dispatch_get_main_queue(), ^{ 399 EX_ENSURE_STRONGIFY(self); 400 [self.delegate reactAppManager:self failedToLoadJavaScriptWithError:error]; 401 }); 402 } 403} 404 405# pragma mark app loading & splash screen 406 407- (void)_handleReactContentEvent:(NSNotification *)notification 408{ 409 if ([notification.name isEqualToString:[self versionedString:RCTContentDidAppearNotification]] 410 && notification.object == self.reactRootView) { 411 EX_WEAKIFY(self); 412 dispatch_async(dispatch_get_main_queue(), ^{ 413 EX_ENSURE_STRONGIFY(self); 414 [self.delegate reactAppManagerAppContentDidAppear:self]; 415 [self _appLoadingFinished]; 416 }); 417 } 418} 419 420- (void)_handleBridgeEvent:(NSNotification *)notification 421{ 422 if ([notification.name isEqualToString:[self versionedString:RCTBridgeWillReloadNotification]]) { 423 EX_WEAKIFY(self); 424 dispatch_async(dispatch_get_main_queue(), ^{ 425 EX_ENSURE_STRONGIFY(self); 426 [self.delegate reactAppManagerAppContentWillReload:self]; 427 }); 428 } 429} 430 431- (void)_appLoadingFinished 432{ 433 EX_WEAKIFY(self); 434 dispatch_async(dispatch_get_main_queue(), ^{ 435 EX_ENSURE_STRONGIFY(self); 436 if (self.appRecord.scopeKey) { 437 [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager experienceFinishedLoadingWithScopeKey:self.appRecord.scopeKey]; 438 } 439 [self.delegate reactAppManagerFinishedLoadingJavaScript:self]; 440 }); 441} 442 443#pragma mark - dev tools 444 445- (RCTLogFunction)logFunction 446{ 447 return (([self enablesDeveloperTools]) ? EXDeveloperRCTLogFunction : EXDefaultRCTLogFunction); 448} 449 450- (RCTLogLevel)logLevel 451{ 452 return ([self enablesDeveloperTools]) ? RCTLogLevelInfo : RCTLogLevelWarning; 453} 454 455- (BOOL)enablesDeveloperTools 456{ 457 EXManifestsManifest *manifest = _appRecord.appLoader.manifest; 458 if (manifest) { 459 return manifest.isUsingDeveloperTool; 460 } 461 return false; 462} 463 464- (BOOL)requiresValidManifests 465{ 466 return YES; 467} 468 469- (void)showDevMenu 470{ 471 if ([self enablesDeveloperTools]) { 472 dispatch_async(dispatch_get_main_queue(), ^{ 473 [self.versionManager showDevMenuForBridge:self.reactBridge]; 474 }); 475 } 476} 477 478- (void)reloadBridge 479{ 480 if ([self enablesDeveloperTools]) { 481 [self.reactBridge reload]; 482 } 483} 484 485- (void)disableRemoteDebugging 486{ 487 if ([self enablesDeveloperTools]) { 488 [self.versionManager disableRemoteDebuggingForBridge:self.reactBridge]; 489 } 490} 491 492- (void)toggleRemoteDebugging 493{ 494 if ([self enablesDeveloperTools]) { 495 [self.versionManager toggleRemoteDebuggingForBridge:self.reactBridge]; 496 } 497} 498 499- (void)togglePerformanceMonitor 500{ 501 if ([self enablesDeveloperTools]) { 502 [self.versionManager togglePerformanceMonitorForBridge:self.reactBridge]; 503 } 504} 505 506- (void)toggleElementInspector 507{ 508 if ([self enablesDeveloperTools]) { 509 [self.versionManager toggleElementInspectorForBridge:self.reactBridge]; 510 } 511} 512 513- (void)reconnectReactDevTools 514{ 515 if ([self enablesDeveloperTools]) { 516 // Emit the `RCTDevMenuShown` for the app to reconnect react-devtools 517 // https://github.com/facebook/react-native/blob/22ba1e45c52edcc345552339c238c1f5ef6dfc65/Libraries/Core/setUpReactDevTools.js#L80 518 [self.reactBridge enqueueJSCall:@"RCTNativeAppEventEmitter.emit" args:@[@"RCTDevMenuShown"]]; 519 } 520} 521 522- (void)toggleDevMenu 523{ 524 if ([EXEnvironment sharedEnvironment].isDetached) { 525 [[EXKernel sharedInstance].visibleApp.appManager showDevMenu]; 526 } else { 527 [[EXKernel sharedInstance] switchTasks]; 528 } 529} 530 531- (void)setupWebSocketControls 532{ 533 if ([self enablesDeveloperTools]) { 534 if ([_versionManager respondsToSelector:@selector(addWebSocketNotificationHandler:queue:forMethod:)]) { 535 __weak __typeof(self) weakSelf = self; 536 537 // Attach listeners to the bundler's dev server web socket connection. 538 // This enables tools to automatically reload the client remotely (i.e. in expo-cli). 539 540 // Enable a lot of tools under the same command namespace 541 [_versionManager addWebSocketNotificationHandler:^(id params) { 542 if (params != [NSNull null] && (NSDictionary *)params) { 543 NSDictionary *_params = (NSDictionary *)params; 544 if (_params[@"name"] != nil && (NSString *)_params[@"name"]) { 545 NSString *name = _params[@"name"]; 546 if ([name isEqualToString:@"reload"]) { 547 [[EXKernel sharedInstance] reloadVisibleApp]; 548 } else if ([name isEqualToString:@"toggleDevMenu"]) { 549 [weakSelf toggleDevMenu]; 550 } else if ([name isEqualToString:@"toggleRemoteDebugging"]) { 551 [weakSelf toggleRemoteDebugging]; 552 } else if ([name isEqualToString:@"toggleElementInspector"]) { 553 [weakSelf toggleElementInspector]; 554 } else if ([name isEqualToString:@"togglePerformanceMonitor"]) { 555 [weakSelf togglePerformanceMonitor]; 556 } else if ([name isEqualToString:@"reconnectReactDevTools"]) { 557 [weakSelf reconnectReactDevTools]; 558 } 559 } 560 } 561 } 562 queue:dispatch_get_main_queue() 563 forMethod:@"sendDevCommand"]; 564 565 // These (reload and devMenu) are here to match RN dev tooling. 566 567 // Reload the app on "reload" 568 [_versionManager addWebSocketNotificationHandler:^(id params) { 569 [[EXKernel sharedInstance] reloadVisibleApp]; 570 } 571 queue:dispatch_get_main_queue() 572 forMethod:@"reload"]; 573 574 // Open the dev menu on "devMenu" 575 [_versionManager addWebSocketNotificationHandler:^(id params) { 576 [weakSelf toggleDevMenu]; 577 } 578 queue:dispatch_get_main_queue() 579 forMethod:@"devMenu"]; 580 } 581 } 582} 583 584- (NSDictionary<NSString *, NSString *> *)devMenuItems 585{ 586 return [self.versionManager devMenuItemsForBridge:self.reactBridge]; 587} 588 589- (void)selectDevMenuItemWithKey:(NSString *)key 590{ 591 dispatch_async(dispatch_get_main_queue(), ^{ 592 [self.versionManager selectDevMenuItemWithKey:key onBridge:self.reactBridge]; 593 }); 594} 595 596#pragma mark - RN configuration 597 598- (NSComparisonResult)_compareVersionTo:(NSUInteger)version 599{ 600 // Unversioned projects are always considered to be on the latest version 601 if (!_validatedVersion || _validatedVersion.length == 0 || [_validatedVersion isEqualToString:@"UNVERSIONED"]) { 602 return NSOrderedDescending; 603 } 604 605 NSUInteger projectVersionNumber = _validatedVersion.integerValue; 606 if (projectVersionNumber == version) { 607 return NSOrderedSame; 608 } 609 return (projectVersionNumber < version) ? NSOrderedAscending : NSOrderedDescending; 610} 611 612- (NSDictionary *)launchOptionsForBridge 613{ 614 if ([EXEnvironment sharedEnvironment].isDetached) { 615 // pass the native app's launch options to standalone bridge. 616 return [ExpoKit sharedInstance].launchOptions; 617 } 618 return @{}; 619} 620 621- (Class)moduleRegistryDelegateClass 622{ 623 if ([EXEnvironment sharedEnvironment].isDetached) { 624 return [ExpoKit sharedInstance].moduleRegistryDelegateClass; 625 } 626 return nil; 627} 628 629- (NSString *)applicationKeyForRootView 630{ 631 EXManifestsManifest *manifest = _appRecord.appLoader.manifest; 632 if (manifest && manifest.appKey) { 633 return manifest.appKey; 634 } 635 636 NSURL *bundleUrl = [self bundleUrl]; 637 if (bundleUrl) { 638 NSURLComponents *components = [NSURLComponents componentsWithURL:bundleUrl resolvingAgainstBaseURL:YES]; 639 NSArray<NSURLQueryItem *> *queryItems = components.queryItems; 640 for (NSURLQueryItem *item in queryItems) { 641 if ([item.name isEqualToString:@"app"]) { 642 return item.value; 643 } 644 } 645 } 646 647 return @"main"; 648} 649 650- (NSDictionary * _Nullable)initialPropertiesForRootView 651{ 652 NSMutableDictionary *props = [NSMutableDictionary dictionary]; 653 NSMutableDictionary *expProps = [NSMutableDictionary dictionary]; 654 655 NSAssert(_appRecord.scopeKey, @"Experience scope key should be nonnull when getting initial properties for root view"); 656 657 NSDictionary *errorRecoveryProps = [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager developerInfoForScopeKey:_appRecord.scopeKey]; 658 if ([[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager scopeKeyIsRecoveringFromError:_appRecord.scopeKey]) { 659 [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager increaseAutoReloadBuffer]; 660 if (errorRecoveryProps) { 661 expProps[@"errorRecovery"] = errorRecoveryProps; 662 } 663 } 664 665 expProps[@"shell"] = @(_appRecord == [EXKernel sharedInstance].appRegistry.standaloneAppRecord); 666 expProps[@"appOwnership"] = [self _appOwnership]; 667 if (_initialProps) { 668 [expProps addEntriesFromDictionary:_initialProps]; 669 } 670 EXPendingNotification *initialNotification = [[EXKernel sharedInstance].serviceRegistry.notificationsManager initialNotification]; 671 if (initialNotification) { 672 expProps[@"notification"] = initialNotification.properties; 673 } 674 675 NSString *manifestString = nil; 676 EXManifestsManifest *manifest = _appRecord.appLoader.manifest; 677 if (manifest && [NSJSONSerialization isValidJSONObject:manifest.rawManifestJSON]) { 678 NSError *error; 679 NSData *jsonData = [NSJSONSerialization dataWithJSONObject:manifest.rawManifestJSON options:0 error:&error]; 680 if (jsonData) { 681 manifestString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; 682 } else { 683 DDLogWarn(@"Failed to serialize JSON manifest: %@", error); 684 } 685 } 686 687 expProps[@"manifestString"] = manifestString; 688 if (_appRecord.appLoader.manifestUrl) { 689 expProps[@"initialUri"] = [_appRecord.appLoader.manifestUrl absoluteString]; 690 } 691 props[@"exp"] = expProps; 692 return props; 693} 694 695- (NSString *)_appOwnership 696{ 697 if (_appRecord == [EXKernel sharedInstance].appRegistry.standaloneAppRecord) { 698 return @"standalone"; 699 } 700 return @"expo"; 701} 702 703- (NSString *)_executionEnvironment 704{ 705 if ([EXEnvironment sharedEnvironment].isDetached) { 706 return EXConstantsExecutionEnvironmentStandalone; 707 } else { 708 return EXConstantsExecutionEnvironmentStoreClient; 709 } 710} 711 712- (NSString *)scopedDocumentDirectory 713{ 714 NSString *escapedScopeKey = [self escapedResourceName:_appRecord.scopeKey]; 715 NSString *mainDocumentDirectory = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; 716 NSString *exponentDocumentDirectory = [mainDocumentDirectory stringByAppendingPathComponent:@"ExponentExperienceData"]; 717 return [[exponentDocumentDirectory stringByAppendingPathComponent:escapedScopeKey] stringByStandardizingPath]; 718} 719 720- (NSString *)scopedCachesDirectory 721{ 722 NSString *escapedScopeKey = [self escapedResourceName:_appRecord.scopeKey]; 723 NSString *mainCachesDirectory = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject; 724 NSString *exponentCachesDirectory = [mainCachesDirectory stringByAppendingPathComponent:@"ExponentExperienceData"]; 725 return [[exponentCachesDirectory stringByAppendingPathComponent:escapedScopeKey] stringByStandardizingPath]; 726} 727 728- (void *)jsExecutorFactoryForBridge:(id)bridge 729{ 730 return [_versionManager versionedJsExecutorFactoryForBridge:bridge]; 731} 732 733@end 734