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 "EXVersions.h"
21#import "EXUpdatesManager.h"
22#import "EXUtil.h"
23
24#import <EXSplashScreen/EXSplashScreenService.h>
25#import <React/RCTUtils.h>
26#import <UMCore/UMModuleRegistryProvider.h>
27
28#if __has_include(<EXScreenOrientation/EXScreenOrientationRegistry.h>)
29#import <EXScreenOrientation/EXScreenOrientationRegistry.h>
30#endif
31
32#import <React/RCTAppearance.h>
33#if __has_include(<ABI40_0_0React/ABI40_0_0RCTAppearance.h>)
34#import <ABI40_0_0React/ABI40_0_0RCTAppearance.h>
35#endif
36#if __has_include(<ABI39_0_0React/ABI39_0_0RCTAppearance.h>)
37#import <ABI39_0_0React/ABI39_0_0RCTAppearance.h>
38#endif
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 Go 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#pragma mark - Lifecycle
92
93- (instancetype)initWithAppRecord:(EXKernelAppRecord *)record
94{
95  if (self = [super init]) {
96    _appRecord = record;
97    _isStandalone = [EXEnvironment sharedEnvironment].isDetached;
98  }
99  return self;
100}
101
102- (void)dealloc
103{
104  [self _invalidateRecoveryTimer];
105  [[NSNotificationCenter defaultCenter] removeObserver:self];
106}
107
108- (void)viewDidLoad
109{
110  [super viewDidLoad];
111
112  // EXKernel.appRegistry.homeAppRecord does not contain any homeAppRecord until this point,
113  // therefore we cannot move this property initialization to the constructor/initializer
114  _isHomeApp = _appRecord == [EXKernel sharedInstance].appRegistry.homeAppRecord;
115
116  // show LoadingCancelView in managed apps only
117  if (!self.isStandalone && !self.isHomeApp) {
118    self.appLoadingCancelView = [EXAppLoadingCancelView new];
119    // if home app is available then LoadingCancelView can show `go to home` button
120    if ([EXKernel sharedInstance].appRegistry.homeAppRecord) {
121      self.appLoadingCancelView.delegate = self;
122    }
123    [self.view addSubview:self.appLoadingCancelView];
124    [self.view bringSubviewToFront:self.appLoadingCancelView];
125  }
126
127  // show LoadingProgressWindow in the development client for all apps other than production home
128  BOOL isProductionHomeApp = self.isHomeApp && ![EXEnvironment sharedEnvironment].isDebugXCodeScheme;
129  self.appLoadingProgressWindowController = [[EXAppLoadingProgressWindowController alloc] initWithEnabled:!self.isStandalone && !isProductionHomeApp];
130
131  // show SplashScreen in standalone apps and home app only
132  // SplashScreen for managed is shown once the manifest is available
133  if (self.isHomeApp) {
134    EXHomeAppSplashScreenViewProvider *homeAppSplashScreenViewProvider = [EXHomeAppSplashScreenViewProvider new];
135    [self _showSplashScreenWithProvider:homeAppSplashScreenViewProvider];
136  } else if (self.isStandalone) {
137    [self _showSplashScreenWithProvider:[EXSplashScreenViewNativeProvider new]];
138  }
139
140  self.view.backgroundColor = [UIColor whiteColor];
141  _appRecord.appManager.delegate = self;
142  self.isLoading = YES;
143}
144
145- (void)viewDidAppear:(BOOL)animated
146{
147  [super viewDidAppear:animated];
148  if (_appRecord && _appRecord.status == kEXKernelAppRecordStatusNew) {
149    _appRecord.appLoader.delegate = self;
150    _appRecord.appLoader.dataSource = _appRecord.appManager;
151    [self refresh];
152  }
153}
154
155- (BOOL)shouldAutorotate
156{
157  return YES;
158}
159
160- (void)viewWillLayoutSubviews
161{
162  [super viewWillLayoutSubviews];
163  if (_appLoadingCancelView) {
164    _appLoadingCancelView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height);
165  }
166  if (_contentView) {
167    _contentView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height);
168  }
169}
170
171- (void)viewWillDisappear:(BOOL)animated
172{
173  [_appLoadingProgressWindowController hide];
174  [super viewWillDisappear:animated];
175}
176
177/**
178 * Force presented view controllers to use the same user interface style.
179 */
180- (void)presentViewController:(UIViewController *)viewControllerToPresent animated: (BOOL)flag completion:(void (^ __nullable)(void))completion
181{
182  [super presentViewController:viewControllerToPresent animated:flag completion:completion];
183  [self _overrideUserInterfaceStyleOf:viewControllerToPresent];
184}
185
186/**
187 * Force child view controllers to use the same user interface style.
188 */
189- (void)addChildViewController:(UIViewController *)childController
190{
191  [super addChildViewController:childController];
192  [self _overrideUserInterfaceStyleOf:childController];
193}
194
195#pragma mark - Public
196
197- (void)maybeShowError:(NSError *)error
198{
199  self.isLoading = NO;
200  if ([self _willAutoRecoverFromError:error]) {
201    return;
202  }
203  if (error && ![error isKindOfClass:[NSError class]]) {
204#if DEBUG
205    NSAssert(NO, @"AppViewController error handler was called on an object that isn't an NSError");
206#endif
207    return;
208  }
209
210  // we don't ever want to show any Expo UI in a production standalone app, so hard crash
211  if ([EXEnvironment sharedEnvironment].isDetached && ![_appRecord.appManager enablesDeveloperTools]) {
212    NSException *e = [NSException exceptionWithName:@"ExpoFatalError"
213                                             reason:[NSString stringWithFormat:@"Expo encountered a fatal error: %@", [error localizedDescription]]
214                                           userInfo:@{NSUnderlyingErrorKey: error}];
215    @throw e;
216  }
217
218  NSString *domain = (error && error.domain) ? error.domain : @"";
219  BOOL isNetworkError = ([domain isEqualToString:(NSString *)kCFErrorDomainCFNetwork] || [domain isEqualToString:NSURLErrorDomain] || [domain isEqualToString:EXNetworkErrorDomain]);
220
221  if (isNetworkError) {
222    // show a human-readable reachability error
223    dispatch_async(dispatch_get_main_queue(), ^{
224      [self _showErrorWithType:kEXFatalErrorTypeLoading error:error];
225    });
226  } else if ([domain isEqualToString:@"JSServer"] && [_appRecord.appManager enablesDeveloperTools]) {
227    // RCTRedBox already handled this
228  } else if ([domain rangeOfString:RCTErrorDomain].length > 0 && [_appRecord.appManager enablesDeveloperTools]) {
229    // RCTRedBox already handled this
230  } else {
231    dispatch_async(dispatch_get_main_queue(), ^{
232      [self _showErrorWithType:kEXFatalErrorTypeException error:error];
233    });
234  }
235}
236
237- (void)refresh
238{
239  self.isLoading = YES;
240  self.isBridgeAlreadyLoading = NO;
241  [self _invalidateRecoveryTimer];
242  [_appRecord.appLoader request];
243}
244
245- (void)reloadFromCache
246{
247  self.isLoading = YES;
248  self.isBridgeAlreadyLoading = NO;
249  [self _invalidateRecoveryTimer];
250  [_appRecord.appLoader requestFromCache];
251}
252
253- (void)appStateDidBecomeActive
254{
255  dispatch_async(dispatch_get_main_queue(), ^{
256    // Reset the root view background color and window color if we switch between Expo home and project
257    [self _setBackgroundColor:self.view];
258  });
259  [_appRecord.appManager appStateDidBecomeActive];
260}
261
262- (void)appStateDidBecomeInactive
263{
264  [_appRecord.appManager appStateDidBecomeInactive];
265}
266
267- (void)_rebuildBridge
268{
269  if (!self.isBridgeAlreadyLoading) {
270    self.isBridgeAlreadyLoading = YES;
271    dispatch_async(dispatch_get_main_queue(), ^{
272      [self _overrideUserInterfaceStyleOf:self];
273      [self _overrideAppearanceModuleBehaviour];
274      [self _invalidateRecoveryTimer];
275      [[EXKernel sharedInstance] logAnalyticsEvent:@"LOAD_EXPERIENCE" forAppRecord:self.appRecord];
276      [self.appRecord.appManager rebuildBridge];
277    });
278  }
279}
280
281- (void)foregroundControllers
282{
283  if (_backgroundedControllers != nil) {
284    __block UIViewController *parentController = self;
285
286    [_backgroundedControllers enumerateObjectsUsingBlock:^(UIViewController * _Nonnull viewController, NSUInteger idx, BOOL * _Nonnull stop) {
287      [parentController presentViewController:viewController animated:NO completion:nil];
288      parentController = viewController;
289    }];
290
291    _backgroundedControllers = nil;
292  }
293}
294
295- (void)backgroundControllers
296{
297  UIViewController *childController = [self presentedViewController];
298
299  if (childController != nil) {
300    if (_backgroundedControllers == nil) {
301      _backgroundedControllers = [NSMutableArray new];
302    }
303
304    while (childController != nil) {
305      [_backgroundedControllers addObject:childController];
306      childController = childController.presentedViewController;
307    }
308  }
309}
310
311/**
312 * In managed app we expect two kinds of manifest:
313 * - optimistic one (served from cache)
314 * - actual one served when app is fetched.
315 * For each of them we should show SplashScreen,
316 * therefore for any consecutive SplashScreen.show call we just reconfigure what's already visible.
317 * In HomeApp or standalone apps this function is no-op as SplashScreen is managed differently.
318 */
319- (void)_showOrReconfigureManagedAppSplashScreen:(NSDictionary *)manifest
320{
321  if (_isStandalone || _isHomeApp) {
322    return;
323  }
324  if (!_managedAppSplashScreenViewProvider) {
325    _managedAppSplashScreenViewProvider = [[EXManagedAppSplashScreenViewProvider alloc] initWithManifest:manifest];
326
327    [self _showSplashScreenWithProvider:_managedAppSplashScreenViewProvider];
328  } else {
329    [_managedAppSplashScreenViewProvider updateSplashScreenViewWithManifest:manifest];
330  }
331}
332
333- (void)_showCachedExperienceAlert
334{
335  if (self.isStandalone || self.isHomeApp) {
336    return;
337  }
338
339  dispatch_async(dispatch_get_main_queue(), ^{
340    UIAlertController *alert = [UIAlertController
341                                alertControllerWithTitle:@"Using a cached project"
342                                message:@"If you did not intend to use a cached project, check your network connection and reload."
343                                preferredStyle:UIAlertControllerStyleAlert];
344    [alert addAction:[UIAlertAction actionWithTitle:@"Reload" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
345      [self refresh];
346    }]];
347    [alert addAction:[UIAlertAction actionWithTitle:@"Use cache" style:UIAlertActionStyleCancel handler:nil]];
348    [self presentViewController:alert animated:YES completion:nil];
349  });
350}
351
352- (void)_setLoadingViewStatusIfEnabledFromAppLoader:(EXAppLoader *)appLoader
353{
354  if (appLoader.shouldShowRemoteUpdateStatus) {
355    [self.appLoadingProgressWindowController updateStatus:appLoader.remoteUpdateStatus];
356  } else {
357    [self.appLoadingProgressWindowController hide];
358  }
359}
360
361- (void)_showSplashScreenWithProvider:(id<EXSplashScreenViewProvider>)provider
362{
363  EXSplashScreenService *splashScreenService = (EXSplashScreenService *)[UMModuleRegistryProvider getSingletonModuleForClass:[EXSplashScreenService class]];
364
365  // EXSplashScreenService presents a splash screen on a root view controller
366  // at the start of the app. Since we want the EXAppViewController to manage
367  // the lifecycle of the splash screen we need to:
368  // 1. present the splash screen on EXAppViewController
369  // 2. hide the splash screen of root view controller
370  // Disclaimer:
371  //  there's only one root view controller, but possibly many EXAppViewControllers
372  //  (in Expo Go: one project -> one EXAppViewController)
373  //  and we want to hide SplashScreen only once for the root view controller, hence the "once"
374  static dispatch_once_t once;
375  void (^hideRootViewControllerSplashScreen)(void) = ^void() {
376    dispatch_once(&once, ^{
377      UIViewController *rootViewController = [UIApplication sharedApplication].keyWindow.rootViewController;
378      [splashScreenService hideSplashScreenFor:rootViewController
379                               successCallback:^(BOOL hasEffect){}
380                               failureCallback:^(NSString * _Nonnull message) {
381        UMLogWarn(@"Hiding splash screen from root view controller did not succeed: %@", message);
382      }];
383    });
384  };
385
386  UM_WEAKIFY(self);
387  dispatch_async(dispatch_get_main_queue(), ^{
388    UM_ENSURE_STRONGIFY(self);
389    [splashScreenService showSplashScreenFor:self
390                    splashScreenViewProvider:provider
391                             successCallback:hideRootViewControllerSplashScreen
392                             failureCallback:^(NSString *message){ UMLogWarn(@"%@", message); }];
393  });
394}
395
396- (void)hideLoadingProgressWindow
397{
398  [self.appLoadingProgressWindowController hide];
399}
400
401#pragma mark - EXAppLoaderDelegate
402
403- (void)appLoader:(EXAppLoader *)appLoader didLoadOptimisticManifest:(NSDictionary *)manifest
404{
405  if (_appLoadingCancelView) {
406    UM_WEAKIFY(self);
407    dispatch_async(dispatch_get_main_queue(), ^{
408      UM_ENSURE_STRONGIFY(self);
409      [self.appLoadingCancelView removeFromSuperview];
410      self.appLoadingCancelView = nil;
411    });
412  }
413  [self _showOrReconfigureManagedAppSplashScreen:manifest];
414  [self _setLoadingViewStatusIfEnabledFromAppLoader:appLoader];
415  if ([EXKernel sharedInstance].browserController) {
416    [[EXKernel sharedInstance].browserController addHistoryItemWithUrl:appLoader.manifestUrl manifest:manifest];
417  }
418  [self _rebuildBridge];
419}
420
421- (void)appLoader:(EXAppLoader *)appLoader didLoadBundleWithProgress:(EXLoadingProgress *)progress
422{
423  [self.appLoadingProgressWindowController updateStatusWithProgress:progress];
424}
425
426- (void)appLoader:(EXAppLoader *)appLoader didFinishLoadingManifest:(NSDictionary *)manifest bundle:(NSData *)data
427{
428  [self _showOrReconfigureManagedAppSplashScreen:manifest];
429  [self _rebuildBridge];
430  if (self->_appRecord.appManager.status == kEXReactAppManagerStatusBridgeLoading) {
431    [self->_appRecord.appManager appLoaderFinished];
432  }
433
434  if (!appLoader.isUpToDate && appLoader.shouldShowRemoteUpdateStatus) {
435    [self _showCachedExperienceAlert];
436  }
437}
438
439- (void)appLoader:(EXAppLoader *)appLoader didFailWithError:(NSError *)error
440{
441  if (_appRecord.appManager.status == kEXReactAppManagerStatusBridgeLoading) {
442    [_appRecord.appManager appLoaderFailedWithError:error];
443  }
444  [self maybeShowError:error];
445}
446
447- (void)appLoader:(EXAppLoader *)appLoader didResolveUpdatedBundleWithManifest:(NSDictionary * _Nullable)manifest isFromCache:(BOOL)isFromCache error:(NSError * _Nullable)error
448{
449  [[EXKernel sharedInstance].serviceRegistry.updatesManager notifyApp:_appRecord ofDownloadWithManifest:manifest isNew:!isFromCache error:error];
450}
451
452#pragma mark - EXReactAppManagerDelegate
453
454- (void)reactAppManagerIsReadyForLoad:(EXReactAppManager *)appManager
455{
456  UIView *reactView = appManager.rootView;
457  reactView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height);
458  reactView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
459
460
461  [_contentView removeFromSuperview];
462  _contentView = reactView;
463  [self.view addSubview:_contentView];
464  [self.view sendSubviewToBack:_contentView];
465  [reactView becomeFirstResponder];
466
467  // Set root view background color after adding as subview so we can access window
468  [self _setBackgroundColor:reactView];
469}
470
471- (void)reactAppManagerStartedLoadingJavaScript:(EXReactAppManager *)appManager
472{
473  EXAssertMainThread();
474  self.isLoading = YES;
475}
476
477- (void)reactAppManagerFinishedLoadingJavaScript:(EXReactAppManager *)appManager
478{
479  EXAssertMainThread();
480  self.isLoading = NO;
481  if ([EXKernel sharedInstance].browserController) {
482    [[EXKernel sharedInstance].browserController appDidFinishLoadingSuccessfully:_appRecord];
483  }
484}
485
486- (void)reactAppManagerAppContentDidAppear:(EXReactAppManager *)appManager
487{
488  EXSplashScreenService *splashScreenService = (EXSplashScreenService *)[UMModuleRegistryProvider getSingletonModuleForClass:[EXSplashScreenService class]];
489  [splashScreenService onAppContentDidAppear:self];
490}
491
492- (void)reactAppManagerAppContentWillReload:(EXReactAppManager *)appManager {
493  EXSplashScreenService *splashScreenService = (EXSplashScreenService *)[UMModuleRegistryProvider getSingletonModuleForClass:[EXSplashScreenService class]];
494  [splashScreenService onAppContentWillReload:self];
495}
496
497- (void)reactAppManager:(EXReactAppManager *)appManager failedToLoadJavaScriptWithError:(NSError *)error
498{
499  EXAssertMainThread();
500  [self maybeShowError:error];
501}
502
503- (void)reactAppManagerDidInvalidate:(EXReactAppManager *)appManager
504{
505}
506
507- (void)errorViewDidSelectRetry:(EXErrorView *)errorView
508{
509  [self refresh];
510}
511
512#pragma mark - orientation
513
514- (UIInterfaceOrientationMask)supportedInterfaceOrientations
515{
516#if __has_include(<EXScreenOrientation/EXScreenOrientationRegistry.h>)
517  EXScreenOrientationRegistry *screenOrientationRegistry = (EXScreenOrientationRegistry *)[UMModuleRegistryProvider getSingletonModuleForClass:[EXScreenOrientationRegistry class]];
518  if (screenOrientationRegistry && [screenOrientationRegistry requiredOrientationMask] > 0) {
519    return [screenOrientationRegistry requiredOrientationMask];
520  }
521#endif
522  return [self orientationMaskFromManifestOrDefault];
523}
524
525- (UIInterfaceOrientationMask)orientationMaskFromManifestOrDefault {
526  if (_appRecord.appLoader.manifest) {
527    NSString *orientationConfig = _appRecord.appLoader.manifest[@"orientation"];
528    if ([orientationConfig isEqualToString:@"portrait"]) {
529      // lock to portrait
530      return UIInterfaceOrientationMaskPortrait;
531    } else if ([orientationConfig isEqualToString:@"landscape"]) {
532      // lock to landscape
533      return UIInterfaceOrientationMaskLandscape;
534    }
535  }
536  // no config or default value: allow autorotation
537  return UIInterfaceOrientationMaskAllButUpsideDown;
538}
539
540- (void)traitCollectionDidChange:(nullable UITraitCollection *)previousTraitCollection {
541  [super traitCollectionDidChange:previousTraitCollection];
542  if ((self.traitCollection.verticalSizeClass != previousTraitCollection.verticalSizeClass)
543      || (self.traitCollection.horizontalSizeClass != previousTraitCollection.horizontalSizeClass)) {
544
545    #if __has_include(<EXScreenOrientation/EXScreenOrientationRegistry.h>)
546      EXScreenOrientationRegistry *screenOrientationRegistryController = (EXScreenOrientationRegistry *)[UMModuleRegistryProvider getSingletonModuleForClass:[EXScreenOrientationRegistry class]];
547      [screenOrientationRegistryController traitCollectionDidChangeTo:self.traitCollection];
548    #endif
549  }
550}
551
552#pragma mark - RCTAppearanceModule
553
554/**
555 * This function overrides behaviour of RCTAppearanceModule
556 * basing on 'userInterfaceStyle' option from the app manifest.
557 * It also defaults the RCTAppearanceModule to 'light'.
558 */
559- (void)_overrideAppearanceModuleBehaviour
560{
561  NSString *userInterfaceStyle = [self _readUserInterfaceStyleFromManifest:_appRecord.appLoader.manifest];
562  NSString *appearancePreference = nil;
563  if (!userInterfaceStyle || [userInterfaceStyle isEqualToString:@"light"]) {
564    appearancePreference = @"light";
565  } else if ([userInterfaceStyle isEqualToString:@"dark"]) {
566    appearancePreference = @"dark";
567  } else if ([userInterfaceStyle isEqualToString:@"automatic"]) {
568    appearancePreference = nil;
569  }
570  RCTOverrideAppearancePreference(appearancePreference);
571#if __has_include(<ABI40_0_0React/ABI40_0_0RCTAppearance.h>)
572  ABI40_0_0RCTOverrideAppearancePreference(appearancePreference);
573#endif
574#if __has_include(<ABI39_0_0React/ABI39_0_0RCTAppearance.h>)
575  ABI39_0_0RCTOverrideAppearancePreference(appearancePreference);
576#endif
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