expo-dev-menu 55.0.29 → 55.0.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -10,6 +10,17 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 55.0.31 — 2026-06-25
14
+
15
+ ### 🐛 Bug fixes
16
+
17
+ - [Android] Restore the "Open React Native dev menu" entry in the dev menu's Tools section (regressed in [#38759](https://github.com/expo/expo/pull/38759)). ([#47047](https://github.com/expo/expo/pull/47047) by [@lindboe](https://github.com/lindboe))
18
+ - [Android] Re-enable the Fast Refresh toggle in the dev menu's Tools section. ([#47136](https://github.com/expo/expo/pull/47136) by [@lukmccall](https://github.com/lukmccall))
19
+
20
+ ## 55.0.30 — 2026-05-19
21
+
22
+ _This version does not introduce any user-facing changes._
23
+
13
24
  ## 55.0.29 — 2026-05-15
14
25
 
15
26
  _This version does not introduce any user-facing changes._
@@ -12,7 +12,7 @@ apply plugin: 'expo-module-gradle-plugin'
12
12
  apply plugin: 'org.jetbrains.kotlin.plugin.compose'
13
13
 
14
14
  group = 'host.exp.exponent'
15
- version = '55.0.29'
15
+ version = '55.0.31'
16
16
 
17
17
  def hasDevLauncher = findProject(":expo-dev-launcher") != null
18
18
  def configureInRelease = findProperty("expo.devmenu.configureInRelease") == "true"
