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