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