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.rawManifestJSON, 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.rawManifestJSON, 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 EXUpdatesRawManifest *)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.rawID; 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 EXUpdatesRawManifest *manifest = _appRecord.appLoader.manifest; 573 if (manifest) { 574 return manifest.isUsingDeveloperTool; 575 } 576 return false; 577} 578 579- (BOOL)requiresValidManifests 580{ 581 return YES; 582} 583 584- (void)showDevMenu 585{ 586 if ([self enablesDeveloperTools]) { 587 dispatch_async(dispatch_get_main_queue(), ^{ 588 [self.versionManager showDevMenuForBridge:self.reactBridge]; 589 }); 590 } 591} 592 593- (void)reloadBridge 594{ 595 if ([self enablesDeveloperTools]) { 596 [self.reactBridge reload]; 597 } 598} 599 600- (void)disableRemoteDebugging 601{ 602 if ([self enablesDeveloperTools]) { 603 [self.versionManager disableRemoteDebuggingForBridge:self.reactBridge]; 604 } 605} 606 607- (void)toggleRemoteDebugging 608{ 609 if ([self enablesDeveloperTools]) { 610 [self.versionManager toggleRemoteDebuggingForBridge:self.reactBridge]; 611 } 612} 613 614- (void)togglePerformanceMonitor 615{ 616 if ([self enablesDeveloperTools]) { 617 [self.versionManager togglePerformanceMonitorForBridge:self.reactBridge]; 618 } 619} 620 621- (void)toggleElementInspector 622{ 623 if ([self enablesDeveloperTools]) { 624 [self.versionManager toggleElementInspectorForBridge:self.reactBridge]; 625 } 626} 627 628- (void)toggleDevMenu 629{ 630 if ([EXEnvironment sharedEnvironment].isDetached) { 631 [[EXKernel sharedInstance].visibleApp.appManager showDevMenu]; 632 } else { 633 [[EXKernel sharedInstance] switchTasks]; 634 } 635} 636 637- (void)setupWebSocketControls 638{ 639#if DEBUG || RCT_DEV 640 if ([self enablesDeveloperTools]) { 641 if ([_versionManager respondsToSelector:@selector(addWebSocketNotificationHandler:queue:forMethod:)]) { 642 __weak __typeof(self) weakSelf = self; 643 644 // Attach listeners to the bundler's dev server web socket connection. 645 // This enables tools to automatically reload the client remotely (i.e. in expo-cli). 646 647 // Enable a lot of tools under the same command namespace 648 [_versionManager addWebSocketNotificationHandler:^(id params) { 649 if (params != [NSNull null] && (NSDictionary *)params) { 650 NSDictionary *_params = (NSDictionary *)params; 651 if (_params[@"name"] != nil && (NSString *)_params[@"name"]) { 652 NSString *name = _params[@"name"]; 653 if ([name isEqualToString:@"reload"]) { 654 [[EXKernel sharedInstance] reloadVisibleApp]; 655 } else if ([name isEqualToString:@"toggleDevMenu"]) { 656 [weakSelf toggleDevMenu]; 657 } else if ([name isEqualToString:@"toggleRemoteDebugging"]) { 658 [weakSelf toggleRemoteDebugging]; 659 } else if ([name isEqualToString:@"toggleElementInspector"]) { 660 [weakSelf toggleElementInspector]; 661 } else if ([name isEqualToString:@"togglePerformanceMonitor"]) { 662 [weakSelf togglePerformanceMonitor]; 663 } 664 } 665 } 666 } 667 queue:dispatch_get_main_queue() 668 forMethod:@"sendDevCommand"]; 669 670 // These (reload and devMenu) are here to match RN dev tooling. 671 672 // Reload the app on "reload" 673 [_versionManager addWebSocketNotificationHandler:^(id params) { 674 [[EXKernel sharedInstance] reloadVisibleApp]; 675 } 676 queue:dispatch_get_main_queue() 677 forMethod:@"reload"]; 678 679 // Open the dev menu on "devMenu" 680 [_versionManager addWebSocketNotificationHandler:^(id params) { 681 [weakSelf toggleDevMenu]; 682 } 683 queue:dispatch_get_main_queue() 684 forMethod:@"devMenu"]; 685 } 686 } 687#endif 688} 689 690- (NSDictionary<NSString *, NSString *> *)devMenuItems 691{ 692 return [self.versionManager devMenuItemsForBridge:self.reactBridge]; 693} 694 695- (void)selectDevMenuItemWithKey:(NSString *)key 696{ 697 dispatch_async(dispatch_get_main_queue(), ^{ 698 [self.versionManager selectDevMenuItemWithKey:key onBridge:self.reactBridge]; 699 }); 700} 701 702#pragma mark - RN configuration 703 704- (NSComparisonResult)_compareVersionTo:(NSUInteger)version 705{ 706 // Unversioned projects are always considered to be on the latest version 707 if (!_validatedVersion || _validatedVersion.length == 0 || [_validatedVersion isEqualToString:@"UNVERSIONED"]) { 708 return NSOrderedDescending; 709 } 710 711 NSUInteger projectVersionNumber = _validatedVersion.integerValue; 712 if (projectVersionNumber == version) { 713 return NSOrderedSame; 714 } 715 return (projectVersionNumber < version) ? NSOrderedAscending : NSOrderedDescending; 716} 717 718- (NSDictionary *)launchOptionsForBridge 719{ 720 if ([EXEnvironment sharedEnvironment].isDetached) { 721 // pass the native app's launch options to standalone bridge. 722 return [ExpoKit sharedInstance].launchOptions; 723 } 724 return @{}; 725} 726 727- (Class)moduleRegistryDelegateClass 728{ 729 if ([EXEnvironment sharedEnvironment].isDetached) { 730 return [ExpoKit sharedInstance].moduleRegistryDelegateClass; 731 } 732 return nil; 733} 734 735- (NSString *)applicationKeyForRootView 736{ 737 EXUpdatesRawManifest *manifest = _appRecord.appLoader.manifest; 738 if (manifest && manifest.appKey) { 739 return manifest.appKey; 740 } 741 742 NSURL *bundleUrl = [self bundleUrl]; 743 if (bundleUrl) { 744 NSURLComponents *components = [NSURLComponents componentsWithURL:bundleUrl resolvingAgainstBaseURL:YES]; 745 NSArray<NSURLQueryItem *> *queryItems = components.queryItems; 746 for (NSURLQueryItem *item in queryItems) { 747 if ([item.name isEqualToString:@"app"]) { 748 return item.value; 749 } 750 } 751 } 752 753 return @"main"; 754} 755 756- (NSDictionary * _Nullable)initialPropertiesForRootView 757{ 758 NSMutableDictionary *props = [NSMutableDictionary dictionary]; 759 NSMutableDictionary *expProps = [NSMutableDictionary dictionary]; 760 761 NSAssert(_appRecord.experienceId, @"Experience id should be nonnull when getting initial properties for root view"); 762 763 NSDictionary *errorRecoveryProps = [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager developerInfoForExperienceId:_appRecord.experienceId]; 764 if ([[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager experienceIdIsRecoveringFromError:_appRecord.experienceId]) { 765 [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager increaseAutoReloadBuffer]; 766 if (errorRecoveryProps) { 767 expProps[@"errorRecovery"] = errorRecoveryProps; 768 } 769 } 770 771 expProps[@"shell"] = @(_appRecord == [EXKernel sharedInstance].appRegistry.standaloneAppRecord); 772 expProps[@"appOwnership"] = [self _appOwnership]; 773 if (_initialProps) { 774 [expProps addEntriesFromDictionary:_initialProps]; 775 } 776 EXPendingNotification *initialNotification = [[EXKernel sharedInstance].serviceRegistry.notificationsManager initialNotificationForExperience:_appRecord.experienceId]; 777 if (initialNotification) { 778 expProps[@"notification"] = initialNotification.properties; 779 } 780 781 NSString *manifestString = nil; 782 if (_appRecord.appLoader.manifest && [NSJSONSerialization isValidJSONObject:_appRecord.appLoader.manifest]) { 783 NSError *error; 784 NSData *jsonData = [NSJSONSerialization dataWithJSONObject:_appRecord.appLoader.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 *escapedExperienceId = [self escapedResourceName:_appRecord.experienceId]; 820 NSString *mainDocumentDirectory = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; 821 NSString *exponentDocumentDirectory = [mainDocumentDirectory stringByAppendingPathComponent:@"ExponentExperienceData"]; 822 return [[exponentDocumentDirectory stringByAppendingPathComponent:escapedExperienceId] stringByStandardizingPath]; 823} 824 825- (NSString *)scopedCachesDirectory 826{ 827 NSString *escapedExperienceId = [self escapedResourceName:_appRecord.experienceId]; 828 NSString *mainCachesDirectory = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject; 829 NSString *exponentCachesDirectory = [mainCachesDirectory stringByAppendingPathComponent:@"ExponentExperienceData"]; 830 return [[exponentCachesDirectory stringByAppendingPathComponent:escapedExperienceId] stringByStandardizingPath]; 831} 832 833- (void *)jsExecutorFactoryForBridge:(id)bridge 834{ 835 return [_versionManager versionedJsExecutorFactoryForBridge:bridge]; 836} 837 838@end 839