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