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