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