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