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