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