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