/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import #import #import #import #import #import #import #import #import "CoreModulesPlugins.h" #if RCT_DEV_MENU #if RCT_ENABLE_INSPECTOR #import #endif NSString *const RCTShowDevMenuNotification = @"RCTShowDevMenuNotification"; @implementation UIWindow (RCTDevMenu) - (void)RCT_motionEnded:(__unused UIEventSubtype)motion withEvent:(UIEvent *)event { if (event.subtype == UIEventSubtypeMotionShake) { [[NSNotificationCenter defaultCenter] postNotificationName:RCTShowDevMenuNotification object:nil]; } } @end @implementation RCTDevMenuItem { RCTDevMenuItemTitleBlock _titleBlock; dispatch_block_t _handler; } - (instancetype)initWithTitleBlock:(RCTDevMenuItemTitleBlock)titleBlock handler:(dispatch_block_t)handler { if ((self = [super init])) { _titleBlock = [titleBlock copy]; _handler = [handler copy]; } return self; } RCT_NOT_IMPLEMENTED(-(instancetype)init) + (instancetype)buttonItemWithTitleBlock:(NSString * (^)(void))titleBlock handler:(dispatch_block_t)handler { return [[self alloc] initWithTitleBlock:titleBlock handler:handler]; } + (instancetype)buttonItemWithTitle:(NSString *)title handler:(dispatch_block_t)handler { return [[self alloc] initWithTitleBlock:^NSString * { return title; } handler:handler]; } - (void)callHandler { if (_handler) { _handler(); } } - (NSString *)title { if (_titleBlock) { return _titleBlock(); } return nil; } @end typedef void (^RCTDevMenuAlertActionHandler)(UIAlertAction *action); @interface RCTDevMenu () @end @implementation RCTDevMenu { UIAlertController *_actionSheet; NSMutableArray *_extraMenuItems; } @synthesize bridge = _bridge; @synthesize moduleRegistry = _moduleRegistry; @synthesize callableJSModules = _callableJSModules; @synthesize bundleManager = _bundleManager; RCT_EXPORT_MODULE() + (void)initialize { // We're swizzling here because it's poor form to override methods in a category, // however UIWindow doesn't actually implement motionEnded:withEvent:, so there's // no need to call the original implementation. RCTSwapInstanceMethods([UIWindow class], @selector(motionEnded:withEvent:), @selector(RCT_motionEnded:withEvent:)); } + (BOOL)requiresMainQueueSetup { return YES; } - (instancetype)init { if ((self = [super init])) { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(showOnShake) name:RCTShowDevMenuNotification object:nil]; _extraMenuItems = [NSMutableArray new]; [self registerHotkeys]; } return self; } - (void)registerHotkeys { #if TARGET_OS_SIMULATOR || TARGET_OS_MACCATALYST RCTKeyCommands *commands = [RCTKeyCommands sharedInstance]; __weak __typeof(self) weakSelf = self; // Toggle debug menu [commands registerKeyCommandWithInput:@"d" modifierFlags:UIKeyModifierCommand action:^(__unused UIKeyCommand *command) { [weakSelf toggle]; }]; // Toggle element inspector [commands registerKeyCommandWithInput:@"i" modifierFlags:UIKeyModifierCommand action:^(__unused UIKeyCommand *command) { [(RCTDevSettings *)[weakSelf.moduleRegistry moduleForName:"DevSettings"] toggleElementInspector]; }]; // Reload in normal mode [commands registerKeyCommandWithInput:@"n" modifierFlags:UIKeyModifierCommand action:^(__unused UIKeyCommand *command) { [(RCTDevSettings *)[weakSelf.moduleRegistry moduleForName:"DevSettings"] setIsDebuggingRemotely:NO]; }]; #endif } - (void)unregisterHotkeys { #if TARGET_OS_SIMULATOR || TARGET_OS_MACCATALYST RCTKeyCommands *commands = [RCTKeyCommands sharedInstance]; [commands unregisterKeyCommandWithInput:@"d" modifierFlags:UIKeyModifierCommand]; [commands unregisterKeyCommandWithInput:@"i" modifierFlags:UIKeyModifierCommand]; [commands unregisterKeyCommandWithInput:@"n" modifierFlags:UIKeyModifierCommand]; #endif } - (BOOL)isHotkeysRegistered { #if TARGET_OS_SIMULATOR || TARGET_OS_MACCATALYST RCTKeyCommands *commands = [RCTKeyCommands sharedInstance]; return [commands isKeyCommandRegisteredForInput:@"d" modifierFlags:UIKeyModifierCommand] && [commands isKeyCommandRegisteredForInput:@"i" modifierFlags:UIKeyModifierCommand] && [commands isKeyCommandRegisteredForInput:@"n" modifierFlags:UIKeyModifierCommand]; #else return NO; #endif } - (dispatch_queue_t)methodQueue { return dispatch_get_main_queue(); } - (void)invalidate { _presentedItems = nil; [_actionSheet dismissViewControllerAnimated:YES completion:^(void){ }]; } - (void)showOnShake { if ([((RCTDevSettings *)[_moduleRegistry moduleForName:"DevSettings"]) isShakeToShowDevMenuEnabled]) { for (UIWindow *window in [RCTSharedApplication() windows]) { NSString *recursiveDescription = [window valueForKey:@"recursiveDescription"]; if ([recursiveDescription containsString:@"RCTView"]) { [self show]; return; } } } } - (void)toggle { if (_actionSheet) { [_actionSheet dismissViewControllerAnimated:YES completion:^(void){ }]; _actionSheet = nil; } else { [self show]; } } - (BOOL)isActionSheetShown { return _actionSheet != nil; } - (void)addItem:(NSString *)title handler:(void (^)(void))handler { [self addItem:[RCTDevMenuItem buttonItemWithTitle:title handler:handler]]; } - (void)addItem:(RCTDevMenuItem *)item { [_extraMenuItems addObject:item]; } - (void)setDefaultJSBundle { [[RCTBundleURLProvider sharedSettings] resetToDefaults]; self->_bundleManager.bundleURL = [[RCTBundleURLProvider sharedSettings] jsBundleURLForFallbackExtension:nil]; RCTTriggerReloadCommandListeners(@"Dev menu - reset to default"); } - (NSArray *)_menuItemsToPresent { NSMutableArray *items = [NSMutableArray new]; // Add built-in items __weak RCTDevSettings *devSettings = [_moduleRegistry moduleForName:"DevSettings"]; __weak RCTDevMenu *weakSelf = self; __weak RCTBundleManager *bundleManager = _bundleManager; [items addObject:[RCTDevMenuItem buttonItemWithTitle:@"Reload" handler:^{ RCTTriggerReloadCommandListeners(@"Dev menu - reload"); }]]; if (!devSettings.isProfilingEnabled) { #if RCT_ENABLE_INSPECTOR if (devSettings.isDeviceDebuggingAvailable) { // For on-device debugging we link out to Flipper. // Since we're assuming Flipper is available, also include the DevTools. // Note: For parity with the Android code. [items addObject:[RCTDevMenuItem buttonItemWithTitleBlock:^NSString * { return @"Open Debugger"; } handler:^{ [RCTInspectorDevServerHelper openURL:@"flipper://null/Hermesdebuggerrn?device=React%20Native" withBundleURL:bundleManager.bundleURL withErrorMessage:@"Failed to open Flipper. Please check that Metro is running."]; }]]; [items addObject:[RCTDevMenuItem buttonItemWithTitleBlock:^NSString * { return @"Open React DevTools"; } handler:^{ [RCTInspectorDevServerHelper openURL:@"flipper://null/React?device=React%20Native" withBundleURL:bundleManager.bundleURL withErrorMessage:@"Failed to open Flipper. Please check that Metro is running."]; }]]; } else if (devSettings.isRemoteDebuggingAvailable) { #else if (devSettings.isRemoteDebuggingAvailable) { #endif // For remote debugging, we open up Chrome running the app in a web worker. // Note that this requires async communication, which will not work for Turbo Modules. [items addObject:[RCTDevMenuItem buttonItemWithTitleBlock:^NSString * { return devSettings.isDebuggingRemotely ? @"Stop Debugging" : @"Debug with Chrome"; } handler:^{ devSettings.isDebuggingRemotely = !devSettings.isDebuggingRemotely; }]]; } else { // If neither are available, we're defaulting to a message that tells you about remote debugging. [items addObject:[RCTDevMenuItem buttonItemWithTitle:@"Debugger Unavailable" handler:^{ NSString *message = RCTTurboModuleEnabled() ? @"Debugging with Chrome is not supported when TurboModules are enabled." : @"Include the RCTWebSocket library to enable JavaScript debugging."; UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Debugger Unavailable" message:message preferredStyle:UIAlertControllerStyleAlert]; __weak __typeof__(alertController) weakAlertController = alertController; [alertController addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(__unused UIAlertAction *action) { [weakAlertController dismissViewControllerAnimated:YES completion:nil]; }]]; [RCTPresentedViewController() presentViewController:alertController animated:YES completion:NULL]; }]]; } } [items addObject:[RCTDevMenuItem buttonItemWithTitleBlock:^NSString * { return devSettings.isElementInspectorShown ? @"Hide Inspector" : @"Show Inspector"; } handler:^{ [devSettings toggleElementInspector]; }]]; if (devSettings.isHotLoadingAvailable) { [items addObject:[RCTDevMenuItem buttonItemWithTitleBlock:^NSString * { // Previously known as "Hot Reloading". We won't use this term anymore. return devSettings.isHotLoadingEnabled ? @"Disable Fast Refresh" : @"Enable Fast Refresh"; } handler:^{ devSettings.isHotLoadingEnabled = !devSettings.isHotLoadingEnabled; }]]; } [items addObject:[RCTDevMenuItem buttonItemWithTitleBlock:^NSString * { return @"Configure Bundler"; } handler:^{ UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Configure Bundler" message:@"Provide a custom bundler address, port, and entrypoint." preferredStyle:UIAlertControllerStyleAlert]; [alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) { textField.placeholder = @"0.0.0.0"; }]; [alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) { textField.placeholder = @"8081"; }]; [alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) { textField.placeholder = @"index"; }]; [alertController addAction:[UIAlertAction actionWithTitle:@"Apply Changes" style:UIAlertActionStyleDefault handler:^(__unused UIAlertAction *action) { NSArray *textfields = alertController.textFields; UITextField *ipTextField = textfields[0]; UITextField *portTextField = textfields[1]; UITextField *bundleRootTextField = textfields[2]; NSString *bundleRoot = bundleRootTextField.text; if (ipTextField.text.length == 0 && portTextField.text.length == 0) { [weakSelf setDefaultJSBundle]; return; } NSNumberFormatter *formatter = [NSNumberFormatter new]; formatter.numberStyle = NSNumberFormatterDecimalStyle; NSNumber *portNumber = [formatter numberFromString:portTextField.text]; if (portNumber == nil) { portNumber = [NSNumber numberWithInt:RCT_METRO_PORT]; } [RCTBundleURLProvider sharedSettings].jsLocation = [NSString stringWithFormat:@"%@:%d", ipTextField.text, portNumber.intValue]; if (bundleRoot.length == 0) { [bundleManager resetBundleURL]; } else { bundleManager.bundleURL = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:bundleRoot]; } RCTTriggerReloadCommandListeners(@"Dev menu - apply changes"); }]]; [alertController addAction:[UIAlertAction actionWithTitle:@"Reset to Default" style:UIAlertActionStyleDefault handler:^(__unused UIAlertAction *action) { [weakSelf setDefaultJSBundle]; }]]; [alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(__unused UIAlertAction *action) { return; }]]; [RCTPresentedViewController() presentViewController:alertController animated:YES completion:NULL]; }]]; [items addObjectsFromArray:_extraMenuItems]; return items; } RCT_EXPORT_METHOD(show) { if (_actionSheet || RCTRunningInAppExtension()) { return; } NSString *bridgeDescription = _bridge.bridgeDescription; NSString *description = bridgeDescription.length > 0 ? [NSString stringWithFormat:@"Running %@", bridgeDescription] : nil; // On larger devices we don't have an anchor point for the action sheet UIAlertControllerStyle style = [[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone ? UIAlertControllerStyleActionSheet : UIAlertControllerStyleAlert; NSString *devMenuType = self.bridge ? @"Bridge" : @"Bridgeless"; NSString *devMenuTitle = [NSString stringWithFormat:@"React Native Dev Menu (%@)", devMenuType]; _actionSheet = [UIAlertController alertControllerWithTitle:devMenuTitle message:description preferredStyle:style]; NSArray *items = [self _menuItemsToPresent]; for (RCTDevMenuItem *item in items) { [_actionSheet addAction:[UIAlertAction actionWithTitle:item.title style:UIAlertActionStyleDefault handler:[self alertActionHandlerForDevItem:item]]]; } [_actionSheet addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:[self alertActionHandlerForDevItem:nil]]]; _presentedItems = items; [RCTPresentedViewController() presentViewController:_actionSheet animated:YES completion:nil]; [_callableJSModules invokeModule:@"RCTNativeAppEventEmitter" method:@"emit" withArgs:@[ @"RCTDevMenuShown" ]]; } - (RCTDevMenuAlertActionHandler)alertActionHandlerForDevItem:(RCTDevMenuItem *__nullable)item { return ^(__unused UIAlertAction *action) { if (item) { [item callHandler]; } self->_actionSheet = nil; }; } #pragma mark - deprecated methods and properties #define WARN_DEPRECATED_DEV_MENU_EXPORT() \ RCTLogWarn(@"Using deprecated method %s, use RCTDevSettings instead", __func__) - (void)setShakeToShow:(BOOL)shakeToShow { ((RCTDevSettings *)[_moduleRegistry moduleForName:"DevSettings"]).isShakeToShowDevMenuEnabled = shakeToShow; } - (BOOL)shakeToShow { return ((RCTDevSettings *)[_moduleRegistry moduleForName:"DevSettings"]).isShakeToShowDevMenuEnabled; } RCT_EXPORT_METHOD(reload) { WARN_DEPRECATED_DEV_MENU_EXPORT(); RCTTriggerReloadCommandListeners(@"Unknown from JS"); } RCT_EXPORT_METHOD(debugRemotely : (BOOL)enableDebug) { WARN_DEPRECATED_DEV_MENU_EXPORT(); ((RCTDevSettings *)[_moduleRegistry moduleForName:"DevSettings"]).isDebuggingRemotely = enableDebug; } RCT_EXPORT_METHOD(setProfilingEnabled : (BOOL)enabled) { WARN_DEPRECATED_DEV_MENU_EXPORT(); ((RCTDevSettings *)[_moduleRegistry moduleForName:"DevSettings"]).isProfilingEnabled = enabled; } - (BOOL)profilingEnabled { return ((RCTDevSettings *)[_moduleRegistry moduleForName:"DevSettings"]).isProfilingEnabled; } RCT_EXPORT_METHOD(setHotLoadingEnabled : (BOOL)enabled) { WARN_DEPRECATED_DEV_MENU_EXPORT(); ((RCTDevSettings *)[_moduleRegistry moduleForName:"DevSettings"]).isHotLoadingEnabled = enabled; } - (BOOL)hotLoadingEnabled { return ((RCTDevSettings *)[_moduleRegistry moduleForName:"DevSettings"]).isHotLoadingEnabled; } - (void)setHotkeysEnabled:(BOOL)enabled { if (enabled) { [self registerHotkeys]; } else { [self unregisterHotkeys]; } } - (BOOL)hotkeysEnabled { return [self isHotkeysRegistered]; } - (std::shared_ptr)getTurboModule: (const facebook::react::ObjCTurboModule::InitParams &)params { return std::make_shared(params); } @end #else // Unavailable when not in dev mode @interface RCTDevMenu () @end @implementation RCTDevMenu - (void)show { } - (void)reload { } - (void)addItem:(NSString *)title handler:(dispatch_block_t)handler { } - (void)addItem:(RCTDevMenu *)item { } - (void)debugRemotely:(BOOL)enableDebug { } - (BOOL)isActionSheetShown { return NO; } + (NSString *)moduleName { return @"DevMenu"; } - (std::shared_ptr)getTurboModule: (const facebook::react::ObjCTurboModule::InitParams &)params { return std::make_shared(params); } @end @implementation RCTDevMenuItem + (instancetype)buttonItemWithTitle:(NSString *)title handler:(void (^)(void))handler { return nil; } + (instancetype)buttonItemWithTitleBlock:(NSString * (^)(void))titleBlock handler:(void (^)(void))handler { return nil; } @end #endif @implementation RCTBridge (RCTDevMenu) - (RCTDevMenu *)devMenu { #if RCT_DEV_MENU return [self moduleForClass:[RCTDevMenu class]]; #else return nil; #endif } @end Class RCTDevMenuCls(void) { return RCTDevMenu.class; }