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