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