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