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