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