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