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