@@ -29,7 +29,7 @@ android {
29
29
 
30
30
  defaultConfig {
31
31
  versionCode 10
32
- versionName '55.0.29'
32
+ versionName '55.0.31'
33
33
  }
34
34
 
35
35
  buildTypes {
@@ -122,6 +122,22 @@ object MenuIcons {
122
122
  )
123
123
  }
124
124
 
125
+ @Composable
126
+ fun Gear(
127
+ size: Dp,
128
+ tint: Color,
129
+ modifier: Modifier = Modifier
130
+ ) {
131
+ Icon(
132
+ painter = painterResource(R.drawable.gear_fill),
133
+ contentDescription = "React Native dev menu",
134
+ tint = tint,
135
+ modifier = Modifier
136
+ .size(size)
137
+ .then(modifier)
138
+ )
139
+ }
140
+
125
141
  @Composable
126
142
  fun Refresh(
127
143
  size: Dp,
@@ -86,31 +86,50 @@ fun ToolsSection(
86
86
  }
87
87
  )
88
88
 
89
- // TODO(@lukmccall): Re-enable when toggling fast refresh is not longer crashing app
90
- // Divider(thickness = 0.5.dp)
91
- //
92
- // NewMenuButton(
93
- // withSurface = false,
94
- // icon = {
95
- // MenuIcons.Refresh(
96
- // size = 20.dp,
97
- // tint = NewAppTheme.colors.icon.tertiary
98
- // )
99
- // },
100
- // content = {
101
- // NewText(
102
- // text = "Fast Refresh"
103
- // )
104
- // },
105
- // rightComponent = {
106
- // ToggleSwitch(
107
- // isToggled = devToolsSettings.isHotLoadingEnabled
108
- // )
109
- // },
110
- // onClick = {
111
- // onAction(DevMenuAction.ToggleFastRefresh(!devToolsSettings.isHotLoadingEnabled))
112
- // }
113
- // )
89
+ Divider(thickness = 0.5.dp)
90
+
91
+ NewMenuButton(
92
+ withSurface = false,
93
+ icon = {
94
+ MenuIcons.Gear(
95
+ size = 20.dp,
96
+ tint = NewAppTheme.colors.icon.tertiary
97
+ )
98
+ },
99
+ content = {
100
+ NewText(
101
+ text = "Open React Native dev menu"
102
+ )
103
+ },
104
+ onClick = {
105
+ onAction(DevMenuAction.OpenReactNativeDevMenu)
106
+ }
107
+ )
108
+
109
+ Divider(thickness = 0.5.dp)
110
+
111
+ NewMenuButton(
112
+ withSurface = false,
113
+ icon = {
114
+ MenuIcons.Refresh(
115
+ size = 20.dp,
116
+ tint = NewAppTheme.colors.icon.tertiary
117
+ )
118
+ },
119
+ content = {
120
+ NewText(
121
+ text = "Fast Refresh"
122
+ )
123
+ },
124
+ rightComponent = {
125
+ ToggleSwitch(
126
+ isToggled = devToolsSettings.isHotLoadingEnabled
127
+ )
128
+ },
129
+ onClick = {
130
+ onAction(DevMenuAction.ToggleFastRefresh(!devToolsSettings.isHotLoadingEnabled))
131
+ }
132
+ )
114
133
 
115
134
  // Hide FAB toggle on Quest devices since FAB is always on there
116
135
  if (!VRUtilities.isQuest()) {
@@ -19,6 +19,7 @@ import kotlinx.coroutines.Dispatchers
19
19
  import kotlinx.coroutines.GlobalScope
20
20
  import kotlinx.coroutines.launch
21
21
  import java.lang.ref.WeakReference
22
+ import java.lang.reflect.Proxy
22
23
 
23
24
  class DevMenuDevToolsDelegate(
24
25
  private val weakDevSupportManager: WeakReference<out DevSupportManager>
@@ -77,9 +78,9 @@ class DevMenuDevToolsDelegate(
77
78
  internalSettings.isHotModuleReplacementEnabled = nextEnabled
78
79
 
79
80
  if (nextEnabled) {
80
- reactContext?.getJSModule(HMRClient::class.java)?.enable()
81
+ reactContext?.callHMRClientMethod("enable")
81
82
  } else {
82
- reactContext?.getJSModule(HMRClient::class.java)?.disable()
83
+ reactContext?.callHMRClientMethod("disable")
83
84
  }
84
85
 
85
86
  if (nextEnabled && !internalSettings.isJSDevModeEnabled) {
@@ -88,6 +89,33 @@ class DevMenuDevToolsDelegate(
88
89
  }
89
90
  }
90
91
 
92
+ /**
93
+ * Invokes a no-argument [HMRClient] method (`enable`/`disable`) without tripping a bug in React
94
+ * Native's bridgeless JS module proxy.
95
+ *
96
+ * When a zero-argument method is invoked through the proxy returned by [ReactContext.getJSModule],
97
+ * the JDK passes a `null` args array to the invocation handler. In bridgeless mode that handler
98
+ * (`BridgelessReactContext.BridgelessJSModuleInvocationHandler`) declares the parameter non-null,
99
+ * so Kotlin's null check throws a [NullPointerException] before the call ever reaches JS. We
100
+ * invoke the handler directly with an explicit empty args array to sidestep the null. The legacy
101
+ * (bridge) handler tolerates the empty array too, so this is safe on both architectures.
102
+ */
103
+ private fun ReactContext.callHMRClientMethod(methodName: String) {
104
+ val hmrClient = getJSModule(HMRClient::class.java) ?: return
105
+ val method = HMRClient::class.java.getMethod(methodName)
106
+ if (Proxy.isProxyClass(hmrClient.javaClass)) {
107
+ Proxy
108
+ .getInvocationHandler(hmrClient)
109
+ .invoke(
110
+ hmrClient,
111
+ method,
112
+ emptyArray<Any?>()
113
+ )
114
+ } else {
115
+ method.invoke(hmrClient)
116
+ }
117
+ }
118
+
91
119
  @OptIn(DelicateCoroutinesApi::class)
92
120
  fun openJSInspector() {
93
121
  val devSettings = devSettings ?: return
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-dev-menu",
3
- "version": "55.0.29",
3
+ "version": "55.0.31",
4
4
  "description": "Expo/React Native module with the developer menu.",
5
5
  "main": "build/DevMenu.js",
6
6
  "types": "build/DevMenu.d.ts",
@@ -39,7 +39,7 @@
39
39
  "@babel/preset-typescript": "^7.7.4",
40
40
  "@testing-library/react-native": "^13.3.0",
41
41
  "babel-plugin-module-resolver": "^5.0.0",
42
- "babel-preset-expo": "~55.0.21",
42
+ "babel-preset-expo": "~55.0.23",
43
43
  "expo-module-scripts": "^55.0.2",
44
44
  "react": "19.2.0",
45
45
  "react-native": "0.83.6"
@@ -47,5 +47,5 @@
47
47
  "peerDependencies": {
48
48
  "expo": "*"
49
49
  },
50
- "gitHead": "f654536b3d1b6fe32dd01a4067d6353fb302f7df"
50
+ "gitHead": "0c1476ccb1494a2019171f5df1d7a1b7803455e9"
51
51
  }