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