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