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  [self _invalidateAndClearDelegate:NO];
117  [self computeVersionSymbolPrefix];
118
119  if ([self isReadyToLoad]) {
120    Class versionManagerClass = [self versionedClassFromString:@"EXVersionManager"];
121    Class bridgeClass = [self versionedClassFromString:@"RCTBridge"];
122    Class rootViewClass = [self versionedClassFromString:@"RCTRootView"];
123
124    _versionManager = [[versionManagerClass alloc] initWithParams:[self extraParams]
125                                                         manifest:_appRecord.appLoader.manifest
126                                                     fatalHandler:handleFatalReactError
127                                                      logFunction:[self logFunction]
128                                                     logThreshold:[self logLevel]];
129    _reactBridge = [[bridgeClass alloc] initWithDelegate:self launchOptions:[self launchOptionsForBridge]];
130
131    if (!_isHeadless) {
132      // We don't want to run the whole JS app if app launches in the background,
133      // so we're omitting creation of RCTRootView that triggers runApplication and sets up React view hierarchy.
134      _reactRootView = [[rootViewClass alloc] initWithBridge:_reactBridge
135                                                  moduleName:[self applicationKeyForRootView]
136                                           initialProperties:[self initialPropertiesForRootView]];
137    }
138
139    [self setupWebSocketControls];
140    [_delegate reactAppManagerIsReadyForLoad:self];
141
142    NSAssert([_reactBridge isLoading], @"React bridge should be loading once initialized");
143    [_versionManager bridgeWillStartLoading:_reactBridge];
144  }
145}
146
147- (NSDictionary *)extraParams
148{
149  // we allow the vanilla RN dev menu in some circumstances.
150  BOOL isStandardDevMenuAllowed = [EXEnvironment sharedEnvironment].isDetached;
151  NSMutableDictionary *params = [NSMutableDictionary dictionaryWithDictionary:@{
152    @"manifest": _appRecord.appLoader.manifest.rawManifestJSON,
153    @"constants": @{
154        @"linkingUri": RCTNullIfNil([EXKernelLinkingManager linkingUriForExperienceUri:_appRecord.appLoader.manifestUrl useLegacy:[self _compareVersionTo:27] == NSOrderedAscending]),
155        @"experienceUrl": RCTNullIfNil(_appRecord.appLoader.manifestUrl? _appRecord.appLoader.manifestUrl.absoluteString: nil),
156        @"expoRuntimeVersion": [EXBuildConstants sharedInstance].expoRuntimeVersion,
157        @"manifest": _appRecord.appLoader.manifest.rawManifestJSON,
158        @"executionEnvironment": [self _executionEnvironment],
159        @"appOwnership": [self _appOwnership],
160        @"isHeadless": @(_isHeadless),
161        @"supportedExpoSdks": [EXVersions sharedInstance].versions[@"sdkVersions"],
162    },
163    @"exceptionsManagerDelegate": _exceptionHandler,
164    @"initialUri": RCTNullIfNil([EXKernelLinkingManager initialUriWithManifestUrl:_appRecord.appLoader.manifestUrl]),
165    @"isDeveloper": @([self enablesDeveloperTools]),
166    @"isStandardDevMenuAllowed": @(isStandardDevMenuAllowed),
167    @"testEnvironment": @([EXEnvironment sharedEnvironment].testEnvironment),
168    @"services": [EXKernel sharedInstance].serviceRegistry.allServices,
169    @"singletonModules": [EXModuleRegistryProvider singletonModules],
170    @"moduleRegistryDelegateClass": RCTNullIfNil([self moduleRegistryDelegateClass]),
171  }];
172  if ([@"expo" isEqualToString:[self _appOwnership]]) {
173    [params addEntriesFromDictionary:@{
174      @"fileSystemDirectories": @{
175          @"documentDirectory": [self scopedDocumentDirectory],
176          @"cachesDirectory": [self scopedCachesDirectory]
177      }
178    }];
179  }
180  return params;
181}
182
183- (void)invalidate
184{
185  [self _invalidateAndClearDelegate:YES];
186}
187
188- (void)_invalidateAndClearDelegate:(BOOL)clearDelegate
189{
190  [self _stopObservingBridgeNotifications];
191  if (_viewTestTimer) {
192    [_viewTestTimer invalidate];
193    _viewTestTimer = nil;
194  }
195  if (_versionManager) {
196    [_versionManager invalidate];
197    _versionManager = nil;
198  }
199  if (_reactRootView) {
200    [_reactRootView removeFromSuperview];
201    _reactRootView = nil;
202  }
203  if (_reactBridge) {
204    [_reactBridge invalidate];
205    _reactBridge = nil;
206    if (_delegate) {
207      [_delegate reactAppManagerDidInvalidate:self];
208      if (clearDelegate) {
209        _delegate = nil;
210      }
211    }
212  }
213  _isBridgeRunning = NO;
214  [self _invalidateVersionState];
215}
216
217- (void)computeVersionSymbolPrefix
218{
219  // TODO: ben: kernel checks detached versions here
220  _validatedVersion = [[EXVersions sharedInstance] availableSdkVersionForManifest:_appRecord.appLoader.manifest];
221  _versionSymbolPrefix = [[EXVersions sharedInstance] symbolPrefixForSdkVersion:self.validatedVersion isKernel:NO];
222}
223
224- (void)_invalidateVersionState
225{
226  _versionSymbolPrefix = @"";
227  _validatedVersion = nil;
228}
229
230- (Class)versionedClassFromString: (NSString *)classString
231{
232  return NSClassFromString([self versionedString:classString]);
233}
234
235- (NSString *)versionedString: (NSString *)string
236{
237  return [EXVersions versionedString:string withPrefix:_versionSymbolPrefix];
238}
239
240- (NSString *)escapedResourceName:(NSString *)string
241{
242  NSString *charactersToEscape = @"!*'();:@&=+$,/?%#[]";
243  NSCharacterSet *allowedCharacters = [[NSCharacterSet characterSetWithCharactersInString:charactersToEscape] invertedSet];
244  return [string stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacters];
245}
246
247- (BOOL)isReadyToLoad
248{
249  if (_appRecord) {
250    return (_appRecord.appLoader.status == kEXAppLoaderStatusHasManifest || _appRecord.appLoader.status == kEXAppLoaderStatusHasManifestAndBundle);
251  }
252  return NO;
253}
254
255- (NSURL *)bundleUrl
256{
257  return [EXApiUtil bundleUrlFromManifest:_appRecord.appLoader.manifest];
258}
259
260- (void)appStateDidBecomeActive
261{
262  if ([_versionManager respondsToSelector:@selector(bridgeDidForeground)]) {
263    // supported before SDK 29 / unimodules
264    [_versionManager bridgeDidForeground];
265  }
266}
267
268- (void)appStateDidBecomeInactive
269{
270  if ([_versionManager respondsToSelector:@selector(bridgeDidBackground)]) {
271    [_versionManager bridgeDidBackground];
272  }
273}
274
275#pragma mark - EXAppFetcherDataSource
276
277- (NSString *)bundleResourceNameForAppFetcher:(EXAppFetcher *)appFetcher withManifest:(nonnull EXManifestsManifest *)manifest
278{
279  if ([EXEnvironment sharedEnvironment].isDetached) {
280    NSLog(@"Standalone bundle remote url is %@", [EXEnvironment sharedEnvironment].standaloneManifestUrl);
281    return kEXEmbeddedBundleResourceName;
282  } else {
283    return manifest.legacyId;
284  }
285}
286
287- (BOOL)appFetcherShouldInvalidateBundleCache:(EXAppFetcher *)appFetcher
288{
289  return NO;
290}
291
292#pragma mark - RCTBridgeDelegate
293
294- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
295{
296  return [self bundleUrl];
297}
298
299- (void)loadSourceForBridge:(RCTBridge *)bridge withBlock:(RCTSourceLoadBlock)loadCallback
300{
301  // clear any potentially old loading state
302  if (_appRecord.scopeKey) {
303    [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager setError:nil forScopeKey:_appRecord.scopeKey];
304  }
305  [self _stopObservingBridgeNotifications];
306  [self _startObservingBridgeNotificationsForBridge:bridge];
307
308  if ([self enablesDeveloperTools]) {
309    if ([_appRecord.appLoader supportsBundleReload]) {
310      [_appRecord.appLoader forceBundleReload];
311    } else {
312      NSAssert(_appRecord.scopeKey, @"EXKernelAppRecord.scopeKey should be nonnull if we have a manifest with developer tools enabled");
313      [[EXKernel sharedInstance] reloadAppWithScopeKey:_appRecord.scopeKey];
314    }
315  }
316
317  _loadCallback = loadCallback;
318  if (_appRecord.appLoader.status == kEXAppLoaderStatusHasManifestAndBundle) {
319    // finish loading immediately (app loader won't call this since it's already done)
320    [self appLoaderFinished];
321  } else {
322    // wait for something else to call `appLoaderFinished` or `appLoaderFailed` later.
323  }
324}
325
326- (NSArray *)extraModulesForBridge:(RCTBridge *)bridge
327{
328  return [self.versionManager extraModulesForBridge:bridge];
329}
330
331- (void)appLoaderFinished
332{
333  NSData *data = _appRecord.appLoader.bundle;
334  if (_loadCallback) {
335    if ([self _compareVersionTo:22] == NSOrderedAscending) {
336      SDK21RCTSourceLoadBlock legacyLoadCallback = (SDK21RCTSourceLoadBlock)_loadCallback;
337      legacyLoadCallback(nil, data, data.length);
338    } else {
339      _loadCallback(nil, [[RCTSource alloc] initWithURL:[self bundleUrl] data:data]);
340    }
341    _loadCallback = nil;
342  }
343}
344
345- (void)appLoaderFailedWithError:(NSError *)error
346{
347  // RN is going to call RCTFatal() on this error, so keep a reference to it for later
348  // so we can distinguish this non-fatal error from actual fatal cases.
349  if (_appRecord.scopeKey) {
350    [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager setError:error forScopeKey:_appRecord.scopeKey];
351  }
352
353  // react won't post this for us
354  [[NSNotificationCenter defaultCenter] postNotificationName:[self versionedString:RCTJavaScriptDidFailToLoadNotification] object:error];
355
356  if (_loadCallback) {
357    if ([self _compareVersionTo:22] == NSOrderedAscending) {
358      SDK21RCTSourceLoadBlock legacyLoadCallback = (SDK21RCTSourceLoadBlock)_loadCallback;
359      legacyLoadCallback(error, nil, 0);
360    } else {
361      _loadCallback(error, nil);
362    }
363    _loadCallback = nil;
364  }
365}
366
367#pragma mark - JavaScript loading
368
369- (void)_startObservingBridgeNotificationsForBridge:(RCTBridge *)bridge
370{
371  NSAssert(bridge, @"Must subscribe to loading notifs for a non-null bridge");
372
373  [[NSNotificationCenter defaultCenter] addObserver:self
374                                           selector:@selector(_handleJavaScriptStartLoadingEvent:)
375                                               name:[self versionedString:RCTJavaScriptWillStartLoadingNotification]
376                                             object:bridge];
377  [[NSNotificationCenter defaultCenter] addObserver:self
378                                           selector:@selector(_handleJavaScriptLoadEvent:)
379                                               name:[self versionedString:RCTJavaScriptDidLoadNotification]
380                                             object:bridge];
381  [[NSNotificationCenter defaultCenter] addObserver:self
382                                           selector:@selector(_handleJavaScriptLoadEvent:)
383                                               name:[self versionedString:RCTJavaScriptDidFailToLoadNotification]
384                                             object:bridge];
385  [[NSNotificationCenter defaultCenter] addObserver:self
386                                           selector:@selector(_handleReactContentEvent:)
387                                               name:[self versionedString:RCTContentDidAppearNotification]
388                                             object:nil];
389  [[NSNotificationCenter defaultCenter] addObserver:self
390                                           selector:@selector(_handleBridgeEvent:)
391                                               name:[self versionedString:RCTBridgeWillReloadNotification]
392                                             object:bridge];
393}
394
395- (void)_stopObservingBridgeNotifications
396{
397  [[NSNotificationCenter defaultCenter] removeObserver:self name:[self versionedString:RCTJavaScriptWillStartLoadingNotification] object:_reactBridge];
398  [[NSNotificationCenter defaultCenter] removeObserver:self name:[self versionedString:RCTJavaScriptDidLoadNotification] object:_reactBridge];
399  [[NSNotificationCenter defaultCenter] removeObserver:self name:[self versionedString:RCTJavaScriptDidFailToLoadNotification] object:_reactBridge];
400  [[NSNotificationCenter defaultCenter] removeObserver:self name:[self versionedString:RCTContentDidAppearNotification] object:_reactBridge];
401  [[NSNotificationCenter defaultCenter] removeObserver:self name:[self versionedString:RCTBridgeWillReloadNotification] object:_reactBridge];
402}
403
404- (void)_handleJavaScriptStartLoadingEvent:(NSNotification *)notification
405{
406  __weak __typeof(self) weakSelf = self;
407  dispatch_async(dispatch_get_main_queue(), ^{
408    __strong __typeof(self) strongSelf = weakSelf;
409    if (strongSelf) {
410      [strongSelf.delegate reactAppManagerStartedLoadingJavaScript:strongSelf];
411    }
412  });
413}
414
415- (void)_handleJavaScriptLoadEvent:(NSNotification *)notification
416{
417  if ([notification.name isEqualToString:[self versionedString:RCTJavaScriptDidLoadNotification]]) {
418    _isBridgeRunning = YES;
419    _hasBridgeEverLoaded = YES;
420    [_versionManager bridgeFinishedLoading:_reactBridge];
421    [self appStateDidBecomeActive];
422
423    // TODO: temporary solution for hiding LoadingProgressWindow
424    if (_appRecord.viewController) {
425      [_appRecord.viewController hideLoadingProgressWindow];
426    }
427
428    // TODO: To be removed once SDK 38 is phased out
429    // Above SDK 38 this code is invoked in different place
430    if ([self _compareVersionTo:39] == NSOrderedAscending) {
431      [self _preSDK39BeginWaitingForAppLoading];
432    }
433  } else if ([notification.name isEqualToString:[self versionedString:RCTJavaScriptDidFailToLoadNotification]]) {
434    NSError *error = (notification.userInfo) ? notification.userInfo[@"error"] : nil;
435    if (_appRecord.scopeKey) {
436      [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager setError:error forScopeKey:_appRecord.scopeKey];
437    }
438
439    EX_WEAKIFY(self);
440    dispatch_async(dispatch_get_main_queue(), ^{
441      EX_ENSURE_STRONGIFY(self);
442      [self.delegate reactAppManager:self failedToLoadJavaScriptWithError:error];
443    });
444  }
445}
446
447# pragma mark app loading & splash screen
448
449- (void)_handleReactContentEvent:(NSNotification *)notification
450{
451  if ([notification.name isEqualToString:[self versionedString:RCTContentDidAppearNotification]]
452      && notification.object == self.reactRootView) {
453    EX_WEAKIFY(self);
454    dispatch_async(dispatch_get_main_queue(), ^{
455      EX_ENSURE_STRONGIFY(self);
456      [self.delegate reactAppManagerAppContentDidAppear:self];
457
458      if ([self _compareVersionTo:38] == NSOrderedDescending) {
459        // Post SDK 38 code
460        // Up to SDK 38 this code is invoked in different place
461        [self _appLoadingFinished];
462      }
463    });
464  }
465}
466
467- (void)_handleBridgeEvent:(NSNotification *)notification
468{
469  if ([notification.name isEqualToString:[self versionedString:RCTBridgeWillReloadNotification]]) {
470    EX_WEAKIFY(self);
471    dispatch_async(dispatch_get_main_queue(), ^{
472      EX_ENSURE_STRONGIFY(self);
473      [self.delegate reactAppManagerAppContentWillReload:self];
474    });
475  }
476}
477
478/**
479 * TODO: Remove once SDK 38 is phased out.
480 */
481- (void)_preSDK39BeginWaitingForAppLoading
482{
483  if (_viewTestTimer) {
484    [_viewTestTimer invalidate];
485    _viewTestTimer = nil;
486  }
487
488  // SplashScreen.preventAutoHide is called despite actual JS method call.
489  // Prior SDK 39, SplashScreen was basing on started & finished flags that are set via legacy Expo.SplashScreen JS methods calls.
490  EXSplashScreenService *splashScreenService = (EXSplashScreenService *)[EXModuleRegistryProvider getSingletonModuleForClass:[EXSplashScreenService class]];
491  [splashScreenService preventSplashScreenAutoHideFor:(UIViewController *) _appRecord.viewController
492                                      successCallback:^(BOOL hasEffect) {}
493                                      failureCallback:^(NSString * _Nonnull message) { RCTLogWarn(@"%@", message); }];
494  _viewTestTimer = [NSTimer scheduledTimerWithTimeInterval:0.02
495                                                    target:self
496                                                  selector:@selector(_preSDK39CheckAppFinishedLoading:)
497                                                  userInfo:nil
498                                                   repeats:YES];
499}
500
501/**
502 * TODO: Remove once SDK 38 is phased out.
503 */
504- (id)_preSDK39AppLoadingManagerInstance
505{
506  Class loadingManagerClass = [self versionedClassFromString:@"EXSplashScreen"];
507  for (Class klass in [self.reactBridge moduleClasses]) {
508    if ([klass isSubclassOfClass:loadingManagerClass]) {
509      return [self.reactBridge moduleForClass:loadingManagerClass];
510    }
511  }
512  return nil;
513}
514
515/**
516 * TODO: Remove once SDK 38 is phased out.
517 */
518- (void)_preSDK39CheckAppFinishedLoading:(NSTimer *)timer
519{
520  // When root view has been filled with something, there are two cases:
521  //   1. AppLoading was never mounted, in which case we hide the loading indicator immediately
522  //   2. AppLoading was mounted, in which case we wait till it is unmounted to hide the loading indicator
523  if ([_appRecord.appManager rootView] &&
524      [_appRecord.appManager rootView].subviews.count > 0 &&
525      [_appRecord.appManager rootView].subviews.firstObject.subviews.count > 0) {
526
527    // Remove once SDK 38 is phased out.
528    id<PreSDK39EXSplashScreenManagerProtocol> splashManager = [self _preSDK39AppLoadingManagerInstance];
529
530    // SplashScreen: at this point SplashScreen is prevented from autohiding,
531    // so we can safely hide it when the flags set.
532    if (!splashManager || !splashManager.started || splashManager.finished) {
533      [_viewTestTimer invalidate];
534      _viewTestTimer = nil;
535
536      EXSplashScreenService *splashScreenService = (EXSplashScreenService *)[EXModuleRegistryProvider getSingletonModuleForClass:[EXSplashScreenService class]];
537      [splashScreenService hideSplashScreenFor:(UIViewController *) _appRecord.viewController
538                               successCallback:^(BOOL hasEffect) {}
539                               failureCallback:^(NSString * _Nonnull message) { RCTLogWarn(@"%@", message); }];
540      [self _appLoadingFinished];
541    }
542  }
543}
544
545- (void)_appLoadingFinished
546{
547  EX_WEAKIFY(self);
548  dispatch_async(dispatch_get_main_queue(), ^{
549    EX_ENSURE_STRONGIFY(self);
550    if (self.appRecord.scopeKey) {
551      [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager experienceFinishedLoadingWithScopeKey:self.appRecord.scopeKey];
552    }
553    [self.delegate reactAppManagerFinishedLoadingJavaScript:self];
554  });
555}
556
557#pragma mark - dev tools
558
559- (RCTLogFunction)logFunction
560{
561  return (([self enablesDeveloperTools]) ? EXDeveloperRCTLogFunction : EXDefaultRCTLogFunction);
562}
563
564- (RCTLogLevel)logLevel
565{
566  return ([self enablesDeveloperTools]) ? RCTLogLevelInfo : RCTLogLevelWarning;
567}
568
569- (BOOL)enablesDeveloperTools
570{
571  EXManifestsManifest *manifest = _appRecord.appLoader.manifest;
572  if (manifest) {
573    return manifest.isUsingDeveloperTool;
574  }
575  return false;
576}
577
578- (BOOL)requiresValidManifests
579{
580  return YES;
581}
582
583- (void)showDevMenu
584{
585  if ([self enablesDeveloperTools]) {
586    dispatch_async(dispatch_get_main_queue(), ^{
587      [self.versionManager showDevMenuForBridge:self.reactBridge];
588    });
589  }
590}
591
592- (void)reloadBridge
593{
594  if ([self enablesDeveloperTools]) {
595    [self.reactBridge reload];
596  }
597}
598
599- (void)disableRemoteDebugging
600{
601  if ([self enablesDeveloperTools]) {
602    [self.versionManager disableRemoteDebuggingForBridge:self.reactBridge];
603  }
604}
605
606- (void)toggleRemoteDebugging
607{
608  if ([self enablesDeveloperTools]) {
609    [self.versionManager toggleRemoteDebuggingForBridge:self.reactBridge];
610  }
611}
612
613- (void)togglePerformanceMonitor
614{
615  if ([self enablesDeveloperTools]) {
616    [self.versionManager togglePerformanceMonitorForBridge:self.reactBridge];
617  }
618}
619
620- (void)toggleElementInspector
621{
622  if ([self enablesDeveloperTools]) {
623    [self.versionManager toggleElementInspectorForBridge:self.reactBridge];
624  }
625}
626
627- (void)toggleDevMenu
628{
629  if ([EXEnvironment sharedEnvironment].isDetached) {
630    [[EXKernel sharedInstance].visibleApp.appManager showDevMenu];
631  } else {
632    [[EXKernel sharedInstance] switchTasks];
633  }
634}
635
636- (void)setupWebSocketControls
637{
638#if DEBUG || RCT_DEV
639  if ([self enablesDeveloperTools]) {
640    if ([_versionManager respondsToSelector:@selector(addWebSocketNotificationHandler:queue:forMethod:)]) {
641      __weak __typeof(self) weakSelf = self;
642
643      // Attach listeners to the bundler's dev server web socket connection.
644      // This enables tools to automatically reload the client remotely (i.e. in expo-cli).
645
646      // Enable a lot of tools under the same command namespace
647      [_versionManager addWebSocketNotificationHandler:^(id params) {
648        if (params != [NSNull null] && (NSDictionary *)params) {
649          NSDictionary *_params = (NSDictionary *)params;
650          if (_params[@"name"] != nil && (NSString *)_params[@"name"]) {
651            NSString *name = _params[@"name"];
652            if ([name isEqualToString:@"reload"]) {
653              [[EXKernel sharedInstance] reloadVisibleApp];
654            } else if ([name isEqualToString:@"toggleDevMenu"]) {
655              [weakSelf toggleDevMenu];
656            } else if ([name isEqualToString:@"toggleRemoteDebugging"]) {
657              [weakSelf toggleRemoteDebugging];
658            } else if ([name isEqualToString:@"toggleElementInspector"]) {
659              [weakSelf toggleElementInspector];
660            } else if ([name isEqualToString:@"togglePerformanceMonitor"]) {
661              [weakSelf togglePerformanceMonitor];
662            }
663          }
664        }
665      }
666                                                 queue:dispatch_get_main_queue()
667                                             forMethod:@"sendDevCommand"];
668
669      // These (reload and devMenu) are here to match RN dev tooling.
670
671      // Reload the app on "reload"
672      [_versionManager addWebSocketNotificationHandler:^(id params) {
673        [[EXKernel sharedInstance] reloadVisibleApp];
674      }
675                                                 queue:dispatch_get_main_queue()
676                                             forMethod:@"reload"];
677
678      // Open the dev menu on "devMenu"
679      [_versionManager addWebSocketNotificationHandler:^(id params) {
680        [weakSelf toggleDevMenu];
681      }
682                                                 queue:dispatch_get_main_queue()
683                                             forMethod:@"devMenu"];
684    }
685  }
686#endif
687}
688
689- (NSDictionary<NSString *, NSString *> *)devMenuItems
690{
691  return [self.versionManager devMenuItemsForBridge:self.reactBridge];
692}
693
694- (void)selectDevMenuItemWithKey:(NSString *)key
695{
696  dispatch_async(dispatch_get_main_queue(), ^{
697    [self.versionManager selectDevMenuItemWithKey:key onBridge:self.reactBridge];
698  });
699}
700
701#pragma mark - RN configuration
702
703- (NSComparisonResult)_compareVersionTo:(NSUInteger)version
704{
705  // Unversioned projects are always considered to be on the latest version
706  if (!_validatedVersion || _validatedVersion.length == 0 || [_validatedVersion isEqualToString:@"UNVERSIONED"]) {
707    return NSOrderedDescending;
708  }
709
710  NSUInteger projectVersionNumber = _validatedVersion.integerValue;
711  if (projectVersionNumber == version) {
712    return NSOrderedSame;
713  }
714  return (projectVersionNumber < version) ? NSOrderedAscending : NSOrderedDescending;
715}
716
717- (NSDictionary *)launchOptionsForBridge
718{
719  if ([EXEnvironment sharedEnvironment].isDetached) {
720    // pass the native app's launch options to standalone bridge.
721    return [ExpoKit sharedInstance].launchOptions;
722  }
723  return @{};
724}
725
726- (Class)moduleRegistryDelegateClass
727{
728  if ([EXEnvironment sharedEnvironment].isDetached) {
729    return [ExpoKit sharedInstance].moduleRegistryDelegateClass;
730  }
731  return nil;
732}
733
734- (NSString *)applicationKeyForRootView
735{
736  EXManifestsManifest *manifest = _appRecord.appLoader.manifest;
737  if (manifest && manifest.appKey) {
738    return manifest.appKey;
739  }
740
741  NSURL *bundleUrl = [self bundleUrl];
742  if (bundleUrl) {
743    NSURLComponents *components = [NSURLComponents componentsWithURL:bundleUrl resolvingAgainstBaseURL:YES];
744    NSArray<NSURLQueryItem *> *queryItems = components.queryItems;
745    for (NSURLQueryItem *item in queryItems) {
746      if ([item.name isEqualToString:@"app"]) {
747        return item.value;
748      }
749    }
750  }
751
752  return @"main";
753}
754
755- (NSDictionary * _Nullable)initialPropertiesForRootView
756{
757  NSMutableDictionary *props = [NSMutableDictionary dictionary];
758  NSMutableDictionary *expProps = [NSMutableDictionary dictionary];
759
760  NSAssert(_appRecord.scopeKey, @"Experience scope key should be nonnull when getting initial properties for root view");
761
762  NSDictionary *errorRecoveryProps = [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager developerInfoForScopeKey:_appRecord.scopeKey];
763  if ([[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager scopeKeyIsRecoveringFromError:_appRecord.scopeKey]) {
764    [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager increaseAutoReloadBuffer];
765    if (errorRecoveryProps) {
766      expProps[@"errorRecovery"] = errorRecoveryProps;
767    }
768  }
769
770  expProps[@"shell"] = @(_appRecord == [EXKernel sharedInstance].appRegistry.standaloneAppRecord);
771  expProps[@"appOwnership"] = [self _appOwnership];
772  if (_initialProps) {
773    [expProps addEntriesFromDictionary:_initialProps];
774  }
775  EXPendingNotification *initialNotification = [[EXKernel sharedInstance].serviceRegistry.notificationsManager initialNotification];
776  if (initialNotification) {
777    expProps[@"notification"] = initialNotification.properties;
778  }
779
780  NSString *manifestString = nil;
781  EXManifestsManifest *manifest = _appRecord.appLoader.manifest;
782  if (manifest && [NSJSONSerialization isValidJSONObject:manifest.rawManifestJSON]) {
783    NSError *error;
784    NSData *jsonData = [NSJSONSerialization dataWithJSONObject:manifest.rawManifestJSON options:0 error:&error];
785    if (jsonData) {
786      manifestString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
787    } else {
788      DDLogWarn(@"Failed to serialize JSON manifest: %@", error);
789    }
790  }
791
792  expProps[@"manifestString"] = manifestString;
793  if (_appRecord.appLoader.manifestUrl) {
794    expProps[@"initialUri"] = [_appRecord.appLoader.manifestUrl absoluteString];
795  }
796  props[@"exp"] = expProps;
797  return props;
798}
799
800- (NSString *)_appOwnership
801{
802  if (_appRecord == [EXKernel sharedInstance].appRegistry.standaloneAppRecord) {
803    return @"standalone";
804  }
805  return @"expo";
806}
807
808- (NSString *)_executionEnvironment
809{
810  if ([EXEnvironment sharedEnvironment].isDetached) {
811    return EXConstantsExecutionEnvironmentStandalone;
812  } else {
813    return EXConstantsExecutionEnvironmentStoreClient;
814  }
815}
816
817- (NSString *)scopedDocumentDirectory
818{
819  NSString *escapedScopeKey = [self escapedResourceName:_appRecord.scopeKey];
820  NSString *mainDocumentDirectory = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
821  NSString *exponentDocumentDirectory = [mainDocumentDirectory stringByAppendingPathComponent:@"ExponentExperienceData"];
822  return [[exponentDocumentDirectory stringByAppendingPathComponent:escapedScopeKey] stringByStandardizingPath];
823}
824
825- (NSString *)scopedCachesDirectory
826{
827  NSString *escapedScopeKey = [self escapedResourceName:_appRecord.scopeKey];
828  NSString *mainCachesDirectory = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;
829  NSString *exponentCachesDirectory = [mainCachesDirectory stringByAppendingPathComponent:@"ExponentExperienceData"];
830  return [[exponentCachesDirectory stringByAppendingPathComponent:escapedScopeKey] stringByStandardizingPath];
831}
832
833- (void *)jsExecutorFactoryForBridge:(id)bridge
834{
835  return [_versionManager versionedJsExecutorFactoryForBridge:bridge];
836}
837
838@end
839