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