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