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