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