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