1// Copyright 2015-present 650 Industries. All rights reserved.
2
3@import UIKit;
4
5#import "EXAnalytics.h"
6#import "EXAppLoader.h"
7#import "EXAppViewController.h"
8#import "EXAppLoadingProgressWindowController.h"
9#import "EXAppLoadingCancelView.h"
10#import "EXManagedAppSplashScreenViewProvider.h"
11#import "EXManagedAppSplashScreenConfigurationBuilder.h"
12#import "EXManagedAppSplashScreenViewController.h"
13#import "EXHomeAppSplashScreenViewProvider.h"
14#import "EXEnvironment.h"
15#import "EXErrorRecoveryManager.h"
16#import "EXErrorView.h"
17#import "EXFileDownloader.h"
18#import "EXKernel.h"
19#import "EXKernelUtil.h"
20#import "EXReactAppManager.h"
21#import "EXScreenOrientationManager.h"
22#import "EXVersions.h"
23#import "EXUpdatesManager.h"
24#import "EXUtil.h"
25
26#import <EXSplashScreen/EXSplashScreenService.h>
27#import <React/RCTUtils.h>
28#import <ExpoModulesCore/EXModuleRegistryProvider.h>
29
30#if __has_include(<EXScreenOrientation/EXScreenOrientationRegistry.h>)
31#import <EXScreenOrientation/EXScreenOrientationRegistry.h>
32#endif
33
34#import <React/RCTAppearance.h>
35#if defined(INCLUDES_VERSIONED_CODE) && __has_include(<ABI43_0_0React/ABI43_0_0RCTAppearance.h>)
36#import <ABI43_0_0React/ABI43_0_0RCTAppearance.h>
37#endif
38#if defined(INCLUDES_VERSIONED_CODE) && __has_include(<ABI42_0_0React/ABI42_0_0RCTAppearance.h>)
39#import <ABI42_0_0React/ABI42_0_0RCTAppearance.h>
40#endif
41#if defined(INCLUDES_VERSIONED_CODE) && __has_include(<ABI41_0_0React/ABI41_0_0RCTAppearance.h>)
42#import <ABI41_0_0React/ABI41_0_0RCTAppearance.h>
43#endif
44
45#define EX_INTERFACE_ORIENTATION_USE_MANIFEST 0
46
47// when we encounter an error and auto-refresh, we may actually see a series of errors.
48// we only want to trigger refresh once, so we debounce refresh on a timer.
49const CGFloat kEXAutoReloadDebounceSeconds = 0.1;
50
51// in development only, some errors can happen before we even start loading
52// (e.g. certain packager errors, such as an invalid bundle url)
53// and we want to make sure not to cover the error with a loading view or other chrome.
54const CGFloat kEXDevelopmentErrorCoolDownSeconds = 0.1;
55
56// copy of RNScreens protocol
57@protocol EXKernelRNSScreenWindowTraits
58
59+ (BOOL)shouldAskScreensForScreenOrientationInViewController:(UIViewController *)vc;
60
61@end
62
63NS_ASSUME_NONNULL_BEGIN
64
65@interface EXAppViewController ()
66  <EXReactAppManagerUIDelegate, EXAppLoaderDelegate, EXErrorViewDelegate, EXAppLoadingCancelViewDelegate>
67
68@property (nonatomic, assign) BOOL isLoading;
69@property (nonatomic, assign) BOOL isBridgeAlreadyLoading;
70@property (nonatomic, weak) EXKernelAppRecord *appRecord;
71@property (nonatomic, strong) EXErrorView *errorView;
72@property (nonatomic, assign) UIInterfaceOrientationMask supportedInterfaceOrientations; // override super
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@synthesize supportedInterfaceOrientations = _supportedInterfaceOrientations;
107
108#pragma mark - Lifecycle
109
110- (instancetype)initWithAppRecord:(EXKernelAppRecord *)record
111{
112  if (self = [super init]) {
113    _appRecord = record;
114    _supportedInterfaceOrientations = EX_INTERFACE_ORIENTATION_USE_MANIFEST;
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- (void)appStateDidBecomeActive
272{
273  dispatch_async(dispatch_get_main_queue(), ^{
274    [self _enforceDesiredDeviceOrientation];
275
276    // Reset the root view background color and window color if we switch between Expo home and project
277    [self _setBackgroundColor:self.view];
278  });
279  [_appRecord.appManager appStateDidBecomeActive];
280}
281
282- (void)appStateDidBecomeInactive
283{
284  [_appRecord.appManager appStateDidBecomeInactive];
285}
286
287- (void)_rebuildBridge
288{
289  if (!self.isBridgeAlreadyLoading) {
290    self.isBridgeAlreadyLoading = YES;
291    dispatch_async(dispatch_get_main_queue(), ^{
292      [self _overrideUserInterfaceStyleOf:self];
293      [self _overrideAppearanceModuleBehaviour];
294      [self _enforceDesiredDeviceOrientation];
295      [self _invalidateRecoveryTimer];
296      [[EXKernel sharedInstance] logAnalyticsEvent:@"LOAD_EXPERIENCE" forAppRecord:self.appRecord];
297      [self.appRecord.appManager rebuildBridge];
298    });
299  }
300}
301
302- (void)foregroundControllers
303{
304  if (_backgroundedControllers != nil) {
305    __block UIViewController *parentController = self;
306
307    [_backgroundedControllers enumerateObjectsUsingBlock:^(UIViewController * _Nonnull viewController, NSUInteger idx, BOOL * _Nonnull stop) {
308      [parentController presentViewController:viewController animated:NO completion:nil];
309      parentController = viewController;
310    }];
311
312    _backgroundedControllers = nil;
313  }
314}
315
316- (void)backgroundControllers
317{
318  UIViewController *childController = [self presentedViewController];
319
320  if (childController != nil) {
321    if (_backgroundedControllers == nil) {
322      _backgroundedControllers = [NSMutableArray new];
323    }
324
325    while (childController != nil) {
326      [_backgroundedControllers addObject:childController];
327      childController = childController.presentedViewController;
328    }
329  }
330}
331
332/**
333 * In managed app we expect two kinds of manifest:
334 * - optimistic one (served from cache)
335 * - actual one served when app is fetched.
336 * For each of them we should show SplashScreen,
337 * therefore for any consecutive SplashScreen.show call we just reconfigure what's already visible.
338 * In HomeApp or standalone apps this function is no-op as SplashScreen is managed differently.
339 */
340- (void)_showOrReconfigureManagedAppSplashScreen:(EXManifestsManifest *)manifest
341{
342  if (_isStandalone || _isHomeApp) {
343    return;
344  }
345  if (!_managedAppSplashScreenViewProvider) {
346    _managedAppSplashScreenViewProvider = [[EXManagedAppSplashScreenViewProvider alloc] initWithManifest:manifest];
347
348    [self _showManagedSplashScreenWithProvider:_managedAppSplashScreenViewProvider];
349  } else {
350    [_managedAppSplashScreenViewProvider updateSplashScreenViewWithManifest:manifest];
351  }
352}
353
354- (void)_showCachedExperienceAlert
355{
356  if (self.isStandalone || self.isHomeApp) {
357    return;
358  }
359
360  dispatch_async(dispatch_get_main_queue(), ^{
361    UIAlertController *alert = [UIAlertController
362                                alertControllerWithTitle:@"Using a cached project"
363                                message:@"If you did not intend to use a cached project, check your network connection and reload."
364                                preferredStyle:UIAlertControllerStyleAlert];
365    [alert addAction:[UIAlertAction actionWithTitle:@"Reload" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
366      [self refresh];
367    }]];
368    [alert addAction:[UIAlertAction actionWithTitle:@"Use cache" style:UIAlertActionStyleCancel handler:nil]];
369    [self presentViewController:alert animated:YES completion:nil];
370  });
371}
372
373- (void)_setLoadingViewStatusIfEnabledFromAppLoader:(EXAppLoader *)appLoader
374{
375  if (appLoader.shouldShowRemoteUpdateStatus) {
376    [self.appLoadingProgressWindowController updateStatus:appLoader.remoteUpdateStatus];
377  } else {
378    [self.appLoadingProgressWindowController hide];
379  }
380}
381
382- (void)_showSplashScreenWithProvider:(id<EXSplashScreenViewProvider>)provider
383{
384  EXSplashScreenService *splashScreenService = (EXSplashScreenService *)[EXModuleRegistryProvider getSingletonModuleForClass:[EXSplashScreenService class]];
385
386  // EXSplashScreenService presents a splash screen on a root view controller
387  // at the start of the app. Since we want the EXAppViewController to manage
388  // the lifecycle of the splash screen we need to:
389  // 1. present the splash screen on EXAppViewController
390  // 2. hide the splash screen of root view controller
391  // Disclaimer:
392  //  there's only one root view controller, but possibly many EXAppViewControllers
393  //  (in Expo Go: one project -> one EXAppViewController)
394  //  and we want to hide SplashScreen only once for the root view controller, hence the "once"
395  static dispatch_once_t once;
396  void (^hideRootViewControllerSplashScreen)(void) = ^void() {
397    dispatch_once(&once, ^{
398      UIViewController *rootViewController = [UIApplication sharedApplication].keyWindow.rootViewController;
399      [splashScreenService hideSplashScreenFor:rootViewController
400                               successCallback:^(BOOL hasEffect){}
401                               failureCallback:^(NSString * _Nonnull message) {
402        EXLogWarn(@"Hiding splash screen from root view controller did not succeed: %@", message);
403      }];
404    });
405  };
406
407  EX_WEAKIFY(self);
408  dispatch_async(dispatch_get_main_queue(), ^{
409    EX_ENSURE_STRONGIFY(self);
410    [splashScreenService showSplashScreenFor:self
411                    splashScreenViewProvider:provider
412                             successCallback:hideRootViewControllerSplashScreen
413                             failureCallback:^(NSString *message){ EXLogWarn(@"%@", message); }];
414  });
415}
416
417- (void)_showManagedSplashScreenWithProvider:(id<EXSplashScreenViewProvider>)provider
418{
419
420  EXSplashScreenService *splashScreenService = (EXSplashScreenService *)[EXModuleRegistryProvider getSingletonModuleForClass:[EXSplashScreenService class]];
421
422  EX_WEAKIFY(self);
423  dispatch_async(dispatch_get_main_queue(), ^{
424    EX_ENSURE_STRONGIFY(self);
425
426    UIView *rootView = self.view;
427    UIView *splashScreenView = [provider createSplashScreenView];
428    self.managedSplashScreenController = [[EXManagedAppSplashScreenViewController alloc] initWithRootView:rootView
429                                                                                                 splashScreenView:splashScreenView];
430    [splashScreenService showSplashScreenFor:self
431                      splashScreenController:self.managedSplashScreenController
432                             successCallback:^{}
433                             failureCallback:^(NSString *message){ EXLogWarn(@"%@", message); }];
434  });
435
436}
437
438- (void)hideLoadingProgressWindow
439{
440  [self.appLoadingProgressWindowController hide];
441  if (self.managedSplashScreenController) {
442    [self.managedSplashScreenController startSplashScreenVisibleTimer];
443  }
444}
445
446#pragma mark - EXAppLoaderDelegate
447
448- (void)appLoader:(EXAppLoader *)appLoader didLoadOptimisticManifest:(EXManifestsManifest *)manifest
449{
450  if (_appLoadingCancelView) {
451    EX_WEAKIFY(self);
452    dispatch_async(dispatch_get_main_queue(), ^{
453      EX_ENSURE_STRONGIFY(self);
454      [self.appLoadingCancelView removeFromSuperview];
455      self.appLoadingCancelView = nil;
456    });
457  }
458  [self _showOrReconfigureManagedAppSplashScreen:manifest];
459  [self _setLoadingViewStatusIfEnabledFromAppLoader:appLoader];
460  if ([EXKernel sharedInstance].browserController) {
461    [[EXKernel sharedInstance].browserController addHistoryItemWithUrl:appLoader.manifestUrl manifest:manifest];
462  }
463  [self _rebuildBridge];
464}
465
466- (void)appLoader:(EXAppLoader *)appLoader didLoadBundleWithProgress:(EXLoadingProgress *)progress
467{
468  if (self->_appRecord.appManager.status != kEXReactAppManagerStatusRunning) {
469    [self.appLoadingProgressWindowController updateStatusWithProgress:progress];
470  }
471}
472
473- (void)appLoader:(EXAppLoader *)appLoader didFinishLoadingManifest:(EXManifestsManifest *)manifest bundle:(NSData *)data
474{
475  [self _showOrReconfigureManagedAppSplashScreen:manifest];
476  [self _rebuildBridge];
477  if (self->_appRecord.appManager.status == kEXReactAppManagerStatusBridgeLoading) {
478    [self->_appRecord.appManager appLoaderFinished];
479  }
480
481  if (!appLoader.isUpToDate && appLoader.shouldShowRemoteUpdateStatus) {
482    [self _showCachedExperienceAlert];
483  }
484}
485
486- (void)appLoader:(EXAppLoader *)appLoader didFailWithError:(NSError *)error
487{
488  if (_appRecord.appManager.status == kEXReactAppManagerStatusBridgeLoading) {
489    [_appRecord.appManager appLoaderFailedWithError:error];
490  }
491  [self maybeShowError:error];
492}
493
494- (void)appLoader:(EXAppLoader *)appLoader didResolveUpdatedBundleWithManifest:(EXManifestsManifest * _Nullable)manifest isFromCache:(BOOL)isFromCache error:(NSError * _Nullable)error
495{
496  [[EXKernel sharedInstance].serviceRegistry.updatesManager notifyApp:_appRecord ofDownloadWithManifest:manifest isNew:!isFromCache error:error];
497}
498
499#pragma mark - EXReactAppManagerDelegate
500
501- (void)reactAppManagerIsReadyForLoad:(EXReactAppManager *)appManager
502{
503  UIView *reactView = appManager.rootView;
504  reactView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height);
505  reactView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
506
507
508  [_contentView removeFromSuperview];
509  _contentView = reactView;
510  [self.view addSubview:_contentView];
511  [self.view sendSubviewToBack:_contentView];
512  [reactView becomeFirstResponder];
513
514  // Set root view background color after adding as subview so we can access window
515  [self _setBackgroundColor:reactView];
516}
517
518- (void)reactAppManagerStartedLoadingJavaScript:(EXReactAppManager *)appManager
519{
520  EXAssertMainThread();
521  self.isLoading = YES;
522}
523
524- (void)reactAppManagerFinishedLoadingJavaScript:(EXReactAppManager *)appManager
525{
526  EXAssertMainThread();
527  self.isLoading = NO;
528  if ([EXKernel sharedInstance].browserController) {
529    [[EXKernel sharedInstance].browserController appDidFinishLoadingSuccessfully:_appRecord];
530  }
531}
532
533- (void)reactAppManagerAppContentDidAppear:(EXReactAppManager *)appManager
534{
535  EXSplashScreenService *splashScreenService = (EXSplashScreenService *)[EXModuleRegistryProvider getSingletonModuleForClass:[EXSplashScreenService class]];
536  [splashScreenService onAppContentDidAppear:self];
537}
538
539- (void)reactAppManagerAppContentWillReload:(EXReactAppManager *)appManager {
540  EXSplashScreenService *splashScreenService = (EXSplashScreenService *)[EXModuleRegistryProvider getSingletonModuleForClass:[EXSplashScreenService class]];
541  [splashScreenService onAppContentWillReload:self];
542}
543
544- (void)reactAppManager:(EXReactAppManager *)appManager failedToLoadJavaScriptWithError:(NSError *)error
545{
546  EXAssertMainThread();
547  [self maybeShowError:error];
548}
549
550- (void)reactAppManagerDidInvalidate:(EXReactAppManager *)appManager
551{
552}
553
554- (void)errorViewDidSelectRetry:(EXErrorView *)errorView
555{
556  [self refresh];
557}
558
559#pragma mark - orientation
560
561- (UIInterfaceOrientationMask)supportedInterfaceOrientations
562{
563  if ([self shouldUseRNScreenOrientation]) {
564    return [super supportedInterfaceOrientations];
565  }
566
567#if __has_include(<EXScreenOrientation/EXScreenOrientationRegistry.h>)
568  EXScreenOrientationRegistry *screenOrientationRegistry = (EXScreenOrientationRegistry *)[EXModuleRegistryProvider getSingletonModuleForClass:[EXScreenOrientationRegistry class]];
569  if (screenOrientationRegistry && [screenOrientationRegistry requiredOrientationMask] > 0) {
570    return [screenOrientationRegistry requiredOrientationMask];
571  }
572#endif
573
574  // TODO: Remove once sdk 37 is phased out
575  if (_supportedInterfaceOrientations != EX_INTERFACE_ORIENTATION_USE_MANIFEST) {
576    return _supportedInterfaceOrientations;
577  }
578
579  return [self orientationMaskFromManifestOrDefault];
580}
581
582- (BOOL)shouldUseRNScreenOrientation
583{
584  Class screenWindowTraitsClass = [self->_appRecord.appManager versionedClassFromString:@"RNSScreenWindowTraits"];
585  if ([screenWindowTraitsClass respondsToSelector:@selector(shouldAskScreensForScreenOrientationInViewController:)]) {
586    id<EXKernelRNSScreenWindowTraits> screenWindowTraits = (id<EXKernelRNSScreenWindowTraits>)screenWindowTraitsClass;
587    return [screenWindowTraits shouldAskScreensForScreenOrientationInViewController:self];
588  }
589  return NO;
590}
591
592- (UIInterfaceOrientationMask)orientationMaskFromManifestOrDefault {
593  if (_appRecord.appLoader.manifest) {
594    NSString *orientationConfig = _appRecord.appLoader.manifest.orientation;
595    if ([orientationConfig isEqualToString:@"portrait"]) {
596      // lock to portrait
597      return UIInterfaceOrientationMaskPortrait;
598    } else if ([orientationConfig isEqualToString:@"landscape"]) {
599      // lock to landscape
600      return UIInterfaceOrientationMaskLandscape;
601    }
602  }
603  // no config or default value: allow autorotation
604  return UIInterfaceOrientationMaskAllButUpsideDown;
605}
606
607// TODO: Remove once sdk 37 is phased out
608- (void)setSupportedInterfaceOrientations:(UIInterfaceOrientationMask)supportedInterfaceOrientations
609{
610  _supportedInterfaceOrientations = supportedInterfaceOrientations;
611  [self _enforceDesiredDeviceOrientation];
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    #if __has_include(<EXScreenOrientation/EXScreenOrientationRegistry.h>)
620      EXScreenOrientationRegistry *screenOrientationRegistryController = (EXScreenOrientationRegistry *)[EXModuleRegistryProvider getSingletonModuleForClass:[EXScreenOrientationRegistry class]];
621      [screenOrientationRegistryController traitCollectionDidChangeTo:self.traitCollection];
622    #endif
623
624    // TODO: Remove once sdk 37 is phased out
625    [[EXKernel sharedInstance].serviceRegistry.screenOrientationManager handleScreenOrientationChange:self.traitCollection];
626  }
627}
628
629// TODO: Remove once sdk 37 is phased out
630- (void)_enforceDesiredDeviceOrientation
631{
632  RCTAssertMainQueue();
633  UIInterfaceOrientationMask mask = [self supportedInterfaceOrientations];
634  UIDeviceOrientation currentOrientation = [[UIDevice currentDevice] orientation];
635  UIInterfaceOrientation newOrientation = UIInterfaceOrientationUnknown;
636  switch (mask) {
637    case UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskPortraitUpsideDown:
638      if (!UIDeviceOrientationIsPortrait(currentOrientation)) {
639        newOrientation = UIInterfaceOrientationPortrait;
640      }
641      break;
642    case UIInterfaceOrientationMaskPortrait:
643      newOrientation = UIInterfaceOrientationPortrait;
644      break;
645    case UIInterfaceOrientationMaskPortraitUpsideDown:
646      newOrientation = UIInterfaceOrientationPortraitUpsideDown;
647      break;
648    case UIInterfaceOrientationMaskLandscape:
649      if (!UIDeviceOrientationIsLandscape(currentOrientation)) {
650        newOrientation = UIInterfaceOrientationLandscapeLeft;
651      }
652      break;
653    case UIInterfaceOrientationMaskLandscapeLeft:
654      newOrientation = UIInterfaceOrientationLandscapeLeft;
655      break;
656    case UIInterfaceOrientationMaskLandscapeRight:
657      newOrientation = UIInterfaceOrientationLandscapeRight;
658      break;
659    case UIInterfaceOrientationMaskAllButUpsideDown:
660      if (currentOrientation == UIDeviceOrientationFaceDown) {
661        newOrientation = UIInterfaceOrientationPortrait;
662      }
663      break;
664    default:
665      break;
666  }
667  if (newOrientation != UIInterfaceOrientationUnknown) {
668    [[UIDevice currentDevice] setValue:@(newOrientation) forKey:@"orientation"];
669  }
670  [UIViewController attemptRotationToDeviceOrientation];
671}
672
673#pragma mark - RCTAppearanceModule
674
675/**
676 * This function overrides behaviour of RCTAppearanceModule
677 * basing on 'userInterfaceStyle' option from the app manifest.
678 * It also defaults the RCTAppearanceModule to 'light'.
679 */
680- (void)_overrideAppearanceModuleBehaviour
681{
682  NSString *userInterfaceStyle = [self _readUserInterfaceStyleFromManifest:_appRecord.appLoader.manifest];
683  NSString *appearancePreference = nil;
684  if (!userInterfaceStyle || [userInterfaceStyle isEqualToString:@"light"]) {
685    appearancePreference = @"light";
686  } else if ([userInterfaceStyle isEqualToString:@"dark"]) {
687    appearancePreference = @"dark";
688  } else if ([userInterfaceStyle isEqualToString:@"automatic"]) {
689    appearancePreference = nil;
690  }
691  RCTOverrideAppearancePreference(appearancePreference);
692#if defined(INCLUDES_VERSIONED_CODE) && __has_include(<ABI43_0_0React/ABI43_0_0RCTAppearance.h>)
693  ABI43_0_0RCTOverrideAppearancePreference(appearancePreference);
694#endif
695
696#if defined(INCLUDES_VERSIONED_CODE) && __has_include(<ABI42_0_0React/ABI42_0_0RCTAppearance.h>)
697  ABI42_0_0RCTOverrideAppearancePreference(appearancePreference);
698#endif
699#if defined(INCLUDES_VERSIONED_CODE) && __has_include(<ABI41_0_0React/ABI41_0_0RCTAppearance.h>)
700  ABI41_0_0RCTOverrideAppearancePreference(appearancePreference);
701#endif
702}
703
704#pragma mark - user interface style
705
706- (void)_overrideUserInterfaceStyleOf:(UIViewController *)viewController
707{
708  if (@available(iOS 13.0, *)) {
709    NSString *userInterfaceStyle = [self _readUserInterfaceStyleFromManifest:_appRecord.appLoader.manifest];
710    viewController.overrideUserInterfaceStyle = [self _userInterfaceStyleForString:userInterfaceStyle];
711  }
712}
713
714- (NSString * _Nullable)_readUserInterfaceStyleFromManifest:(EXManifestsManifest *)manifest
715{
716  return manifest.userInterfaceStyle;
717}
718
719- (UIUserInterfaceStyle)_userInterfaceStyleForString:(NSString *)userInterfaceStyleString API_AVAILABLE(ios(12.0)) {
720  if ([userInterfaceStyleString isEqualToString:@"dark"]) {
721    return UIUserInterfaceStyleDark;
722  }
723  if ([userInterfaceStyleString isEqualToString:@"automatic"]) {
724    return UIUserInterfaceStyleUnspecified;
725  }
726  return UIUserInterfaceStyleLight;
727}
728
729#pragma mark - root view and window background color
730
731- (void)_setBackgroundColor:(UIView *)view
732{
733    NSString *backgroundColorString = [self _readBackgroundColorFromManifest:_appRecord.appLoader.manifest];
734    UIColor *backgroundColor = [EXUtil colorWithHexString:backgroundColorString];
735
736    if (backgroundColor) {
737      view.backgroundColor = backgroundColor;
738      // NOTE(brentvatne): it may be desirable at some point to split the window backgroundColor out from the
739      // root view, we can do if use case is presented to us.
740      view.window.backgroundColor = backgroundColor;
741    } else {
742      view.backgroundColor = [UIColor whiteColor];
743
744      // NOTE(brentvatne): we used to use white as a default background color for window but this caused
745      // problems when using form sheet presentation style with vcs eg: <Modal /> and native-stack. Most
746      // users expect the background behind these to be black, which is the default if backgroundColor is nil.
747      view.window.backgroundColor = nil;
748
749      // NOTE(brentvatne): we may want to default to respecting the default system background color
750      // on iOS13 and higher, but if we do make this choice then we will have to implement it on Android
751      // as well. This would also be a breaking change. Leaaving this here as a placeholder for the future.
752      // if (@available(iOS 13.0, *)) {
753      //   view.backgroundColor = [UIColor systemBackgroundColor];
754      // } else {
755      //  view.backgroundColor = [UIColor whiteColor];
756      // }
757    }
758}
759
760- (NSString * _Nullable)_readBackgroundColorFromManifest:(EXManifestsManifest *)manifest
761{
762  return manifest.iosOrRootBackgroundColor;
763}
764
765
766#pragma mark - Internal
767
768- (void)_showErrorWithType:(EXFatalErrorType)type error:(nullable NSError *)error
769{
770  EXAssertMainThread();
771  _dtmLastFatalErrorShown = [NSDate date];
772  if (_errorView && _contentView == _errorView) {
773    // already showing, just update
774    _errorView.type = type;
775    _errorView.error = error;
776  } {
777    [_contentView removeFromSuperview];
778    if (!_errorView) {
779      _errorView = [[EXErrorView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
780      _errorView.delegate = self;
781      _errorView.appRecord = _appRecord;
782    }
783    _errorView.type = type;
784    _errorView.error = error;
785    _contentView = _errorView;
786    [self.view addSubview:_contentView];
787    [[EXAnalytics sharedInstance] logErrorVisibleEvent];
788  }
789}
790
791- (void)setIsLoading:(BOOL)isLoading
792{
793  if ([_appRecord.appManager enablesDeveloperTools] && _dtmLastFatalErrorShown) {
794    if ([_dtmLastFatalErrorShown timeIntervalSinceNow] >= -kEXDevelopmentErrorCoolDownSeconds) {
795      // we just showed a fatal error very recently, do not begin loading.
796      // this can happen in some cases where react native sends the 'started loading' notif
797      // in spite of a packager error.
798      return;
799    }
800  }
801  _isLoading = isLoading;
802  EX_WEAKIFY(self);
803  dispatch_async(dispatch_get_main_queue(), ^{
804    EX_ENSURE_STRONGIFY(self);
805    if (!isLoading) {
806      [self.appLoadingProgressWindowController hide];
807    }
808  });
809}
810
811#pragma mark - error recovery
812
813- (BOOL)_willAutoRecoverFromError:(NSError *)error
814{
815  if (![_appRecord.appManager enablesDeveloperTools]) {
816    BOOL shouldRecover = [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager experienceShouldReloadOnError:_appRecord.scopeKey];
817    if (shouldRecover) {
818      [self _invalidateRecoveryTimer];
819      _tmrAutoReloadDebounce = [NSTimer scheduledTimerWithTimeInterval:kEXAutoReloadDebounceSeconds
820                                                                target:self
821                                                              selector:@selector(refresh)
822                                                              userInfo:nil
823                                                               repeats:NO];
824    }
825    return shouldRecover;
826  }
827  return NO;
828}
829
830- (void)_invalidateRecoveryTimer
831{
832  if (_tmrAutoReloadDebounce) {
833    [_tmrAutoReloadDebounce invalidate];
834    _tmrAutoReloadDebounce = nil;
835  }
836}
837
838#pragma mark - EXAppLoadingCancelViewDelegate
839
840- (void)appLoadingCancelViewDidCancel:(EXAppLoadingCancelView *)view {
841  if ([EXKernel sharedInstance].browserController) {
842    [[EXKernel sharedInstance].browserController moveHomeToVisible];
843  }
844}
845
846@end
847
848NS_ASSUME_NONNULL_END
849