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