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