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