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@interface EXVersionManager (Legacy)
28// TODO: remove after non-unimodules SDK versions are dropped
29
30- (void)bridgeDidForeground;
31- (void)bridgeDidBackground;
32
33@end
34
35typedef void (^SDK21RCTSourceLoadBlock)(NSError *error, NSData *source, int64_t sourceLength);
36
37/**
38 * TODO: Remove once SDK 38 is phased out.
39 */
40@protocol PreSDK39EXSplashScreenManagerProtocol
41
42@property (assign) BOOL started;
43@property (assign) BOOL finished;
44
45@end
46
47@implementation RCTSource (EXReactAppManager)
48
49- (instancetype)initWithURL:(nonnull NSURL *)url data:(nonnull NSData *)data
50{
51  if (self = [super init]) {
52    // Use KVO since RN publicly declares these properties as readonly and privately defines the
53    // ivars
54    [self setValue:url forKey:@"url"];
55    [self setValue:data forKey:@"data"];
56    [self setValue:@(data.length) forKey:@"length"];
57    [self setValue:@(RCTSourceFilesChangedCountNotBuiltByBundler) forKey:@"filesChangedCount"];
58  }
59  return self;
60}
61
62@end
63
64@interface EXReactAppManager () <RCTBridgeDelegate, RCTCxxBridgeDelegate>
65
66@property (nonatomic, strong) UIView * __nullable reactRootView;
67@property (nonatomic, copy) RCTSourceLoadBlock loadCallback;
68@property (nonatomic, strong) NSDictionary *initialProps;
69@property (nonatomic, strong) NSTimer *viewTestTimer;
70
71@end
72
73@implementation EXReactAppManager
74
75- (instancetype)initWithAppRecord:(EXKernelAppRecord *)record initialProps:(NSDictionary *)initialProps
76{
77  if (self = [super init]) {
78    _appRecord = record;
79    _initialProps = initialProps;
80    _isHeadless = NO;
81    _exceptionHandler = [[EXReactAppExceptionHandler alloc] initWithAppRecord:_appRecord];
82  }
83  return self;
84}
85
86- (void)setAppRecord:(EXKernelAppRecord *)appRecord
87{
88  _appRecord = appRecord;
89  _exceptionHandler = [[EXReactAppExceptionHandler alloc] initWithAppRecord:appRecord];
90}
91
92- (EXReactAppManagerStatus)status
93{
94  if (!_appRecord) {
95    return kEXReactAppManagerStatusError;
96  }
97  if (_loadCallback) {
98    // we have a RCTBridge load callback so we're ready to receive load events
99    return kEXReactAppManagerStatusBridgeLoading;
100  }
101  if (_isBridgeRunning) {
102    return kEXReactAppManagerStatusRunning;
103  }
104  return kEXReactAppManagerStatusNew;
105}
106
107- (UIView *)rootView
108{
109  return _reactRootView;
110}
111
112- (void)rebuildBridge
113{
114  EXAssertMainThread();
115  NSAssert((_delegate != nil), @"Cannot init react app without EXReactAppManagerDelegate");
116
117  [self _invalidateAndClearDelegate:NO];
118  [self computeVersionSymbolPrefix];
119
120  // Assert early so we can catch the error before instantiating the bridge, otherwise we would be passing a
121  // nullish scope key to the scoped modules.
122  // Alternatively we could skip instantiating the scoped modules but then singletons like the one used in
123  // expo-updates would be loaded as bare modules. In the case of expo-updates, this would throw a fatal error
124  // because Expo.plist is not available in the Expo Go app.
125  NSAssert(_appRecord.scopeKey, @"Experience scope key should be nonnull when getting initial properties for root view. This can occur when the manifest JSON, loaded from the server, is missing keys.");
126
127
128  if ([self isReadyToLoad]) {
129    Class versionManagerClass = [self versionedClassFromString:@"EXVersionManager"];
130    Class bridgeClass = [self versionedClassFromString:@"RCTBridge"];
131    Class rootViewClass = [self versionedClassFromString:@"RCTRootView"];
132
133    _versionManager = [[versionManagerClass alloc] initWithParams:[self extraParams]
134                                                         manifest:_appRecord.appLoader.manifest
135                                                     fatalHandler:handleFatalReactError
136                                                      logFunction:[self logFunction]
137                                                     logThreshold:[self logLevel]];
138    _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- (void)appStateDidBecomeActive
270{
271  if ([_versionManager respondsToSelector:@selector(bridgeDidForeground)]) {
272    // supported before SDK 29 / unimodules
273    [_versionManager bridgeDidForeground];
274  }
275}
276
277- (void)appStateDidBecomeInactive
278{
279  if ([_versionManager respondsToSelector:@selector(bridgeDidBackground)]) {
280    [_versionManager bridgeDidBackground];
281  }
282}
283
284#pragma mark - EXAppFetcherDataSource
285
286- (NSString *)bundleResourceNameForAppFetcher:(EXAppFetcher *)appFetcher withManifest:(nonnull EXManifestsManifest *)manifest
287{
288  if ([EXEnvironment sharedEnvironment].isDetached) {
289    NSLog(@"Standalone bundle remote url is %@", [EXEnvironment sharedEnvironment].standaloneManifestUrl);
290    return kEXEmbeddedBundleResourceName;
291  } else {
292    return manifest.legacyId;
293  }
294}
295
296- (BOOL)appFetcherShouldInvalidateBundleCache:(EXAppFetcher *)appFetcher
297{
298  return NO;
299}
300
301#pragma mark - RCTBridgeDelegate
302
303- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
304{
305  return [self bundleUrl];
306}
307
308- (void)loadSourceForBridge:(RCTBridge *)bridge withBlock:(RCTSourceLoadBlock)loadCallback
309{
310  // clear any potentially old loading state
311  if (_appRecord.scopeKey) {
312    [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager setError:nil forScopeKey:_appRecord.scopeKey];
313  }
314  [self _stopObservingBridgeNotifications];
315  [self _startObservingBridgeNotificationsForBridge:bridge];
316
317  if ([self enablesDeveloperTools]) {
318    if ([_appRecord.appLoader supportsBundleReload]) {
319      [_appRecord.appLoader forceBundleReload];
320    } else {
321      NSAssert(_appRecord.scopeKey, @"EXKernelAppRecord.scopeKey should be nonnull if we have a manifest with developer tools enabled");
322      [[EXKernel sharedInstance] reloadAppWithScopeKey:_appRecord.scopeKey];
323    }
324  }
325
326  _loadCallback = loadCallback;
327  if (_appRecord.appLoader.status == kEXAppLoaderStatusHasManifestAndBundle) {
328    // finish loading immediately (app loader won't call this since it's already done)
329    [self appLoaderFinished];
330  } else {
331    // wait for something else to call `appLoaderFinished` or `appLoaderFailed` later.
332  }
333}
334
335- (NSArray *)extraModulesForBridge:(RCTBridge *)bridge
336{
337  return [self.versionManager extraModulesForBridge:bridge];
338}
339
340- (void)appLoaderFinished
341{
342  NSData *data = _appRecord.appLoader.bundle;
343  if (_loadCallback) {
344    if ([self _compareVersionTo:22] == NSOrderedAscending) {
345      SDK21RCTSourceLoadBlock legacyLoadCallback = (SDK21RCTSourceLoadBlock)_loadCallback;
346      legacyLoadCallback(nil, data, data.length);
347    } else {
348      _loadCallback(nil, [[RCTSource alloc] initWithURL:[self bundleUrl] data:data]);
349    }
350    _loadCallback = nil;
351  }
352}
353
354- (void)appLoaderFailedWithError:(NSError *)error
355{
356  // RN is going to call RCTFatal() on this error, so keep a reference to it for later
357  // so we can distinguish this non-fatal error from actual fatal cases.
358  if (_appRecord.scopeKey) {
359    [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager setError:error forScopeKey:_appRecord.scopeKey];
360  }
361
362  // react won't post this for us
363  [[NSNotificationCenter defaultCenter] postNotificationName:[self versionedString:RCTJavaScriptDidFailToLoadNotification] object:error];
364
365  if (_loadCallback) {
366    if ([self _compareVersionTo:22] == NSOrderedAscending) {
367      SDK21RCTSourceLoadBlock legacyLoadCallback = (SDK21RCTSourceLoadBlock)_loadCallback;
368      legacyLoadCallback(error, nil, 0);
369    } else {
370      _loadCallback(error, nil);
371    }
372    _loadCallback = nil;
373  }
374}
375
376#pragma mark - JavaScript loading
377
378- (void)_startObservingBridgeNotificationsForBridge:(RCTBridge *)bridge
379{
380  NSAssert(bridge, @"Must subscribe to loading notifs for a non-null bridge");
381
382  [[NSNotificationCenter defaultCenter] addObserver:self
383                                           selector:@selector(_handleJavaScriptStartLoadingEvent:)
384                                               name:[self versionedString:RCTJavaScriptWillStartLoadingNotification]
385                                             object:bridge];
386  [[NSNotificationCenter defaultCenter] addObserver:self
387                                           selector:@selector(_handleJavaScriptLoadEvent:)
388                                               name:[self versionedString:RCTJavaScriptDidLoadNotification]
389                                             object:bridge];
390  [[NSNotificationCenter defaultCenter] addObserver:self
391                                           selector:@selector(_handleJavaScriptLoadEvent:)
392                                               name:[self versionedString:RCTJavaScriptDidFailToLoadNotification]
393                                             object:bridge];
394  [[NSNotificationCenter defaultCenter] addObserver:self
395                                           selector:@selector(_handleReactContentEvent:)
396                                               name:[self versionedString:RCTContentDidAppearNotification]
397                                             object:nil];
398  [[NSNotificationCenter defaultCenter] addObserver:self
399                                           selector:@selector(_handleBridgeEvent:)
400                                               name:[self versionedString:RCTBridgeWillReloadNotification]
401                                             object:bridge];
402}
403
404- (void)_stopObservingBridgeNotifications
405{
406  [[NSNotificationCenter defaultCenter] removeObserver:self name:[self versionedString:RCTJavaScriptWillStartLoadingNotification] object:_reactBridge];
407  [[NSNotificationCenter defaultCenter] removeObserver:self name:[self versionedString:RCTJavaScriptDidLoadNotification] object:_reactBridge];
408  [[NSNotificationCenter defaultCenter] removeObserver:self name:[self versionedString:RCTJavaScriptDidFailToLoadNotification] object:_reactBridge];
409  [[NSNotificationCenter defaultCenter] removeObserver:self name:[self versionedString:RCTContentDidAppearNotification] object:_reactBridge];
410  [[NSNotificationCenter defaultCenter] removeObserver:self name:[self versionedString:RCTBridgeWillReloadNotification] object:_reactBridge];
411}
412
413- (void)_handleJavaScriptStartLoadingEvent:(NSNotification *)notification
414{
415  __weak __typeof(self) weakSelf = self;
416  dispatch_async(dispatch_get_main_queue(), ^{
417    __strong __typeof(self) strongSelf = weakSelf;
418    if (strongSelf) {
419      [strongSelf.delegate reactAppManagerStartedLoadingJavaScript:strongSelf];
420    }
421  });
422}
423
424- (void)_handleJavaScriptLoadEvent:(NSNotification *)notification
425{
426  if ([notification.name isEqualToString:[self versionedString:RCTJavaScriptDidLoadNotification]]) {
427    _isBridgeRunning = YES;
428    _hasBridgeEverLoaded = YES;
429    [_versionManager bridgeFinishedLoading:_reactBridge];
430    [self appStateDidBecomeActive];
431
432    // TODO: temporary solution for hiding LoadingProgressWindow
433    if (_appRecord.viewController) {
434      [_appRecord.viewController hideLoadingProgressWindow];
435    }
436
437    // TODO: To be removed once SDK 38 is phased out
438    // Above SDK 38 this code is invoked in different place
439    if ([self _compareVersionTo:39] == NSOrderedAscending) {
440      [self _preSDK39BeginWaitingForAppLoading];
441    }
442  } else if ([notification.name isEqualToString:[self versionedString:RCTJavaScriptDidFailToLoadNotification]]) {
443    NSError *error = (notification.userInfo) ? notification.userInfo[@"error"] : nil;
444    if (_appRecord.scopeKey) {
445      [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager setError:error forScopeKey:_appRecord.scopeKey];
446    }
447
448    EX_WEAKIFY(self);
449    dispatch_async(dispatch_get_main_queue(), ^{
450      EX_ENSURE_STRONGIFY(self);
451      [self.delegate reactAppManager:self failedToLoadJavaScriptWithError:error];
452    });
453  }
454}
455
456# pragma mark app loading & splash screen
457
458- (void)_handleReactContentEvent:(NSNotification *)notification
459{
460  if ([notification.name isEqualToString:[self versionedString:RCTContentDidAppearNotification]]
461      && notification.object == self.reactRootView) {
462    EX_WEAKIFY(self);
463    dispatch_async(dispatch_get_main_queue(), ^{
464      EX_ENSURE_STRONGIFY(self);
465      [self.delegate reactAppManagerAppContentDidAppear:self];
466
467      if ([self _compareVersionTo:38] == NSOrderedDescending) {
468        // Post SDK 38 code
469        // Up to SDK 38 this code is invoked in different place
470        [self _appLoadingFinished];
471      }
472    });
473  }
474}
475
476- (void)_handleBridgeEvent:(NSNotification *)notification
477{
478  if ([notification.name isEqualToString:[self versionedString:RCTBridgeWillReloadNotification]]) {
479    EX_WEAKIFY(self);
480    dispatch_async(dispatch_get_main_queue(), ^{
481      EX_ENSURE_STRONGIFY(self);
482      [self.delegate reactAppManagerAppContentWillReload:self];
483    });
484  }
485}
486
487/**
488 * TODO: Remove once SDK 38 is phased out.
489 */
490- (void)_preSDK39BeginWaitingForAppLoading
491{
492  if (_viewTestTimer) {
493    [_viewTestTimer invalidate];
494    _viewTestTimer = nil;
495  }
496
497  // SplashScreen.preventAutoHide is called despite actual JS method call.
498  // Prior SDK 39, SplashScreen was basing on started & finished flags that are set via legacy Expo.SplashScreen JS methods calls.
499  EXSplashScreenService *splashScreenService = (EXSplashScreenService *)[EXModuleRegistryProvider getSingletonModuleForClass:[EXSplashScreenService class]];
500  [splashScreenService preventSplashScreenAutoHideFor:(UIViewController *) _appRecord.viewController
501                                      successCallback:^(BOOL hasEffect) {}
502                                      failureCallback:^(NSString * _Nonnull message) { RCTLogWarn(@"%@", message); }];
503  _viewTestTimer = [NSTimer scheduledTimerWithTimeInterval:0.02
504                                                    target:self
505                                                  selector:@selector(_preSDK39CheckAppFinishedLoading:)
506                                                  userInfo:nil
507                                                   repeats:YES];
508}
509
510/**
511 * TODO: Remove once SDK 38 is phased out.
512 */
513- (id)_preSDK39AppLoadingManagerInstance
514{
515  Class loadingManagerClass = [self versionedClassFromString:@"EXSplashScreen"];
516  for (Class klass in [self.reactBridge moduleClasses]) {
517    if ([klass isSubclassOfClass:loadingManagerClass]) {
518      return [self.reactBridge moduleForClass:loadingManagerClass];
519    }
520  }
521  return nil;
522}
523
524/**
525 * TODO: Remove once SDK 38 is phased out.
526 */
527- (void)_preSDK39CheckAppFinishedLoading:(NSTimer *)timer
528{
529  // When root view has been filled with something, there are two cases:
530  //   1. AppLoading was never mounted, in which case we hide the loading indicator immediately
531  //   2. AppLoading was mounted, in which case we wait till it is unmounted to hide the loading indicator
532  if ([_appRecord.appManager rootView] &&
533      [_appRecord.appManager rootView].subviews.count > 0 &&
534      [_appRecord.appManager rootView].subviews.firstObject.subviews.count > 0) {
535
536    // Remove once SDK 38 is phased out.
537    id<PreSDK39EXSplashScreenManagerProtocol> splashManager = [self _preSDK39AppLoadingManagerInstance];
538
539    // SplashScreen: at this point SplashScreen is prevented from autohiding,
540    // so we can safely hide it when the flags set.
541    if (!splashManager || !splashManager.started || splashManager.finished) {
542      [_viewTestTimer invalidate];
543      _viewTestTimer = nil;
544
545      EXSplashScreenService *splashScreenService = (EXSplashScreenService *)[EXModuleRegistryProvider getSingletonModuleForClass:[EXSplashScreenService class]];
546      [splashScreenService hideSplashScreenFor:(UIViewController *) _appRecord.viewController
547                               successCallback:^(BOOL hasEffect) {}
548                               failureCallback:^(NSString * _Nonnull message) { RCTLogWarn(@"%@", message); }];
549      [self _appLoadingFinished];
550    }
551  }
552}
553
554- (void)_appLoadingFinished
555{
556  EX_WEAKIFY(self);
557  dispatch_async(dispatch_get_main_queue(), ^{
558    EX_ENSURE_STRONGIFY(self);
559    if (self.appRecord.scopeKey) {
560      [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager experienceFinishedLoadingWithScopeKey:self.appRecord.scopeKey];
561    }
562    [self.delegate reactAppManagerFinishedLoadingJavaScript:self];
563  });
564}
565
566#pragma mark - dev tools
567
568- (RCTLogFunction)logFunction
569{
570  return (([self enablesDeveloperTools]) ? EXDeveloperRCTLogFunction : EXDefaultRCTLogFunction);
571}
572
573- (RCTLogLevel)logLevel
574{
575  return ([self enablesDeveloperTools]) ? RCTLogLevelInfo : RCTLogLevelWarning;
576}
577
578- (BOOL)enablesDeveloperTools
579{
580  EXManifestsManifest *manifest = _appRecord.appLoader.manifest;
581  if (manifest) {
582    return manifest.isUsingDeveloperTool;
583  }
584  return false;
585}
586
587- (BOOL)requiresValidManifests
588{
589  return YES;
590}
591
592- (void)showDevMenu
593{
594  if ([self enablesDeveloperTools]) {
595    dispatch_async(dispatch_get_main_queue(), ^{
596      [self.versionManager showDevMenuForBridge:self.reactBridge];
597    });
598  }
599}
600
601- (void)reloadBridge
602{
603  if ([self enablesDeveloperTools]) {
604    [self.reactBridge reload];
605  }
606}
607
608- (void)disableRemoteDebugging
609{
610  if ([self enablesDeveloperTools]) {
611    [self.versionManager disableRemoteDebuggingForBridge:self.reactBridge];
612  }
613}
614
615- (void)toggleRemoteDebugging
616{
617  if ([self enablesDeveloperTools]) {
618    [self.versionManager toggleRemoteDebuggingForBridge:self.reactBridge];
619  }
620}
621
622- (void)togglePerformanceMonitor
623{
624  if ([self enablesDeveloperTools]) {
625    [self.versionManager togglePerformanceMonitorForBridge:self.reactBridge];
626  }
627}
628
629- (void)toggleElementInspector
630{
631  if ([self enablesDeveloperTools]) {
632    [self.versionManager toggleElementInspectorForBridge:self.reactBridge];
633  }
634}
635
636- (void)toggleDevMenu
637{
638  if ([EXEnvironment sharedEnvironment].isDetached) {
639    [[EXKernel sharedInstance].visibleApp.appManager showDevMenu];
640  } else {
641    [[EXKernel sharedInstance] switchTasks];
642  }
643}
644
645- (void)setupWebSocketControls
646{
647#if DEBUG || RCT_DEV
648  if ([self enablesDeveloperTools]) {
649    if ([_versionManager respondsToSelector:@selector(addWebSocketNotificationHandler:queue:forMethod:)]) {
650      __weak __typeof(self) weakSelf = self;
651
652      // Attach listeners to the bundler's dev server web socket connection.
653      // This enables tools to automatically reload the client remotely (i.e. in expo-cli).
654
655      // Enable a lot of tools under the same command namespace
656      [_versionManager addWebSocketNotificationHandler:^(id params) {
657        if (params != [NSNull null] && (NSDictionary *)params) {
658          NSDictionary *_params = (NSDictionary *)params;
659          if (_params[@"name"] != nil && (NSString *)_params[@"name"]) {
660            NSString *name = _params[@"name"];
661            if ([name isEqualToString:@"reload"]) {
662              [[EXKernel sharedInstance] reloadVisibleApp];
663            } else if ([name isEqualToString:@"toggleDevMenu"]) {
664              [weakSelf toggleDevMenu];
665            } else if ([name isEqualToString:@"toggleRemoteDebugging"]) {
666              [weakSelf toggleRemoteDebugging];
667            } else if ([name isEqualToString:@"toggleElementInspector"]) {
668              [weakSelf toggleElementInspector];
669            } else if ([name isEqualToString:@"togglePerformanceMonitor"]) {
670              [weakSelf togglePerformanceMonitor];
671            }
672          }
673        }
674      }
675                                                 queue:dispatch_get_main_queue()
676                                             forMethod:@"sendDevCommand"];
677
678      // These (reload and devMenu) are here to match RN dev tooling.
679
680      // Reload the app on "reload"
681      [_versionManager addWebSocketNotificationHandler:^(id params) {
682        [[EXKernel sharedInstance] reloadVisibleApp];
683      }
684                                                 queue:dispatch_get_main_queue()
685                                             forMethod:@"reload"];
686
687      // Open the dev menu on "devMenu"
688      [_versionManager addWebSocketNotificationHandler:^(id params) {
689        [weakSelf toggleDevMenu];
690      }
691                                                 queue:dispatch_get_main_queue()
692                                             forMethod:@"devMenu"];
693    }
694  }
695#endif
696}
697
698- (NSDictionary<NSString *, NSString *> *)devMenuItems
699{
700  return [self.versionManager devMenuItemsForBridge:self.reactBridge];
701}
702
703- (void)selectDevMenuItemWithKey:(NSString *)key
704{
705  dispatch_async(dispatch_get_main_queue(), ^{
706    [self.versionManager selectDevMenuItemWithKey:key onBridge:self.reactBridge];
707  });
708}
709
710#pragma mark - RN configuration
711
712- (NSComparisonResult)_compareVersionTo:(NSUInteger)version
713{
714  // Unversioned projects are always considered to be on the latest version
715  if (!_validatedVersion || _validatedVersion.length == 0 || [_validatedVersion isEqualToString:@"UNVERSIONED"]) {
716    return NSOrderedDescending;
717  }
718
719  NSUInteger projectVersionNumber = _validatedVersion.integerValue;
720  if (projectVersionNumber == version) {
721    return NSOrderedSame;
722  }
723  return (projectVersionNumber < version) ? NSOrderedAscending : NSOrderedDescending;
724}
725
726- (NSDictionary *)launchOptionsForBridge
727{
728  if ([EXEnvironment sharedEnvironment].isDetached) {
729    // pass the native app's launch options to standalone bridge.
730    return [ExpoKit sharedInstance].launchOptions;
731  }
732  return @{};
733}
734
735- (Class)moduleRegistryDelegateClass
736{
737  if ([EXEnvironment sharedEnvironment].isDetached) {
738    return [ExpoKit sharedInstance].moduleRegistryDelegateClass;
739  }
740  return nil;
741}
742
743- (NSString *)applicationKeyForRootView
744{
745  EXManifestsManifest *manifest = _appRecord.appLoader.manifest;
746  if (manifest && manifest.appKey) {
747    return manifest.appKey;
748  }
749
750  NSURL *bundleUrl = [self bundleUrl];
751  if (bundleUrl) {
752    NSURLComponents *components = [NSURLComponents componentsWithURL:bundleUrl resolvingAgainstBaseURL:YES];
753    NSArray<NSURLQueryItem *> *queryItems = components.queryItems;
754    for (NSURLQueryItem *item in queryItems) {
755      if ([item.name isEqualToString:@"app"]) {
756        return item.value;
757      }
758    }
759  }
760
761  return @"main";
762}
763
764- (NSDictionary * _Nullable)initialPropertiesForRootView
765{
766  NSMutableDictionary *props = [NSMutableDictionary dictionary];
767  NSMutableDictionary *expProps = [NSMutableDictionary dictionary];
768
769  NSAssert(_appRecord.scopeKey, @"Experience scope key should be nonnull when getting initial properties for root view");
770
771  NSDictionary *errorRecoveryProps = [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager developerInfoForScopeKey:_appRecord.scopeKey];
772  if ([[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager scopeKeyIsRecoveringFromError:_appRecord.scopeKey]) {
773    [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager increaseAutoReloadBuffer];
774    if (errorRecoveryProps) {
775      expProps[@"errorRecovery"] = errorRecoveryProps;
776    }
777  }
778
779  expProps[@"shell"] = @(_appRecord == [EXKernel sharedInstance].appRegistry.standaloneAppRecord);
780  expProps[@"appOwnership"] = [self _appOwnership];
781  if (_initialProps) {
782    [expProps addEntriesFromDictionary:_initialProps];
783  }
784  EXPendingNotification *initialNotification = [[EXKernel sharedInstance].serviceRegistry.notificationsManager initialNotification];
785  if (initialNotification) {
786    expProps[@"notification"] = initialNotification.properties;
787  }
788
789  NSString *manifestString = nil;
790  EXManifestsManifest *manifest = _appRecord.appLoader.manifest;
791  if (manifest && [NSJSONSerialization isValidJSONObject:manifest.rawManifestJSON]) {
792    NSError *error;
793    NSData *jsonData = [NSJSONSerialization dataWithJSONObject:manifest.rawManifestJSON options:0 error:&error];
794    if (jsonData) {
795      manifestString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
796    } else {
797      DDLogWarn(@"Failed to serialize JSON manifest: %@", error);
798    }
799  }
800
801  expProps[@"manifestString"] = manifestString;
802  if (_appRecord.appLoader.manifestUrl) {
803    expProps[@"initialUri"] = [_appRecord.appLoader.manifestUrl absoluteString];
804  }
805  props[@"exp"] = expProps;
806  return props;
807}
808
809- (NSString *)_appOwnership
810{
811  if (_appRecord == [EXKernel sharedInstance].appRegistry.standaloneAppRecord) {
812    return @"standalone";
813  }
814  return @"expo";
815}
816
817- (NSString *)_executionEnvironment
818{
819  if ([EXEnvironment sharedEnvironment].isDetached) {
820    return EXConstantsExecutionEnvironmentStandalone;
821  } else {
822    return EXConstantsExecutionEnvironmentStoreClient;
823  }
824}
825
826- (NSString *)scopedDocumentDirectory
827{
828  NSString *escapedScopeKey = [self escapedResourceName:_appRecord.scopeKey];
829  NSString *mainDocumentDirectory = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
830  NSString *exponentDocumentDirectory = [mainDocumentDirectory stringByAppendingPathComponent:@"ExponentExperienceData"];
831  return [[exponentDocumentDirectory stringByAppendingPathComponent:escapedScopeKey] stringByStandardizingPath];
832}
833
834- (NSString *)scopedCachesDirectory
835{
836  NSString *escapedScopeKey = [self escapedResourceName:_appRecord.scopeKey];
837  NSString *mainCachesDirectory = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;
838  NSString *exponentCachesDirectory = [mainCachesDirectory stringByAppendingPathComponent:@"ExponentExperienceData"];
839  return [[exponentCachesDirectory stringByAppendingPathComponent:escapedScopeKey] stringByStandardizingPath];
840}
841
842- (void *)jsExecutorFactoryForBridge:(id)bridge
843{
844  return [_versionManager versionedJsExecutorFactoryForBridge:bridge];
845}
846
847@end
848