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