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