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