1#import <React/RCTBundleURLProvider.h> 2#import <React/RCTRootView.h> 3#import <React/RCTDevLoadingViewSetEnabled.h> 4#import <React/RCTDevMenu.h> 5#import <React/RCTDevSettings.h> 6#import <React/RCTRootContentView.h> 7#import <React/RCTAppearance.h> 8#import <React/RCTConstants.h> 9#import <React/RCTKeyCommands.h> 10 11#import <EXDevLauncher/EXDevLauncherController.h> 12#import <EXDevLauncher/EXDevLauncherRCTBridge.h> 13#import <EXDevLauncher/EXDevLauncherManifestParser.h> 14#import <EXDevLauncher/EXDevLauncherLoadingView.h> 15#import <EXDevLauncher/EXDevLauncherRCTDevSettings.h> 16#import <EXDevLauncher/EXDevLauncherUpdatesHelper.h> 17#import <EXDevLauncher/RCTPackagerConnection+EXDevLauncherPackagerConnectionInterceptor.h> 18 19#import <EXDevLauncher/EXDevLauncherBridgeDelegate.h> 20 21#if __has_include(<EXDevLauncher/EXDevLauncher-Swift.h>) 22// For cocoapods framework, the generated swift header will be inside EXDevLauncher module 23#import <EXDevLauncher/EXDevLauncher-Swift.h> 24#else 25#import <EXDevLauncher-Swift.h> 26#endif 27 28#ifdef RCT_NEW_ARCH_ENABLED 29#import <React/RCTSurfaceView.h> 30#endif 31 32@import EXManifests; 33@import EXDevMenu; 34 35#ifdef EX_DEV_LAUNCHER_VERSION 36#define STRINGIZE(x) #x 37#define STRINGIZE2(x) STRINGIZE(x) 38 39#define VERSION @ STRINGIZE2(EX_DEV_LAUNCHER_VERSION) 40#endif 41 42#define EX_DEV_LAUNCHER_PACKAGER_PATH @"index.bundle?platform=ios&dev=true&minify=false" 43 44 45@interface EXDevLauncherController () 46 47@property (nonatomic, weak) UIWindow *window; 48@property (nonatomic, weak) id<EXDevLauncherControllerDelegate> delegate; 49@property (nonatomic, strong) NSDictionary *launchOptions; 50@property (nonatomic, strong) NSURL *sourceUrl; 51@property (nonatomic, assign) BOOL shouldPreferUpdatesInterfaceSourceUrl; 52@property (nonatomic, strong) EXManifestsManifest *manifest; 53@property (nonatomic, strong) NSURL *manifestURL; 54@property (nonatomic, strong) NSURL *possibleManifestURL; 55@property (nonatomic, strong) EXDevLauncherErrorManager *errorManager; 56@property (nonatomic, strong) EXDevLauncherInstallationIDHelper *installationIDHelper; 57@property (nonatomic, strong) EXDevLauncherNetworkInterceptor *networkInterceptor; 58@property (nonatomic, assign) BOOL isStarted; 59@property (nonatomic, strong) EXDevLauncherBridgeDelegate *bridgeDelegate; 60@property (nonatomic, strong) NSURL *lastOpenedAppUrl; 61 62@end 63 64 65@implementation EXDevLauncherController 66 67+ (instancetype)sharedInstance 68{ 69 static EXDevLauncherController *theController; 70 static dispatch_once_t once; 71 dispatch_once(&once, ^{ 72 if (!theController) { 73 theController = [[EXDevLauncherController alloc] init]; 74 } 75 }); 76 return theController; 77} 78 79- (instancetype)init { 80 if (self = [super init]) { 81 self.recentlyOpenedAppsRegistry = [EXDevLauncherRecentlyOpenedAppsRegistry new]; 82 self.pendingDeepLinkRegistry = [EXDevLauncherPendingDeepLinkRegistry new]; 83 self.errorManager = [[EXDevLauncherErrorManager alloc] initWithController:self]; 84 self.installationIDHelper = [EXDevLauncherInstallationIDHelper new]; 85 self.networkInterceptor = [EXDevLauncherNetworkInterceptor new]; 86 self.shouldPreferUpdatesInterfaceSourceUrl = NO; 87 self.bridgeDelegate = [EXDevLauncherBridgeDelegate new]; 88 } 89 return self; 90} 91 92- (NSArray<id<RCTBridgeModule>> *)extraModulesForBridge:(RCTBridge *)bridge 93{ 94 95 NSMutableArray<id<RCTBridgeModule>> *modules = [NSMutableArray new]; 96 97 [modules addObject:[RCTDevMenu new]]; 98#ifndef EX_DEV_LAUNCHER_URL 99 [modules addObject:[EXDevLauncherRCTDevSettings new]]; 100#endif 101 [modules addObject:[EXDevLauncherLoadingView new]]; 102 103 return modules; 104} 105 106+ (NSString * _Nullable)version { 107#ifdef VERSION 108 return VERSION; 109#endif 110 return nil; 111} 112 113// Expo developers: Enable the below code by running 114// export EX_DEV_LAUNCHER_URL=http://localhost:8090 115// in your shell before doing pod install. This will cause the controller to see if 116// the expo-launcher packager is running, and if so, use that instead of 117// the prebuilt bundle. 118// See the pod_target_xcconfig definition in expo-dev-launcher.podspec 119 120- (nullable NSURL *)devLauncherBaseURL 121{ 122#ifdef EX_DEV_LAUNCHER_URL 123 return [NSURL URLWithString:@EX_DEV_LAUNCHER_URL]; 124#endif 125 return nil; 126} 127- (nullable NSURL *)devLauncherURL 128{ 129#ifdef EX_DEV_LAUNCHER_URL 130 return [NSURL URLWithString:EX_DEV_LAUNCHER_PACKAGER_PATH 131 relativeToURL:[self devLauncherBaseURL]]; 132#endif 133 return nil; 134} 135 136- (nullable NSURL *)devLauncherStatusURL 137{ 138#ifdef EX_DEV_LAUNCHER_URL 139 return [NSURL URLWithString:@"status" 140 relativeToURL:[self devLauncherBaseURL]]; 141#endif 142 return nil; 143} 144 145- (BOOL)isLauncherPackagerRunning 146{ 147 // Shamelessly copied from RN core (RCTBundleURLProvider) 148 149 // If we are not running in the main thread, run away 150 if (![NSThread isMainThread]) { 151 return NO; 152 } 153 154 NSURL *url = [self devLauncherStatusURL]; 155 NSURLSession *session = [NSURLSession sharedSession]; 156 NSURLRequest *request = [NSURLRequest requestWithURL:url 157 cachePolicy:NSURLRequestUseProtocolCachePolicy 158 timeoutInterval:1]; 159 __block NSURLResponse *response; 160 __block NSData *data; 161 162 dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); 163 [[session dataTaskWithRequest:request 164 completionHandler:^(NSData *d, NSURLResponse *res, __unused NSError *err) { 165 data = d; 166 response = res; 167 dispatch_semaphore_signal(semaphore); 168 }] resume]; 169 dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); 170 171 NSString *status = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; 172 return [status isEqualToString:@"packager-status:running"]; 173} 174 175- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 176{ 177 NSURL *launcherURL = [self devLauncherURL]; 178 if (launcherURL != nil && [self isLauncherPackagerRunning]) { 179 return launcherURL; 180 } 181 NSURL *bundleURL = [[NSBundle mainBundle] URLForResource:@"EXDevLauncher" withExtension:@"bundle"]; 182 return [[NSBundle bundleWithURL:bundleURL] URLForResource:@"main" withExtension:@"jsbundle"]; 183} 184 185 186- (void)clearRecentlyOpenedApps 187{ 188 return [_recentlyOpenedAppsRegistry clearRegistry]; 189} 190 191- (NSDictionary<UIApplicationLaunchOptionsKey, NSObject*> *)getLaunchOptions; 192{ 193 NSMutableDictionary *launchOptions = [self.launchOptions mutableCopy]; 194 NSURL *deepLink = [self.pendingDeepLinkRegistry consumePendingDeepLink]; 195 196 if (deepLink) { 197 // Passes pending deep link to initialURL if any 198 launchOptions[UIApplicationLaunchOptionsURLKey] = deepLink; 199 } else if (launchOptions[UIApplicationLaunchOptionsURLKey] && [EXDevLauncherURLHelper isDevLauncherURL:launchOptions[UIApplicationLaunchOptionsURLKey]]) { 200 // Strips initialURL if it is from myapp://expo-development-client/?url=... 201 // That would make dev-launcher acts like a normal app. 202 launchOptions[UIApplicationLaunchOptionsURLKey] = nil; 203 } 204 205 if ([launchOptions[UIApplicationLaunchOptionsUserActivityDictionaryKey][UIApplicationLaunchOptionsUserActivityTypeKey] isEqualToString:NSUserActivityTypeBrowsingWeb]) { 206 // Strips universal launch link if it is from https://expo-development-client/?url=... 207 // That would make dev-launcher acts like a normal app, though this case should rarely happen. 208 NSUserActivity *userActivity = launchOptions[UIApplicationLaunchOptionsUserActivityDictionaryKey][@"UIApplicationLaunchOptionsUserActivityKey"]; 209 if (userActivity.webpageURL && [EXDevLauncherURLHelper isDevLauncherURL:userActivity.webpageURL]) { 210 userActivity.webpageURL = nil; 211 } 212 } 213 214 return launchOptions; 215} 216 217- (EXManifestsManifest *)appManifest 218{ 219 return self.manifest; 220} 221 222- (NSURL * _Nullable)appManifestURL 223{ 224 return self.manifestURL; 225} 226 227- (nullable NSURL *)appManifestURLWithFallback 228{ 229 if (_manifestURL) { 230 return _manifestURL; 231 } 232 return _possibleManifestURL; 233} 234 235- (UIWindow *)currentWindow 236{ 237 return _window; 238} 239 240- (EXDevLauncherErrorManager *)errorManage 241{ 242 return _errorManager; 243} 244 245- (void)startWithWindow:(UIWindow *)window delegate:(id<EXDevLauncherControllerDelegate>)delegate launchOptions:(NSDictionary *)launchOptions 246{ 247 _isStarted = YES; 248 _delegate = delegate; 249 _launchOptions = launchOptions; 250 _window = window; 251 EXDevLauncherUncaughtExceptionHandler.isInstalled = true; 252 253 if (launchOptions[UIApplicationLaunchOptionsURLKey]) { 254 // For deeplink launch, we need the keyWindow for expo-splash-screen to setup correctly. 255 [_window makeKeyWindow]; 256 return; 257 } 258 259 BOOL shouldTryToLaunchLastOpenedBundle = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"DEV_CLIENT_TRY_TO_LAUNCH_LAST_BUNDLE"]; 260 if (_lastOpenedAppUrl != nil && shouldTryToLaunchLastOpenedBundle) { 261 [self loadApp:_lastOpenedAppUrl withProjectUrl:nil onSuccess:nil onError:^(NSError *error) { 262 __weak typeof(self) weakSelf = self; 263 dispatch_async(dispatch_get_main_queue(), ^{ 264 typeof(self) self = weakSelf; 265 if (!self) { 266 return; 267 } 268 269 [self navigateToLauncher]; 270 }); 271 }]; 272 return; 273 } 274 [self navigateToLauncher]; 275} 276 277- (void)autoSetupPrepare:(id<EXDevLauncherControllerDelegate>)delegate launchOptions:(NSDictionary * _Nullable)launchOptions 278{ 279 _delegate = delegate; 280 _launchOptions = launchOptions; 281 NSDictionary *lastOpenedApp = [self.recentlyOpenedAppsRegistry mostRecentApp]; 282 if (lastOpenedApp != nil) { 283 _lastOpenedAppUrl = [NSURL URLWithString:lastOpenedApp[@"url"]]; 284 } 285 EXDevLauncherBundleURLProviderInterceptor.isInstalled = true; 286} 287 288- (void)autoSetupStart:(UIWindow *)window 289{ 290 if (_delegate != nil) { 291 [self startWithWindow:window delegate:_delegate launchOptions:_launchOptions]; 292 } else { 293 @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"[EXDevLauncherController autoSetupStart:] was called before autoSetupPrepare:. Make sure you've set up expo-modules correctly in AppDelegate and are using ReactDelegate to create a bridge before calling [super application:didFinishLaunchingWithOptions:]." userInfo:nil]; 294 } 295} 296 297- (void)navigateToLauncher 298{ 299 NSAssert([NSThread isMainThread], @"This function must be called on main thread"); 300 301 [_appBridge invalidate]; 302 [self invalidateDevMenuApp]; 303 304 self.manifest = nil; 305 self.manifestURL = nil; 306 307 if (@available(iOS 12, *)) { 308 [self _applyUserInterfaceStyle:UIUserInterfaceStyleUnspecified]; 309 } 310 311 [self _removeInitModuleObserver]; 312 UIView *rootView = [_bridgeDelegate createRootViewWithModuleName:@"main" launchOptions:_launchOptions application:UIApplication.sharedApplication]; 313 _launcherBridge = _bridgeDelegate.bridge; 314 315 [self _ensureUserInterfaceStyleIsInSyncWithTraitEnv:rootView]; 316 317 [[NSNotificationCenter defaultCenter] addObserver:self 318 selector:@selector(onAppContentDidAppear) 319 name:RCTContentDidAppearNotification 320 object:rootView]; 321 322 rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1]; 323 324 UIViewController *rootViewController = [UIViewController new]; 325 rootViewController.view = rootView; 326 _window.rootViewController = rootViewController; 327 328#if RCT_DEV 329 NSURL *url = [self devLauncherURL]; 330 if (url != nil) { 331 // Connect to the websocket 332 [[RCTPackagerConnection sharedPackagerConnection] setSocketConnectionURL:url]; 333 } else { 334 [self _addInitModuleObserver]; 335 } 336#endif 337 338 [_window makeKeyAndVisible]; 339} 340 341- (BOOL)onDeepLink:(NSURL *)url options:(NSDictionary *)options 342{ 343 if (![EXDevLauncherURLHelper isDevLauncherURL:url]) { 344 return [self _handleExternalDeepLink:url options:options]; 345 } 346 347 if (![EXDevLauncherURLHelper hasUrlQueryParam:url]) { 348 // edgecase: this is a dev launcher url but it doesnt specify what url to open 349 // fallback to navigating to the launcher home screen 350 [self navigateToLauncher]; 351 return true; 352 } 353 354 [self loadApp:url onSuccess:nil onError:^(NSError *error) { 355 __weak typeof(self) weakSelf = self; 356 dispatch_async(dispatch_get_main_queue(), ^{ 357 typeof(self) self = weakSelf; 358 if (!self) { 359 return; 360 } 361 362 EXDevLauncherUrl *devLauncherUrl = [[EXDevLauncherUrl alloc] init:url]; 363 NSURL *appUrl = devLauncherUrl.url; 364 NSString *errorMessage = [NSString stringWithFormat:@"Failed to load app from %@ with error: %@", appUrl.absoluteString, error.localizedDescription]; 365 EXDevLauncherAppError *appError = [[EXDevLauncherAppError alloc] initWithMessage:errorMessage stack:nil]; 366 [self.errorManager showError:appError]; 367 }); 368 }]; 369 370 return true; 371} 372 373- (BOOL)_handleExternalDeepLink:(NSURL *)url options:(NSDictionary *)options 374{ 375 if ([self isAppRunning]) { 376 return false; 377 } 378 379 self.pendingDeepLinkRegistry.pendingDeepLink = url; 380 381 // cold boot -- need to initialize the dev launcher app RN app to handle the link 382 if (![_launcherBridge isValid]) { 383 [self navigateToLauncher]; 384 } 385 386 return true; 387} 388 389- (nullable NSURL *)sourceUrl 390{ 391 if (_shouldPreferUpdatesInterfaceSourceUrl && _updatesInterface && ((id<EXUpdatesExternalInterface>)_updatesInterface).launchAssetURL) { 392 return ((id<EXUpdatesExternalInterface>)_updatesInterface).launchAssetURL; 393 } 394 return _sourceUrl; 395} 396 397- (BOOL)isEASUpdateURL:(NSURL *)url 398{ 399 if ([url.host isEqual: @"u.expo.dev"]) { 400 return true; 401 } 402 403 return false; 404} 405 406-(void)loadApp:(NSURL *)url onSuccess:(void (^ _Nullable)(void))onSuccess onError:(void (^ _Nullable)(NSError *error))onError 407{ 408 [self loadApp:url withProjectUrl:nil onSuccess:onSuccess onError:onError]; 409} 410 411/** 412 * This method is the external entry point into loading an app with the dev launcher (e.g. via the 413 * dev launcher UI or a deep link). It takes a URL, determines what type of server it points to 414 * (react-native-cli, expo-cli, or published project), downloads a manifest if there is one, 415 * downloads all the project's assets (via expo-updates) in the case of a published project, and 416 * then calls `_initAppWithUrl:bundleUrl:manifest:` if successful. 417 */ 418- (void)loadApp:(NSURL *)url withProjectUrl:(NSURL * _Nullable)projectUrl onSuccess:(void (^ _Nullable)(void))onSuccess onError:(void (^ _Nullable)(NSError *error))onError 419{ 420 EXDevLauncherUrl *devLauncherUrl = [[EXDevLauncherUrl alloc] init:url]; 421 NSURL *expoUrl = devLauncherUrl.url; 422 [self _resetRemoteDebuggingForAppLoad]; 423 _possibleManifestURL = expoUrl; 424 BOOL isEASUpdate = [self isEASUpdateURL:expoUrl]; 425 426 // an update url requires a matching projectUrl 427 // if one isn't provided, default to the configured project url in Expo.plist 428 if (isEASUpdate && projectUrl == nil) { 429 NSString *projectUrlString = [self getUpdatesConfigForKey:@"EXUpdatesURL"]; 430 projectUrl = [NSURL URLWithString:projectUrlString]; 431 } 432 433 // if there is no project url and its not an updates url, the project url can be the same as the app url 434 if (!isEASUpdate && projectUrl == nil) { 435 projectUrl = expoUrl; 436 } 437 438 // Disable onboarding popup if "&disableOnboarding=1" is a param 439 [EXDevLauncherURLHelper disableOnboardingPopupIfNeeded:expoUrl]; 440 441 NSString *installationID = [_installationIDHelper getOrCreateInstallationID]; 442 443 NSDictionary *updatesConfiguration = [EXDevLauncherUpdatesHelper createUpdatesConfigurationWithURL:expoUrl 444 projectURL:projectUrl 445 installationID:installationID]; 446 447 void (^launchReactNativeApp)(void) = ^{ 448 self->_shouldPreferUpdatesInterfaceSourceUrl = NO; 449 RCTDevLoadingViewSetEnabled(NO); 450 [self.recentlyOpenedAppsRegistry appWasOpened:[expoUrl absoluteString] queryParams:devLauncherUrl.queryParams manifest:nil]; 451 if ([expoUrl.path isEqual:@"/"] || [expoUrl.path isEqual:@""]) { 452 [self _initAppWithUrl:expoUrl bundleUrl:[NSURL URLWithString:@"index.bundle?platform=ios&dev=true&minify=false" relativeToURL:expoUrl] manifest:nil]; 453 } else { 454 [self _initAppWithUrl:expoUrl bundleUrl:expoUrl manifest:nil]; 455 } 456 if (onSuccess) { 457 onSuccess(); 458 } 459 }; 460 461 void (^launchExpoApp)(NSURL *, EXManifestsManifest *) = ^(NSURL *bundleURL, EXManifestsManifest *manifest) { 462 self->_shouldPreferUpdatesInterfaceSourceUrl = !manifest.isUsingDeveloperTool; 463 RCTDevLoadingViewSetEnabled(manifest.isUsingDeveloperTool); 464 [self.recentlyOpenedAppsRegistry appWasOpened:[expoUrl absoluteString] queryParams:devLauncherUrl.queryParams manifest:manifest]; 465 [self _initAppWithUrl:expoUrl bundleUrl:bundleURL manifest:manifest]; 466 if (onSuccess) { 467 onSuccess(); 468 } 469 }; 470 471 if (_updatesInterface) { 472 [_updatesInterface reset]; 473 } 474 475 EXDevLauncherManifestParser *manifestParser = [[EXDevLauncherManifestParser alloc] initWithURL:expoUrl installationID:installationID session:[NSURLSession sharedSession]]; 476 477 void (^onIsManifestURL)(BOOL) = ^(BOOL isManifestURL) { 478 if (!isManifestURL) { 479 // assume this is a direct URL to a bundle hosted by metro 480 launchReactNativeApp(); 481 return; 482 } 483 484 if (!self->_updatesInterface) { 485 [manifestParser tryToParseManifest:^(EXManifestsManifest *manifest) { 486 if (!manifest.isUsingDeveloperTool) { 487 onError([NSError errorWithDomain:@"DevelopmentClient" code:1 userInfo:@{NSLocalizedDescriptionKey: @"expo-updates is not properly installed or integrated. In order to load published projects with this development client, follow all installation and setup instructions for both the expo-dev-client and expo-updates packages."}]); 488 return; 489 } 490 launchExpoApp([NSURL URLWithString:manifest.bundleUrl], manifest); 491 } onError:onError]; 492 return; 493 } 494 495 [self->_updatesInterface fetchUpdateWithConfiguration:updatesConfiguration onManifest:^BOOL(NSDictionary *manifest) { 496 EXManifestsManifest *devLauncherManifest = [EXManifestsManifestFactory manifestForManifestJSON:manifest]; 497 if (devLauncherManifest.isUsingDeveloperTool) { 498 // launch right away rather than continuing to load through EXUpdates 499 launchExpoApp([NSURL URLWithString:devLauncherManifest.bundleUrl], devLauncherManifest); 500 return NO; 501 } 502 return YES; 503 } progress:^(NSUInteger successfulAssetCount, NSUInteger failedAssetCount, NSUInteger totalAssetCount) { 504 // do nothing for now 505 } success:^(NSDictionary * _Nullable manifest) { 506 if (manifest) { 507 launchExpoApp(((id<EXUpdatesExternalInterface>)self->_updatesInterface).launchAssetURL, [EXManifestsManifestFactory manifestForManifestJSON:manifest]); 508 } 509 } error:onError]; 510 }; 511 512 [manifestParser isManifestURLWithCompletion:onIsManifestURL onError:^(NSError * _Nonnull error) { 513 if (@available(iOS 14, *)) { 514 // Try to retry if the network connection was rejected because of the luck of the lan network permission. 515 static BOOL shouldRetry = true; 516 NSString *host = expoUrl.host; 517 518 if (shouldRetry && ([host hasPrefix:@"192.168."] || [host hasPrefix:@"172."] || [host hasPrefix:@"10."])) { 519 shouldRetry = false; 520 [manifestParser isManifestURLWithCompletion:onIsManifestURL onError:onError]; 521 return; 522 } 523 } 524 525 onError(error); 526 }]; 527} 528 529/** 530 * Internal helper method for this class, which takes a bundle URL and (optionally) a manifest and 531 * launches the app in the bridge and UI. 532 * 533 * The bundle URL may point to a locally downloaded file (for published projects) or a remote 534 * packager server (for locally hosted projects in development). 535 */ 536- (void)_initAppWithUrl:(NSURL *)appUrl bundleUrl:(NSURL *)bundleUrl manifest:(EXManifestsManifest * _Nullable)manifest 537{ 538 self.manifest = manifest; 539 self.manifestURL = appUrl; 540 _possibleManifestURL = nil; 541 __block UIInterfaceOrientation orientation = [EXDevLauncherManifestHelper exportManifestOrientation:manifest.orientation]; 542 __block UIColor *backgroundColor = [EXDevLauncherManifestHelper hexStringToColor:manifest.iosOrRootBackgroundColor]; 543 544 __weak __typeof(self) weakSelf = self; 545 dispatch_async(dispatch_get_main_queue(), ^{ 546 if (!weakSelf) { 547 return; 548 } 549 __typeof(self) self = weakSelf; 550 551 self.sourceUrl = bundleUrl; 552 553#if RCT_DEV 554 // Connect to the websocket 555 [[RCTPackagerConnection sharedPackagerConnection] setSocketConnectionURL:bundleUrl]; 556#endif 557 558 if (@available(iOS 12, *)) { 559 UIUserInterfaceStyle userInterfaceStyle = [EXDevLauncherManifestHelper exportManifestUserInterfaceStyle:manifest.userInterfaceStyle]; 560 [self _applyUserInterfaceStyle:userInterfaceStyle]; 561 562 // Fix for the community react-native-appearance. 563 // RNC appearance checks the global trait collection and doesn't have another way to override the user interface. 564 // So we swap `currentTraitCollection` with one from the root view controller. 565 // Note that the root view controller will have the correct value of `userInterfaceStyle`. 566 if (@available(iOS 13.0, *)) { 567 if (userInterfaceStyle != UIUserInterfaceStyleUnspecified) { 568 UITraitCollection.currentTraitCollection = [self.window.rootViewController.traitCollection copy]; 569 } 570 } 571 } 572 573 [self _addInitModuleObserver]; 574 575 [self.delegate devLauncherController:self didStartWithSuccess:YES]; 576 577 [self setDevMenuAppBridge]; 578 579 [self _ensureUserInterfaceStyleIsInSyncWithTraitEnv:self.window.rootViewController]; 580 581 if (backgroundColor) { 582 self.window.rootViewController.view.backgroundColor = backgroundColor; 583 self.window.backgroundColor = backgroundColor; 584 } 585 586 if (self.updatesInterface) { 587 ((id<EXUpdatesExternalInterface>)self.updatesInterface).bridge = self.appBridge; 588 } 589 }); 590} 591 592- (BOOL)isAppRunning 593{ 594 return [_appBridge isValid]; 595} 596 597/** 598 * Temporary `expo-splash-screen` fix. 599 * 600 * The dev-launcher's bridge doesn't contain unimodules. So the module shows a splash screen but never hides. 601 * For now, we just remove the splash screen view when the launcher is loaded. 602 */ 603- (void)onAppContentDidAppear 604{ 605 [[NSNotificationCenter defaultCenter] removeObserver:self name:RCTContentDidAppearNotification object:nil]; 606 607 dispatch_async(dispatch_get_main_queue(), ^{ 608 #ifdef RCT_NEW_ARCH_ENABLED 609 #define EXPECTED_ROOT_VIEW RCTSurfaceView 610 #else 611 #define EXPECTED_ROOT_VIEW RCTRootContentView 612 #endif 613 NSArray<UIView *> *views = [[[self->_window rootViewController] view] subviews]; 614 for (UIView *view in views) { 615 if (![view isKindOfClass:[EXPECTED_ROOT_VIEW class]]) { 616 [view removeFromSuperview]; 617 } 618 } 619 #undef EXPECTED_ROOT_VIEW 620 }); 621} 622 623/** 624 * We need that function to sync the dev-menu user interface with the main application. 625 */ 626- (void)_ensureUserInterfaceStyleIsInSyncWithTraitEnv:(id<UITraitEnvironment>)env 627{ 628 [[NSNotificationCenter defaultCenter] postNotificationName:RCTUserInterfaceStyleDidChangeNotification 629 object:env 630 userInfo:@{ 631 RCTUserInterfaceStyleDidChangeNotificationTraitCollectionKey : env.traitCollection 632 }]; 633} 634 635- (void)_applyUserInterfaceStyle:(UIUserInterfaceStyle)userInterfaceStyle API_AVAILABLE(ios(12.0)) 636{ 637 NSString *colorSchema = nil; 638 if (userInterfaceStyle == UIUserInterfaceStyleDark) { 639 colorSchema = @"dark"; 640 } else if (userInterfaceStyle == UIUserInterfaceStyleLight) { 641 colorSchema = @"light"; 642 } 643 644 // change RN appearance 645 RCTOverrideAppearancePreference(colorSchema); 646} 647 648- (void)_addInitModuleObserver { 649 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didInitializeModule:) name:RCTDidInitializeModuleNotification object:nil]; 650} 651 652- (void)_removeInitModuleObserver { 653 [[NSNotificationCenter defaultCenter] removeObserver:self name:RCTDidInitializeModuleNotification object:nil]; 654} 655 656- (void)didInitializeModule:(NSNotification *)note { 657 id<RCTBridgeModule> module = note.userInfo[@"module"]; 658 if ([module isKindOfClass:[RCTDevMenu class]]) { 659 // RCTDevMenu registers its global keyboard commands at init. 660 // To avoid clashes with keyboard commands registered by expo-dev-client, we unregister some of them 661 // and this needs to happen after the module has been initialized. 662 // RCTDevMenu registers its commands here: https://github.com/facebook/react-native/blob/f3e8ea9c2910b33db17001e98b96720b07dce0b3/React/CoreModules/RCTDevMenu.mm#L130-L135 663 // expo-dev-menu registers its commands here: https://github.com/expo/expo/blob/6da15324ff0b4a9cb24055e9815b8aa11f0ac3af/packages/expo-dev-menu/ios/Interceptors/DevMenuKeyCommandsInterceptor.swift#L27-L29 664 [[RCTKeyCommands sharedInstance] unregisterKeyCommandWithInput:@"d" 665 modifierFlags:UIKeyModifierCommand]; 666 } 667} 668 669-(NSDictionary *)getBuildInfo 670{ 671 NSMutableDictionary *buildInfo = [NSMutableDictionary new]; 672 673 NSString *appIcon = [self getAppIcon]; 674 NSString *runtimeVersion = [self getUpdatesConfigForKey:@"EXUpdatesRuntimeVersion"]; 675 NSString *sdkVersion = [self getUpdatesConfigForKey:@"EXUpdatesSDKVersion"]; 676 NSString *appVersion = [self getFormattedAppVersion]; 677 NSString *appName = [[NSBundle mainBundle] objectForInfoDictionaryKey: @"CFBundleDisplayName"] ?: [[NSBundle mainBundle] objectForInfoDictionaryKey: @"CFBundleExecutable"]; 678 679 [buildInfo setObject:appName forKey:@"appName"]; 680 [buildInfo setObject:appIcon forKey:@"appIcon"]; 681 [buildInfo setObject:appVersion forKey:@"appVersion"]; 682 [buildInfo setObject:runtimeVersion forKey:@"runtimeVersion"]; 683 [buildInfo setObject:sdkVersion forKey:@"sdkVersion"]; 684 685 return buildInfo; 686} 687 688-(NSString *)getAppIcon 689{ 690 NSString *appIcon = @""; 691 NSString *appIconName = [[[[[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleIcons"] objectForKey:@"CFBundlePrimaryIcon"] objectForKey:@"CFBundleIconFiles"] lastObject]; 692 693 if (appIconName != nil) { 694 NSString *resourcePath = [[NSBundle mainBundle] resourcePath]; 695 NSString *appIconPath = [[resourcePath stringByAppendingString:appIconName] stringByAppendingString:@".png"]; 696 appIcon = [@"file://" stringByAppendingString:appIconPath]; 697 } 698 699 return appIcon; 700} 701 702-(NSString *)getUpdatesConfigForKey:(NSString *)key 703{ 704 NSString *value = @""; 705 NSString *path = [[NSBundle mainBundle] pathForResource:@"Expo" ofType:@"plist"]; 706 707 if (path != nil) { 708 NSDictionary *expoConfig = [NSDictionary dictionaryWithContentsOfFile:path]; 709 710 if (expoConfig != nil) { 711 value = [expoConfig objectForKey:key] ?: @""; 712 } 713 } 714 715 return value; 716} 717 718-(NSString *)getFormattedAppVersion 719{ 720 NSString *shortVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; 721 NSString *buildVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]; 722 NSString *appVersion = [NSString stringWithFormat:@"%@ (%@)", shortVersion, buildVersion]; 723 return appVersion; 724} 725 726-(void)copyToClipboard:(NSString *)content { 727 UIPasteboard *clipboard = [UIPasteboard generalPasteboard]; 728 clipboard.string = (content ? : @""); 729} 730 731- (void)setDevMenuAppBridge 732{ 733 DevMenuManager *manager = [DevMenuManager shared]; 734 manager.currentBridge = self.appBridge; 735 736 if (self.manifest != nil) { 737 manager.currentManifest = self.manifest; 738 manager.currentManifestURL = self.manifestURL; 739 } 740} 741 742- (void)invalidateDevMenuApp 743{ 744 DevMenuManager *manager = [DevMenuManager shared]; 745 manager.currentBridge = nil; 746 manager.currentManifest = nil; 747 manager.currentManifestURL = nil; 748} 749 750-(NSDictionary *)getUpdatesConfig 751{ 752 NSMutableDictionary *updatesConfig = [NSMutableDictionary new]; 753 754 NSString *runtimeVersion = [self getUpdatesConfigForKey:@"EXUpdatesRuntimeVersion"]; 755 NSString *sdkVersion = [self getUpdatesConfigForKey:@"EXUpdatesSDKVersion"]; 756 757 // url structure for EASUpdates: `http://u.expo.dev/{appId}` 758 // this url field is added to app.json.updates when running `eas update:configure` 759 // the `u.expo.dev` determines that it is the modern manifest protocol 760 NSString *projectUrl = [self getUpdatesConfigForKey:@"EXUpdatesURL"]; 761 NSURL *url = [NSURL URLWithString:projectUrl]; 762 NSString *appId = [[url pathComponents] lastObject]; 763 764 BOOL isModernManifestProtocol = [[url host] isEqualToString:@"u.expo.dev"] || [[url host] isEqualToString:@"staging-u.expo.dev"]; 765 BOOL expoUpdatesInstalled = EXDevLauncherController.sharedInstance.updatesInterface != nil; 766 BOOL hasAppId = appId.length > 0; 767 768 BOOL usesEASUpdates = isModernManifestProtocol && expoUpdatesInstalled && hasAppId; 769 770 [updatesConfig setObject:runtimeVersion forKey:@"runtimeVersion"]; 771 [updatesConfig setObject:sdkVersion forKey:@"sdkVersion"]; 772 773 774 if (usesEASUpdates) { 775 [updatesConfig setObject:appId forKey:@"appId"]; 776 [updatesConfig setObject:projectUrl forKey:@"projectUrl"]; 777 } 778 779 [updatesConfig setObject:@(usesEASUpdates) forKey:@"usesEASUpdates"]; 780 781 return updatesConfig; 782} 783 784/** 785 * Reset remote debugging to its initial setting. Relies on behavior from react-native's 786 * RCTDevSettings.mm and must be kept in sync there. 787 */ 788- (void)_resetRemoteDebuggingForAppLoad 789{ 790 // Must be kept in sync with RCTDevSettings.mm 791 NSString *kRCTDevSettingsUserDefaultsKey = @"RCTDevMenu"; 792 NSString *kRCTDevSettingIsDebuggingRemotely = @"isDebuggingRemotely"; 793 794 NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; 795 NSMutableDictionary *existingSettings = ((NSDictionary *)[userDefaults objectForKey:kRCTDevSettingsUserDefaultsKey]).mutableCopy; 796 if (!existingSettings) { 797 return; 798 } 799 [existingSettings removeObjectForKey:kRCTDevSettingIsDebuggingRemotely]; 800 [userDefaults setObject:existingSettings forKey:kRCTDevSettingsUserDefaultsKey]; 801} 802 803@end 804