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