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 [_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, 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, 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 NSDictionary *)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[@"id"]; 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.experienceId) { 303 [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager setError:nil forExperienceId:_appRecord.experienceId]; 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.experienceId, @"EXKernelAppRecord.experienceId should be nonnull if we have a manifest with developer tools enabled"); 313 [[EXKernel sharedInstance] reloadAppWithExperienceId:_appRecord.experienceId]; 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.experienceId) { 350 [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager setError:error forExperienceId:_appRecord.experienceId]; 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.experienceId) { 436 [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager setError:error forExperienceId:_appRecord.experienceId]; 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.experienceId) { 551 [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager experienceFinishedLoadingWithId:self.appRecord.experienceId]; 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 NSDictionary *manifest = _appRecord.appLoader.manifest; 572 if (manifest) { 573 NSDictionary *manifestDeveloperConfig = manifest[@"developer"]; 574 BOOL isDeployedFromTool = (manifestDeveloperConfig && manifestDeveloperConfig[@"tool"] != nil); 575 return (isDeployedFromTool); 576 } 577 return false; 578} 579 580- (BOOL)requiresValidManifests 581{ 582 return YES; 583} 584 585- (void)showDevMenu 586{ 587 if ([self enablesDeveloperTools]) { 588 dispatch_async(dispatch_get_main_queue(), ^{ 589 [self.versionManager showDevMenuForBridge:self.reactBridge]; 590 }); 591 } 592} 593 594- (void)reloadBridge 595{ 596 if ([self enablesDeveloperTools]) { 597 [self.reactBridge reload]; 598 } 599} 600 601- (void)disableRemoteDebugging 602{ 603 if ([self enablesDeveloperTools]) { 604 [self.versionManager disableRemoteDebuggingForBridge:self.reactBridge]; 605 } 606} 607 608- (void)toggleRemoteDebugging 609{ 610 if ([self enablesDeveloperTools]) { 611 [self.versionManager toggleRemoteDebuggingForBridge:self.reactBridge]; 612 } 613} 614 615- (void)togglePerformanceMonitor 616{ 617 if ([self enablesDeveloperTools]) { 618 [self.versionManager togglePerformanceMonitorForBridge:self.reactBridge]; 619 } 620} 621 622- (void)toggleElementInspector 623{ 624 if ([self enablesDeveloperTools]) { 625 [self.versionManager toggleElementInspectorForBridge:self.reactBridge]; 626 } 627} 628 629- (NSDictionary<NSString *, NSString *> *)devMenuItems 630{ 631 return [self.versionManager devMenuItemsForBridge:self.reactBridge]; 632} 633 634- (void)selectDevMenuItemWithKey:(NSString *)key 635{ 636 dispatch_async(dispatch_get_main_queue(), ^{ 637 [self.versionManager selectDevMenuItemWithKey:key onBridge:self.reactBridge]; 638 }); 639} 640 641#pragma mark - RN configuration 642 643- (NSComparisonResult)_compareVersionTo:(NSUInteger)version 644{ 645 // Unversioned projects are always considered to be on the latest version 646 if (!_validatedVersion || _validatedVersion.length == 0 || [_validatedVersion isEqualToString:@"UNVERSIONED"]) { 647 return NSOrderedDescending; 648 } 649 650 NSUInteger projectVersionNumber = _validatedVersion.integerValue; 651 if (projectVersionNumber == version) { 652 return NSOrderedSame; 653 } 654 return (projectVersionNumber < version) ? NSOrderedAscending : NSOrderedDescending; 655} 656 657- (NSDictionary *)launchOptionsForBridge 658{ 659 if ([EXEnvironment sharedEnvironment].isDetached) { 660 // pass the native app's launch options to standalone bridge. 661 return [ExpoKit sharedInstance].launchOptions; 662 } 663 return @{}; 664} 665 666- (Class)moduleRegistryDelegateClass 667{ 668 if ([EXEnvironment sharedEnvironment].isDetached) { 669 return [ExpoKit sharedInstance].moduleRegistryDelegateClass; 670 } 671 return nil; 672} 673 674- (NSString *)applicationKeyForRootView 675{ 676 NSDictionary *manifest = _appRecord.appLoader.manifest; 677 if (manifest && manifest[@"appKey"]) { 678 return manifest[@"appKey"]; 679 } 680 681 NSURL *bundleUrl = [self bundleUrl]; 682 if (bundleUrl) { 683 NSURLComponents *components = [NSURLComponents componentsWithURL:bundleUrl resolvingAgainstBaseURL:YES]; 684 NSArray<NSURLQueryItem *> *queryItems = components.queryItems; 685 for (NSURLQueryItem *item in queryItems) { 686 if ([item.name isEqualToString:@"app"]) { 687 return item.value; 688 } 689 } 690 } 691 692 return @"main"; 693} 694 695- (NSDictionary * _Nullable)initialPropertiesForRootView 696{ 697 NSMutableDictionary *props = [NSMutableDictionary dictionary]; 698 NSMutableDictionary *expProps = [NSMutableDictionary dictionary]; 699 700 NSAssert(_appRecord.experienceId, @"Experience id should be nonnull when getting initial properties for root view"); 701 702 NSDictionary *errorRecoveryProps = [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager developerInfoForExperienceId:_appRecord.experienceId]; 703 if ([[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager experienceIdIsRecoveringFromError:_appRecord.experienceId]) { 704 [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager increaseAutoReloadBuffer]; 705 if (errorRecoveryProps) { 706 expProps[@"errorRecovery"] = errorRecoveryProps; 707 } 708 } 709 710 expProps[@"shell"] = @(_appRecord == [EXKernel sharedInstance].appRegistry.standaloneAppRecord); 711 expProps[@"appOwnership"] = [self _appOwnership]; 712 if (_initialProps) { 713 [expProps addEntriesFromDictionary:_initialProps]; 714 } 715 EXPendingNotification *initialNotification = [[EXKernel sharedInstance].serviceRegistry.notificationsManager initialNotificationForExperience:_appRecord.experienceId]; 716 if (initialNotification) { 717 expProps[@"notification"] = initialNotification.properties; 718 } 719 720 NSString *manifestString = nil; 721 if (_appRecord.appLoader.manifest && [NSJSONSerialization isValidJSONObject:_appRecord.appLoader.manifest]) { 722 NSError *error; 723 NSData *jsonData = [NSJSONSerialization dataWithJSONObject:_appRecord.appLoader.manifest options:0 error:&error]; 724 if (jsonData) { 725 manifestString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; 726 } else { 727 DDLogWarn(@"Failed to serialize JSON manifest: %@", error); 728 } 729 } 730 731 expProps[@"manifestString"] = manifestString; 732 if (_appRecord.appLoader.manifestUrl) { 733 expProps[@"initialUri"] = [_appRecord.appLoader.manifestUrl absoluteString]; 734 } 735 props[@"exp"] = expProps; 736 return props; 737} 738 739- (NSString *)_appOwnership 740{ 741 if (_appRecord == [EXKernel sharedInstance].appRegistry.standaloneAppRecord) { 742 return @"standalone"; 743 } 744 return @"expo"; 745} 746 747- (NSString *)_executionEnvironment 748{ 749 if ([EXEnvironment sharedEnvironment].isDetached) { 750 return EXConstantsExecutionEnvironmentStandalone; 751 } else { 752 return EXConstantsExecutionEnvironmentStoreClient; 753 } 754} 755 756- (NSString *)scopedDocumentDirectory 757{ 758 NSString *escapedExperienceId = [self escapedResourceName:_appRecord.experienceId]; 759 NSString *mainDocumentDirectory = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; 760 NSString *exponentDocumentDirectory = [mainDocumentDirectory stringByAppendingPathComponent:@"ExponentExperienceData"]; 761 return [[exponentDocumentDirectory stringByAppendingPathComponent:escapedExperienceId] stringByStandardizingPath]; 762} 763 764- (NSString *)scopedCachesDirectory 765{ 766 NSString *escapedExperienceId = [self escapedResourceName:_appRecord.experienceId]; 767 NSString *mainCachesDirectory = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject; 768 NSString *exponentCachesDirectory = [mainCachesDirectory stringByAppendingPathComponent:@"ExponentExperienceData"]; 769 return [[exponentCachesDirectory stringByAppendingPathComponent:escapedExperienceId] stringByStandardizingPath]; 770} 771 772- (void *)jsExecutorFactoryForBridge:(id)bridge 773{ 774 return [_versionManager versionedJsExecutorFactoryForBridge:bridge]; 775} 776 777@end 778