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