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 "EXAbstractLoader.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 "EXVersionManagerObjC.h"
16#import "EXVersions.h"
17#import "EXAppViewController.h"
18#import <ExpoModulesCore/EXModuleRegistryProvider.h>
19#import <EXConstants/EXConstantsService.h>
20#import <EXSplashScreen/EXSplashScreenService.h>
21
22// When `use_frameworks!` is used, the generated Swift header is inside modules.
23// Otherwise, it's available only locally with double-quoted imports.
24#if __has_include(<EXManifests/EXManifests-Swift.h>)
25#import <EXManifests/EXManifests-Swift.h>
26#else
27#import "EXManifests-Swift.h"
28#endif
29
30#import <React/RCTBridge.h>
31#import <React/RCTCxxBridgeDelegate.h>
32#import <React/JSCExecutorFactory.h>
33#import <React/RCTRootView.h>
34
35@implementation RCTSource (EXReactAppManager)
36
37- (instancetype)initWithURL:(nonnull NSURL *)url data:(nonnull NSData *)data
38{
39  if (self = [super init]) {
40    // Use KVO since RN publicly declares these properties as readonly and privately defines the
41    // ivars
42    [self setValue:url forKey:@"url"];
43    [self setValue:data forKey:@"data"];
44    [self setValue:@(data.length) forKey:@"length"];
45    [self setValue:@(RCTSourceFilesChangedCountNotBuiltByBundler) forKey:@"filesChangedCount"];
46  }
47  return self;
48}
49
50@end
51
52@interface EXReactAppManager () <RCTBridgeDelegate, RCTCxxBridgeDelegate>
53
54@property (nonatomic, strong) UIView * __nullable reactRootView;
55@property (nonatomic, copy) RCTSourceLoadBlock loadCallback;
56@property (nonatomic, strong) NSDictionary *initialProps;
57@property (nonatomic, strong) NSTimer *viewTestTimer;
58
59@end
60
61@protocol EXVersionManagerProtocol
62
63+ (instancetype)alloc;
64
65- (instancetype)initWithParams:(nonnull NSDictionary *)params
66                      manifest:(nonnull EXManifestsManifest *)manifest
67                  fatalHandler:(void (^)(NSError *))fatalHandler
68                   logFunction:(RCTLogFunction)logFunction
69                  logThreshold:(NSInteger)threshold;
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
117  [self _invalidateAndClearDelegate:NO];
118  [self computeVersionSymbolPrefix];
119
120  // Assert early so we can catch the error before instantiating the bridge, otherwise we would be passing a
121  // nullish scope key to the scoped modules.
122  // Alternatively we could skip instantiating the scoped modules but then singletons like the one used in
123  // expo-updates would be loaded as bare modules. In the case of expo-updates, this would throw a fatal error
124  // because Expo.plist is not available in the Expo Go app.
125  NSAssert(_appRecord.scopeKey, @"Experience scope key should be nonnull when getting initial properties for root view. This can occur when the manifest JSON, loaded from the server, is missing keys.");
126
127
128  if ([self isReadyToLoad]) {
129    Class<EXVersionManagerProtocol> versionManagerClass = [self versionedClassFromString:@"EXVersionManager"];
130    Class bridgeClass = [self versionedClassFromString:@"RCTBridge"];
131    Class rootViewClass = [self versionedClassFromString:@"RCTRootView"];
132
133    _versionManager = [[versionManagerClass alloc] initWithParams:[self extraParams]
134                                                         manifest:_appRecord.appLoader.manifest
135                                                     fatalHandler:handleFatalReactError
136                                                      logFunction:[self logFunction]
137                                                     logThreshold:[self logLevel]];
138
139    _reactBridge = [[bridgeClass alloc] initWithDelegate:self launchOptions:[self launchOptionsForBridge]];
140
141    if (!_isHeadless) {
142      // We don't want to run the whole JS app if app launches in the background,
143      // so we're omitting creation of RCTRootView that triggers runApplication and sets up React view hierarchy.
144      _reactRootView = [[rootViewClass alloc] initWithBridge:_reactBridge
145                                                  moduleName:[self applicationKeyForRootView]
146                                           initialProperties:[self initialPropertiesForRootView]];
147    }
148
149    [self setupWebSocketControls];
150    [_delegate reactAppManagerIsReadyForLoad:self];
151
152    NSAssert([_reactBridge isLoading], @"React bridge should be loading once initialized");
153    [_versionManager bridgeWillStartLoading:_reactBridge];
154  }
155}
156
157- (NSDictionary *)extraParams
158{
159  // we allow the vanilla RN dev menu in some circumstances.
160  BOOL isStandardDevMenuAllowed = [EXEnvironment sharedEnvironment].isDetached;
161  NSMutableDictionary *params = [NSMutableDictionary dictionaryWithDictionary:@{
162    @"manifest": _appRecord.appLoader.manifest.rawManifestJSON,
163    @"constants": @{
164        @"linkingUri": RCTNullIfNil([EXKernelLinkingManager linkingUriForExperienceUri:_appRecord.appLoader.manifestUrl useLegacy:[self _compareVersionTo:27] == NSOrderedAscending]),
165        @"experienceUrl": RCTNullIfNil(_appRecord.appLoader.manifestUrl? _appRecord.appLoader.manifestUrl.absoluteString: nil),
166        @"expoRuntimeVersion": [EXBuildConstants sharedInstance].expoRuntimeVersion,
167        @"manifest": _appRecord.appLoader.manifest.rawManifestJSON,
168        @"executionEnvironment": [self _executionEnvironment],
169        @"appOwnership": [self _appOwnership],
170        @"isHeadless": @(_isHeadless),
171        @"supportedExpoSdks": [EXVersions sharedInstance].versions[@"sdkVersions"],
172    },
173    @"exceptionsManagerDelegate": _exceptionHandler,
174    @"initialUri": RCTNullIfNil([EXKernelLinkingManager initialUriWithManifestUrl:_appRecord.appLoader.manifestUrl]),
175    @"isDeveloper": @([self enablesDeveloperTools]),
176    @"isStandardDevMenuAllowed": @(isStandardDevMenuAllowed),
177    @"testEnvironment": @([EXEnvironment sharedEnvironment].testEnvironment),
178    @"services": [EXKernel sharedInstance].serviceRegistry.allServices,
179    @"singletonModules": [EXModuleRegistryProvider singletonModules],
180    @"moduleRegistryDelegateClass": RCTNullIfNil([self moduleRegistryDelegateClass]),
181  }];
182  if ([@"expo" isEqualToString:[self _appOwnership]]) {
183    [params addEntriesFromDictionary:@{
184      @"fileSystemDirectories": @{
185          @"documentDirectory": [self scopedDocumentDirectory],
186          @"cachesDirectory": [self scopedCachesDirectory]
187      }
188    }];
189  }
190  return params;
191}
192
193- (void)invalidate
194{
195  [self _invalidateAndClearDelegate:YES];
196}
197
198- (void)_invalidateAndClearDelegate:(BOOL)clearDelegate
199{
200  [self _stopObservingBridgeNotifications];
201  if (_viewTestTimer) {
202    [_viewTestTimer invalidate];
203    _viewTestTimer = nil;
204  }
205  if (_versionManager) {
206    [_versionManager invalidate];
207    _versionManager = nil;
208  }
209  if (_reactRootView) {
210    [_reactRootView removeFromSuperview];
211    _reactRootView = nil;
212  }
213  if (_reactBridge) {
214    [_reactBridge invalidate];
215    _reactBridge = nil;
216    if (_delegate) {
217      [_delegate reactAppManagerDidInvalidate:self];
218      if (clearDelegate) {
219        _delegate = nil;
220      }
221    }
222  }
223  _isBridgeRunning = NO;
224  [self _invalidateVersionState];
225}
226
227- (void)computeVersionSymbolPrefix
228{
229  // TODO: ben: kernel checks detached versions here
230  _validatedVersion = [[EXVersions sharedInstance] availableSdkVersionForManifest:_appRecord.appLoader.manifest];
231  _versionSymbolPrefix = [[EXVersions sharedInstance] symbolPrefixForSdkVersion:self.validatedVersion isKernel:NO];
232}
233
234- (void)_invalidateVersionState
235{
236  _versionSymbolPrefix = @"";
237  _validatedVersion = nil;
238}
239
240- (Class)versionedClassFromString: (NSString *)classString
241{
242  return NSClassFromString([self versionedString:classString]);
243}
244
245- (NSString *)versionedString: (NSString *)string
246{
247  return [EXVersions versionedString:string withPrefix:_versionSymbolPrefix];
248}
249
250- (NSString *)escapedResourceName:(NSString *)string
251{
252  NSString *charactersToEscape = @"!*'();:@&=+$,/?%#[]";
253  NSCharacterSet *allowedCharacters = [[NSCharacterSet characterSetWithCharactersInString:charactersToEscape] invertedSet];
254  return [string stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacters];
255}
256
257- (BOOL)isReadyToLoad
258{
259  if (_appRecord) {
260    return (_appRecord.appLoader.status == kEXAppLoaderStatusHasManifest || _appRecord.appLoader.status == kEXAppLoaderStatusHasManifestAndBundle);
261  }
262  return NO;
263}
264
265- (NSURL *)bundleUrl
266{
267  return [EXApiUtil bundleUrlFromManifest:_appRecord.appLoader.manifest];
268}
269
270#pragma mark - EXAppFetcherDataSource
271
272- (NSString *)bundleResourceNameForAppFetcher:(EXAppFetcher *)appFetcher withManifest:(nonnull EXManifestsManifest *)manifest
273{
274  if ([EXEnvironment sharedEnvironment].isDetached) {
275    NSLog(@"Standalone bundle remote url is %@", [EXEnvironment sharedEnvironment].standaloneManifestUrl);
276    return kEXEmbeddedBundleResourceName;
277  } else {
278    return manifest.legacyId;
279  }
280}
281
282- (BOOL)appFetcherShouldInvalidateBundleCache:(EXAppFetcher *)appFetcher
283{
284  return NO;
285}
286
287#pragma mark - RCTBridgeDelegate
288
289- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
290{
291  return [self bundleUrl];
292}
293
294- (void)loadSourceForBridge:(RCTBridge *)bridge withBlock:(RCTSourceLoadBlock)loadCallback
295{
296  // clear any potentially old loading state
297  if (_appRecord.scopeKey) {
298    [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager setError:nil forScopeKey:_appRecord.scopeKey];
299  }
300  [self _stopObservingBridgeNotifications];
301  [self _startObservingBridgeNotificationsForBridge:bridge];
302
303  if ([self enablesDeveloperTools]) {
304    if ([_appRecord.appLoader supportsBundleReload]) {
305      [_appRecord.appLoader forceBundleReload];
306    } else {
307      NSAssert(_appRecord.scopeKey, @"EXKernelAppRecord.scopeKey should be nonnull if we have a manifest with developer tools enabled");
308      [[EXKernel sharedInstance] reloadAppWithScopeKey:_appRecord.scopeKey];
309    }
310  }
311
312  _loadCallback = loadCallback;
313  if (_appRecord.appLoader.status == kEXAppLoaderStatusHasManifestAndBundle) {
314    // finish loading immediately (app loader won't call this since it's already done)
315    [self appLoaderFinished];
316  } else {
317    // wait for something else to call `appLoaderFinished` or `appLoaderFailed` later.
318  }
319}
320
321- (NSArray *)extraModulesForBridge:(RCTBridge *)bridge
322{
323  return [self.versionManager extraModulesForBridge:bridge];
324}
325
326- (void)appLoaderFinished
327{
328  NSData *data = _appRecord.appLoader.bundle;
329  if (_loadCallback) {
330    _loadCallback(nil, [[RCTSource alloc] initWithURL:[self bundleUrl] data:data]);
331    _loadCallback = nil;
332  }
333}
334
335- (void)appLoaderFailedWithError:(NSError *)error
336{
337  // RN is going to call RCTFatal() on this error, so keep a reference to it for later
338  // so we can distinguish this non-fatal error from actual fatal cases.
339  if (_appRecord.scopeKey) {
340    [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager setError:error forScopeKey:_appRecord.scopeKey];
341  }
342
343  // react won't post this for us
344  [[NSNotificationCenter defaultCenter] postNotificationName:[self versionedString:RCTJavaScriptDidFailToLoadNotification] object:error];
345
346  if (_loadCallback) {
347    _loadCallback(error, nil);
348    _loadCallback = nil;
349  }
350}
351
352#pragma mark - JavaScript loading
353
354- (void)_startObservingBridgeNotificationsForBridge:(RCTBridge *)bridge
355{
356  NSAssert(bridge, @"Must subscribe to loading notifs for a non-null bridge");
357
358  [[NSNotificationCenter defaultCenter] addObserver:self
359                                           selector:@selector(_handleJavaScriptStartLoadingEvent:)
360                                               name:[self versionedString:RCTJavaScriptWillStartLoadingNotification]
361                                             object:bridge];
362  [[NSNotificationCenter defaultCenter] addObserver:self
363                                           selector:@selector(_handleJavaScriptLoadEvent:)
364                                               name:[self versionedString:RCTJavaScriptDidLoadNotification]
365                                             object:bridge];
366  [[NSNotificationCenter defaultCenter] addObserver:self
367                                           selector:@selector(_handleJavaScriptLoadEvent:)
368                                               name:[self versionedString:RCTJavaScriptDidFailToLoadNotification]
369                                             object:bridge];
370  [[NSNotificationCenter defaultCenter] addObserver:self
371                                           selector:@selector(_handleReactContentEvent:)
372                                               name:[self versionedString:RCTContentDidAppearNotification]
373                                             object:nil];
374  [[NSNotificationCenter defaultCenter] addObserver:self
375                                           selector:@selector(_handleBridgeEvent:)
376                                               name:[self versionedString:RCTBridgeWillReloadNotification]
377                                             object:bridge];
378}
379
380- (void)_stopObservingBridgeNotifications
381{
382  [[NSNotificationCenter defaultCenter] removeObserver:self name:[self versionedString:RCTJavaScriptWillStartLoadingNotification] object:_reactBridge];
383  [[NSNotificationCenter defaultCenter] removeObserver:self name:[self versionedString:RCTJavaScriptDidLoadNotification] object:_reactBridge];
384  [[NSNotificationCenter defaultCenter] removeObserver:self name:[self versionedString:RCTJavaScriptDidFailToLoadNotification] object:_reactBridge];
385  [[NSNotificationCenter defaultCenter] removeObserver:self name:[self versionedString:RCTContentDidAppearNotification] object:_reactBridge];
386  [[NSNotificationCenter defaultCenter] removeObserver:self name:[self versionedString:RCTBridgeWillReloadNotification] object:_reactBridge];
387}
388
389- (void)_handleJavaScriptStartLoadingEvent:(NSNotification *)notification
390{
391  __weak __typeof(self) weakSelf = self;
392  dispatch_async(dispatch_get_main_queue(), ^{
393    __strong __typeof(self) strongSelf = weakSelf;
394    if (strongSelf) {
395      [strongSelf.delegate reactAppManagerStartedLoadingJavaScript:strongSelf];
396    }
397  });
398}
399
400- (void)_handleJavaScriptLoadEvent:(NSNotification *)notification
401{
402  if ([notification.name isEqualToString:[self versionedString:RCTJavaScriptDidLoadNotification]]) {
403    _isBridgeRunning = YES;
404    _hasBridgeEverLoaded = YES;
405    [_versionManager bridgeFinishedLoading:_reactBridge];
406
407    // TODO: temporary solution for hiding LoadingProgressWindow
408    if (_appRecord.viewController) {
409      [_appRecord.viewController hideLoadingProgressWindow];
410    }
411  } else if ([notification.name isEqualToString:[self versionedString:RCTJavaScriptDidFailToLoadNotification]]) {
412    NSError *error = (notification.userInfo) ? notification.userInfo[@"error"] : nil;
413    if (_appRecord.scopeKey) {
414      [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager setError:error forScopeKey:_appRecord.scopeKey];
415    }
416
417    EX_WEAKIFY(self);
418    dispatch_async(dispatch_get_main_queue(), ^{
419      EX_ENSURE_STRONGIFY(self);
420      [self.delegate reactAppManager:self failedToLoadJavaScriptWithError:error];
421    });
422  }
423}
424
425# pragma mark app loading & splash screen
426
427- (void)_handleReactContentEvent:(NSNotification *)notification
428{
429  if ([notification.name isEqualToString:[self versionedString:RCTContentDidAppearNotification]]
430      && notification.object == self.reactRootView) {
431    EX_WEAKIFY(self);
432    dispatch_async(dispatch_get_main_queue(), ^{
433      EX_ENSURE_STRONGIFY(self);
434      [self.delegate reactAppManagerAppContentDidAppear:self];
435      [self _appLoadingFinished];
436    });
437  }
438}
439
440- (void)_handleBridgeEvent:(NSNotification *)notification
441{
442  if ([notification.name isEqualToString:[self versionedString:RCTBridgeWillReloadNotification]]) {
443    EX_WEAKIFY(self);
444    dispatch_async(dispatch_get_main_queue(), ^{
445      EX_ENSURE_STRONGIFY(self);
446      [self.delegate reactAppManagerAppContentWillReload:self];
447    });
448  }
449}
450
451- (void)_appLoadingFinished
452{
453  EX_WEAKIFY(self);
454  dispatch_async(dispatch_get_main_queue(), ^{
455    EX_ENSURE_STRONGIFY(self);
456    if (self.appRecord.scopeKey) {
457      [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager experienceFinishedLoadingWithScopeKey:self.appRecord.scopeKey];
458    }
459    [self.delegate reactAppManagerFinishedLoadingJavaScript:self];
460  });
461}
462
463#pragma mark - dev tools
464
465- (RCTLogFunction)logFunction
466{
467  return (([self enablesDeveloperTools]) ? EXDeveloperRCTLogFunction : EXDefaultRCTLogFunction);
468}
469
470- (RCTLogLevel)logLevel
471{
472  return ([self enablesDeveloperTools]) ? RCTLogLevelInfo : RCTLogLevelWarning;
473}
474
475- (BOOL)enablesDeveloperTools
476{
477  EXManifestsManifest *manifest = _appRecord.appLoader.manifest;
478  if (manifest) {
479    return manifest.isUsingDeveloperTool;
480  }
481  return false;
482}
483
484- (BOOL)requiresValidManifests
485{
486  return YES;
487}
488
489- (void)showDevMenu
490{
491  if ([self enablesDeveloperTools]) {
492    dispatch_async(dispatch_get_main_queue(), ^{
493      [self.versionManager showDevMenuForBridge:self.reactBridge];
494    });
495  }
496}
497
498- (void)reloadBridge
499{
500  if ([self enablesDeveloperTools]) {
501    [(RCTBridge *) self.reactBridge reload];
502  }
503}
504
505- (void)disableRemoteDebugging
506{
507  if ([self enablesDeveloperTools]) {
508    [self.versionManager disableRemoteDebuggingForBridge:self.reactBridge];
509  }
510}
511
512- (void)toggleRemoteDebugging
513{
514  if ([self enablesDeveloperTools]) {
515    [self.versionManager toggleRemoteDebuggingForBridge:self.reactBridge];
516  }
517}
518
519- (void)togglePerformanceMonitor
520{
521  if ([self enablesDeveloperTools]) {
522    [self.versionManager togglePerformanceMonitorForBridge:self.reactBridge];
523  }
524}
525
526- (void)toggleElementInspector
527{
528  if ([self enablesDeveloperTools]) {
529    [self.versionManager toggleElementInspectorForBridge:self.reactBridge];
530  }
531}
532
533- (void)reconnectReactDevTools
534{
535  if ([self enablesDeveloperTools]) {
536    // Emit the `RCTDevMenuShown` for the app to reconnect react-devtools
537    // https://github.com/facebook/react-native/blob/22ba1e45c52edcc345552339c238c1f5ef6dfc65/Libraries/Core/setUpReactDevTools.js#L80
538    [self.reactBridge enqueueJSCall:@"RCTNativeAppEventEmitter.emit" args:@[@"RCTDevMenuShown"]];
539  }
540}
541
542- (void)toggleDevMenu
543{
544  if ([EXEnvironment sharedEnvironment].isDetached) {
545    [[EXKernel sharedInstance].visibleApp.appManager showDevMenu];
546  } else {
547    [[EXKernel sharedInstance] switchTasks];
548  }
549}
550
551- (void)setupWebSocketControls
552{
553  if ([self enablesDeveloperTools]) {
554    if ([_versionManager respondsToSelector:@selector(addWebSocketNotificationHandler:queue:forMethod:)]) {
555      __weak __typeof(self) weakSelf = self;
556
557      // Attach listeners to the bundler's dev server web socket connection.
558      // This enables tools to automatically reload the client remotely (i.e. in expo-cli).
559
560      // Enable a lot of tools under the same command namespace
561      [_versionManager addWebSocketNotificationHandler:^(id params) {
562        if (params != [NSNull null] && (NSDictionary *)params) {
563          NSDictionary *_params = (NSDictionary *)params;
564          if (_params[@"name"] != nil && (NSString *)_params[@"name"]) {
565            NSString *name = _params[@"name"];
566            if ([name isEqualToString:@"reload"]) {
567              [[EXKernel sharedInstance] reloadVisibleApp];
568            } else if ([name isEqualToString:@"toggleDevMenu"]) {
569              [weakSelf toggleDevMenu];
570            } else if ([name isEqualToString:@"toggleRemoteDebugging"]) {
571              [weakSelf toggleRemoteDebugging];
572            } else if ([name isEqualToString:@"toggleElementInspector"]) {
573              [weakSelf toggleElementInspector];
574            } else if ([name isEqualToString:@"togglePerformanceMonitor"]) {
575              [weakSelf togglePerformanceMonitor];
576            } else if ([name isEqualToString:@"reconnectReactDevTools"]) {
577              [weakSelf reconnectReactDevTools];
578            }
579          }
580        }
581      }
582                                                 queue:dispatch_get_main_queue()
583                                             forMethod:@"sendDevCommand"];
584
585      // These (reload and devMenu) are here to match RN dev tooling.
586
587      // Reload the app on "reload"
588      [_versionManager addWebSocketNotificationHandler:^(id params) {
589        [[EXKernel sharedInstance] reloadVisibleApp];
590      }
591                                                 queue:dispatch_get_main_queue()
592                                             forMethod:@"reload"];
593
594      // Open the dev menu on "devMenu"
595      [_versionManager addWebSocketNotificationHandler:^(id params) {
596        [weakSelf toggleDevMenu];
597      }
598                                                 queue:dispatch_get_main_queue()
599                                             forMethod:@"devMenu"];
600    }
601  }
602}
603
604- (NSDictionary<NSString *, NSString *> *)devMenuItems
605{
606  return [self.versionManager devMenuItemsForBridge:self.reactBridge];
607}
608
609- (void)selectDevMenuItemWithKey:(NSString *)key
610{
611  dispatch_async(dispatch_get_main_queue(), ^{
612    [self.versionManager selectDevMenuItemWithKey:key onBridge:self.reactBridge];
613  });
614}
615
616#pragma mark - RN configuration
617
618- (NSComparisonResult)_compareVersionTo:(NSUInteger)version
619{
620  // Unversioned projects are always considered to be on the latest version
621  if (!_validatedVersion || _validatedVersion.length == 0 || [_validatedVersion isEqualToString:@"UNVERSIONED"]) {
622    return NSOrderedDescending;
623  }
624
625  NSUInteger projectVersionNumber = _validatedVersion.integerValue;
626  if (projectVersionNumber == version) {
627    return NSOrderedSame;
628  }
629  return (projectVersionNumber < version) ? NSOrderedAscending : NSOrderedDescending;
630}
631
632- (NSDictionary *)launchOptionsForBridge
633{
634  if ([EXEnvironment sharedEnvironment].isDetached) {
635    // pass the native app's launch options to standalone bridge.
636    return [ExpoKit sharedInstance].launchOptions;
637  }
638  return @{};
639}
640
641- (Class)moduleRegistryDelegateClass
642{
643  if ([EXEnvironment sharedEnvironment].isDetached) {
644    return [ExpoKit sharedInstance].moduleRegistryDelegateClass;
645  }
646  return nil;
647}
648
649- (NSString *)applicationKeyForRootView
650{
651  EXManifestsManifest *manifest = _appRecord.appLoader.manifest;
652  if (manifest && manifest.appKey) {
653    return manifest.appKey;
654  }
655
656  NSURL *bundleUrl = [self bundleUrl];
657  if (bundleUrl) {
658    NSURLComponents *components = [NSURLComponents componentsWithURL:bundleUrl resolvingAgainstBaseURL:YES];
659    NSArray<NSURLQueryItem *> *queryItems = components.queryItems;
660    for (NSURLQueryItem *item in queryItems) {
661      if ([item.name isEqualToString:@"app"]) {
662        return item.value;
663      }
664    }
665  }
666
667  return @"main";
668}
669
670- (NSDictionary * _Nullable)initialPropertiesForRootView
671{
672  NSMutableDictionary *props = [NSMutableDictionary dictionary];
673  NSMutableDictionary *expProps = [NSMutableDictionary dictionary];
674
675  NSAssert(_appRecord.scopeKey, @"Experience scope key should be nonnull when getting initial properties for root view");
676
677  NSDictionary *errorRecoveryProps = [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager developerInfoForScopeKey:_appRecord.scopeKey];
678  if ([[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager scopeKeyIsRecoveringFromError:_appRecord.scopeKey]) {
679    [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager increaseAutoReloadBuffer];
680    if (errorRecoveryProps) {
681      expProps[@"errorRecovery"] = errorRecoveryProps;
682    }
683  }
684
685  expProps[@"shell"] = @(_appRecord == [EXKernel sharedInstance].appRegistry.standaloneAppRecord);
686  expProps[@"appOwnership"] = [self _appOwnership];
687  if (_initialProps) {
688    [expProps addEntriesFromDictionary:_initialProps];
689  }
690  EXPendingNotification *initialNotification = [[EXKernel sharedInstance].serviceRegistry.notificationsManager initialNotification];
691  if (initialNotification) {
692    expProps[@"notification"] = initialNotification.properties;
693  }
694
695  NSString *manifestString = nil;
696  EXManifestsManifest *manifest = _appRecord.appLoader.manifest;
697  if (manifest && [NSJSONSerialization isValidJSONObject:manifest.rawManifestJSON]) {
698    NSError *error;
699    NSData *jsonData = [NSJSONSerialization dataWithJSONObject:manifest.rawManifestJSON options:0 error:&error];
700    if (jsonData) {
701      manifestString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
702    } else {
703      DDLogWarn(@"Failed to serialize JSON manifest: %@", error);
704    }
705  }
706
707  expProps[@"manifestString"] = manifestString;
708  if (_appRecord.appLoader.manifestUrl) {
709    expProps[@"initialUri"] = [_appRecord.appLoader.manifestUrl absoluteString];
710  }
711  props[@"exp"] = expProps;
712  return props;
713}
714
715- (NSString *)_appOwnership
716{
717  if (_appRecord == [EXKernel sharedInstance].appRegistry.standaloneAppRecord) {
718    return @"standalone";
719  }
720  return @"expo";
721}
722
723- (NSString *)_executionEnvironment
724{
725  if ([EXEnvironment sharedEnvironment].isDetached) {
726    return EXConstantsExecutionEnvironmentStandalone;
727  } else {
728    return EXConstantsExecutionEnvironmentStoreClient;
729  }
730}
731
732- (NSString *)scopedDocumentDirectory
733{
734  NSString *escapedScopeKey = [self escapedResourceName:_appRecord.scopeKey];
735  NSString *mainDocumentDirectory = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
736  NSString *exponentDocumentDirectory = [mainDocumentDirectory stringByAppendingPathComponent:@"ExponentExperienceData"];
737  return [[exponentDocumentDirectory stringByAppendingPathComponent:escapedScopeKey] stringByStandardizingPath];
738}
739
740- (NSString *)scopedCachesDirectory
741{
742  NSString *escapedScopeKey = [self escapedResourceName:_appRecord.scopeKey];
743  NSString *mainCachesDirectory = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;
744  NSString *exponentCachesDirectory = [mainCachesDirectory stringByAppendingPathComponent:@"ExponentExperienceData"];
745  return [[exponentCachesDirectory stringByAppendingPathComponent:escapedScopeKey] stringByStandardizingPath];
746}
747
748- (void *)jsExecutorFactoryForBridge:(id)bridge
749{
750  return [_versionManager versionedJsExecutorFactoryForBridge:bridge];
751}
752
753@end
754