1// Copyright 2015-present 650 Industries. All rights reserved.
2
3@import UIKit;
4
5#import "EXAnalytics.h"
6#import "EXAppLoader.h"
7#import "EXAppViewController.h"
8#import "EXAppLoadingProgressWindowController.h"
9#import "EXAppLoadingCancelView.h"
10#import "EXManagedAppSplashScreenViewProvider.h"
11#import "EXManagedAppSplashScreenConfigurationBuilder.h"
12#import "EXManagedAppSplashScreenViewController.h"
13#import "EXHomeAppSplashScreenViewProvider.h"
14#import "EXEnvironment.h"
15#import "EXErrorRecoveryManager.h"
16#import "EXErrorView.h"
17#import "EXFileDownloader.h"
18#import "EXKernel.h"
19#import "EXKernelUtil.h"
20#import "EXReactAppManager.h"
21#import "EXVersions.h"
22#import "EXUpdatesManager.h"
23#import "EXUtil.h"
24
25#import <EXSplashScreen/EXSplashScreenService.h>
26#import <React/RCTUtils.h>
27#import <ExpoModulesCore/EXModuleRegistryProvider.h>
28
29#if __has_include(<EXScreenOrientation/EXScreenOrientationRegistry.h>)
30#import <EXScreenOrientation/EXScreenOrientationRegistry.h>
31#endif
32
33#import <React/RCTAppearance.h>
34#if defined(INCLUDES_VERSIONED_CODE) && __has_include(<ABI44_0_0React/ABI44_0_0RCTAppearance.h>)
35#import <ABI44_0_0React/ABI44_0_0RCTAppearance.h>
36#endif
37#if defined(INCLUDES_VERSIONED_CODE) && __has_include(<ABI43_0_0React/ABI43_0_0RCTAppearance.h>)
38#import <ABI43_0_0React/ABI43_0_0RCTAppearance.h>
39#endif
40
41#define EX_INTERFACE_ORIENTATION_USE_MANIFEST 0
42
43// when we encounter an error and auto-refresh, we may actually see a series of errors.
44// we only want to trigger refresh once, so we debounce refresh on a timer.
45const CGFloat kEXAutoReloadDebounceSeconds = 0.1;
46
47// in development only, some errors can happen before we even start loading
48// (e.g. certain packager errors, such as an invalid bundle url)
49// and we want to make sure not to cover the error with a loading view or other chrome.
50const CGFloat kEXDevelopmentErrorCoolDownSeconds = 0.1;
51
52// copy of RNScreens protocol
53@protocol EXKernelRNSScreenWindowTraits
54
55+ (BOOL)shouldAskScreensForScreenOrientationInViewController:(UIViewController *)vc;
56
57@end
58
59NS_ASSUME_NONNULL_BEGIN
60
61@interface EXAppViewController ()
62  <EXReactAppManagerUIDelegate, EXAppLoaderDelegate, EXErrorViewDelegate, EXAppLoadingCancelViewDelegate>
63
64@property (nonatomic, assign) BOOL isLoading;
65@property (nonatomic, assign) BOOL isBridgeAlreadyLoading;
66@property (nonatomic, weak) EXKernelAppRecord *appRecord;
67@property (nonatomic, strong) EXErrorView *errorView;
68@property (nonatomic, strong) NSTimer *tmrAutoReloadDebounce;
69@property (nonatomic, strong) NSDate *dtmLastFatalErrorShown;
70@property (nonatomic, strong) NSMutableArray<UIViewController *> *backgroundedControllers;
71
72@property (nonatomic, assign) BOOL isStandalone;
73@property (nonatomic, assign) BOOL isHomeApp;
74
75/*
76 * Controller for handling all messages from bundler/fetcher.
77 * It shows another UIWindow with text and percentage progress.
78 * Enabled only in managed workflow or home when in development mode.
79 * It should appear once manifest is fetched.
80 */
81@property (nonatomic, strong, nonnull) EXAppLoadingProgressWindowController *appLoadingProgressWindowController;
82
83/**
84 * SplashScreenViewProvider that is used only in managed workflow app.
85 * Managed app does not need any specific SplashScreenViewProvider as it uses generic one povided by the SplashScreen module.
86 * See also EXHomeAppSplashScreenViewProvider in self.viewDidLoad
87 */
88@property (nonatomic, strong, nullable) EXManagedAppSplashScreenViewProvider *managedAppSplashScreenViewProvider;
89@property (nonatomic, strong, nullable) EXManagedAppSplashScreenViewController *managedSplashScreenController;
90
91/*
92 * This view is available in managed apps run in Expo Go only.
93 * It is shown only before any managed app manifest is delivered by the app loader.
94 */
95@property (nonatomic, strong, nullable) EXAppLoadingCancelView *appLoadingCancelView;
96
97@end
98
99@implementation EXAppViewController
100
101#pragma mark - Lifecycle
102
103- (instancetype)initWithAppRecord:(EXKernelAppRecord *)record
104{
105  if (self = [super init]) {
106    _appRecord = record;
107    _isStandalone = [EXEnvironment sharedEnvironment].isDetached;
108  }
109  return self;
110}
111
112- (void)dealloc
113{
114  [self _invalidateRecoveryTimer];
115  [[NSNotificationCenter defaultCenter] removeObserver:self];
116}
117
118- (void)viewDidLoad
119{
120  [super viewDidLoad];
121
122  // EXKernel.appRegistry.homeAppRecord does not contain any homeAppRecord until this point,
123  // therefore we cannot move this property initialization to the constructor/initializer
124  _isHomeApp = _appRecord == [EXKernel sharedInstance].appRegistry.homeAppRecord;
125
126  // show LoadingCancelView in managed apps only
127  if (!self.isStandalone && !self.isHomeApp) {
128    self.appLoadingCancelView = [EXAppLoadingCancelView new];
129    // if home app is available then LoadingCancelView can show `go to home` button
130    if ([EXKernel sharedInstance].appRegistry.homeAppRecord) {
131      self.appLoadingCancelView.delegate = self;
132    }
133    [self.view addSubview:self.appLoadingCancelView];
134    [self.view bringSubviewToFront:self.appLoadingCancelView];
135  }
136
137  // show LoadingProgressWindow in the development client for all apps other than production home
138  BOOL isProductionHomeApp = self.isHomeApp && ![EXEnvironment sharedEnvironment].isDebugXCodeScheme;
139  self.appLoadingProgressWindowController = [[EXAppLoadingProgressWindowController alloc] initWithEnabled:!self.isStandalone && !isProductionHomeApp];
140
141  // show SplashScreen in standalone apps and home app only
142  // SplashScreen for managed is shown once the manifest is available
143  if (self.isHomeApp) {
144    EXHomeAppSplashScreenViewProvider *homeAppSplashScreenViewProvider = [EXHomeAppSplashScreenViewProvider new];
145    [self _showSplashScreenWithProvider:homeAppSplashScreenViewProvider];
146  } else if (self.isStandalone) {
147    [self _showSplashScreenWithProvider:[EXSplashScreenViewNativeProvider new]];
148  }
149
150  self.view.backgroundColor = [UIColor whiteColor];
151  _appRecord.appManager.delegate = self;
152  self.isLoading = YES;
153}
154
155- (void)viewDidAppear:(BOOL)animated
156{
157  [super viewDidAppear:animated];
158  if (_appRecord && _appRecord.status == kEXKernelAppRecordStatusNew) {
159    _appRecord.appLoader.delegate = self;
160    _appRecord.appLoader.dataSource = _appRecord.appManager;
161    [self refresh];
162  }
163}
164
165- (BOOL)shouldAutorotate
166{
167  return YES;
168}
169
170- (void)viewWillLayoutSubviews
171{
172  [super viewWillLayoutSubviews];
173  if (_appLoadingCancelView) {
174    _appLoadingCancelView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height);
175  }
176  if (_contentView) {
177    _contentView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height);
178  }
179}
180
181- (void)viewWillDisappear:(BOOL)animated
182{
183  [_appLoadingProgressWindowController hide];
184  [super viewWillDisappear:animated];
185}
186
187/**
188 * Force presented view controllers to use the same user interface style.
189 */
190- (void)presentViewController:(UIViewController *)viewControllerToPresent animated: (BOOL)flag completion:(void (^ __nullable)(void))completion
191{
192  [super presentViewController:viewControllerToPresent animated:flag completion:completion];
193  [self _overrideUserInterfaceStyleOf:viewControllerToPresent];
194}
195
196/**
197 * Force child view controllers to use the same user interface style.
198 */
199- (void)addChildViewController:(UIViewController *)childController
200{
201  [super addChildViewController:childController];
202  [self _overrideUserInterfaceStyleOf:childController];
203}
204
205#pragma mark - Public
206
207- (void)maybeShowError:(NSError *)error
208{
209  self.isLoading = NO;
210  if ([self _willAutoRecoverFromError:error]) {
211    return;
212  }
213  if (error && ![error isKindOfClass:[NSError class]]) {
214#if DEBUG
215    NSAssert(NO, @"AppViewController error handler was called on an object that isn't an NSError");
216#endif
217    return;
218  }
219
220  // we don't ever want to show any Expo UI in a production standalone app, so hard crash
221  if ([EXEnvironment sharedEnvironment].isDetached && ![_appRecord.appManager enablesDeveloperTools]) {
222    NSException *e = [NSException exceptionWithName:@"ExpoFatalError"
223                                             reason:[NSString stringWithFormat:@"Expo encountered a fatal error: %@", [error localizedDescription]]
224                                           userInfo:@{NSUnderlyingErrorKey: error}];
225    @throw e;
226  }
227
228  NSString *domain = (error && error.domain) ? error.domain : @"";
229  BOOL isNetworkError = ([domain isEqualToString:(NSString *)kCFErrorDomainCFNetwork] || [domain isEqualToString:NSURLErrorDomain] || [domain isEqualToString:EXNetworkErrorDomain]);
230
231  if (isNetworkError) {
232    // show a human-readable reachability error
233    dispatch_async(dispatch_get_main_queue(), ^{
234      [self _showErrorWithType:kEXFatalErrorTypeLoading error:error];
235    });
236  } else if ([domain isEqualToString:@"JSServer"] && [_appRecord.appManager enablesDeveloperTools]) {
237    // RCTRedBox already handled this
238  } else if ([domain rangeOfString:RCTErrorDomain].length > 0 && [_appRecord.appManager enablesDeveloperTools]) {
239    // RCTRedBox already handled this
240  } else {
241    dispatch_async(dispatch_get_main_queue(), ^{
242      [self _showErrorWithType:kEXFatalErrorTypeException error:error];
243    });
244  }
245}
246
247- (void)refresh
248{
249  self.isLoading = YES;
250  self.isBridgeAlreadyLoading = NO;
251  [self _invalidateRecoveryTimer];
252  [_appRecord.appLoader request];
253}
254
255- (void)reloadFromCache
256{
257  self.isLoading = YES;
258  self.isBridgeAlreadyLoading = NO;
259  [self _invalidateRecoveryTimer];
260  [_appRecord.appLoader requestFromCache];
261}
262
263- (void)appStateDidBecomeActive
264{
265  dispatch_async(dispatch_get_main_queue(), ^{
266    // Reset the root view background color and window color if we switch between Expo home and project
267    [self _setBackgroundColor];
268  });
269}
270
271- (void)appStateDidBecomeInactive
272{
273}
274
275- (void)_rebuildBridge
276{
277  if (!self.isBridgeAlreadyLoading) {
278    self.isBridgeAlreadyLoading = YES;
279    dispatch_async(dispatch_get_main_queue(), ^{
280      [self _overrideUserInterfaceStyleOf:self];
281      [self _overrideAppearanceModuleBehaviour];
282      [self _invalidateRecoveryTimer];
283      [[EXKernel sharedInstance] logAnalyticsEvent:@"LOAD_EXPERIENCE" forAppRecord:self.appRecord];
284      [self.appRecord.appManager rebuildBridge];
285    });
286  }
287}
288
289- (void)foregroundControllers
290{
291  if (_backgroundedControllers != nil) {
292    __block UIViewController *parentController = self;
293
294    [_backgroundedControllers enumerateObjectsUsingBlock:^(UIViewController * _Nonnull viewController, NSUInteger idx, BOOL * _Nonnull stop) {
295      [parentController presentViewController:viewController animated:NO completion:nil];
296      parentController = viewController;
297    }];
298
299    _backgroundedControllers = nil;
300  }
301}
302
303- (void)backgroundControllers
304{
305  UIViewController *childController = [self presentedViewController];
306
307  if (childController != nil) {
308    if (_backgroundedControllers == nil) {
309      _backgroundedControllers = [NSMutableArray new];
310    }
311
312    while (childController != nil) {
313      [_backgroundedControllers addObject:childController];
314      childController = childController.presentedViewController;
315    }
316  }
317}
318
319/**
320 * In managed app we expect two kinds of manifest:
321 * - optimistic one (served from cache)
322 * - actual one served when app is fetched.
323 * For each of them we should show SplashScreen,
324 * therefore for any consecutive SplashScreen.show call we just reconfigure what's already visible.
325 * In HomeApp or standalone apps this function is no-op as SplashScreen is managed differently.
326 */
327- (void)_showOrReconfigureManagedAppSplashScreen:(EXManifestsManifest *)manifest
328{
329  if (_isStandalone || _isHomeApp) {
330    return;
331  }
332  if (!_managedAppSplashScreenViewProvider) {
333    _managedAppSplashScreenViewProvider = [[EXManagedAppSplashScreenViewProvider alloc] initWithManifest:manifest];
334
335    [self _showManagedSplashScreenWithProvider:_managedAppSplashScreenViewProvider];
336  } else {
337    [_managedAppSplashScreenViewProvider updateSplashScreenViewWithManifest:manifest];
338  }
339}
340
341- (void)_showCachedExperienceAlert
342{
343  if (self.isStandalone || self.isHomeApp) {
344    return;
345  }
346
347  dispatch_async(dispatch_get_main_queue(), ^{
348    UIAlertController *alert = [UIAlertController
349                                alertControllerWithTitle:@"Using a cached project"
350                                message:@"If you did not intend to use a cached project, check your network connection and reload."
351                                preferredStyle:UIAlertControllerStyleAlert];
352    [alert addAction:[UIAlertAction actionWithTitle:@"Reload" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
353      [self refresh];
354    }]];
355    [alert addAction:[UIAlertAction actionWithTitle:@"Use cache" style:UIAlertActionStyleCancel handler:nil]];
356    [self presentViewController:alert animated:YES completion:nil];
357  });
358}
359
360- (void)_setLoadingViewStatusIfEnabledFromAppLoader:(EXAppLoader *)appLoader
361{
362  if (appLoader.shouldShowRemoteUpdateStatus) {
363    [self.appLoadingProgressWindowController updateStatus:appLoader.remoteUpdateStatus];
364  } else {
365    [self.appLoadingProgressWindowController hide];
366  }
367}
368
369- (void)_showSplashScreenWithProvider:(id<EXSplashScreenViewProvider>)provider
370{
371  EXSplashScreenService *splashScreenService = (EXSplashScreenService *)[EXModuleRegistryProvider getSingletonModuleForClass:[EXSplashScreenService class]];
372
373  // EXSplashScreenService presents a splash screen on a root view controller
374  // at the start of the app. Since we want the EXAppViewController to manage
375  // the lifecycle of the splash screen we need to:
376  // 1. present the splash screen on EXAppViewController
377  // 2. hide the splash screen of root view controller
378  // Disclaimer:
379  //  there's only one root view controller, but possibly many EXAppViewControllers
380  //  (in Expo Go: one project -> one EXAppViewController)
381  //  and we want to hide SplashScreen only once for the root view controller, hence the "once"
382  static dispatch_once_t once;
383  void (^hideRootViewControllerSplashScreen)(void) = ^void() {
384    dispatch_once(&once, ^{
385      UIViewController *rootViewController = [UIApplication sharedApplication].keyWindow.rootViewController;
386      [splashScreenService hideSplashScreenFor:rootViewController
387                               successCallback:^(BOOL hasEffect){}
388                               failureCallback:^(NSString * _Nonnull message) {
389        EXLogWarn(@"Hiding splash screen from root view controller did not succeed: %@", message);
390      }];
391    });
392  };
393
394  EX_WEAKIFY(self);
395  dispatch_async(dispatch_get_main_queue(), ^{
396    EX_ENSURE_STRONGIFY(self);
397    [splashScreenService showSplashScreenFor:self
398                    splashScreenViewProvider:provider
399                             successCallback:hideRootViewControllerSplashScreen
400                             failureCallback:^(NSString *message){ EXLogWarn(@"%@", message); }];
401  });
402}
403
404- (void)_showManagedSplashScreenWithProvider:(id<EXSplashScreenViewProvider>)provider
405{
406
407  EXSplashScreenService *splashScreenService = (EXSplashScreenService *)[EXModuleRegistryProvider getSingletonModuleForClass:[EXSplashScreenService class]];
408
409  EX_WEAKIFY(self);
410  dispatch_async(dispatch_get_main_queue(), ^{
411    EX_ENSURE_STRONGIFY(self);
412
413    UIView *rootView = self.view;
414    UIView *splashScreenView = [provider createSplashScreenView];
415    self.managedSplashScreenController = [[EXManagedAppSplashScreenViewController alloc] initWithRootView:rootView
416                                                                                                 splashScreenView:splashScreenView];
417    [splashScreenService showSplashScreenFor:self
418                      splashScreenController:self.managedSplashScreenController
419                             successCallback:^{}
420                             failureCallback:^(NSString *message){ EXLogWarn(@"%@", message); }];
421  });
422
423}
424
425- (void)hideLoadingProgressWindow
426{
427  [self.appLoadingProgressWindowController hide];
428  if (self.managedSplashScreenController) {
429    [self.managedSplashScreenController startSplashScreenVisibleTimer];
430  }
431}
432
433#pragma mark - EXAppLoaderDelegate
434
435- (void)appLoader:(EXAppLoader *)appLoader didLoadOptimisticManifest:(EXManifestsManifest *)manifest
436{
437  if (_appLoadingCancelView) {
438    EX_WEAKIFY(self);
439    dispatch_async(dispatch_get_main_queue(), ^{
440      EX_ENSURE_STRONGIFY(self);
441      [self.appLoadingCancelView removeFromSuperview];
442      self.appLoadingCancelView = nil;
443    });
444  }
445  [self _showOrReconfigureManagedAppSplashScreen:manifest];
446  [self _setLoadingViewStatusIfEnabledFromAppLoader:appLoader];
447  if ([EXKernel sharedInstance].browserController) {
448    [[EXKernel sharedInstance].browserController addHistoryItemWithUrl:appLoader.manifestUrl manifest:manifest];
449  }
450  [self _rebuildBridge];
451}
452
453- (void)appLoader:(EXAppLoader *)appLoader didLoadBundleWithProgress:(EXLoadingProgress *)progress
454{
455  if (self->_appRecord.appManager.status != kEXReactAppManagerStatusRunning) {
456    [self.appLoadingProgressWindowController updateStatusWithProgress:progress];
457  }
458}
459
460- (void)appLoader:(EXAppLoader *)appLoader didFinishLoadingManifest:(EXManifestsManifest *)manifest bundle:(NSData *)data
461{
462  [self _showOrReconfigureManagedAppSplashScreen:manifest];
463  [self _rebuildBridge];
464  if (self->_appRecord.appManager.status == kEXReactAppManagerStatusBridgeLoading) {
465    [self->_appRecord.appManager appLoaderFinished];
466  }
467
468  if (!appLoader.isUpToDate && appLoader.shouldShowRemoteUpdateStatus) {
469    [self _showCachedExperienceAlert];
470  }
471}
472
473- (void)appLoader:(EXAppLoader *)appLoader didFailWithError:(NSError *)error
474{
475  if (_appRecord.appManager.status == kEXReactAppManagerStatusBridgeLoading) {
476    [_appRecord.appManager appLoaderFailedWithError:error];
477  }
478  [self maybeShowError:error];
479}
480
481- (void)appLoader:(EXAppLoader *)appLoader didResolveUpdatedBundleWithManifest:(EXManifestsManifest * _Nullable)manifest isFromCache:(BOOL)isFromCache error:(NSError * _Nullable)error
482{
483  [[EXKernel sharedInstance].serviceRegistry.updatesManager notifyApp:_appRecord ofDownloadWithManifest:manifest isNew:!isFromCache error:error];
484}
485
486#pragma mark - EXReactAppManagerDelegate
487
488- (void)reactAppManagerIsReadyForLoad:(EXReactAppManager *)appManager
489{
490  UIView *reactView = appManager.rootView;
491  reactView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height);
492  reactView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
493  // Set this view to transparent so the root view background color aligns with custom development clients where the
494  // background color is the view controller root view.
495  reactView.backgroundColor = [UIColor clearColor];
496
497  [_contentView removeFromSuperview];
498  _contentView = reactView;
499  [self.view addSubview:_contentView];
500  [self.view sendSubviewToBack:_contentView];
501  [reactView becomeFirstResponder];
502
503  // Set root view background color after adding as subview so we can access window
504  [self _setBackgroundColor];
505}
506
507- (void)reactAppManagerStartedLoadingJavaScript:(EXReactAppManager *)appManager
508{
509  EXAssertMainThread();
510  self.isLoading = YES;
511}
512
513- (void)reactAppManagerFinishedLoadingJavaScript:(EXReactAppManager *)appManager
514{
515  EXAssertMainThread();
516  self.isLoading = NO;
517  if ([EXKernel sharedInstance].browserController) {
518    [[EXKernel sharedInstance].browserController appDidFinishLoadingSuccessfully:_appRecord];
519  }
520}
521
522- (void)reactAppManagerAppContentDidAppear:(EXReactAppManager *)appManager
523{
524  EXSplashScreenService *splashScreenService = (EXSplashScreenService *)[EXModuleRegistryProvider getSingletonModuleForClass:[EXSplashScreenService class]];
525  [splashScreenService onAppContentDidAppear:self];
526}
527
528- (void)reactAppManagerAppContentWillReload:(EXReactAppManager *)appManager {
529  EXSplashScreenService *splashScreenService = (EXSplashScreenService *)[EXModuleRegistryProvider getSingletonModuleForClass:[EXSplashScreenService class]];
530  [splashScreenService onAppContentWillReload:self];
531}
532
533- (void)reactAppManager:(EXReactAppManager *)appManager failedToLoadJavaScriptWithError:(NSError *)error
534{
535  EXAssertMainThread();
536  [self maybeShowError:error];
537}
538
539- (void)reactAppManagerDidInvalidate:(EXReactAppManager *)appManager
540{
541}
542
543- (void)errorViewDidSelectRetry:(EXErrorView *)errorView
544{
545  [self refresh];
546}
547
548#pragma mark - orientation
549
550- (UIInterfaceOrientationMask)supportedInterfaceOrientations
551{
552  if ([self shouldUseRNScreenOrientation]) {
553    return [super supportedInterfaceOrientations];
554  }
555
556#if __has_include(<EXScreenOrientation/EXScreenOrientationRegistry.h>)
557  EXScreenOrientationRegistry *screenOrientationRegistry = (EXScreenOrientationRegistry *)[EXModuleRegistryProvider getSingletonModuleForClass:[EXScreenOrientationRegistry class]];
558  if (screenOrientationRegistry && [screenOrientationRegistry requiredOrientationMask] > 0) {
559    return [screenOrientationRegistry requiredOrientationMask];
560  }
561#endif
562
563  return [self orientationMaskFromManifestOrDefault];
564}
565
566- (BOOL)shouldUseRNScreenOrientation
567{
568  Class screenWindowTraitsClass = [self->_appRecord.appManager versionedClassFromString:@"RNSScreenWindowTraits"];
569  if ([screenWindowTraitsClass respondsToSelector:@selector(shouldAskScreensForScreenOrientationInViewController:)]) {
570    id<EXKernelRNSScreenWindowTraits> screenWindowTraits = (id<EXKernelRNSScreenWindowTraits>)screenWindowTraitsClass;
571    return [screenWindowTraits shouldAskScreensForScreenOrientationInViewController:self];
572  }
573  return NO;
574}
575
576- (UIInterfaceOrientationMask)orientationMaskFromManifestOrDefault {
577  if (_appRecord.appLoader.manifest) {
578    NSString *orientationConfig = _appRecord.appLoader.manifest.orientation;
579    if ([orientationConfig isEqualToString:@"portrait"]) {
580      // lock to portrait
581      return UIInterfaceOrientationMaskPortrait;
582    } else if ([orientationConfig isEqualToString:@"landscape"]) {
583      // lock to landscape
584      return UIInterfaceOrientationMaskLandscape;
585    }
586  }
587  // no config or default value: allow autorotation
588  return UIInterfaceOrientationMaskAllButUpsideDown;
589}
590
591- (void)traitCollectionDidChange:(nullable UITraitCollection *)previousTraitCollection {
592  [super traitCollectionDidChange:previousTraitCollection];
593  if ((self.traitCollection.verticalSizeClass != previousTraitCollection.verticalSizeClass)
594      || (self.traitCollection.horizontalSizeClass != previousTraitCollection.horizontalSizeClass)) {
595
596    #if __has_include(<EXScreenOrientation/EXScreenOrientationRegistry.h>)
597      EXScreenOrientationRegistry *screenOrientationRegistryController = (EXScreenOrientationRegistry *)[EXModuleRegistryProvider getSingletonModuleForClass:[EXScreenOrientationRegistry class]];
598      [screenOrientationRegistryController traitCollectionDidChangeTo:self.traitCollection];
599    #endif
600  }
601}
602
603#pragma mark - RCTAppearanceModule
604
605/**
606 * This function overrides behaviour of RCTAppearanceModule
607 * basing on 'userInterfaceStyle' option from the app manifest.
608 * It also defaults the RCTAppearanceModule to 'light'.
609 */
610- (void)_overrideAppearanceModuleBehaviour
611{
612  NSString *userInterfaceStyle = [self _readUserInterfaceStyleFromManifest:_appRecord.appLoader.manifest];
613  NSString *appearancePreference = nil;
614  if (!userInterfaceStyle || [userInterfaceStyle isEqualToString:@"light"]) {
615    appearancePreference = @"light";
616  } else if ([userInterfaceStyle isEqualToString:@"dark"]) {
617    appearancePreference = @"dark";
618  } else if ([userInterfaceStyle isEqualToString:@"automatic"]) {
619    appearancePreference = nil;
620  }
621  RCTOverrideAppearancePreference(appearancePreference);
622#if defined(INCLUDES_VERSIONED_CODE) && __has_include(<ABI44_0_0React/ABI44_0_0RCTAppearance.h>)
623  ABI44_0_0RCTOverrideAppearancePreference(appearancePreference);
624#endif
625#if defined(INCLUDES_VERSIONED_CODE) && __has_include(<ABI43_0_0React/ABI43_0_0RCTAppearance.h>)
626  ABI43_0_0RCTOverrideAppearancePreference(appearancePreference);
627#endif
628
629}
630
631#pragma mark - user interface style
632
633- (void)_overrideUserInterfaceStyleOf:(UIViewController *)viewController
634{
635  if (@available(iOS 13.0, *)) {
636    NSString *userInterfaceStyle = [self _readUserInterfaceStyleFromManifest:_appRecord.appLoader.manifest];
637    viewController.overrideUserInterfaceStyle = [self _userInterfaceStyleForString:userInterfaceStyle];
638  }
639}
640
641- (NSString * _Nullable)_readUserInterfaceStyleFromManifest:(EXManifestsManifest *)manifest
642{
643  return manifest.userInterfaceStyle;
644}
645
646- (UIUserInterfaceStyle)_userInterfaceStyleForString:(NSString *)userInterfaceStyleString API_AVAILABLE(ios(12.0)) {
647  if ([userInterfaceStyleString isEqualToString:@"dark"]) {
648    return UIUserInterfaceStyleDark;
649  }
650  if ([userInterfaceStyleString isEqualToString:@"automatic"]) {
651    return UIUserInterfaceStyleUnspecified;
652  }
653  return UIUserInterfaceStyleLight;
654}
655
656#pragma mark - root view and window background color
657
658- (void)_setBackgroundColor
659{
660    NSString *backgroundColorString = [self _readBackgroundColorFromManifest:_appRecord.appLoader.manifest];
661    UIColor *backgroundColor = [EXUtil colorWithHexString:backgroundColorString];
662    self.view.backgroundColor = [UIColor clearColor];
663
664    // NOTE(evanbacon): `self.view.window.rootViewController.view` represents the top-most window's root view controller's view which is the same
665    // view we set in `expo-system-ui`'s `setBackgroundColorAsync` method.
666    if (backgroundColor) {
667      if (self.view.window.rootViewController != nil && self.view.window.rootViewController.view != nil) {
668        self.view.window.rootViewController.view.backgroundColor = backgroundColor;
669      }
670      self.view.window.backgroundColor = backgroundColor;
671    } else {
672      // Reset this color to white so splash and other screens don't load against a black background.
673      if (self.view.window.rootViewController != nil && self.view.window.rootViewController.view != nil) {
674        self.view.window.rootViewController.view.backgroundColor = [UIColor whiteColor];
675      }
676      // NOTE(brentvatne): we used to use white as a default background color for window but this caused
677      // problems when using form sheet presentation style with vcs eg: <Modal /> and native-stack. Most
678      // users expect the background behind these to be black, which is the default if backgroundColor is nil.
679      self.view.window.backgroundColor = nil;
680
681      // NOTE(brentvatne): we may want to default to respecting the default system background color
682      // on iOS13 and higher, but if we do make this choice then we will have to implement it on Android
683      // as well. This would also be a breaking change. Leaving this here as a placeholder for the future.
684      // if (@available(iOS 13.0, *)) {
685      //   self.view.backgroundColor = [UIColor systemBackgroundColor];
686      // } else {
687      //  self.view.backgroundColor = [UIColor whiteColor];
688      // }
689    }
690}
691
692- (NSString * _Nullable)_readBackgroundColorFromManifest:(EXManifestsManifest *)manifest
693{
694  return manifest.iosOrRootBackgroundColor;
695}
696
697
698#pragma mark - Internal
699
700- (void)_showErrorWithType:(EXFatalErrorType)type error:(nullable NSError *)error
701{
702  EXAssertMainThread();
703  _dtmLastFatalErrorShown = [NSDate date];
704  if (_errorView && _contentView == _errorView) {
705    // already showing, just update
706    _errorView.type = type;
707    _errorView.error = error;
708  } {
709    [_contentView removeFromSuperview];
710    if (!_errorView) {
711      _errorView = [[EXErrorView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
712      _errorView.delegate = self;
713      _errorView.appRecord = _appRecord;
714    }
715    _errorView.type = type;
716    _errorView.error = error;
717    _contentView = _errorView;
718    [self.view addSubview:_contentView];
719    [[EXAnalytics sharedInstance] logErrorVisibleEvent];
720  }
721}
722
723- (void)setIsLoading:(BOOL)isLoading
724{
725  if ([_appRecord.appManager enablesDeveloperTools] && _dtmLastFatalErrorShown) {
726    if ([_dtmLastFatalErrorShown timeIntervalSinceNow] >= -kEXDevelopmentErrorCoolDownSeconds) {
727      // we just showed a fatal error very recently, do not begin loading.
728      // this can happen in some cases where react native sends the 'started loading' notif
729      // in spite of a packager error.
730      return;
731    }
732  }
733  _isLoading = isLoading;
734  EX_WEAKIFY(self);
735  dispatch_async(dispatch_get_main_queue(), ^{
736    EX_ENSURE_STRONGIFY(self);
737    if (!isLoading) {
738      [self.appLoadingProgressWindowController hide];
739    }
740  });
741}
742
743#pragma mark - error recovery
744
745- (BOOL)_willAutoRecoverFromError:(NSError *)error
746{
747  if (![_appRecord.appManager enablesDeveloperTools]) {
748    BOOL shouldRecover = [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager experienceShouldReloadOnError:_appRecord.scopeKey];
749    if (shouldRecover) {
750      [self _invalidateRecoveryTimer];
751      _tmrAutoReloadDebounce = [NSTimer scheduledTimerWithTimeInterval:kEXAutoReloadDebounceSeconds
752                                                                target:self
753                                                              selector:@selector(refresh)
754                                                              userInfo:nil
755                                                               repeats:NO];
756    }
757    return shouldRecover;
758  }
759  return NO;
760}
761
762- (void)_invalidateRecoveryTimer
763{
764  if (_tmrAutoReloadDebounce) {
765    [_tmrAutoReloadDebounce invalidate];
766    _tmrAutoReloadDebounce = nil;
767  }
768}
769
770#pragma mark - EXAppLoadingCancelViewDelegate
771
772- (void)appLoadingCancelViewDidCancel:(EXAppLoadingCancelView *)view {
773  if ([EXKernel sharedInstance].browserController) {
774    [[EXKernel sharedInstance].browserController moveHomeToVisible];
775  }
776}
777
778@end
779
780NS_ASSUME_NONNULL_END
781