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