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