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