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