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