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