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