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 "EXAppLoader.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 <UMCore/UMModuleRegistryProvider.h> 19#import <EXConstants/EXConstantsService.h> 20#import <EXSplashScreen/EXSplashScreenService.h> 21 22#import <React/RCTBridge.h> 23#import <React/RCTCxxBridgeDelegate.h> 24#import <React/JSCExecutorFactory.h> 25#import <React/RCTRootView.h> 26 27@interface EXVersionManager (Legacy) 28// TODO: remove after non-unimodules SDK versions are dropped 29 30- (void)bridgeDidForeground; 31- (void)bridgeDidBackground; 32 33@end 34 35typedef void (^SDK21RCTSourceLoadBlock)(NSError *error, NSData *source, int64_t sourceLength); 36 37/** 38 * TODO: Remove once SDK 38 is phased out. 39 */ 40@protocol PreSDK39EXSplashScreenManagerProtocol 41 42@property (assign) BOOL started; 43@property (assign) BOOL finished; 44 45@end 46 47@implementation RCTSource (EXReactAppManager) 48 49- (instancetype)initWithURL:(nonnull NSURL *)url data:(nonnull NSData *)data 50{ 51 if (self = [super init]) { 52 // Use KVO since RN publicly declares these properties as readonly and privately defines the 53 // ivars 54 [self setValue:url forKey:@"url"]; 55 [self setValue:data forKey:@"data"]; 56 [self setValue:@(data.length) forKey:@"length"]; 57 [self setValue:@(RCTSourceFilesChangedCountNotBuiltByBundler) forKey:@"filesChangedCount"]; 58 } 59 return self; 60} 61 62@end 63 64@interface EXReactAppManager () <RCTBridgeDelegate, RCTCxxBridgeDelegate> 65 66@property (nonatomic, strong) UIView * __nullable reactRootView; 67@property (nonatomic, copy) RCTSourceLoadBlock loadCallback; 68@property (nonatomic, strong) NSDictionary *initialProps; 69@property (nonatomic, strong) NSTimer *viewTestTimer; 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 [self _invalidateAndClearDelegate:NO]; 117 [self computeVersionSymbolPrefix]; 118 119 if ([self isReadyToLoad]) { 120 Class versionManagerClass = [self versionedClassFromString:@"EXVersionManager"]; 121 Class bridgeClass = [self versionedClassFromString:@"RCTBridge"]; 122 Class rootViewClass = [self versionedClassFromString:@"RCTRootView"]; 123 124 _versionManager = [[versionManagerClass alloc] initWithParams:[self extraParams] 125 manifest:_appRecord.appLoader.manifest 126 fatalHandler:handleFatalReactError 127 logFunction:[self logFunction] 128 logThreshold:[self logLevel]]; 129 _reactBridge = [[bridgeClass alloc] initWithDelegate:self launchOptions:[self launchOptionsForBridge]]; 130 131 if (!_isHeadless) { 132 // We don't want to run the whole JS app if app launches in the background, 133 // so we're omitting creation of RCTRootView that triggers runApplication and sets up React view hierarchy. 134 _reactRootView = [[rootViewClass alloc] initWithBridge:_reactBridge 135 moduleName:[self applicationKeyForRootView] 136 initialProperties:[self initialPropertiesForRootView]]; 137 } 138 139 [self setupWebSocketControls]; 140 [_delegate reactAppManagerIsReadyForLoad:self]; 141 142 NSAssert([_reactBridge isLoading], @"React bridge should be loading once initialized"); 143 [_versionManager bridgeWillStartLoading:_reactBridge]; 144 } 145} 146 147- (NSDictionary *)extraParams 148{ 149 // we allow the vanilla RN dev menu in some circumstances. 150 BOOL isStandardDevMenuAllowed = [EXEnvironment sharedEnvironment].isDetached; 151 NSMutableDictionary *params = [NSMutableDictionary dictionaryWithDictionary:@{ 152 @"manifest": _appRecord.appLoader.manifest.rawManifestJSON, 153 @"constants": @{ 154 @"linkingUri": RCTNullIfNil([EXKernelLinkingManager linkingUriForExperienceUri:_appRecord.appLoader.manifestUrl useLegacy:[self _compareVersionTo:27] == NSOrderedAscending]), 155 @"experienceUrl": RCTNullIfNil(_appRecord.appLoader.manifestUrl? _appRecord.appLoader.manifestUrl.absoluteString: nil), 156 @"expoRuntimeVersion": [EXBuildConstants sharedInstance].expoRuntimeVersion, 157 @"manifest": _appRecord.appLoader.manifest.rawManifestJSON, 158 @"executionEnvironment": [self _executionEnvironment], 159 @"appOwnership": [self _appOwnership], 160 @"isHeadless": @(_isHeadless), 161 @"supportedExpoSdks": [EXVersions sharedInstance].versions[@"sdkVersions"], 162 }, 163 @"exceptionsManagerDelegate": _exceptionHandler, 164 @"initialUri": RCTNullIfNil([EXKernelLinkingManager initialUriWithManifestUrl:_appRecord.appLoader.manifestUrl]), 165 @"isDeveloper": @([self enablesDeveloperTools]), 166 @"isStandardDevMenuAllowed": @(isStandardDevMenuAllowed), 167 @"testEnvironment": @([EXEnvironment sharedEnvironment].testEnvironment), 168 @"services": [EXKernel sharedInstance].serviceRegistry.allServices, 169 @"singletonModules": [UMModuleRegistryProvider singletonModules], 170 @"moduleRegistryDelegateClass": RCTNullIfNil([self moduleRegistryDelegateClass]), 171 }]; 172 if ([@"expo" isEqualToString:[self _appOwnership]]) { 173 [params addEntriesFromDictionary:@{ 174 @"fileSystemDirectories": @{ 175 @"documentDirectory": [self scopedDocumentDirectory], 176 @"cachesDirectory": [self scopedCachesDirectory] 177 } 178 }]; 179 } 180 return params; 181} 182 183- (void)invalidate 184{ 185 [self _invalidateAndClearDelegate:YES]; 186} 187 188- (void)_invalidateAndClearDelegate:(BOOL)clearDelegate 189{ 190 [self _stopObservingBridgeNotifications]; 191 if (_viewTestTimer) { 192 [_viewTestTimer invalidate]; 193 _viewTestTimer = nil; 194 } 195 if (_versionManager) { 196 [_versionManager invalidate]; 197 _versionManager = nil; 198 } 199 if (_reactRootView) { 200 [_reactRootView removeFromSuperview]; 201 _reactRootView = nil; 202 } 203 if (_reactBridge) { 204 [_reactBridge invalidate]; 205 _reactBridge = nil; 206 if (_delegate) { 207 [_delegate reactAppManagerDidInvalidate:self]; 208 if (clearDelegate) { 209 _delegate = nil; 210 } 211 } 212 } 213 _isBridgeRunning = NO; 214 [self _invalidateVersionState]; 215} 216 217- (void)computeVersionSymbolPrefix 218{ 219 // TODO: ben: kernel checks detached versions here 220 _validatedVersion = [[EXVersions sharedInstance] availableSdkVersionForManifest:_appRecord.appLoader.manifest]; 221 _versionSymbolPrefix = [[EXVersions sharedInstance] symbolPrefixForSdkVersion:self.validatedVersion isKernel:NO]; 222} 223 224- (void)_invalidateVersionState 225{ 226 _versionSymbolPrefix = @""; 227 _validatedVersion = nil; 228} 229 230- (Class)versionedClassFromString: (NSString *)classString 231{ 232 return NSClassFromString([self versionedString:classString]); 233} 234 235- (NSString *)versionedString: (NSString *)string 236{ 237 return [EXVersions versionedString:string withPrefix:_versionSymbolPrefix]; 238} 239 240- (NSString *)escapedResourceName:(NSString *)string 241{ 242 NSString *charactersToEscape = @"!*'();:@&=+$,/?%#[]"; 243 NSCharacterSet *allowedCharacters = [[NSCharacterSet characterSetWithCharactersInString:charactersToEscape] invertedSet]; 244 return [string stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacters]; 245} 246 247- (BOOL)isReadyToLoad 248{ 249 if (_appRecord) { 250 return (_appRecord.appLoader.status == kEXAppLoaderStatusHasManifest || _appRecord.appLoader.status == kEXAppLoaderStatusHasManifestAndBundle); 251 } 252 return NO; 253} 254 255- (NSURL *)bundleUrl 256{ 257 return [EXApiUtil bundleUrlFromManifest:_appRecord.appLoader.manifest]; 258} 259 260- (void)appStateDidBecomeActive 261{ 262 if ([_versionManager respondsToSelector:@selector(bridgeDidForeground)]) { 263 // supported before SDK 29 / unimodules 264 [_versionManager bridgeDidForeground]; 265 } 266} 267 268- (void)appStateDidBecomeInactive 269{ 270 if ([_versionManager respondsToSelector:@selector(bridgeDidBackground)]) { 271 [_versionManager bridgeDidBackground]; 272 } 273} 274 275#pragma mark - EXAppFetcherDataSource 276 277- (NSString *)bundleResourceNameForAppFetcher:(EXAppFetcher *)appFetcher withManifest:(nonnull EXUpdatesRawManifest *)manifest 278{ 279 if ([EXEnvironment sharedEnvironment].isDetached) { 280 NSLog(@"Standalone bundle remote url is %@", [EXEnvironment sharedEnvironment].standaloneManifestUrl); 281 return kEXEmbeddedBundleResourceName; 282 } else { 283 return manifest.legacyId; 284 } 285} 286 287- (BOOL)appFetcherShouldInvalidateBundleCache:(EXAppFetcher *)appFetcher 288{ 289 return NO; 290} 291 292#pragma mark - RCTBridgeDelegate 293 294- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 295{ 296 return [self bundleUrl]; 297} 298 299- (void)loadSourceForBridge:(RCTBridge *)bridge withBlock:(RCTSourceLoadBlock)loadCallback 300{ 301 // clear any potentially old loading state 302 if (_appRecord.scopeKey) { 303 [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager setError:nil forScopeKey:_appRecord.scopeKey]; 304 } 305 [self _stopObservingBridgeNotifications]; 306 [self _startObservingBridgeNotificationsForBridge:bridge]; 307 308 if ([self enablesDeveloperTools]) { 309 if ([_appRecord.appLoader supportsBundleReload]) { 310 [_appRecord.appLoader forceBundleReload]; 311 } else { 312 NSAssert(_appRecord.scopeKey, @"EXKernelAppRecord.scopeKey should be nonnull if we have a manifest with developer tools enabled"); 313 [[EXKernel sharedInstance] reloadAppWithScopeKey:_appRecord.scopeKey]; 314 } 315 } 316 317 _loadCallback = loadCallback; 318 if (_appRecord.appLoader.status == kEXAppLoaderStatusHasManifestAndBundle) { 319 // finish loading immediately (app loader won't call this since it's already done) 320 [self appLoaderFinished]; 321 } else { 322 // wait for something else to call `appLoaderFinished` or `appLoaderFailed` later. 323 } 324} 325 326- (NSArray *)extraModulesForBridge:(RCTBridge *)bridge 327{ 328 return [self.versionManager extraModulesForBridge:bridge]; 329} 330 331- (void)appLoaderFinished 332{ 333 NSData *data = _appRecord.appLoader.bundle; 334 if (_loadCallback) { 335 if ([self _compareVersionTo:22] == NSOrderedAscending) { 336 SDK21RCTSourceLoadBlock legacyLoadCallback = (SDK21RCTSourceLoadBlock)_loadCallback; 337 legacyLoadCallback(nil, data, data.length); 338 } else { 339 _loadCallback(nil, [[RCTSource alloc] initWithURL:[self bundleUrl] data:data]); 340 } 341 _loadCallback = nil; 342 } 343} 344 345- (void)appLoaderFailedWithError:(NSError *)error 346{ 347 // RN is going to call RCTFatal() on this error, so keep a reference to it for later 348 // so we can distinguish this non-fatal error from actual fatal cases. 349 if (_appRecord.scopeKey) { 350 [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager setError:error forScopeKey:_appRecord.scopeKey]; 351 } 352 353 // react won't post this for us 354 [[NSNotificationCenter defaultCenter] postNotificationName:[self versionedString:RCTJavaScriptDidFailToLoadNotification] object:error]; 355 356 if (_loadCallback) { 357 if ([self _compareVersionTo:22] == NSOrderedAscending) { 358 SDK21RCTSourceLoadBlock legacyLoadCallback = (SDK21RCTSourceLoadBlock)_loadCallback; 359 legacyLoadCallback(error, nil, 0); 360 } else { 361 _loadCallback(error, nil); 362 } 363 _loadCallback = nil; 364 } 365} 366 367#pragma mark - JavaScript loading 368 369- (void)_startObservingBridgeNotificationsForBridge:(RCTBridge *)bridge 370{ 371 NSAssert(bridge, @"Must subscribe to loading notifs for a non-null bridge"); 372 373 [[NSNotificationCenter defaultCenter] addObserver:self 374 selector:@selector(_handleJavaScriptStartLoadingEvent:) 375 name:[self versionedString:RCTJavaScriptWillStartLoadingNotification] 376 object:bridge]; 377 [[NSNotificationCenter defaultCenter] addObserver:self 378 selector:@selector(_handleJavaScriptLoadEvent:) 379 name:[self versionedString:RCTJavaScriptDidLoadNotification] 380 object:bridge]; 381 [[NSNotificationCenter defaultCenter] addObserver:self 382 selector:@selector(_handleJavaScriptLoadEvent:) 383 name:[self versionedString:RCTJavaScriptDidFailToLoadNotification] 384 object:bridge]; 385 [[NSNotificationCenter defaultCenter] addObserver:self 386 selector:@selector(_handleReactContentEvent:) 387 name:[self versionedString:RCTContentDidAppearNotification] 388 object:nil]; 389 [[NSNotificationCenter defaultCenter] addObserver:self 390 selector:@selector(_handleBridgeEvent:) 391 name:[self versionedString:RCTBridgeWillReloadNotification] 392 object:bridge]; 393} 394 395- (void)_stopObservingBridgeNotifications 396{ 397 [[NSNotificationCenter defaultCenter] removeObserver:self name:[self versionedString:RCTJavaScriptWillStartLoadingNotification] object:_reactBridge]; 398 [[NSNotificationCenter defaultCenter] removeObserver:self name:[self versionedString:RCTJavaScriptDidLoadNotification] object:_reactBridge]; 399 [[NSNotificationCenter defaultCenter] removeObserver:self name:[self versionedString:RCTJavaScriptDidFailToLoadNotification] object:_reactBridge]; 400 [[NSNotificationCenter defaultCenter] removeObserver:self name:[self versionedString:RCTContentDidAppearNotification] object:_reactBridge]; 401 [[NSNotificationCenter defaultCenter] removeObserver:self name:[self versionedString:RCTBridgeWillReloadNotification] object:_reactBridge]; 402} 403 404- (void)_handleJavaScriptStartLoadingEvent:(NSNotification *)notification 405{ 406 __weak __typeof(self) weakSelf = self; 407 dispatch_async(dispatch_get_main_queue(), ^{ 408 __strong __typeof(self) strongSelf = weakSelf; 409 if (strongSelf) { 410 [strongSelf.delegate reactAppManagerStartedLoadingJavaScript:strongSelf]; 411 } 412 }); 413} 414 415- (void)_handleJavaScriptLoadEvent:(NSNotification *)notification 416{ 417 if ([notification.name isEqualToString:[self versionedString:RCTJavaScriptDidLoadNotification]]) { 418 _isBridgeRunning = YES; 419 _hasBridgeEverLoaded = YES; 420 [_versionManager bridgeFinishedLoading:_reactBridge]; 421 [self appStateDidBecomeActive]; 422 423 // TODO: temporary solution for hiding LoadingProgressWindow 424 if (_appRecord.viewController) { 425 [_appRecord.viewController hideLoadingProgressWindow]; 426 } 427 428 // TODO: To be removed once SDK 38 is phased out 429 // Above SDK 38 this code is invoked in different place 430 if ([self _compareVersionTo:39] == NSOrderedAscending) { 431 [self _preSDK39BeginWaitingForAppLoading]; 432 } 433 } else if ([notification.name isEqualToString:[self versionedString:RCTJavaScriptDidFailToLoadNotification]]) { 434 NSError *error = (notification.userInfo) ? notification.userInfo[@"error"] : nil; 435 if (_appRecord.scopeKey) { 436 [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager setError:error forScopeKey:_appRecord.scopeKey]; 437 } 438 439 UM_WEAKIFY(self); 440 dispatch_async(dispatch_get_main_queue(), ^{ 441 UM_ENSURE_STRONGIFY(self); 442 [self.delegate reactAppManager:self failedToLoadJavaScriptWithError:error]; 443 }); 444 } 445} 446 447# pragma mark app loading & splash screen 448 449- (void)_handleReactContentEvent:(NSNotification *)notification 450{ 451 if ([notification.name isEqualToString:[self versionedString:RCTContentDidAppearNotification]] 452 && notification.object == self.reactRootView) { 453 UM_WEAKIFY(self); 454 dispatch_async(dispatch_get_main_queue(), ^{ 455 UM_ENSURE_STRONGIFY(self); 456 [self.delegate reactAppManagerAppContentDidAppear:self]; 457 458 if ([self _compareVersionTo:38] == NSOrderedDescending) { 459 // Post SDK 38 code 460 // Up to SDK 38 this code is invoked in different place 461 [self _appLoadingFinished]; 462 } 463 }); 464 } 465} 466 467- (void)_handleBridgeEvent:(NSNotification *)notification 468{ 469 if ([notification.name isEqualToString:[self versionedString:RCTBridgeWillReloadNotification]]) { 470 UM_WEAKIFY(self); 471 dispatch_async(dispatch_get_main_queue(), ^{ 472 UM_ENSURE_STRONGIFY(self); 473 [self.delegate reactAppManagerAppContentWillReload:self]; 474 }); 475 } 476} 477 478/** 479 * TODO: Remove once SDK 38 is phased out. 480 */ 481- (void)_preSDK39BeginWaitingForAppLoading 482{ 483 if (_viewTestTimer) { 484 [_viewTestTimer invalidate]; 485 _viewTestTimer = nil; 486 } 487 488 // SplashScreen.preventAutoHide is called despite actual JS method call. 489 // Prior SDK 39, SplashScreen was basing on started & finished flags that are set via legacy Expo.SplashScreen JS methods calls. 490 EXSplashScreenService *splashScreenService = (EXSplashScreenService *)[UMModuleRegistryProvider getSingletonModuleForClass:[EXSplashScreenService class]]; 491 [splashScreenService preventSplashScreenAutoHideFor:(UIViewController *) _appRecord.viewController 492 successCallback:^(BOOL hasEffect) {} 493 failureCallback:^(NSString * _Nonnull message) { RCTLogWarn(@"%@", message); }]; 494 _viewTestTimer = [NSTimer scheduledTimerWithTimeInterval:0.02 495 target:self 496 selector:@selector(_preSDK39CheckAppFinishedLoading:) 497 userInfo:nil 498 repeats:YES]; 499} 500 501/** 502 * TODO: Remove once SDK 38 is phased out. 503 */ 504- (id)_preSDK39AppLoadingManagerInstance 505{ 506 Class loadingManagerClass = [self versionedClassFromString:@"EXSplashScreen"]; 507 for (Class klass in [self.reactBridge moduleClasses]) { 508 if ([klass isSubclassOfClass:loadingManagerClass]) { 509 return [self.reactBridge moduleForClass:loadingManagerClass]; 510 } 511 } 512 return nil; 513} 514 515/** 516 * TODO: Remove once SDK 38 is phased out. 517 */ 518- (void)_preSDK39CheckAppFinishedLoading:(NSTimer *)timer 519{ 520 // When root view has been filled with something, there are two cases: 521 // 1. AppLoading was never mounted, in which case we hide the loading indicator immediately 522 // 2. AppLoading was mounted, in which case we wait till it is unmounted to hide the loading indicator 523 if ([_appRecord.appManager rootView] && 524 [_appRecord.appManager rootView].subviews.count > 0 && 525 [_appRecord.appManager rootView].subviews.firstObject.subviews.count > 0) { 526 527 // Remove once SDK 38 is phased out. 528 id<PreSDK39EXSplashScreenManagerProtocol> splashManager = [self _preSDK39AppLoadingManagerInstance]; 529 530 // SplashScreen: at this point SplashScreen is prevented from autohiding, 531 // so we can safely hide it when the flags set. 532 if (!splashManager || !splashManager.started || splashManager.finished) { 533 [_viewTestTimer invalidate]; 534 _viewTestTimer = nil; 535 536 EXSplashScreenService *splashScreenService = (EXSplashScreenService *)[UMModuleRegistryProvider getSingletonModuleForClass:[EXSplashScreenService class]]; 537 [splashScreenService hideSplashScreenFor:(UIViewController *) _appRecord.viewController 538 successCallback:^(BOOL hasEffect) {} 539 failureCallback:^(NSString * _Nonnull message) { RCTLogWarn(@"%@", message); }]; 540 [self _appLoadingFinished]; 541 } 542 } 543} 544 545- (void)_appLoadingFinished 546{ 547 UM_WEAKIFY(self); 548 dispatch_async(dispatch_get_main_queue(), ^{ 549 UM_ENSURE_STRONGIFY(self); 550 if (self.appRecord.scopeKey) { 551 [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager experienceFinishedLoadingWithScopeKey:self.appRecord.scopeKey]; 552 } 553 [self.delegate reactAppManagerFinishedLoadingJavaScript:self]; 554 }); 555} 556 557#pragma mark - dev tools 558 559- (RCTLogFunction)logFunction 560{ 561 return (([self enablesDeveloperTools]) ? EXDeveloperRCTLogFunction : EXDefaultRCTLogFunction); 562} 563 564- (RCTLogLevel)logLevel 565{ 566 return ([self enablesDeveloperTools]) ? RCTLogLevelInfo : RCTLogLevelWarning; 567} 568 569- (BOOL)enablesDeveloperTools 570{ 571 EXUpdatesRawManifest *manifest = _appRecord.appLoader.manifest; 572 if (manifest) { 573 return manifest.isUsingDeveloperTool; 574 } 575 return false; 576} 577 578- (BOOL)requiresValidManifests 579{ 580 return YES; 581} 582 583- (void)showDevMenu 584{ 585 if ([self enablesDeveloperTools]) { 586 dispatch_async(dispatch_get_main_queue(), ^{ 587 [self.versionManager showDevMenuForBridge:self.reactBridge]; 588 }); 589 } 590} 591 592- (void)reloadBridge 593{ 594 if ([self enablesDeveloperTools]) { 595 [self.reactBridge reload]; 596 } 597} 598 599- (void)disableRemoteDebugging 600{ 601 if ([self enablesDeveloperTools]) { 602 [self.versionManager disableRemoteDebuggingForBridge:self.reactBridge]; 603 } 604} 605 606- (void)toggleRemoteDebugging 607{ 608 if ([self enablesDeveloperTools]) { 609 [self.versionManager toggleRemoteDebuggingForBridge:self.reactBridge]; 610 } 611} 612 613- (void)togglePerformanceMonitor 614{ 615 if ([self enablesDeveloperTools]) { 616 [self.versionManager togglePerformanceMonitorForBridge:self.reactBridge]; 617 } 618} 619 620- (void)toggleElementInspector 621{ 622 if ([self enablesDeveloperTools]) { 623 [self.versionManager toggleElementInspectorForBridge:self.reactBridge]; 624 } 625} 626 627- (void)toggleDevMenu 628{ 629 if ([EXEnvironment sharedEnvironment].isDetached) { 630 [[EXKernel sharedInstance].visibleApp.appManager showDevMenu]; 631 } else { 632 [[EXKernel sharedInstance] switchTasks]; 633 } 634} 635 636- (void)setupWebSocketControls 637{ 638#if DEBUG || RCT_DEV 639 if ([self enablesDeveloperTools]) { 640 if ([_versionManager respondsToSelector:@selector(addWebSocketNotificationHandler:queue:forMethod:)]) { 641 __weak __typeof(self) weakSelf = self; 642 643 // Attach listeners to the bundler's dev server web socket connection. 644 // This enables tools to automatically reload the client remotely (i.e. in expo-cli). 645 646 // Enable a lot of tools under the same command namespace 647 [_versionManager addWebSocketNotificationHandler:^(id params) { 648 if (params != [NSNull null] && (NSDictionary *)params) { 649 NSDictionary *_params = (NSDictionary *)params; 650 if (_params[@"name"] != nil && (NSString *)_params[@"name"]) { 651 NSString *name = _params[@"name"]; 652 if ([name isEqualToString:@"reload"]) { 653 [[EXKernel sharedInstance] reloadVisibleApp]; 654 } else if ([name isEqualToString:@"toggleDevMenu"]) { 655 [weakSelf toggleDevMenu]; 656 } else if ([name isEqualToString:@"toggleRemoteDebugging"]) { 657 [weakSelf toggleRemoteDebugging]; 658 } else if ([name isEqualToString:@"toggleElementInspector"]) { 659 [weakSelf toggleElementInspector]; 660 } else if ([name isEqualToString:@"togglePerformanceMonitor"]) { 661 [weakSelf togglePerformanceMonitor]; 662 } 663 } 664 } 665 } 666 queue:dispatch_get_main_queue() 667 forMethod:@"sendDevCommand"]; 668 669 // These (reload and devMenu) are here to match RN dev tooling. 670 671 // Reload the app on "reload" 672 [_versionManager addWebSocketNotificationHandler:^(id params) { 673 [[EXKernel sharedInstance] reloadVisibleApp]; 674 } 675 queue:dispatch_get_main_queue() 676 forMethod:@"reload"]; 677 678 // Open the dev menu on "devMenu" 679 [_versionManager addWebSocketNotificationHandler:^(id params) { 680 [weakSelf toggleDevMenu]; 681 } 682 queue:dispatch_get_main_queue() 683 forMethod:@"devMenu"]; 684 } 685 } 686#endif 687} 688 689- (NSDictionary<NSString *, NSString *> *)devMenuItems 690{ 691 return [self.versionManager devMenuItemsForBridge:self.reactBridge]; 692} 693 694- (void)selectDevMenuItemWithKey:(NSString *)key 695{ 696 dispatch_async(dispatch_get_main_queue(), ^{ 697 [self.versionManager selectDevMenuItemWithKey:key onBridge:self.reactBridge]; 698 }); 699} 700 701#pragma mark - RN configuration 702 703- (NSComparisonResult)_compareVersionTo:(NSUInteger)version 704{ 705 // Unversioned projects are always considered to be on the latest version 706 if (!_validatedVersion || _validatedVersion.length == 0 || [_validatedVersion isEqualToString:@"UNVERSIONED"]) { 707 return NSOrderedDescending; 708 } 709 710 NSUInteger projectVersionNumber = _validatedVersion.integerValue; 711 if (projectVersionNumber == version) { 712 return NSOrderedSame; 713 } 714 return (projectVersionNumber < version) ? NSOrderedAscending : NSOrderedDescending; 715} 716 717- (NSDictionary *)launchOptionsForBridge 718{ 719 if ([EXEnvironment sharedEnvironment].isDetached) { 720 // pass the native app's launch options to standalone bridge. 721 return [ExpoKit sharedInstance].launchOptions; 722 } 723 return @{}; 724} 725 726- (Class)moduleRegistryDelegateClass 727{ 728 if ([EXEnvironment sharedEnvironment].isDetached) { 729 return [ExpoKit sharedInstance].moduleRegistryDelegateClass; 730 } 731 return nil; 732} 733 734- (NSString *)applicationKeyForRootView 735{ 736 EXUpdatesRawManifest *manifest = _appRecord.appLoader.manifest; 737 if (manifest && manifest.appKey) { 738 return manifest.appKey; 739 } 740 741 NSURL *bundleUrl = [self bundleUrl]; 742 if (bundleUrl) { 743 NSURLComponents *components = [NSURLComponents componentsWithURL:bundleUrl resolvingAgainstBaseURL:YES]; 744 NSArray<NSURLQueryItem *> *queryItems = components.queryItems; 745 for (NSURLQueryItem *item in queryItems) { 746 if ([item.name isEqualToString:@"app"]) { 747 return item.value; 748 } 749 } 750 } 751 752 return @"main"; 753} 754 755- (NSDictionary * _Nullable)initialPropertiesForRootView 756{ 757 NSMutableDictionary *props = [NSMutableDictionary dictionary]; 758 NSMutableDictionary *expProps = [NSMutableDictionary dictionary]; 759 760 NSAssert(_appRecord.scopeKey, @"Experience scope key should be nonnull when getting initial properties for root view"); 761 762 NSDictionary *errorRecoveryProps = [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager developerInfoForScopeKey:_appRecord.scopeKey]; 763 if ([[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager scopeKeyIsRecoveringFromError:_appRecord.scopeKey]) { 764 [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager increaseAutoReloadBuffer]; 765 if (errorRecoveryProps) { 766 expProps[@"errorRecovery"] = errorRecoveryProps; 767 } 768 } 769 770 expProps[@"shell"] = @(_appRecord == [EXKernel sharedInstance].appRegistry.standaloneAppRecord); 771 expProps[@"appOwnership"] = [self _appOwnership]; 772 if (_initialProps) { 773 [expProps addEntriesFromDictionary:_initialProps]; 774 } 775 EXPendingNotification *initialNotification = [[EXKernel sharedInstance].serviceRegistry.notificationsManager initialNotification]; 776 if (initialNotification) { 777 expProps[@"notification"] = initialNotification.properties; 778 } 779 780 NSString *manifestString = nil; 781 EXUpdatesRawManifest *manifest = _appRecord.appLoader.manifest; 782 if (manifest && [NSJSONSerialization isValidJSONObject:manifest.rawManifestJSON]) { 783 NSError *error; 784 NSData *jsonData = [NSJSONSerialization dataWithJSONObject:manifest.rawManifestJSON options:0 error:&error]; 785 if (jsonData) { 786 manifestString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; 787 } else { 788 DDLogWarn(@"Failed to serialize JSON manifest: %@", error); 789 } 790 } 791 792 expProps[@"manifestString"] = manifestString; 793 if (_appRecord.appLoader.manifestUrl) { 794 expProps[@"initialUri"] = [_appRecord.appLoader.manifestUrl absoluteString]; 795 } 796 props[@"exp"] = expProps; 797 return props; 798} 799 800- (NSString *)_appOwnership 801{ 802 if (_appRecord == [EXKernel sharedInstance].appRegistry.standaloneAppRecord) { 803 return @"standalone"; 804 } 805 return @"expo"; 806} 807 808- (NSString *)_executionEnvironment 809{ 810 if ([EXEnvironment sharedEnvironment].isDetached) { 811 return EXConstantsExecutionEnvironmentStandalone; 812 } else { 813 return EXConstantsExecutionEnvironmentStoreClient; 814 } 815} 816 817- (NSString *)scopedDocumentDirectory 818{ 819 NSString *escapedScopeKey = [self escapedResourceName:_appRecord.scopeKey]; 820 NSString *mainDocumentDirectory = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; 821 NSString *exponentDocumentDirectory = [mainDocumentDirectory stringByAppendingPathComponent:@"ExponentExperienceData"]; 822 return [[exponentDocumentDirectory stringByAppendingPathComponent:escapedScopeKey] stringByStandardizingPath]; 823} 824 825- (NSString *)scopedCachesDirectory 826{ 827 NSString *escapedScopeKey = [self escapedResourceName:_appRecord.scopeKey]; 828 NSString *mainCachesDirectory = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject; 829 NSString *exponentCachesDirectory = [mainCachesDirectory stringByAppendingPathComponent:@"ExponentExperienceData"]; 830 return [[exponentCachesDirectory stringByAppendingPathComponent:escapedScopeKey] stringByStandardizingPath]; 831} 832 833- (void *)jsExecutorFactoryForBridge:(id)bridge 834{ 835 return [_versionManager versionedJsExecutorFactoryForBridge:bridge]; 836} 837 838@end 839