1#import "EXApiUtil.h"
2#import "EXBuildConstants.h"
3#import "EXEnvironment.h"
4#import "EXErrorRecoveryManager.h"
5#import "EXUserNotificationManager.h"
6#import "EXKernel.h"
7#import "EXAppLoader.h"
8#import "EXKernelLinkingManager.h"
9#import "EXKernelServiceRegistry.h"
10#import "EXKernelUtil.h"
11#import "EXLog.h"
12#import "ExpoKit.h"
13#import "EXReactAppManager.h"
14#import "EXReactAppManager+Private.h"
15#import "EXVersionManager.h"
16#import "EXVersions.h"
17#import "EXAppViewController.h"
18#import <UMCore/UMModuleRegistryProvider.h>
19#import <EXConstants/EXConstantsService.h>
20#import <EXSplashScreen/EXSplashScreenService.h>
21
22#import <React/RCTBridge.h>
23#import <React/RCTCxxBridgeDelegate.h>
24#import <React/JSCExecutorFactory.h>
25#import <React/RCTRootView.h>
26
27@interface EXVersionManager (Legacy)
28// TODO: remove after non-unimodules SDK versions are dropped
29
30- (void)bridgeDidForeground;
31- (void)bridgeDidBackground;
32
33@end
34
35typedef void (^SDK21RCTSourceLoadBlock)(NSError *error, NSData *source, int64_t sourceLength);
36
37/**
38 * TODO: Remove once SDK 38 is phased out.
39 */
40@protocol PreSDK39EXSplashScreenManagerProtocol
41
42@property (assign) BOOL started;
43@property (assign) BOOL finished;
44
45@end
46
47@implementation RCTSource (EXReactAppManager)
48
49- (instancetype)initWithURL:(nonnull NSURL *)url data:(nonnull NSData *)data
50{
51  if (self = [super init]) {
52    // Use KVO since RN publicly declares these properties as readonly and privately defines the
53    // ivars
54    [self setValue:url forKey:@"url"];
55    [self setValue:data forKey:@"data"];
56    [self setValue:@(data.length) forKey:@"length"];
57    [self setValue:@(RCTSourceFilesChangedCountNotBuiltByBundler) forKey:@"filesChangedCount"];
58  }
59  return self;
60}
61
62@end
63
64@interface EXReactAppManager () <RCTBridgeDelegate, RCTCxxBridgeDelegate>
65
66@property (nonatomic, strong) UIView * __nullable reactRootView;
67@property (nonatomic, copy) RCTSourceLoadBlock loadCallback;
68@property (nonatomic, strong) NSDictionary *initialProps;
69@property (nonatomic, strong) NSTimer *viewTestTimer;
70
71@end
72
73@implementation EXReactAppManager
74
75- (instancetype)initWithAppRecord:(EXKernelAppRecord *)record initialProps:(NSDictionary *)initialProps
76{
77  if (self = [super init]) {
78    _appRecord = record;
79    _initialProps = initialProps;
80    _isHeadless = NO;
81    _exceptionHandler = [[EXReactAppExceptionHandler alloc] initWithAppRecord:_appRecord];
82  }
83  return self;
84}
85
86- (void)setAppRecord:(EXKernelAppRecord *)appRecord
87{
88  _appRecord = appRecord;
89  _exceptionHandler = [[EXReactAppExceptionHandler alloc] initWithAppRecord:appRecord];
90}
91
92- (EXReactAppManagerStatus)status
93{
94  if (!_appRecord) {
95    return kEXReactAppManagerStatusError;
96  }
97  if (_loadCallback) {
98    // we have a RCTBridge load callback so we're ready to receive load events
99    return kEXReactAppManagerStatusBridgeLoading;
100  }
101  if (_isBridgeRunning) {
102    return kEXReactAppManagerStatusRunning;
103  }
104  return kEXReactAppManagerStatusNew;
105}
106
107- (UIView *)rootView
108{
109  return _reactRootView;
110}
111
112- (void)rebuildBridge
113{
114  EXAssertMainThread();
115  NSAssert((_delegate != nil), @"Cannot init react app without EXReactAppManagerDelegate");
116  [self _invalidateAndClearDelegate:NO];
117  [self computeVersionSymbolPrefix];
118
119  if ([self isReadyToLoad]) {
120    Class versionManagerClass = [self versionedClassFromString:@"EXVersionManager"];
121    Class bridgeClass = [self versionedClassFromString:@"RCTBridge"];
122    Class rootViewClass = [self versionedClassFromString:@"RCTRootView"];
123
124    _versionManager = [[versionManagerClass alloc]
125                       initWithParams:[self extraParams]
126                       fatalHandler:handleFatalReactError
127                       logFunction:[self logFunction]
128                       logThreshold:[self logLevel]
129                       ];
130    _reactBridge = [[bridgeClass alloc] initWithDelegate:self launchOptions:[self launchOptionsForBridge]];
131
132    if (!_isHeadless) {
133      // We don't want to run the whole JS app if app launches in the background,
134      // so we're omitting creation of RCTRootView that triggers runApplication and sets up React view hierarchy.
135      _reactRootView = [[rootViewClass alloc] initWithBridge:_reactBridge
136                                                  moduleName:[self applicationKeyForRootView]
137                                           initialProperties:[self initialPropertiesForRootView]];
138    }
139
140    [_delegate reactAppManagerIsReadyForLoad:self];
141
142    NSAssert([_reactBridge isLoading], @"React bridge should be loading once initialized");
143    [_versionManager bridgeWillStartLoading:_reactBridge];
144  }
145}
146
147- (NSDictionary *)extraParams
148{
149  // we allow the vanilla RN dev menu in some circumstances.
150  BOOL isStandardDevMenuAllowed = [EXEnvironment sharedEnvironment].isDetached;
151  NSMutableDictionary *params = [NSMutableDictionary dictionaryWithDictionary:@{
152    @"manifest": _appRecord.appLoader.manifest,
153    @"constants": @{
154        @"linkingUri": RCTNullIfNil([EXKernelLinkingManager linkingUriForExperienceUri:_appRecord.appLoader.manifestUrl useLegacy:[self _compareVersionTo:27] == NSOrderedAscending]),
155        @"experienceUrl": RCTNullIfNil(_appRecord.appLoader.manifestUrl? _appRecord.appLoader.manifestUrl.absoluteString: nil),
156        @"expoRuntimeVersion": [EXBuildConstants sharedInstance].expoRuntimeVersion,
157        @"manifest": _appRecord.appLoader.manifest,
158        @"executionEnvironment": [self _executionEnvironment],
159        @"appOwnership": [self _appOwnership],
160        @"isHeadless": @(_isHeadless),
161        @"supportedExpoSdks": [EXVersions sharedInstance].versions[@"sdkVersions"],
162    },
163    @"exceptionsManagerDelegate": _exceptionHandler,
164    @"initialUri": RCTNullIfNil([EXKernelLinkingManager initialUriWithManifestUrl:_appRecord.appLoader.manifestUrl]),
165    @"isDeveloper": @([self enablesDeveloperTools]),
166    @"isStandardDevMenuAllowed": @(isStandardDevMenuAllowed),
167    @"testEnvironment": @([EXEnvironment sharedEnvironment].testEnvironment),
168    @"services": [EXKernel sharedInstance].serviceRegistry.allServices,
169    @"singletonModules": [UMModuleRegistryProvider singletonModules],
170    @"moduleRegistryDelegateClass": RCTNullIfNil([self moduleRegistryDelegateClass]),
171  }];
172  if ([@"expo" isEqualToString:[self _appOwnership]]) {
173    [params addEntriesFromDictionary:@{
174      @"fileSystemDirectories": @{
175          @"documentDirectory": [self scopedDocumentDirectory],
176          @"cachesDirectory": [self scopedCachesDirectory]
177      }
178    }];
179  }
180  return params;
181}
182
183- (void)invalidate
184{
185  [self _invalidateAndClearDelegate:YES];
186}
187
188- (void)_invalidateAndClearDelegate:(BOOL)clearDelegate
189{
190  [self _stopObservingBridgeNotifications];
191  if (_viewTestTimer) {
192    [_viewTestTimer invalidate];
193    _viewTestTimer = nil;
194  }
195  if (_versionManager) {
196    [_versionManager invalidate];
197    _versionManager = nil;
198  }
199  if (_reactRootView) {
200    [_reactRootView removeFromSuperview];
201    _reactRootView = nil;
202  }
203  if (_reactBridge) {
204    [_reactBridge invalidate];
205    _reactBridge = nil;
206    if (_delegate) {
207      [_delegate reactAppManagerDidInvalidate:self];
208      if (clearDelegate) {
209        _delegate = nil;
210      }
211    }
212  }
213  _isBridgeRunning = NO;
214  [self _invalidateVersionState];
215}
216
217- (void)computeVersionSymbolPrefix
218{
219  // TODO: ben: kernel checks detached versions here
220  _validatedVersion = [[EXVersions sharedInstance] availableSdkVersionForManifest:_appRecord.appLoader.manifest];
221  _versionSymbolPrefix = [[EXVersions sharedInstance] symbolPrefixForSdkVersion:self.validatedVersion isKernel:NO];
222}
223
224- (void)_invalidateVersionState
225{
226  _versionSymbolPrefix = @"";
227  _validatedVersion = nil;
228}
229
230- (Class)versionedClassFromString: (NSString *)classString
231{
232  return NSClassFromString([self versionedString:classString]);
233}
234
235- (NSString *)versionedString: (NSString *)string
236{
237  return [EXVersions versionedString:string withPrefix:_versionSymbolPrefix];
238}
239
240- (NSString *)escapedResourceName:(NSString *)string
241{
242  NSString *charactersToEscape = @"!*'();:@&=+$,/?%#[]";
243  NSCharacterSet *allowedCharacters = [[NSCharacterSet characterSetWithCharactersInString:charactersToEscape] invertedSet];
244  return [string stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacters];
245}
246
247- (BOOL)isReadyToLoad
248{
249  if (_appRecord) {
250    return (_appRecord.appLoader.status == kEXAppLoaderStatusHasManifest || _appRecord.appLoader.status == kEXAppLoaderStatusHasManifestAndBundle);
251  }
252  return NO;
253}
254
255- (NSURL *)bundleUrl
256{
257  return [EXApiUtil bundleUrlFromManifest:_appRecord.appLoader.manifest];
258}
259
260- (void)appStateDidBecomeActive
261{
262  if ([_versionManager respondsToSelector:@selector(bridgeDidForeground)]) {
263    // supported before SDK 29 / unimodules
264    [_versionManager bridgeDidForeground];
265  }
266}
267
268- (void)appStateDidBecomeInactive
269{
270  if ([_versionManager respondsToSelector:@selector(bridgeDidBackground)]) {
271    [_versionManager bridgeDidBackground];
272  }
273}
274
275#pragma mark - EXAppFetcherDataSource
276
277- (NSString *)bundleResourceNameForAppFetcher:(EXAppFetcher *)appFetcher withManifest:(nonnull NSDictionary *)manifest
278{
279  if ([EXEnvironment sharedEnvironment].isDetached) {
280    NSLog(@"Standalone bundle remote url is %@", [EXEnvironment sharedEnvironment].standaloneManifestUrl);
281    return kEXEmbeddedBundleResourceName;
282  } else {
283    return manifest[@"id"];
284  }
285}
286
287- (BOOL)appFetcherShouldInvalidateBundleCache:(EXAppFetcher *)appFetcher
288{
289  return NO;
290}
291
292#pragma mark - RCTBridgeDelegate
293
294- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
295{
296  return [self bundleUrl];
297}
298
299- (void)loadSourceForBridge:(RCTBridge *)bridge withBlock:(RCTSourceLoadBlock)loadCallback
300{
301  // clear any potentially old loading state
302  if (_appRecord.experienceId) {
303    [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager setError:nil forExperienceId:_appRecord.experienceId];
304  }
305  [self _stopObservingBridgeNotifications];
306  [self _startObservingBridgeNotificationsForBridge:bridge];
307
308  if ([self enablesDeveloperTools]) {
309    if ([_appRecord.appLoader supportsBundleReload]) {
310      [_appRecord.appLoader forceBundleReload];
311    } else {
312      NSAssert(_appRecord.experienceId, @"EXKernelAppRecord.experienceId should be nonnull if we have a manifest with developer tools enabled");
313      [[EXKernel sharedInstance] reloadAppWithExperienceId:_appRecord.experienceId];
314    }
315  }
316
317  _loadCallback = loadCallback;
318  if (_appRecord.appLoader.status == kEXAppLoaderStatusHasManifestAndBundle) {
319    // finish loading immediately (app loader won't call this since it's already done)
320    [self appLoaderFinished];
321  } else {
322    // wait for something else to call `appLoaderFinished` or `appLoaderFailed` later.
323  }
324}
325
326- (NSArray *)extraModulesForBridge:(RCTBridge *)bridge
327{
328  return [self.versionManager extraModulesForBridge:bridge];
329}
330
331- (void)appLoaderFinished
332{
333  NSData *data = _appRecord.appLoader.bundle;
334  if (_loadCallback) {
335    if ([self _compareVersionTo:22] == NSOrderedAscending) {
336      SDK21RCTSourceLoadBlock legacyLoadCallback = (SDK21RCTSourceLoadBlock)_loadCallback;
337      legacyLoadCallback(nil, data, data.length);
338    } else {
339      _loadCallback(nil, [[RCTSource alloc] initWithURL:[self bundleUrl] data:data]);
340    }
341    _loadCallback = nil;
342  }
343}
344
345- (void)appLoaderFailedWithError:(NSError *)error
346{
347  // RN is going to call RCTFatal() on this error, so keep a reference to it for later
348  // so we can distinguish this non-fatal error from actual fatal cases.
349  if (_appRecord.experienceId) {
350    [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager setError:error forExperienceId:_appRecord.experienceId];
351  }
352
353  // react won't post this for us
354  [[NSNotificationCenter defaultCenter] postNotificationName:[self versionedString:RCTJavaScriptDidFailToLoadNotification] object:error];
355
356  if (_loadCallback) {
357    if ([self _compareVersionTo:22] == NSOrderedAscending) {
358      SDK21RCTSourceLoadBlock legacyLoadCallback = (SDK21RCTSourceLoadBlock)_loadCallback;
359      legacyLoadCallback(error, nil, 0);
360    } else {
361      _loadCallback(error, nil);
362    }
363    _loadCallback = nil;
364  }
365}
366
367#pragma mark - JavaScript loading
368
369- (void)_startObservingBridgeNotificationsForBridge:(RCTBridge *)bridge
370{
371  NSAssert(bridge, @"Must subscribe to loading notifs for a non-null bridge");
372
373  [[NSNotificationCenter defaultCenter] addObserver:self
374                                           selector:@selector(_handleJavaScriptStartLoadingEvent:)
375                                               name:[self versionedString:RCTJavaScriptWillStartLoadingNotification]
376                                             object:bridge];
377  [[NSNotificationCenter defaultCenter] addObserver:self
378                                           selector:@selector(_handleJavaScriptLoadEvent:)
379                                               name:[self versionedString:RCTJavaScriptDidLoadNotification]
380                                             object:bridge];
381  [[NSNotificationCenter defaultCenter] addObserver:self
382                                           selector:@selector(_handleJavaScriptLoadEvent:)
383                                               name:[self versionedString:RCTJavaScriptDidFailToLoadNotification]
384                                             object:bridge];
385  [[NSNotificationCenter defaultCenter] addObserver:self
386                                           selector:@selector(_handleReactContentEvent:)
387                                               name:[self versionedString:RCTContentDidAppearNotification]
388                                             object:nil];
389  [[NSNotificationCenter defaultCenter] addObserver:self
390                                           selector:@selector(_handleBridgeEvent:)
391                                               name:[self versionedString:RCTBridgeWillReloadNotification]
392                                             object:bridge];
393}
394
395- (void)_stopObservingBridgeNotifications
396{
397  [[NSNotificationCenter defaultCenter] removeObserver:self name:[self versionedString:RCTJavaScriptWillStartLoadingNotification] object:_reactBridge];
398  [[NSNotificationCenter defaultCenter] removeObserver:self name:[self versionedString:RCTJavaScriptDidLoadNotification] object:_reactBridge];
399  [[NSNotificationCenter defaultCenter] removeObserver:self name:[self versionedString:RCTJavaScriptDidFailToLoadNotification] object:_reactBridge];
400  [[NSNotificationCenter defaultCenter] removeObserver:self name:[self versionedString:RCTContentDidAppearNotification] object:_reactBridge];
401  [[NSNotificationCenter defaultCenter] removeObserver:self name:[self versionedString:RCTBridgeWillReloadNotification] object:_reactBridge];
402}
403
404- (void)_handleJavaScriptStartLoadingEvent:(NSNotification *)notification
405{
406  __weak typeof(self) weakSelf = self;
407  dispatch_async(dispatch_get_main_queue(), ^{
408    __strong typeof(self) strongSelf = weakSelf;
409    if (strongSelf) {
410      [strongSelf.delegate reactAppManagerStartedLoadingJavaScript:strongSelf];
411    }
412  });
413}
414
415- (void)_handleJavaScriptLoadEvent:(NSNotification *)notification
416{
417  if ([notification.name isEqualToString:[self versionedString:RCTJavaScriptDidLoadNotification]]) {
418    _isBridgeRunning = YES;
419    _hasBridgeEverLoaded = YES;
420    [_versionManager bridgeFinishedLoading:_reactBridge];
421    [self appStateDidBecomeActive];
422
423    // TODO: temporary solution for hiding LoadingProgressWindow
424    if (_appRecord.viewController) {
425      [_appRecord.viewController hideLoadingProgressWindow];
426    }
427
428    // TODO: To be removed once SDK 38 is phased out
429    // Above SDK 38 this code is invoked in different place
430    if ([self _compareVersionTo:39] == NSOrderedAscending) {
431      [self _preSDK39BeginWaitingForAppLoading];
432    }
433  } else if ([notification.name isEqualToString:[self versionedString:RCTJavaScriptDidFailToLoadNotification]]) {
434    NSError *error = (notification.userInfo) ? notification.userInfo[@"error"] : nil;
435    if (_appRecord.experienceId) {
436      [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager setError:error forExperienceId:_appRecord.experienceId];
437    }
438
439    UM_WEAKIFY(self);
440    dispatch_async(dispatch_get_main_queue(), ^{
441      UM_ENSURE_STRONGIFY(self);
442      [self.delegate reactAppManager:self failedToLoadJavaScriptWithError:error];
443    });
444  }
445}
446
447# pragma mark app loading & splash screen
448
449- (void)_handleReactContentEvent:(NSNotification *)notification
450{
451  if ([notification.name isEqualToString:[self versionedString:RCTContentDidAppearNotification]]
452      && notification.object == self.reactRootView) {
453    UM_WEAKIFY(self);
454    dispatch_async(dispatch_get_main_queue(), ^{
455      UM_ENSURE_STRONGIFY(self);
456      [self.delegate reactAppManagerAppContentDidAppear:self];
457
458      if ([self _compareVersionTo:38] == NSOrderedDescending) {
459        // Post SDK 38 code
460        // Up to SDK 38 this code is invoked in different place
461        [self _appLoadingFinished];
462      }
463    });
464  }
465}
466
467- (void)_handleBridgeEvent:(NSNotification *)notification
468{
469  if ([notification.name isEqualToString:[self versionedString:RCTBridgeWillReloadNotification]]) {
470    UM_WEAKIFY(self);
471    dispatch_async(dispatch_get_main_queue(), ^{
472      UM_ENSURE_STRONGIFY(self);
473      [self.delegate reactAppManagerAppContentWillReload:self];
474    });
475  }
476}
477
478/**
479 * TODO: Remove once SDK 38 is phased out.
480 */
481- (void)_preSDK39BeginWaitingForAppLoading
482{
483  if (_viewTestTimer) {
484    [_viewTestTimer invalidate];
485    _viewTestTimer = nil;
486  }
487
488  // SplashScreen.preventAutoHide is called despite actual JS method call.
489  // Prior SDK 39, SplashScreen was basing on started & finished flags that are set via legacy Expo.SplashScreen JS methods calls.
490  EXSplashScreenService *splashScreenService = (EXSplashScreenService *)[UMModuleRegistryProvider getSingletonModuleForClass:[EXSplashScreenService class]];
491  [splashScreenService preventSplashScreenAutoHideFor:(UIViewController *) _appRecord.viewController
492                                      successCallback:^(BOOL hasEffect) {}
493                                      failureCallback:^(NSString * _Nonnull message) { RCTLogWarn(@"%@", message); }];
494  _viewTestTimer = [NSTimer scheduledTimerWithTimeInterval:0.02
495                                                    target:self
496                                                  selector:@selector(_preSDK39CheckAppFinishedLoading:)
497                                                  userInfo:nil
498                                                   repeats:YES];
499}
500
501/**
502 * TODO: Remove once SDK 38 is phased out.
503 */
504- (id)_preSDK39AppLoadingManagerInstance
505{
506  Class loadingManagerClass = [self versionedClassFromString:@"EXSplashScreen"];
507  for (Class klass in [self.reactBridge moduleClasses]) {
508    if ([klass isSubclassOfClass:loadingManagerClass]) {
509      return [self.reactBridge moduleForClass:loadingManagerClass];
510    }
511  }
512  return nil;
513}
514
515/**
516 * TODO: Remove once SDK 38 is phased out.
517 */
518- (void)_preSDK39CheckAppFinishedLoading:(NSTimer *)timer
519{
520  // When root view has been filled with something, there are two cases:
521  //   1. AppLoading was never mounted, in which case we hide the loading indicator immediately
522  //   2. AppLoading was mounted, in which case we wait till it is unmounted to hide the loading indicator
523  if ([_appRecord.appManager rootView] &&
524      [_appRecord.appManager rootView].subviews.count > 0 &&
525      [_appRecord.appManager rootView].subviews.firstObject.subviews.count > 0) {
526
527    // Remove once SDK 38 is phased out.
528    id<PreSDK39EXSplashScreenManagerProtocol> splashManager = [self _preSDK39AppLoadingManagerInstance];
529
530    // SplashScreen: at this point SplashScreen is prevented from autohiding,
531    // so we can safely hide it when the flags set.
532    if (!splashManager || !splashManager.started || splashManager.finished) {
533      [_viewTestTimer invalidate];
534      _viewTestTimer = nil;
535
536      EXSplashScreenService *splashScreenService = (EXSplashScreenService *)[UMModuleRegistryProvider getSingletonModuleForClass:[EXSplashScreenService class]];
537      [splashScreenService hideSplashScreenFor:(UIViewController *) _appRecord.viewController
538                               successCallback:^(BOOL hasEffect) {}
539                               failureCallback:^(NSString * _Nonnull message) { RCTLogWarn(@"%@", message); }];
540      [self _appLoadingFinished];
541    }
542  }
543}
544
545- (void)_appLoadingFinished
546{
547  UM_WEAKIFY(self);
548  dispatch_async(dispatch_get_main_queue(), ^{
549    UM_ENSURE_STRONGIFY(self);
550    if (self.appRecord.experienceId) {
551      [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager experienceFinishedLoadingWithId:self.appRecord.experienceId];
552    }
553    [self.delegate reactAppManagerFinishedLoadingJavaScript:self];
554  });
555}
556
557#pragma mark - dev tools
558
559- (RCTLogFunction)logFunction
560{
561  return (([self enablesDeveloperTools]) ? EXDeveloperRCTLogFunction : EXDefaultRCTLogFunction);
562}
563
564- (RCTLogLevel)logLevel
565{
566  return ([self enablesDeveloperTools]) ? RCTLogLevelInfo : RCTLogLevelWarning;
567}
568
569- (BOOL)enablesDeveloperTools
570{
571  NSDictionary *manifest = _appRecord.appLoader.manifest;
572  if (manifest) {
573    NSDictionary *manifestDeveloperConfig = manifest[@"developer"];
574    BOOL isDeployedFromTool = (manifestDeveloperConfig && manifestDeveloperConfig[@"tool"] != nil);
575    return (isDeployedFromTool);
576  }
577  return false;
578}
579
580- (BOOL)requiresValidManifests
581{
582  return YES;
583}
584
585- (void)showDevMenu
586{
587  if ([self enablesDeveloperTools]) {
588    dispatch_async(dispatch_get_main_queue(), ^{
589      [self.versionManager showDevMenuForBridge:self.reactBridge];
590    });
591  }
592}
593
594- (void)reloadBridge
595{
596  if ([self enablesDeveloperTools]) {
597    [self.reactBridge reload];
598  }
599}
600
601- (void)disableRemoteDebugging
602{
603  if ([self enablesDeveloperTools]) {
604    [self.versionManager disableRemoteDebuggingForBridge:self.reactBridge];
605  }
606}
607
608- (void)toggleRemoteDebugging
609{
610  if ([self enablesDeveloperTools]) {
611    [self.versionManager toggleRemoteDebuggingForBridge:self.reactBridge];
612  }
613}
614
615- (void)togglePerformanceMonitor
616{
617  if ([self enablesDeveloperTools]) {
618    [self.versionManager togglePerformanceMonitorForBridge:self.reactBridge];
619  }
620}
621
622- (void)toggleElementInspector
623{
624  if ([self enablesDeveloperTools]) {
625    [self.versionManager toggleElementInspectorForBridge:self.reactBridge];
626  }
627}
628
629- (NSDictionary<NSString *, NSString *> *)devMenuItems
630{
631  return [self.versionManager devMenuItemsForBridge:self.reactBridge];
632}
633
634- (void)selectDevMenuItemWithKey:(NSString *)key
635{
636  dispatch_async(dispatch_get_main_queue(), ^{
637    [self.versionManager selectDevMenuItemWithKey:key onBridge:self.reactBridge];
638  });
639}
640
641#pragma mark - RN configuration
642
643- (NSComparisonResult)_compareVersionTo:(NSUInteger)version
644{
645  // Unversioned projects are always considered to be on the latest version
646  if (!_validatedVersion || _validatedVersion.length == 0 || [_validatedVersion isEqualToString:@"UNVERSIONED"]) {
647    return NSOrderedDescending;
648  }
649
650  NSUInteger projectVersionNumber = _validatedVersion.integerValue;
651  if (projectVersionNumber == version) {
652    return NSOrderedSame;
653  }
654  return (projectVersionNumber < version) ? NSOrderedAscending : NSOrderedDescending;
655}
656
657- (NSDictionary *)launchOptionsForBridge
658{
659  if ([EXEnvironment sharedEnvironment].isDetached) {
660    // pass the native app's launch options to standalone bridge.
661    return [ExpoKit sharedInstance].launchOptions;
662  }
663  return @{};
664}
665
666- (Class)moduleRegistryDelegateClass
667{
668  if ([EXEnvironment sharedEnvironment].isDetached) {
669    return [ExpoKit sharedInstance].moduleRegistryDelegateClass;
670  }
671  return nil;
672}
673
674- (NSString *)applicationKeyForRootView
675{
676  NSDictionary *manifest = _appRecord.appLoader.manifest;
677  if (manifest && manifest[@"appKey"]) {
678    return manifest[@"appKey"];
679  }
680
681  NSURL *bundleUrl = [self bundleUrl];
682  if (bundleUrl) {
683    NSURLComponents *components = [NSURLComponents componentsWithURL:bundleUrl resolvingAgainstBaseURL:YES];
684    NSArray<NSURLQueryItem *> *queryItems = components.queryItems;
685    for (NSURLQueryItem *item in queryItems) {
686      if ([item.name isEqualToString:@"app"]) {
687        return item.value;
688      }
689    }
690  }
691
692  return @"main";
693}
694
695- (NSDictionary * _Nullable)initialPropertiesForRootView
696{
697  NSMutableDictionary *props = [NSMutableDictionary dictionary];
698  NSMutableDictionary *expProps = [NSMutableDictionary dictionary];
699
700  NSAssert(_appRecord.experienceId, @"Experience id should be nonnull when getting initial properties for root view");
701
702  NSDictionary *errorRecoveryProps = [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager developerInfoForExperienceId:_appRecord.experienceId];
703  if ([[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager experienceIdIsRecoveringFromError:_appRecord.experienceId]) {
704    [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager increaseAutoReloadBuffer];
705    if (errorRecoveryProps) {
706      expProps[@"errorRecovery"] = errorRecoveryProps;
707    }
708  }
709
710  expProps[@"shell"] = @(_appRecord == [EXKernel sharedInstance].appRegistry.standaloneAppRecord);
711  expProps[@"appOwnership"] = [self _appOwnership];
712  if (_initialProps) {
713    [expProps addEntriesFromDictionary:_initialProps];
714  }
715  EXPendingNotification *initialNotification = [[EXKernel sharedInstance].serviceRegistry.notificationsManager initialNotificationForExperience:_appRecord.experienceId];
716  if (initialNotification) {
717    expProps[@"notification"] = initialNotification.properties;
718  }
719
720  NSString *manifestString = nil;
721  if (_appRecord.appLoader.manifest && [NSJSONSerialization isValidJSONObject:_appRecord.appLoader.manifest]) {
722    NSError *error;
723    NSData *jsonData = [NSJSONSerialization dataWithJSONObject:_appRecord.appLoader.manifest options:0 error:&error];
724    if (jsonData) {
725      manifestString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
726    } else {
727      DDLogWarn(@"Failed to serialize JSON manifest: %@", error);
728    }
729  }
730
731  expProps[@"manifestString"] = manifestString;
732  if (_appRecord.appLoader.manifestUrl) {
733    expProps[@"initialUri"] = [_appRecord.appLoader.manifestUrl absoluteString];
734  }
735  props[@"exp"] = expProps;
736  return props;
737}
738
739- (NSString *)_appOwnership
740{
741  if (_appRecord == [EXKernel sharedInstance].appRegistry.standaloneAppRecord) {
742    return @"standalone";
743  }
744  return @"expo";
745}
746
747- (NSString *)_executionEnvironment
748{
749  if ([EXEnvironment sharedEnvironment].isDetached) {
750    return EXConstantsExecutionEnvironmentStandalone;
751  } else {
752    return EXConstantsExecutionEnvironmentStoreClient;
753  }
754}
755
756- (NSString *)scopedDocumentDirectory
757{
758  NSString *escapedExperienceId = [self escapedResourceName:_appRecord.experienceId];
759  NSString *mainDocumentDirectory = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
760  NSString *exponentDocumentDirectory = [mainDocumentDirectory stringByAppendingPathComponent:@"ExponentExperienceData"];
761  return [[exponentDocumentDirectory stringByAppendingPathComponent:escapedExperienceId] stringByStandardizingPath];
762}
763
764- (NSString *)scopedCachesDirectory
765{
766  NSString *escapedExperienceId = [self escapedResourceName:_appRecord.experienceId];
767  NSString *mainCachesDirectory = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;
768  NSString *exponentCachesDirectory = [mainCachesDirectory stringByAppendingPathComponent:@"ExponentExperienceData"];
769  return [[exponentCachesDirectory stringByAppendingPathComponent:escapedExperienceId] stringByStandardizingPath];
770}
771
772- (void *)jsExecutorFactoryForBridge:(id)bridge
773{
774  return [_versionManager versionedJsExecutorFactoryForBridge:bridge];
775}
776
777@end
778