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