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