1// Copyright 2015-present 650 Industries. All rights reserved.
2
3#import "EXAppState.h"
4#import "EXDevSettings.h"
5#import "EXDisabledDevLoadingView.h"
6#import "EXDisabledDevMenu.h"
7#import "EXDisabledRedBox.h"
8#import "EXVersionedNetworkInterceptor.h"
9#import "EXVersionManagerObjC.h"
10#import "EXScopedBridgeModule.h"
11#import "EXStatusBarManager.h"
12#import "EXUnversioned.h"
13#import "EXTest.h"
14
15#import <React/RCTAssert.h>
16#import <React/RCTBridge.h>
17#import <React/RCTBridge+Private.h>
18#import <React/RCTDevMenu.h>
19#import <React/RCTDevSettings.h>
20#import <React/RCTExceptionsManager.h>
21#import <React/RCTLog.h>
22#import <React/RCTRedBox.h>
23#import <React/RCTPackagerConnection.h>
24#import <React/RCTModuleData.h>
25#import <React/RCTUtils.h>
26#import <React/RCTDataRequestHandler.h>
27#import <React/RCTFileRequestHandler.h>
28#import <React/RCTHTTPRequestHandler.h>
29#import <React/RCTNetworking.h>
30#import <React/RCTLocalAssetImageLoader.h>
31#import <React/RCTGIFImageDecoder.h>
32#import <React/RCTImageLoader.h>
33#import <React/RCTInspectorDevServerHelper.h>
34#import <React/CoreModulesPlugins.h>
35
36#import <ExpoModulesCore/EXNativeModulesProxy.h>
37#import <ExpoModulesCore/EXModuleRegistryHolderReactModule.h>
38#import <EXMediaLibrary/EXMediaLibraryImageLoader.h>
39#import <EXFileSystem/EXFileSystem.h>
40
41// When `use_frameworks!` is used, the generated Swift header is inside modules.
42// Otherwise, it's available only locally with double-quoted imports.
43#if __has_include(<EXManifests/EXManifests-Swift.h>)
44#import <EXManifests/EXManifests-Swift.h>
45#else
46#import "EXManifests-Swift.h"
47#endif
48
49// Import 3rd party modules that need to be scoped.
50#import <RNCAsyncStorage/RNCAsyncStorage.h>
51#import "RNCWebViewManager.h"
52
53#import "EXScopedModuleRegistry.h"
54#import "EXScopedModuleRegistryAdapter.h"
55#import "EXScopedModuleRegistryDelegate.h"
56
57#import "Expo_Go-Swift.h"
58
59RCT_EXTERN NSDictionary<NSString *, NSDictionary *> *EXGetScopedModuleClasses(void);
60RCT_EXTERN void EXRegisterScopedModule(Class, ...);
61
62// this is needed because RCTPerfMonitor does not declare a public interface
63// anywhere that we can import.
64@interface RCTPerfMonitorDevSettingsHack <NSObject>
65
66- (void)hide;
67- (void)show;
68
69@end
70
71@interface RCTBridgeHack <NSObject>
72
73- (void)reload;
74
75@end
76
77@interface EXVersionManagerObjC ()
78
79// is this the first time this ABI has been touched at runtime?
80@property (nonatomic, assign) BOOL isFirstLoad;
81@property (nonatomic, strong) NSDictionary *params;
82@property (nonatomic, strong) EXManifestsManifest *manifest;
83@property (nonatomic, strong) EXVersionedNetworkInterceptor *networkInterceptor;
84
85// Legacy
86@property (nonatomic, strong) EXModuleRegistry *legacyModuleRegistry;
87@property (nonatomic, strong) EXNativeModulesProxy *legacyModulesProxy;
88
89@end
90
91@implementation EXVersionManagerObjC
92
93/**
94 * Uses a params dict since the internal workings may change over time, but we want to keep the interface the same.
95 *  Expected params:
96 *    NSDictionary *constants
97 *    NSURL *initialUri
98 *    @BOOL isDeveloper
99 *    @BOOL isStandardDevMenuAllowed
100 *    @EXTestEnvironment testEnvironment
101 *    NSDictionary *services
102 *
103 * Kernel-only:
104 *    EXKernel *kernel
105 *    NSArray *supportedSdkVersions
106 *    id exceptionsManagerDelegate
107 */
108- (nonnull instancetype)initWithParams:(nonnull NSDictionary *)params
109                              manifest:(nonnull EXManifestsManifest *)manifest
110                          fatalHandler:(void (^ _Nonnull)(NSError * _Nullable))fatalHandler
111                           logFunction:(nonnull RCTLogFunction)logFunction
112                          logThreshold:(RCTLogLevel)logThreshold
113{
114  if (self = [super init]) {
115    _params = params;
116    _manifest = manifest;
117  }
118  return self;
119}
120
121+ (void)load
122{
123  // Register scoped 3rd party modules. Some of them are separate pods that
124  // don't have access to EXScopedModuleRegistry and so they can't register themselves.
125  EXRegisterScopedModule([RNCWebViewManager class], EX_KERNEL_SERVICE_NONE, nil);
126}
127
128- (void)bridgeWillStartLoading:(id)bridge
129{
130  if ([self _isDevModeEnabledForBridge:bridge]) {
131    // Set the bundle url for the packager connection manually
132    NSURL *bundleURL = [bridge bundleURL];
133    NSString *packagerServerHostPort = [NSString stringWithFormat:@"%@:%@", bundleURL.host, bundleURL.port];
134    [[RCTPackagerConnection sharedPackagerConnection] reconnect:packagerServerHostPort];
135    RCTInspectorPackagerConnection *inspectorPackagerConnection = [RCTInspectorDevServerHelper connectWithBundleURL:bundleURL];
136
137    NSDictionary<NSString *, id> *buildProps = [self.manifest getPluginPropertiesWithPackageName:@"expo-build-properties"];
138    NSNumber *enableNetworkInterceptor = [[buildProps objectForKey:@"ios"] objectForKey:@"unstable_networkInspector"];
139    if (enableNetworkInterceptor == nil || [enableNetworkInterceptor boolValue] != NO) {
140      self.networkInterceptor = [[EXVersionedNetworkInterceptor alloc] initWithRCTInspectorPackagerConnection:inspectorPackagerConnection];
141    }
142  }
143
144  // Manually send a "start loading" notif, since the real one happened uselessly inside the RCTBatchedBridge constructor
145  [[NSNotificationCenter defaultCenter]
146   postNotificationName:RCTJavaScriptWillStartLoadingNotification object:bridge];
147}
148
149- (void)bridgeFinishedLoading:(id)bridge
150{
151  // Override the "Reload" button from Redbox to reload the app from manifest
152  // Keep in mind that it is possible this will return a EXDisabledRedBox
153  RCTRedBox *redBox = [self _moduleInstanceForBridge:bridge named:@"RedBox"];
154  [redBox setOverrideReloadAction:^{
155    [[NSNotificationCenter defaultCenter] postNotificationName:EX_UNVERSIONED(@"EXReloadActiveAppRequest") object:nil];
156  }];
157}
158
159- (void)invalidate {
160  self.networkInterceptor = nil;
161}
162
163#pragma mark - Dev menu
164
165- (NSDictionary<NSString *, NSString *> *)devMenuItemsForBridge:(id)bridge
166{
167  RCTDevSettings *devSettings = (RCTDevSettings *)[self _moduleInstanceForBridge:bridge named:@"DevSettings"];
168  BOOL isDevModeEnabled = [self _isDevModeEnabledForBridge:bridge];
169  NSMutableDictionary *items = [NSMutableDictionary new];
170
171  if (isDevModeEnabled) {
172    items[@"dev-inspector"] = @{
173      @"label": devSettings.isElementInspectorShown ? @"Hide Element Inspector" : @"Show Element Inspector",
174      @"isEnabled": @YES
175    };
176  } else {
177    items[@"dev-inspector"] = @{
178      @"label": @"Element Inspector Unavailable",
179      @"isEnabled": @NO
180    };
181  }
182
183  if ([self _isBridgeInspectable:bridge] && isDevModeEnabled) {
184    items[@"dev-remote-debug"] = @{
185      @"label": @"Open JS Debugger",
186      @"isEnabled": @YES
187    };
188  } else if (
189      [self.manifest.expoGoSDKVersion compare:@"49.0.0" options:NSNumericSearch] == NSOrderedAscending &&
190      devSettings.isRemoteDebuggingAvailable &&
191      isDevModeEnabled
192    ) {
193    items[@"dev-remote-debug"] = @{
194      @"label": (devSettings.isDebuggingRemotely) ? @"Stop Remote Debugging" : @"Debug Remote JS",
195      @"isEnabled": @YES
196    };
197  }
198
199  if (devSettings.isHotLoadingAvailable && isDevModeEnabled) {
200    items[@"dev-hmr"] = @{
201      @"label": (devSettings.isHotLoadingEnabled) ? @"Disable Fast Refresh" : @"Enable Fast Refresh",
202      @"isEnabled": @YES,
203    };
204  } else {
205    items[@"dev-hmr"] =  @{
206      @"label": @"Fast Refresh Unavailable",
207      @"isEnabled": @NO,
208      @"detail": @"Use the Reload button above to reload when in production mode. Switch back to development mode to use Fast Refresh."
209    };
210  }
211
212  id perfMonitor = [self _moduleInstanceForBridge:bridge named:@"PerfMonitor"];
213  if (perfMonitor && isDevModeEnabled) {
214    items[@"dev-perf-monitor"] = @{
215      @"label": devSettings.isPerfMonitorShown ? @"Hide Performance Monitor" : @"Show Performance Monitor",
216      @"isEnabled": @YES,
217    };
218  } else {
219    items[@"dev-perf-monitor"] = @{
220      @"label": @"Performance Monitor Unavailable",
221      @"isEnabled": @NO,
222    };
223  }
224
225  return items;
226}
227
228- (void)selectDevMenuItemWithKey:(NSString *)key onBridge:(id)bridge
229{
230  RCTAssertMainQueue();
231  RCTDevSettings *devSettings = (RCTDevSettings *)[self _moduleInstanceForBridge:bridge named:@"DevSettings"];
232  if ([key isEqualToString:@"dev-reload"]) {
233    // bridge could be an RCTBridge of any version and we need to cast it since ARC needs to know
234    // the return type
235    [(RCTBridgeHack *)bridge reload];
236  } else if ([key isEqualToString:@"dev-remote-debug"]) {
237    if ([self _isBridgeInspectable:bridge]) {
238      [self _openJsInspector:bridge];
239    } else {
240      devSettings.isDebuggingRemotely = !devSettings.isDebuggingRemotely;
241    }
242  } else if ([key isEqualToString:@"dev-profiler"]) {
243    devSettings.isProfilingEnabled = !devSettings.isProfilingEnabled;
244  } else if ([key isEqualToString:@"dev-hmr"]) {
245    devSettings.isHotLoadingEnabled = !devSettings.isHotLoadingEnabled;
246  } else if ([key isEqualToString:@"dev-inspector"]) {
247    [devSettings toggleElementInspector];
248  } else if ([key isEqualToString:@"dev-perf-monitor"]) {
249    id perfMonitor = [self _moduleInstanceForBridge:bridge named:@"PerfMonitor"];
250    if (perfMonitor) {
251      if (devSettings.isPerfMonitorShown) {
252        [perfMonitor hide];
253        devSettings.isPerfMonitorShown = NO;
254      } else {
255        [perfMonitor show];
256        devSettings.isPerfMonitorShown = YES;
257      }
258    }
259  }
260}
261
262- (void)showDevMenuForBridge:(id)bridge
263{
264  RCTAssertMainQueue();
265  id devMenu = [self _moduleInstanceForBridge:bridge named:@"DevMenu"];
266  // respondsToSelector: check is required because it's possible this bridge
267  // was instantiated with a `disabledDevMenu` instance and the gesture preference was recently updated.
268  if ([devMenu respondsToSelector:@selector(show)]) {
269    [((RCTDevMenu *)devMenu) show];
270  }
271}
272
273- (void)disableRemoteDebuggingForBridge:(id)bridge
274{
275  RCTDevSettings *devSettings = (RCTDevSettings *)[self _moduleInstanceForBridge:bridge named:@"DevSettings"];
276  devSettings.isDebuggingRemotely = NO;
277}
278
279- (void)toggleRemoteDebuggingForBridge:(id)bridge
280{
281  RCTDevSettings *devSettings = (RCTDevSettings *)[self _moduleInstanceForBridge:bridge named:@"DevSettings"];
282  devSettings.isDebuggingRemotely = !devSettings.isDebuggingRemotely;
283}
284
285- (void)togglePerformanceMonitorForBridge:(id)bridge
286{
287  RCTDevSettings *devSettings = (RCTDevSettings *)[self _moduleInstanceForBridge:bridge named:@"DevSettings"];
288  id perfMonitor = [self _moduleInstanceForBridge:bridge named:@"PerfMonitor"];
289  if (perfMonitor) {
290    if (devSettings.isPerfMonitorShown) {
291      [perfMonitor hide];
292      devSettings.isPerfMonitorShown = NO;
293    } else {
294      [perfMonitor show];
295      devSettings.isPerfMonitorShown = YES;
296    }
297  }
298}
299
300- (void)toggleElementInspectorForBridge:(id)bridge
301{
302  RCTDevSettings *devSettings = (RCTDevSettings *)[self _moduleInstanceForBridge:bridge named:@"DevSettings"];
303  [devSettings toggleElementInspector];
304}
305
306- (uint32_t)addWebSocketNotificationHandler:(void (^)(NSDictionary<NSString *, id> *))handler
307                                    queue:(dispatch_queue_t)queue
308                                forMethod:(NSString *)method
309{
310  return [[RCTPackagerConnection sharedPackagerConnection] addNotificationHandler:handler queue:queue forMethod:method];
311}
312
313#pragma mark - internal
314
315- (BOOL)_isDevModeEnabledForBridge:(id)bridge
316{
317  return ([RCTGetURLQueryParam([bridge bundleURL], @"dev") boolValue]);
318}
319
320- (BOOL)_isBridgeInspectable:(id)bridge
321{
322  return [[bridge batchedBridge] isInspectable];
323}
324
325- (void)_openJsInspector:(id)bridge
326{
327  NSInteger port = [[[bridge bundleURL] port] integerValue] ?: RCT_METRO_PORT;
328  NSString *host = [[bridge bundleURL] host] ?: @"localhost";
329  NSString *url =
330      [NSString stringWithFormat:@"http://%@:%lld/inspector?applicationId=%@", host, (long long)port, NSBundle.mainBundle.bundleIdentifier];
331  NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]];
332  request.HTTPMethod = @"PUT";
333  [[[NSURLSession sharedSession] dataTaskWithRequest:request] resume];
334}
335
336- (id<RCTBridgeModule>)_moduleInstanceForBridge:(id)bridge named:(NSString *)name
337{
338  return [bridge moduleForClass:[self getModuleClassFromName:[name UTF8String]]];
339}
340
341- (NSArray *)extraModulesForBridge:(id)bridge
342{
343  NSDictionary *params = _params;
344  NSDictionary *services = params[@"services"];
345  NSMutableArray *extraModules = [NSMutableArray new];
346
347  // add scoped modules
348  [extraModules addObjectsFromArray:[self _newScopedModulesForServices:services params:params]];
349
350  if (params[@"testEnvironment"]) {
351    EXTestEnvironment testEnvironment = (EXTestEnvironment)[params[@"testEnvironment"] unsignedIntegerValue];
352    if (testEnvironment != EXTestEnvironmentNone) {
353      EXTest *testModule = [[EXTest alloc] initWithEnvironment:testEnvironment];
354      [extraModules addObject:testModule];
355    }
356  }
357
358  if (params[@"browserModuleClass"]) {
359    Class browserModuleClass = params[@"browserModuleClass"];
360    id homeModule = [[browserModuleClass alloc] initWithExperienceStableLegacyId:self.manifest.stableLegacyId
361                                                                        scopeKey:self.manifest.scopeKey
362                                                                    easProjectId:self.manifest.easProjectId
363                                                           kernelServiceDelegate:services[EX_UNVERSIONED(@"EXHomeModuleManager")]
364                                                                          params:params];
365    [extraModules addObject:homeModule];
366  }
367
368  if (!RCTTurboModuleEnabled()) {
369    [extraModules addObject:[self getModuleInstanceFromClass:[self getModuleClassFromName:"DevSettings"]]];
370    id exceptionsManager = [self getModuleInstanceFromClass:RCTExceptionsManagerCls()];
371    if (exceptionsManager) {
372      [extraModules addObject:exceptionsManager];
373    }
374    [extraModules addObject:[self getModuleInstanceFromClass:[self getModuleClassFromName:"DevMenu"]]];
375    [extraModules addObject:[self getModuleInstanceFromClass:[self getModuleClassFromName:"RedBox"]]];
376    [extraModules addObject:[self getModuleInstanceFromClass:RNCAsyncStorage.class]];
377  }
378
379  return extraModules;
380}
381
382- (NSArray *)_newScopedModulesForServices:(NSDictionary *)services params:(NSDictionary *)params
383{
384  NSMutableArray *result = [NSMutableArray array];
385  NSDictionary<NSString *, NSDictionary *> *EXScopedModuleClasses = EXGetScopedModuleClasses();
386  if (EXScopedModuleClasses) {
387    [EXScopedModuleClasses enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull scopedModuleClassName, NSDictionary * _Nonnull kernelServiceClassNames, BOOL * _Nonnull stop) {
388      NSMutableDictionary *moduleServices = [[NSMutableDictionary alloc] init];
389      for (id kernelServiceClassName in kernelServiceClassNames) {
390        NSString *kernelSerivceName = kernelServiceClassNames[kernelServiceClassName];
391        id service = ([kernelSerivceName isEqualToString:EX_KERNEL_SERVICE_NONE]) ? [NSNull null] : services[kernelSerivceName];
392        moduleServices[kernelServiceClassName] = service;
393      }
394
395      id scopedModule;
396      Class scopedModuleClass = NSClassFromString(scopedModuleClassName);
397      if (moduleServices.count > 1) {
398        scopedModule = [[scopedModuleClass alloc] initWithExperienceStableLegacyId:self.manifest.stableLegacyId
399                                                                          scopeKey:self.manifest.scopeKey
400                                                                      easProjectId:self.manifest.easProjectId
401                                                            kernelServiceDelegates:moduleServices
402                                                                            params:params];
403      } else if (moduleServices.count == 0) {
404        scopedModule = [[scopedModuleClass alloc] initWithExperienceStableLegacyId:self.manifest.stableLegacyId
405                                                                          scopeKey:self.manifest.scopeKey
406                                                                      easProjectId:self.manifest.easProjectId
407                                                             kernelServiceDelegate:nil
408                                                                            params:params];
409      } else {
410        scopedModule = [[scopedModuleClass alloc] initWithExperienceStableLegacyId:self.manifest.stableLegacyId
411                                                                          scopeKey:self.manifest.scopeKey
412                                                                      easProjectId:self.manifest.easProjectId
413                                                             kernelServiceDelegate:moduleServices[[moduleServices allKeys][0]]
414                                                                            params:params];
415      }
416
417      if (scopedModule) {
418        [result addObject:scopedModule];
419      }
420    }];
421  }
422  return result;
423}
424
425- (Class)getModuleClassFromName:(const char *)name
426{
427  if (strcmp(name, "DevSettings") == 0) {
428    return EXDevSettings.class;
429  }
430  if (strcmp(name, "DevMenu") == 0) {
431    if (![_params[@"isStandardDevMenuAllowed"] boolValue] || ![_params[@"isDeveloper"] boolValue]) {
432      // non-kernel, or non-development kernel, uses expo menu instead of RCTDevMenu
433      return EXDisabledDevMenu.class;
434    }
435  }
436  if (strcmp(name, "RedBox") == 0) {
437    if (![_params[@"isDeveloper"] boolValue]) {
438      // user-facing (not debugging).
439      // additionally disable RCTRedBox
440      return EXDisabledRedBox.class;
441    }
442  }
443  return RCTCoreModulesClassProvider(name);
444}
445
446- (id<RCTTurboModule>)getModuleInstanceFromClass:(Class)moduleClass
447{
448  // Standard
449  if (moduleClass == RCTImageLoader.class) {
450    return [[moduleClass alloc] initWithRedirectDelegate:nil loadersProvider:^NSArray<id<RCTImageURLLoader>> *(RCTModuleRegistry *) {
451      return @[[RCTLocalAssetImageLoader new], [EXMediaLibraryImageLoader new]];
452    } decodersProvider:^NSArray<id<RCTImageDataDecoder>> *(RCTModuleRegistry *) {
453      return @[[RCTGIFImageDecoder new]];
454    }];
455  } else if (moduleClass == RCTNetworking.class) {
456    return [[moduleClass alloc] initWithHandlersProvider:^NSArray<id<RCTURLRequestHandler>> *(RCTModuleRegistry *) {
457      return @[
458        [RCTHTTPRequestHandler new],
459        [RCTDataRequestHandler new],
460        [RCTFileRequestHandler new],
461      ];
462    }];
463  }
464
465  // Expo-specific
466  if (moduleClass == EXDevSettings.class) {
467    BOOL isDevelopment = ![self _isOpeningHomeInProductionMode] && [_params[@"isDeveloper"] boolValue];
468    return [[moduleClass alloc] initWithScopeKey:self.manifest.scopeKey isDevelopment:isDevelopment];
469  } else if (moduleClass == RCTExceptionsManagerCls()) {
470    id exceptionsManagerDelegate = _params[@"exceptionsManagerDelegate"];
471    if (exceptionsManagerDelegate) {
472      return [[moduleClass alloc] initWithDelegate:exceptionsManagerDelegate];
473    } else {
474      RCTLogWarn(@"No exceptions manager provided when building extra modules for bridge.");
475    }
476  } else if (moduleClass == RNCAsyncStorage.class) {
477    NSString *documentDirectory;
478    if (_params[@"fileSystemDirectories"]) {
479      documentDirectory = _params[@"fileSystemDirectories"][@"documentDirectory"];
480    } else {
481      NSArray<NSString *> *documentPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
482      documentDirectory = [documentPaths objectAtIndex:0];
483    }
484    NSString *localStorageDirectory = [documentDirectory stringByAppendingPathComponent:EX_UNVERSIONED(@"RCTAsyncLocalStorage")];
485    return [[moduleClass alloc] initWithStorageDirectory:localStorageDirectory];
486  }
487
488  return [moduleClass new];
489}
490
491- (BOOL)_isOpeningHomeInProductionMode
492{
493  return _params[@"browserModuleClass"] && !self.manifest.developer;
494}
495
496- (void *)versionedJsExecutorFactoryForBridge:(nonnull RCTBridge *)bridge
497{
498  return [EXVersionUtils versionedJsExecutorFactoryForBridge:bridge engine:_manifest.jsEngine];
499}
500
501@end
502