xref: /expo/packages/expo-dev-menu/README.md (revision efd75dec)
1# �� expo-dev-menu
2
3Expo/React Native module with the developer menu.
4
5# ⚙️ Installation
6
7Firstly, you need to add the `expo-dev-menu` package to your project.
8
9<details>
10<summary>yarn</summary>
11
12```bash
13yarn add expo-dev-menu
14```
15
16</details>
17
18<details>
19<summary>npm</summary>
20
21```bash
22npm install expo-dev-menu
23```
24
25</details>
26
27Then you can start to configure the native projects using steps below.
28
29## �� Android
30
311.  Set up the `DevMenuManager` in the native code.
32
33    You can do it in two ways. We recommend using the basic initialization. However, if you have the custom activity in your application, then the advanced one will be more suitable for you.
34
35    - **Basic**
36
37      Open the `MainActivity.java` or `MainActivity.kt` and make sure that your main activity class extends the `DevMenuAwareReactActivity`.
38
39        <details>
40        <summary>Java</summary>
41
42      ```java
43      ...
44      // You need to import the `DevMenuAwareReactActivity` class
45      import expo.modules.devmenu.react.DevMenuAwareReactActivity;
46      ...
47
48      // Make sure that the `MainActivity` extends the `DevMenuAwareReactActivity` class not the `ReactActivity`
49      public class MainActivity extends DevMenuAwareReactActivity {
50        ...
51      }
52      ```
53
54        </details>
55        <details>
56        <summary>Kotlin</summary>
57
58      ```kotlin
59      ...
60      // You need to import the `DevMenuAwareReactActivity` class
61      import expo.modules.devmenu.react.DevMenuAwareReactActivity;
62      ...
63
64      // Make sure that the `MainActivity` extends the `DevMenuAwareReactActivity` class not the `ReactActivity`
65      class MainActivity : DevMenuAwareReactActivity() {
66        ...
67      }
68      ```
69
70      </details>
71
72      <br/>
73
74    - **Advanced**
75
76
77      I. Open the file with the main activity of your application (`MainActivity.java` or `MainActivity.kt`) and add methods that will communicate with the `DevMenuManager`.
78
79      <details>
80      <summary>Java</summary>
81
82      ```java
83      ...
84      // Add those imports.
85      import android.view.KeyEvent;
86      import android.view.MotionEvent;
87
88      import expo.modules.devmenu.DevMenuManager;
89      ...
90
91      public class MainActivity extends ReactActivity {
92        ...
93        // A function which sends the touch events to the dev menu.
94        @Override
95        public boolean dispatchTouchEvent(MotionEvent ev) {
96          DevMenuManager.INSTANCE.onTouchEvent(ev);
97          return super.dispatchTouchEvent(ev);
98        }
99
100        // A function which handles the key commands.
101        @Override
102        public boolean onKeyUp(int keyCode, KeyEvent event) {
103          return DevMenuManager.INSTANCE.onKeyEvent(keyCode, event) || super.onKeyUp(keyCode, event);
104        }
105      }
106      ```
107
108      </details>
109
110      <details>
111      <summary>Kotlin</summary>
112
113      ```kotlin
114      ...
115      // Add those imports.
116      import android.view.KeyEvent;
117      import android.view.MotionEvent;
118
119      import expo.modules.devmenu.DevMenuManager;
120      ...
121
122      class MainActivity : ReactActivity() {
123        ...
124        // A function which sends the touch events to the dev menu.
125        override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
126          DevMenuManager.onTouchEvent(ev)
127          return super.dispatchTouchEvent(ev)
128        }
129
130        // A function which handles the key commands.
131        override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
132          return DevMenuManager.onKeyEvent(keyCode, event) || super.onKeyUp(keyCode, event)
133        }
134      }
135      ```
136
137      </details>
138
139
140      <br/>
141
142      II. Open the `MainApplication` class (`MainApplication.java` or `MainApplication.kt`) and in `onCreate` method initialize `DevMenuManager`.
143
144      <details>
145      <summary>Java</summary>
146
147      ```java
148      ...
149      public class MainApplication extends Application implements ReactApplication {
150        ...
151        @Override
152        public void onCreate() {
153          ...
154          DevMenuManager.INSTANCE.initializeWithReactNativeHost(getReactNativeHost());
155        }
156      }
157      ```
158
159      </details>
160
161
162      <details>
163      <summary>Kotlin</summary>
164
165      ```kotlin
166      ...
167      public class MainApplication : Application(), ReactApplication {
168        ...
169        override fun onCreate() {
170          ...
171          DevMenuManager.initializeWithReactNativeHost(reactNativeHost);
172        }
173      }
174      ```
175
176      </details>
177
178## �� iOS
179
1801. Add `expo-dev-menu` to your Podfile.
181
182
183      ```ruby
184      ...
185      target '<your app>' do
186        ...
187        pod 'EXDevMenu', path: '../node_modules/expo-dev-menu', :configurations => :debug
188        ...
189      end
190      ```
191
1922. Run `pod install` in `ios` directory.
1933. Open file with your `AppDelegate` (`AppDelegate.m` or `AppDelegate.swift`) and pass bridge to the `DevMenuManager`.
194
195      <details>
196      <summary>Objective-C</summary>
197
198   ```objc
199   ...
200   // Firstly, you need to import EXDevMenu package.
201   #if defined(EX_DEV_MENU_ENABLED)
202   @import EXDevMenu;
203   #endif
204   ...
205
206   @implementation AppDelegate
207   ...
208   - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
209   {
210     ...
211     RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];
212     RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
213                                                      moduleName:@"devMenuDemo"
214                                               initialProperties:nil];
215     // Add those lines only if you aren't using the dev-launcher.
216     #if defined(EX_DEV_MENU_ENABLED)
217     [DevMenuManager configureWithBridge:bridge];
218     #endif
219   }
220   @end
221   ```
222
223      </details>
224
225      <details>
226      <summary>Swift</summary>
227
228   ```swift
229   ...
230   // Firstly, you need to import EXDevMenu package.
231   #if EX_DEV_MENU_ENABLED
232   @import EXDevMenu
233   #endif
234   ...
235
236   @UIApplicationMain
237   class AppDelegate: UMAppDelegateWrapper {
238     override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
239       ...
240
241       if let bridge = RCTBridge(delegate: self, launchOptions: launchOptions) {
242         ...
243         // Add those lines.
244         #if EX_DEV_MENU_ENABLED
245         DevMenuManager.configure(withBridge: bridge)
246         #endif
247       }
248
249       ...
250     }
251   }
252   ```
253
254      </details>
255
256# �� Accessing the Developer Menu
257
258You can access the developer menu by shaking your device, making a three-finger long-press gesture, or by selecting "Shake Gesture" inside the Hardware menu in the iOS simulator. You can also use keyboard shortcuts - `⌘D` on iOS, or `⌘M` on Android when you're using Mac OS and `Ctrl+M` on Windows and Linux. Alternatively for Android, you can run the command `adb shell input keyevent 82` to open the dev menu (`82` being the Menu key code).
259
260> **Note:** if you're using the iOS simulator and keyboard shortcuts don't work, make sure you've selected `Send Keyboard Input to Device` inside the `I/O` menu in the Simulator.
261
262# �� Extending the dev-menu's functionalities
263
264One of the main purposes of this package was to provide an easy way to create extensions. We know that developing a React Native app can be painful - often, developers need to create additional tools, which for example, clear the local storage, to test or debug their applications. Some of their work can be integrated with the application itself to save time and make the development more enjoyable.
265
266The below instructions will show you how to create simple extension that removes a key from the `NSUserDefaults`/`SharedPreferences`.
267
268> **Note:** The tutorial was written using Kotlin and Swift. However, you can also use Java and Objective-C if you want.
269
270  <details>
271  <summary>�� Android</summary>
272
2731. Create a class which extends the `ReactContextBaseJavaModule` and implements the `DevMenuExtensionInterface`.
274
275   ```kotlin
276   // CustomDevMenuExtension.kt
277   package com.devmenudemo.customdevmenuextension
278
279   import com.facebook.react.bridge.ReactApplicationContext
280   import com.facebook.react.bridge.ReactContextBaseJavaModule
281   import expo.interfaces.devmenu.DevMenuExtensionInterface
282   import expo.interfaces.devmenu.items.DevMenuItem
283
284   class CustomDevMenuExtension(reactContext: ReactApplicationContext)
285     : ReactContextBaseJavaModule(reactContext),
286       DevMenuExtensionInterface {
287
288     override fun getName() = "CustomDevMenuExtension" // here you can provide name for your extension
289
290     override fun devMenuItems(): List<DevMenuItem>? {
291       // Firstly, create a function which will be called when the user presses the button.
292       val clearSharedPreferencesOnPress: () -> Unit = {
293         reactApplicationContext
294           .getSharedPreferences("your.shared.preferences", MODE_PRIVATE)
295           .edit()
296           .apply {
297             remove("key_to_remove")
298             Log.i("CustomDevMenuExt", "Remove key from SharedPreferences")
299             apply()
300           }
301       }
302
303       // Then, create `DevMenuAction` object.
304       val clearSharedPreferences = DevMenuAction(
305         actionId = "clear_shared_preferences", // This string identifies your custom action. Make sure that it's unique.
306         action = clearSharedPreferencesOnPress
307       ).apply {
308         label = { "Clear shared preferences" } // This string will be displayed in the dev menu.
309         glyphName = { "delete" } // This is a icon name used to present your action. You can use any icon from the `MaterialCommunityIcons`.
310         importance = DevMenuItemImportance.HIGH.value // This value tells the dev-menu in which order the actions should be rendered.
311         keyCommand = KeyCommand(KeyEvent.KEYCODE_S) // You can associate key commend with your action.
312       }
313
314       // Return created object. Note: you can register multiple actions if you want.
315       return listOf(clearSharedPreferences)
316     }
317   }
318   ```
319
3202. Create a react native package class for the extension.
321
322   ```kotlin
323   // CustomDevMenuExtensionPackage.kt
324   package com.devmenudemo.customdevmenuextension
325
326   import android.view.View
327   import com.facebook.react.ReactPackage
328   import com.facebook.react.bridge.ReactApplicationContext
329   import com.facebook.react.uimanager.ReactShadowNode
330   import com.facebook.react.uimanager.ViewManager
331
332   class CustomDevMenuExtensionPackage : ReactPackage {
333       override fun createNativeModules(reactContext: ReactApplicationContext) = listOf(
334           CustomDevMenuExtension(reactContext) // here you need to export your custom extension
335       )
336
337       override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<View, ReactShadowNode<*>>> = listOf()
338
339   }
340   ```
341
3423. Go to `MainApplication` (`MainApplication.java` or `MainApplication.kt`) and register created package.
343
344   ```java
345   // MainApplication.java
346
347   // You need to import your custom package.
348   import com.devmenudemo.customdevmenuextension.CustomDevMenuExtensionPackage; // the package can be different in your case
349
350   ...
351   public class MainApplication extends Application implements ReactApplication {
352     ...
353
354     @Override
355     protected List<ReactPackage> getPackages() {
356       List<ReactPackage> packages = new PackageList(this).getPackages();
357
358       // Add this line.
359       packages.add(new CustomDevMenuExtensionPackage());
360
361       return packages;
362     }
363   }
364   ```
365
366  </details>
367
368  <details>
369  <summary>�� iOS</summary>
370
3711. Create a Swift class which implements `DevMenuExtensionProtocol` for your extension.
372
373   ```swift
374   // CustomDevMenuExtension.swift
375   import EXDevMenuInterface
376
377   @objc(CustomDevMenuExtension)
378   open class CustomDevMenuExtension: NSObject, RCTBridgeModule, DevMenuExtensionProtocol {
379     public static func moduleName() -> String! {
380       return "CustomDevMenuExtension" // here you can provide name for your extension
381     }
382
383     @objc
384     open func devMenuItems() -> [DevMenuItem]? {
385       // Firstly, create a function which will be called when the user presses the button.
386       let clearNSUserDefaultsOnPress = {
387         let prefs = UserDefaults.standard
388         prefs.removeObject(forKey: "key_to_remove")
389       }
390
391       let clearNSUserDefaults = DevMenuAction(
392         withId: "clear_nsusersdefaults", // This string identifies your custom action. Make sure that it's unique.
393         action: clearNSUserDefaultsOnPress
394       )
395
396       clearNSUserDefaults.label = { "Clear NSUserDefaults" } // This string will be displayed in the dev menu.
397       clearNSUserDefaults.glyphName = { "delete" } // This is a icon name used to present your action. You can use any icon from the `MaterialCommunityIcons`.
398       clearNSUserDefaults.importance = DevMenuItem.ImportanceHigh // This value tells the dev-menu in which order the actions should be rendered.
399       clearNSUserDefaults.registerKeyCommand(input: "p", modifiers: .command) // You can associate key commend with your action.
400
401       // Return created object. Note: you can register multiple actions if you want.
402       return [clearNSUserDefaults]
403     }
404   }
405   ```
406
407   > **Note:** if you don't use Swift in your project earlier, you need to create bridging header. For more information, checks [importing objective-c into swift](https://developer.apple.com/documentation/swift/imported_c_and_objective-c_apis/importing_objective-c_into_swift).
408
4092. Create a `.m` file to integrate Swift class with react native and add the following lines.
410
411   ```objc
412   // CustomDevMenuExtension.m
413
414   #import <React/RCTBridgeModule.h>
415
416   @interface RCT_EXTERN_REMAP_MODULE(CustomDevMenuExtensionObjc, CustomDevMenuExtension, NSObject)
417   @end
418   ```
419
4203. Add the following line into the bridging header.
421
422   ```objc
423   #import <React/RCTBridgeModule.h>
424   ```
425
426  </details>
427
428After all those steps you should see something like this:
429
430![Final result](custom_dev_menu_extension_example.png)
431
432# �� API
433
434```js
435import * as DevMenu from 'expo-dev-menu';
436```
437
438For now, the `DevMenu` module exports only one method - [`openMenu`](#openmenu).
439
440### openMenu()
441
442Opens the dev menu.
443
444#### Example
445
446Using this method you can open the dev menu from your JS code whenever you want. It does nothing when the dev menu is not available (i.e. in release mode).
447
448Below you can find an example of opening the dev menu on button press:
449
450```js
451import * as DevMenu from 'expo-dev-menu';
452import { Button } from 'react-native';
453
454export const DevMenuButton = () => (
455  <Button
456    onPress={() => {
457      DevMenu.openMenu();
458    }}
459    title="Press to open the dev menu ��"
460    color="#841584"
461  />
462);
463```
464
465# �� Contributing
466
467Contributions are very welcome! Please refer to guidelines described in the [contributing guide](https://github.com/expo/expo#contributing).
468