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