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