From cc068b055185e6fb7341bf945f69a74ed3ef4814 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Tue, 17 Sep 2019 06:36:17 -0700 Subject: [PATCH] Export the DevSettings module, add `addMenuItem` method (#25848) Summary: I wanted to configure the RN dev menu without having to write native code. This is pretty useful in a greenfield app since it avoids having to write a custom native module for both platforms (and might enable the feature for expo too). This ended up a bit more involved than planned since callbacks can only be called once. I needed to convert the `DevSettings` module to a `NativeEventEmitter` and use events when buttons are clicked. This means creating a JS wrapper for it. Currently it does not export all methods, they can be added in follow ups as needed. ## Changelog [General] [Added] - Export the DevSettings module, add `addMenuItem` method Pull Request resolved: https://github.com/facebook/react-native/pull/25848 Test Plan: Tested in an app using the following code. ```js if (__DEV__) { DevSettings.addMenuItem('Show Dev Screen', () => { dispatchNavigationAction( NavigationActions.navigate({ routeName: 'dev', }), ); }); } ``` Added an example in RN tester ![devmenu](https://user-images.githubusercontent.com/2677334/62000297-71624680-b0a1-11e9-8403-bc95c4747f0c.gif) Differential Revision: D17394916 Pulled By: cpojer fbshipit-source-id: f9d2c548b09821c594189d1436a27b97cf5a5737 --- .../FBReactNativeSpec-generated.mm | 7 +++ .../FBReactNativeSpec/FBReactNativeSpec.h | 1 + .../NativeModules/specs/NativeDevSettings.js | 1 + Libraries/Utilities/DevSettings.js | 52 +++++++++++++++++++ .../DevSettings/DevSettingsExample.js | 47 +++++++++++++++++ RNTester/js/utils/RNTesterList.android.js | 4 ++ RNTester/js/utils/RNTesterList.ios.js | 4 ++ React/Modules/RCTDevSettings.h | 3 +- React/Modules/RCTDevSettings.mm | 46 ++++++++++------ .../react/testing/ReactSettingsForTests.java | 3 ++ .../facebook/react/CoreModulesPackage.java | 2 +- .../react/devsupport/DevInternalSettings.java | 5 ++ .../modules/debug/DevSettingsModule.java | 30 +++++++++-- .../debug/interfaces/DeveloperSettings.java | 3 ++ index.js | 4 ++ 15 files changed, 190 insertions(+), 22 deletions(-) create mode 100644 Libraries/Utilities/DevSettings.js create mode 100644 RNTester/js/examples/DevSettings/DevSettingsExample.js diff --git a/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec-generated.mm b/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec-generated.mm index 0c086448d40cc4..a94e7cfc4f6efa 100644 --- a/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec-generated.mm +++ b/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec-generated.mm @@ -801,6 +801,10 @@ + (RCTManagedPointer *)JS_NativeAsyncStorage_SpecGetAllKeysCallbackError:(id)jso return static_cast(turboModule).invokeObjCMethod(rt, VoidKind, "toggleElementInspector", @selector(toggleElementInspector), args, count); } + static facebook::jsi::Value __hostFunction_NativeDevSettingsSpecJSI_addMenuItem(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeObjCMethod(rt, VoidKind, "addMenuItem", @selector(addMenuItem:), args, count); + } + static facebook::jsi::Value __hostFunction_NativeDevSettingsSpecJSI_setIsShakeToShowDevMenuEnabled(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { return static_cast(turboModule).invokeObjCMethod(rt, VoidKind, "setIsShakeToShowDevMenuEnabled", @selector(setIsShakeToShowDevMenuEnabled:), args, count); } @@ -824,6 +828,9 @@ + (RCTManagedPointer *)JS_NativeAsyncStorage_SpecGetAllKeysCallbackError:(id)jso methodMap_["toggleElementInspector"] = MethodMetadata {0, __hostFunction_NativeDevSettingsSpecJSI_toggleElementInspector}; + methodMap_["addMenuItem"] = MethodMetadata {1, __hostFunction_NativeDevSettingsSpecJSI_addMenuItem}; + + methodMap_["setIsShakeToShowDevMenuEnabled"] = MethodMetadata {1, __hostFunction_NativeDevSettingsSpecJSI_setIsShakeToShowDevMenuEnabled}; diff --git a/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec.h b/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec.h index 16fe9ab45456da..860b261a615ac9 100644 --- a/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec.h +++ b/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec.h @@ -743,6 +743,7 @@ namespace facebook { - (void)setIsDebuggingRemotely:(BOOL)isDebuggingRemotelyEnabled; - (void)setProfilingEnabled:(BOOL)isProfilingEnabled; - (void)toggleElementInspector; +- (void)addMenuItem:(NSString *)title; - (void)setIsShakeToShowDevMenuEnabled:(BOOL)enabled; @end diff --git a/Libraries/NativeModules/specs/NativeDevSettings.js b/Libraries/NativeModules/specs/NativeDevSettings.js index 9741595601a74d..3a2df4cb455946 100644 --- a/Libraries/NativeModules/specs/NativeDevSettings.js +++ b/Libraries/NativeModules/specs/NativeDevSettings.js @@ -19,6 +19,7 @@ export interface Spec extends TurboModule { +setIsDebuggingRemotely: (isDebuggingRemotelyEnabled: boolean) => void; +setProfilingEnabled: (isProfilingEnabled: boolean) => void; +toggleElementInspector: () => void; + +addMenuItem: (title: string) => void; // iOS only. +setIsShakeToShowDevMenuEnabled: (enabled: boolean) => void; diff --git a/Libraries/Utilities/DevSettings.js b/Libraries/Utilities/DevSettings.js new file mode 100644 index 00000000000000..f88489c75b2dd7 --- /dev/null +++ b/Libraries/Utilities/DevSettings.js @@ -0,0 +1,52 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * @format + */ + +import NativeDevSettings from '../NativeModules/specs/NativeDevSettings'; +import NativeEventEmitter from '../EventEmitter/NativeEventEmitter'; + +class DevSettings extends NativeEventEmitter { + _menuItems: Map mixed>; + + constructor() { + super(NativeDevSettings); + + this._menuItems = new Map(); + } + + addMenuItem(title: string, handler: () => mixed) { + // Make sure items are not added multiple times. This can + // happen when hot reloading the module that registers the + // menu items. The title is used as the id which means we + // don't support multiple items with the same name. + const oldHandler = this._menuItems.get(title); + if (oldHandler != null) { + this.removeListener('didPressMenuItem', oldHandler); + } else { + NativeDevSettings.addMenuItem(title); + } + + this._menuItems.set(title, handler); + this.addListener('didPressMenuItem', event => { + if (event.title === title) { + handler(); + } + }); + } + + reload() { + NativeDevSettings.reload(); + } + + // TODO: Add other dev setting methods exposed by the native module. +} + +// Avoid including the full `NativeDevSettings` class in prod. +class NoopDevSettings { + addMenuItem(title: string, handler: () => mixed) {} + reload() {} +} + +module.exports = __DEV__ ? new DevSettings() : new NoopDevSettings(); diff --git a/RNTester/js/examples/DevSettings/DevSettingsExample.js b/RNTester/js/examples/DevSettings/DevSettingsExample.js new file mode 100644 index 00000000000000..4428c8fa0e52eb --- /dev/null +++ b/RNTester/js/examples/DevSettings/DevSettingsExample.js @@ -0,0 +1,47 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow + */ + +'use strict'; + +import * as React from 'react'; +import {Alert, Button, DevSettings} from 'react-native'; + +exports.title = 'DevSettings'; +exports.description = 'Customize the development settings'; +exports.examples = [ + { + title: 'Add dev menu item', + render(): React.Element { + return ( +