expo-dev-menu 55.0.2 → 55.0.3

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,23 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 55.0.3 — 2026-01-27
14
+
15
+ ### 🎉 New features
16
+
17
+ - [iOS] Add delegate method to control visibility of "Open React Native dev menu" option. ([#42541](https://github.com/expo/expo/pull/42541) by [@alanjhughes](https://github.com/alanjhughes))
18
+ - [iOS] Add action button. ([#42587](https://github.com/expo/expo/pull/42587) by [@alanjhughes](https://github.com/alanjhughes))
19
+
20
+ ### 🐛 Bug fixes
21
+
22
+ - [iOS] Fix tvOS compilation errors in Source Map Explorer by excluding tvOS from unavailable SwiftUI APIs (`navigationBarTitleDisplayMode`, `listStyle(.insetGrouped)`, `Menu`) and adding proper availability checks. Refactored platform-specific modifiers to use `@ViewBuilder` instead of `AnyView` for better type preservation and performance. ([#42574](https://github.com/expo/expo/pull/42574) by [@OtavioStasiak](https://github.com/OtavioStasiak))
23
+ - [Android] Fix dev menu not opening when pressing m key in CLI. ([#42566](https://github.com/expo/expo/pull/42566) by [@lukmccall](https://github.com/lukmccall))
24
+
25
+ ### 💡 Others
26
+
27
+ - Make the Action Button always enabled on Quest. ([#42562](https://github.com/expo/expo/pull/42562) by [@behenate](https://github.com/behenate))
28
+ - Moves connection info lower in the menu. ([#42568](https://github.com/expo/expo/pull/42568) and [#42569](https://github.com/expo/expo/pull/42569) by [@alanjhughes](https://github.com/alanjhughes))
29
+
13
30
  ## 55.0.2 — 2026-01-26
14
31
 
15
32
  ### 🎉 New features
@@ -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.2'
15
+ version = '55.0.3'
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.2'
32
+ versionName '55.0.3'
33
33
  }
34
34
 
35
35
  buildTypes {
@@ -3,6 +3,7 @@ package expo.modules.devmenu
3
3
  import android.app.Application
4
4
  import android.content.Context.MODE_PRIVATE
5
5
  import android.content.SharedPreferences
6
+ import expo.modules.core.utilities.VRUtilities
6
7
  import expo.modules.devmenu.helpers.preferences
7
8
 
8
9
  private const val DEV_SETTINGS_PREFERENCES = "expo.modules.devmenu.sharedpreferences"
@@ -88,7 +89,6 @@ class DevMenuDefaultPreferences(
88
89
  override var isOnboardingFinished: Boolean
89
90
  by preferences(sharedPreferences, false)
90
91
 
91
- // TODO: @behenate, on VR this value should be true by default
92
92
  override var showFab: Boolean
93
- by preferences(sharedPreferences, false)
93
+ by preferences(sharedPreferences, VRUtilities.isQuest())
94
94
  }
@@ -1,5 +1,6 @@
1
1
  package expo.modules.devmenu.compose
2
2
 
3
+ import expo.modules.core.utilities.VRUtilities
3
4
  import expo.modules.devmenu.DevToolsSettings
4
5
  import org.json.JSONObject
5
6
 
@@ -8,7 +9,7 @@ data class DevMenuState(
8
9
  val isOpen: Boolean = false,
9
10
  val devToolsSettings: DevToolsSettings = DevToolsSettings(),
10
11
  val isOnboardingFinished: Boolean = false,
11
- val showFab: Boolean = false,
12
+ val showFab: Boolean = VRUtilities.isQuest(),
12
13
  val customItems: List<CustomItem> = emptyList(),
13
14
  val hasGoHomeAction: Boolean = false,
14
15
  val isInPictureInPictureMode: Boolean = false
@@ -38,10 +38,6 @@ fun DevMenuScreen(
38
38
  }
39
39
 
40
40
  Column {
41
- BundlerInfo(bundlerIp = appInfo.hostUrl)
42
-
43
- Spacer(NewAppTheme.spacing.`2`)
44
-
45
41
  Row(
46
42
  horizontalArrangement = Arrangement.spacedBy(NewAppTheme.spacing.`2`),
47
43
  verticalAlignment = Alignment.CenterVertically
@@ -84,6 +80,10 @@ fun DevMenuScreen(
84
80
  }
85
81
  }
86
82
 
83
+ BundlerInfo(bundlerIp = appInfo.hostUrl)
84
+
85
+ Spacer(NewAppTheme.spacing.`5`)
86
+
87
87
  SystemSection(
88
88
  appInfo.appVersion,
89
89
  appInfo.sdkVersion,
@@ -3,6 +3,7 @@ package expo.modules.devmenu.compose.ui
3
3
  import androidx.compose.foundation.layout.Column
4
4
  import androidx.compose.runtime.Composable
5
5
  import androidx.compose.ui.unit.dp
6
+ import expo.modules.core.utilities.VRUtilities
6
7
  import expo.modules.devmenu.DevToolsSettings
7
8
  import expo.modules.devmenu.compose.DevMenuAction
8
9
  import expo.modules.devmenu.compose.DevMenuActionHandler
@@ -111,30 +112,33 @@ fun ToolsSection(
111
112
  // }
112
113
  // )
113
114
 
114
- Divider(thickness = 0.5.dp)
115
+ // Hide FAB toggle on Quest devices since FAB is always on there
116
+ if (!VRUtilities.isQuest()) {
117
+ Divider(thickness = 0.5.dp)
115
118
 
116
- NewMenuButton(
117
- withSurface = false,
118
- icon = {
119
- MenuIcons.Fab(
120
- size = 20.dp,
121
- tint = NewAppTheme.colors.icon.tertiary
122
- )
123
- },
124
- content = {
125
- NewText(
126
- text = "Action button"
127
- )
128
- },
129
- rightComponent = {
130
- ToggleSwitch(
131
- isToggled = showFab
132
- )
133
- },
134
- onClick = {
135
- onAction(DevMenuAction.ToggleFab)
136
- }
137
- )
119
+ NewMenuButton(
120
+ withSurface = false,
121
+ icon = {
122
+ MenuIcons.Fab(
123
+ size = 20.dp,
124
+ tint = NewAppTheme.colors.icon.tertiary
125
+ )
126
+ },
127
+ content = {
128
+ NewText(
129
+ text = "Action button"
130
+ )
131
+ },
132
+ rightComponent = {
133
+ ToggleSwitch(
134
+ isToggled = showFab
135
+ )
136
+ },
137
+ onClick = {
138
+ onAction(DevMenuAction.ToggleFab)
139
+ }
140
+ )
141
+ }
138
142
  }
139
143
  }
140
144
  }
@@ -6,6 +6,10 @@ import com.facebook.react.packagerconnection.NotificationOnlyHandler
6
6
  import expo.modules.devmenu.devtools.DevMenuDevToolsDelegate
7
7
  import org.json.JSONObject
8
8
  import java.lang.ref.WeakReference
9
+ import java.util.Date
10
+
11
+ private object Mutex
12
+ private var lastMessage = 0L
9
13
 
10
14
  class DevMenuCommandHandlersProvider(
11
15
  weakDevSupportManager: WeakReference<out DevSupportManager>
@@ -32,6 +36,8 @@ class DevMenuCommandHandlersProvider(
32
36
  }
33
37
 
34
38
  fun createCommandHandlers(): Map<String, NotificationOnlyHandler> {
39
+ lastMessage = Date().time
40
+
35
41
  return mapOf(
36
42
  "reload" to onReload,
37
43
  "devMenu" to onDevMenu,
@@ -42,7 +48,21 @@ class DevMenuCommandHandlersProvider(
42
48
  private fun createHandler(action: (params: Any?) -> Unit): NotificationOnlyHandler {
43
49
  return object : NotificationOnlyHandler() {
44
50
  override fun onNotification(params: Any?) {
45
- action(params)
51
+ val currentTime = Date().time
52
+
53
+ synchronized(Mutex) {
54
+ val diff = currentTime - lastMessage
55
+ if (diff < 100) {
56
+ Log.w(
57
+ "DevMenu",
58
+ "Throttling incoming dev menu command. Time since last command: ${diff}ms"
59
+ )
60
+ return
61
+ }
62
+
63
+ action(params)
64
+ lastMessage = currentTime
65
+ }
46
66
  }
47
67
  }
48
68
  }
@@ -7,6 +7,27 @@ import CoreGraphics
7
7
  import CoreMedia
8
8
  import Combine
9
9
 
10
+ /**
11
+ Configuration options for customizing the dev menu appearance.
12
+ Host apps (e.g., Expo Go) can set these to tailor the menu for their context.
13
+ The defaults match the standard dev menu behavior.
14
+ */
15
+ @objc
16
+ public class DevMenuConfiguration: NSObject {
17
+ /// Whether to show the debugging tip (e.g., "Debugging not working? Try manually reloading first.")
18
+ @objc public var showDebuggingTip: Bool = true
19
+
20
+ /// Whether to show the "Connected to:" host URL section
21
+ @objc public var showHostUrl: Bool = true
22
+
23
+ /// Whether to show the Fast Refresh toggle
24
+ @objc public var showFastRefresh: Bool = true
25
+
26
+ /// Custom title for the onboarding text. Use to replace "development builds" with e.g. "Expo Go".
27
+ /// When nil, the default "development builds" text is used.
28
+ @objc public var onboardingAppName: String?
29
+ }
30
+
10
31
  class Dispatch {
11
32
  static func mainSync<T>(_ closure: () -> T) -> T {
12
33
  if Thread.isMainThread {
@@ -51,6 +72,8 @@ open class DevMenuManager: NSObject {
51
72
  var packagerConnectionHandler: DevMenuPackagerConnectionHandler?
52
73
  var canLaunchDevMenuOnStart = true
53
74
 
75
+ @objc public var configuration = DevMenuConfiguration()
76
+
54
77
  static public var wasInitilized = false
55
78
 
56
79
  /**
@@ -67,6 +90,13 @@ open class DevMenuManager: NSObject {
67
90
  */
68
91
  var window: DevMenuWindow?
69
92
 
93
+ #if !os(macOS) && !os(tvOS)
94
+ /**
95
+ The window that hosts the floating action button.
96
+ */
97
+ var fabWindow: DevMenuFABWindow?
98
+ #endif
99
+
70
100
  var currentScreen: String?
71
101
 
72
102
  weak var hostDelegate: DevMenuHostDelegate?
@@ -79,7 +109,10 @@ open class DevMenuManager: NSObject {
79
109
  if let currentBridge {
80
110
  DispatchQueue.main.async {
81
111
  self.disableRNDevMenuHoykeys(for: currentBridge)
112
+ self.updateFABVisibility()
82
113
  }
114
+ } else {
115
+ updateFABVisibility()
83
116
  }
84
117
  }
85
118
  }
@@ -250,7 +283,11 @@ open class DevMenuManager: NSObject {
250
283
  @objc
251
284
  @discardableResult
252
285
  public func hideMenu() -> Bool {
253
- return setVisibility(false)
286
+ let result = setVisibility(false)
287
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
288
+ self?.updateFABVisibility()
289
+ }
290
+ return result
254
291
  }
255
292
 
256
293
  /**
@@ -270,6 +307,15 @@ open class DevMenuManager: NSObject {
270
307
  return delegate.responds(to: #selector(DevMenuHostDelegate.devMenuNavigateHome))
271
308
  }
272
309
 
310
+ @objc
311
+ public var shouldShowReactNativeDevMenu: Bool {
312
+ guard let delegate = hostDelegate,
313
+ delegate.responds(to: #selector(DevMenuHostDelegate.devMenuShouldShowReactNativeDevMenu)) else {
314
+ return true
315
+ }
316
+ return delegate.devMenuShouldShowReactNativeDevMenu?() ?? true
317
+ }
318
+
273
319
  @objc
274
320
  public func navigateHome() {
275
321
  guard let delegate = hostDelegate,
@@ -369,9 +415,10 @@ open class DevMenuManager: NSObject {
369
415
  #if os(macOS)
370
416
  self.window?.makeKeyAndOrderFront(nil)
371
417
  #else
418
+ self.updateFABVisibility()
419
+
372
420
  if self.window?.windowScene == nil {
373
- let keyWindowScene = UIApplication.shared.windows.first(where: { $0.isKeyWindow })?.windowScene
374
- let windowScene = keyWindowScene ?? UIApplication.shared.connectedScenes
421
+ let windowScene = UIApplication.shared.connectedScenes
375
422
  .first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene
376
423
  self.window?.windowScene = windowScene
377
424
  }
@@ -379,7 +426,12 @@ open class DevMenuManager: NSObject {
379
426
  #endif
380
427
  }
381
428
  } else {
382
- DispatchQueue.main.async { self.window?.closeBottomSheet(nil) }
429
+ DispatchQueue.main.async {
430
+ self.window?.closeBottomSheet(nil)
431
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
432
+ self.updateFABVisibility()
433
+ }
434
+ }
383
435
  }
384
436
  return true
385
437
  }
@@ -457,4 +509,33 @@ open class DevMenuManager: NSObject {
457
509
  let devToolsDelegate = getDevToolsDelegate()
458
510
  devToolsDelegate?.toggleFastRefresh()
459
511
  }
512
+
513
+ #if !os(macOS) && !os(tvOS)
514
+ private func setupFABWindowIfNeeded(for windowScene: UIWindowScene) {
515
+ guard fabWindow == nil else { return }
516
+ fabWindow = DevMenuFABWindow(manager: self, windowScene: windowScene)
517
+ }
518
+
519
+ @objc
520
+ public func updateFABVisibility() {
521
+ DispatchQueue.main.async { [weak self] in
522
+ guard let self = self else { return }
523
+
524
+ if self.fabWindow == nil {
525
+ if let windowScene = UIApplication.shared.connectedScenes
526
+ .first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene {
527
+ self.setupFABWindowIfNeeded(for: windowScene)
528
+ }
529
+ }
530
+
531
+ let shouldShow = DevMenuPreferences.showFloatingActionButton && !self.isVisible && self.currentBridge != nil
532
+ self.fabWindow?.setVisible(shouldShow, animated: true)
533
+ }
534
+ }
535
+ #else
536
+ @objc
537
+ public func updateFABVisibility() {
538
+ // FAB not available on macOS/tvOS
539
+ }
540
+ #endif
460
541
  }
@@ -107,6 +107,7 @@ class DevMenuWindow: UIWindow, PresentationControllerDelegate {
107
107
  self.isDismissing = false
108
108
  self.isHidden = true
109
109
  self.backgroundColor = UIColor(white: 0, alpha: 0.4)
110
+ DevMenuManager.shared.updateFABVisibility()
110
111
  completion?()
111
112
  }
112
113
  }
@@ -31,7 +31,7 @@ class EXDevMenuDevSettings: NSObject {
31
31
  devSettings["isHotLoadingEnabled"] = bridgeSettings.isHotLoadingEnabled
32
32
  devSettings["isPerfMonitorShown"] = bridgeSettings.isPerfMonitorShown
33
33
  devSettings["isHotLoadingAvailable"] = bridgeSettings.isHotLoadingAvailable
34
- devSettings["isPerfMonitorAvailable"] = isPerfMonitorAvailable
34
+ devSettings["isPerfMonitorAvailable"] = isPerfMonitorAvailable && manager.currentManifest?.isDevelopmentMode() == true
35
35
  devSettings["isJSInspectorAvailable"] = bridgeSettings.isDeviceDebuggingAvailable
36
36
 
37
37
  let isElementInspectorAvailable = manager.currentManifest?.isDevelopmentMode()
@@ -0,0 +1,195 @@
1
+ // Copyright 2015-present 650 Industries. All rights reserved.
2
+
3
+ #if !os(macOS) && !os(tvOS)
4
+
5
+ import SwiftUI
6
+
7
+ enum FABConstants {
8
+ static let iconSize: CGFloat = 44
9
+ static let margin: CGFloat = 16
10
+ static let dragThreshold: CGFloat = 40
11
+ static let momentumFactor: CGFloat = 0.35
12
+ static let labelDismissDelay: TimeInterval = 10
13
+
14
+ static let snapAnimation: Animation = .spring(
15
+ response: 0.6,
16
+ dampingFraction: 0.7,
17
+ blendDuration: 0
18
+ )
19
+ }
20
+
21
+ struct FabPill: View {
22
+ @Binding var isPressed: Bool
23
+ @State private var showLabel = true
24
+
25
+ var body: some View {
26
+ VStack(spacing: 4) {
27
+ Image(systemName: "gearshape.fill")
28
+ .font(.system(size: 20))
29
+ .foregroundStyle(.white)
30
+ .frame(width: FABConstants.iconSize, height: FABConstants.iconSize)
31
+ .background(Color.blue, in: Circle())
32
+ .overlay(
33
+ Circle()
34
+ .stroke(Color.blue.opacity(0.5), lineWidth: 4)
35
+ .frame(width: FABConstants.iconSize + 4, height: FABConstants.iconSize + 4)
36
+ )
37
+ .opacity(isPressed ? 0.8 : 1.0)
38
+ .scaleEffect(isPressed ? 0.9 : 1.0)
39
+ .animation(.easeInOut(duration: 0.1), value: isPressed)
40
+
41
+ if showLabel {
42
+ Text("Dev tools")
43
+ .font(.system(size: 11, weight: .medium))
44
+ .foregroundStyle(.secondary)
45
+ .fixedSize()
46
+ .padding(.horizontal, 8)
47
+ .padding(.vertical, 3)
48
+ .background(.regularMaterial, in: Capsule())
49
+ .transition(.opacity.combined(with: .scale(scale: 0.8)))
50
+ }
51
+ }
52
+ .task {
53
+ try? await Task.sleep(nanoseconds: UInt64(1_000_000_000 * FABConstants.labelDismissDelay))
54
+ await MainActor.run {
55
+ withAnimation(.easeOut(duration: 0.3)) {
56
+ showLabel = false
57
+ }
58
+ }
59
+ }
60
+ }
61
+ }
62
+
63
+ struct DevMenuFABView: View {
64
+ let onOpenMenu: () -> Void
65
+ let onFrameChange: (CGRect) -> Void
66
+
67
+ private let fabSize = CGSize(width: 72, height: FABConstants.iconSize + 24)
68
+
69
+ @State private var position: CGPoint = .zero
70
+ @State private var isDragging = false
71
+ @State private var isPressed = false
72
+ @State private var dragStartPosition: CGPoint = .zero
73
+
74
+ private var currentFrame: CGRect {
75
+ CGRect(origin: position, size: fabSize)
76
+ }
77
+
78
+ var body: some View {
79
+ GeometryReader { geometry in
80
+ let safeArea = geometry.safeAreaInsets
81
+ FabPill(isPressed: $isPressed)
82
+ .frame(width: fabSize.width, height: fabSize.height)
83
+ .position(x: currentFrame.midX, y: currentFrame.midY)
84
+ .gesture(dragGesture(bounds: geometry.size, safeArea: safeArea))
85
+ .onAppear {
86
+ let initialPos = defaultPosition(bounds: geometry.size, safeArea: safeArea)
87
+ position = initialPos
88
+ onFrameChange(CGRect(origin: initialPos, size: fabSize))
89
+ }
90
+ .onChange(of: geometry.size) { newSize in
91
+ let newPos = snapToEdge(
92
+ from: position,
93
+ velocity: .zero,
94
+ bounds: newSize,
95
+ safeArea: safeArea
96
+ )
97
+ position = newPos
98
+ onFrameChange(CGRect(origin: newPos, size: fabSize))
99
+ }
100
+ .animation(isDragging ? nil : FABConstants.snapAnimation, value: position)
101
+ }
102
+ .ignoresSafeArea()
103
+ }
104
+
105
+ private func dragGesture(bounds: CGSize, safeArea: EdgeInsets) -> some Gesture {
106
+ DragGesture(minimumDistance: 0)
107
+ .onChanged { value in
108
+ if !isPressed {
109
+ isPressed = true
110
+ }
111
+ if !isDragging && value.translation.magnitude > FABConstants.dragThreshold {
112
+ isDragging = true
113
+ isPressed = false
114
+ dragStartPosition = position
115
+ }
116
+ if isDragging {
117
+ position = CGPoint(
118
+ x: dragStartPosition.x + value.translation.width,
119
+ y: dragStartPosition.y + value.translation.height
120
+ )
121
+ onFrameChange(currentFrame)
122
+ }
123
+ }
124
+ .onEnded { value in
125
+ isPressed = false
126
+ let dragDistance = value.translation.magnitude
127
+
128
+ if dragDistance < FABConstants.dragThreshold {
129
+ isDragging = false
130
+ onOpenMenu()
131
+ } else {
132
+ let velocity = CGPoint(
133
+ x: value.predictedEndLocation.x - value.location.x,
134
+ y: value.predictedEndLocation.y - value.location.y
135
+ )
136
+
137
+ let newPos = snapToEdge(
138
+ from: position,
139
+ velocity: velocity,
140
+ bounds: bounds,
141
+ safeArea: safeArea
142
+ )
143
+
144
+ DispatchQueue.main.async {
145
+ isDragging = false
146
+ position = newPos
147
+ onFrameChange(CGRect(origin: newPos, size: fabSize))
148
+ }
149
+ }
150
+ }
151
+ }
152
+
153
+ private func defaultPosition(bounds: CGSize, safeArea: EdgeInsets) -> CGPoint {
154
+ CGPoint(
155
+ x: bounds.width - fabSize.width - FABConstants.margin,
156
+ y: (bounds.height / 2) - fabSize.height
157
+ )
158
+ }
159
+
160
+ private func snapToEdge(
161
+ from point: CGPoint,
162
+ velocity: CGPoint,
163
+ bounds: CGSize,
164
+ safeArea: EdgeInsets
165
+ ) -> CGPoint {
166
+ let margin = FABConstants.margin
167
+ let momentumX = velocity.x * FABConstants.momentumFactor
168
+ let momentumY = velocity.y * FABConstants.momentumFactor
169
+
170
+ let estimatedCenterX = point.x + self.fabSize.width / 2 + momentumX
171
+ let targetX: CGFloat = estimatedCenterX < bounds.width / 2
172
+ ? margin
173
+ : bounds.width - self.fabSize.width - margin
174
+
175
+ let minY = margin + safeArea.top
176
+ let maxY = bounds.height - self.fabSize.height - margin - safeArea.bottom
177
+ let targetY = (point.y + momentumY).clamped(to: minY...maxY)
178
+
179
+ return CGPoint(x: targetX, y: targetY)
180
+ }
181
+ }
182
+
183
+ private extension CGSize {
184
+ var magnitude: CGFloat {
185
+ sqrt(width * width + height * height)
186
+ }
187
+ }
188
+
189
+ private extension Comparable {
190
+ func clamped(to range: ClosedRange<Self>) -> Self {
191
+ min(max(self, range.lowerBound), range.upperBound)
192
+ }
193
+ }
194
+
195
+ #endif
@@ -0,0 +1,105 @@
1
+ // Copyright 2015-present 650 Industries. All rights reserved.
2
+
3
+ #if !os(macOS) && !os(tvOS)
4
+
5
+ import UIKit
6
+ import SwiftUI
7
+
8
+ /// A passthrough window that hosts the floating action button.
9
+ class DevMenuFABWindow: UIWindow {
10
+ private weak var manager: DevMenuManager?
11
+ private var hostingController: UIHostingController<DevMenuFABView>?
12
+ var fabFrame: CGRect = .zero
13
+
14
+ init(manager: DevMenuManager, windowScene: UIWindowScene) {
15
+ self.manager = manager
16
+ super.init(windowScene: windowScene)
17
+ setupWindow()
18
+ }
19
+
20
+ @available(*, unavailable)
21
+ required init?(coder: NSCoder) {
22
+ fatalError("init(coder:) has not been implemented")
23
+ }
24
+
25
+ private func setupWindow() {
26
+ windowLevel = UIWindow.Level.statusBar - 1
27
+ backgroundColor = .clear
28
+ isHidden = true
29
+ alpha = 0
30
+
31
+ let fabView = DevMenuFABView(
32
+ onOpenMenu: { [weak self] in
33
+ self?.manager?.openMenu()
34
+ },
35
+ onFrameChange: { [weak self] frame in
36
+ self?.fabFrame = frame
37
+ }
38
+ )
39
+
40
+ let hostingController = UIHostingController(rootView: fabView)
41
+ hostingController.view.backgroundColor = .clear
42
+ self.hostingController = hostingController
43
+ rootViewController = hostingController
44
+ }
45
+
46
+ private var edgeTranslation: CGAffineTransform {
47
+ let screenWidth = windowScene?.screen.bounds.width ?? UIScreen.main.bounds.width
48
+ let isOnRight = fabFrame == .zero || fabFrame.midX > (screenWidth / 2)
49
+ let dx: CGFloat = isOnRight ? 60 : -60
50
+ return CGAffineTransform(translationX: dx, y: 0)
51
+ }
52
+
53
+ func setVisible(_ visible: Bool, animated: Bool = true) {
54
+ if visible {
55
+ isHidden = false
56
+ if animated {
57
+ alpha = 0
58
+ transform = edgeTranslation
59
+ UIView.animate(
60
+ withDuration: 0.5,
61
+ delay: 0,
62
+ usingSpringWithDamping: 0.6,
63
+ initialSpringVelocity: 0.8,
64
+ options: .curveEaseOut
65
+ ) {
66
+ self.alpha = 1
67
+ self.transform = .identity
68
+ }
69
+ } else {
70
+ alpha = 1
71
+ transform = .identity
72
+ }
73
+ } else {
74
+ if animated {
75
+ UIView.animate(
76
+ withDuration: 0.4,
77
+ delay: 0,
78
+ usingSpringWithDamping: 0.6,
79
+ initialSpringVelocity: 0.8,
80
+ options: .curveEaseIn
81
+ ) {
82
+ self.alpha = 0
83
+ self.transform = self.edgeTranslation
84
+ } completion: { _ in
85
+ self.isHidden = true
86
+ self.transform = .identity
87
+ }
88
+ } else {
89
+ alpha = 0
90
+ isHidden = true
91
+ }
92
+ }
93
+ }
94
+
95
+ override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
96
+ let hitArea = fabFrame.insetBy(dx: -10, dy: -10)
97
+ if hitArea.contains(point) {
98
+ return super.hitTest(point, with: event)
99
+ }
100
+
101
+ return nil
102
+ }
103
+ }
104
+
105
+ #endif
@@ -6,6 +6,7 @@ let touchGestureEnabledKey = "EXDevMenuTouchGestureEnabled"
6
6
  let keyCommandsEnabledKey = "EXDevMenuKeyCommandsEnabled"
7
7
  let showsAtLaunchKey = "EXDevMenuShowsAtLaunch"
8
8
  let isOnboardingFinishedKey = "EXDevMenuIsOnboardingFinished"
9
+ let showFloatingActionButtonKey = "EXDevMenuShowFloatingActionButton"
9
10
 
10
11
  public class DevMenuPreferences: Module {
11
12
  public func definition() -> ModuleDefinition {
@@ -31,7 +32,8 @@ public class DevMenuPreferences: Module {
31
32
  touchGestureEnabledKey: false,
32
33
  keyCommandsEnabledKey: true,
33
34
  showsAtLaunchKey: false,
34
- isOnboardingFinishedKey: true
35
+ isOnboardingFinishedKey: true,
36
+ showFloatingActionButtonKey: false
35
37
  ])
36
38
  #else
37
39
  UserDefaults.standard.register(defaults: [
@@ -39,7 +41,8 @@ public class DevMenuPreferences: Module {
39
41
  touchGestureEnabledKey: true,
40
42
  keyCommandsEnabledKey: true,
41
43
  showsAtLaunchKey: false,
42
- isOnboardingFinishedKey: false
44
+ isOnboardingFinishedKey: false,
45
+ showFloatingActionButtonKey: false
43
46
  ])
44
47
  #endif
45
48
 
@@ -129,6 +132,16 @@ public class DevMenuPreferences: Module {
129
132
  }
130
133
  }
131
134
 
135
+ public static var showFloatingActionButton: Bool {
136
+ get {
137
+ return boolForKey(showFloatingActionButtonKey)
138
+ }
139
+ set {
140
+ setBool(newValue, forKey: showFloatingActionButtonKey)
141
+ DevMenuManager.shared.updateFABVisibility()
142
+ }
143
+ }
144
+
132
145
  /**
133
146
  Serializes settings into a dictionary so they can be passed through the bridge.
134
147
  */
@@ -138,7 +151,8 @@ public class DevMenuPreferences: Module {
138
151
  "touchGestureEnabled": DevMenuPreferences.touchGestureEnabled,
139
152
  "keyCommandsEnabled": DevMenuPreferences.keyCommandsEnabled,
140
153
  "showsAtLaunch": DevMenuPreferences.showsAtLaunch,
141
- "isOnboardingFinished": DevMenuPreferences.isOnboardingFinished
154
+ "isOnboardingFinished": DevMenuPreferences.isOnboardingFinished,
155
+ "showFloatingActionButton": DevMenuPreferences.showFloatingActionButton
142
156
  ]
143
157
  }
144
158
 
@@ -155,6 +169,9 @@ public class DevMenuPreferences: Module {
155
169
  if let showsAtLaunch = settings["showsAtLaunch"] as? Bool {
156
170
  DevMenuPreferences.showsAtLaunch = showsAtLaunch
157
171
  }
172
+ if let showFloatingActionButton = settings["showFloatingActionButton"] as? Bool {
173
+ DevMenuPreferences.showFloatingActionButton = showFloatingActionButton
174
+ }
158
175
  }
159
176
 
160
177
  @objc static func toggleMenu() {
@@ -43,6 +43,7 @@ struct DevMenuAppInfo: View {
43
43
  .disabled(viewModel.clipboardMessage != nil)
44
44
  }
45
45
  }
46
+ .padding(.horizontal)
46
47
  .background(Color.expoSystemBackground)
47
48
  .cornerRadius(18)
48
49
  }
@@ -38,15 +38,28 @@ struct DevMenuDeveloperTools: View {
38
38
  )
39
39
  }
40
40
 
41
+ if viewModel.configuration.showFastRefresh {
42
+ Divider()
43
+
44
+ DevMenuToggleButton(
45
+ title: "Fast refresh",
46
+ icon: "figure.run",
47
+ isEnabled: viewModel.devSettings?.isHotLoadingEnabled ?? false,
48
+ action: viewModel.toggleFastRefresh,
49
+ disabled: !(viewModel.devSettings?.isHotLoadingAvailable ?? true)
50
+ )
51
+ }
52
+
53
+ #if !os(tvOS) && !os(macOS)
41
54
  Divider()
42
55
 
43
56
  DevMenuToggleButton(
44
- title: "Fast refresh",
45
- icon: "figure.run",
46
- isEnabled: viewModel.devSettings?.isHotLoadingEnabled ?? false,
47
- action: viewModel.toggleFastRefresh,
48
- disabled: !(viewModel.devSettings?.isHotLoadingAvailable ?? true)
57
+ title: "Action button",
58
+ icon: "hand.tap",
59
+ isEnabled: viewModel.showFloatingActionButton,
60
+ action: viewModel.toggleFloatingActionButton
49
61
  )
62
+ #endif
50
63
 
51
64
  Divider()
52
65
 
@@ -6,21 +6,11 @@ struct DevMenuMainView: View {
6
6
  var body: some View {
7
7
  ScrollView {
8
8
  VStack(spacing: 32) {
9
- VStack {
10
- if let hostUrl = viewModel.appInfo?.hostUrl {
11
- HostUrl(
12
- hostUrl: hostUrl,
13
- onCopy: viewModel.copyToClipboard,
14
- copiedMessage: viewModel.hostUrlCopiedMessage
15
- )
16
- }
17
-
18
- DevMenuActions(
19
- canNavigateHome: viewModel.canNavigateHome,
20
- onReload: viewModel.reload,
21
- onGoHome: viewModel.goHome
22
- )
23
- }
9
+ DevMenuActions(
10
+ canNavigateHome: viewModel.canNavigateHome,
11
+ onReload: viewModel.reload,
12
+ onGoHome: viewModel.goHome
13
+ )
24
14
 
25
15
  if !viewModel.registeredCallbacks.isEmpty {
26
16
  CustomItems(
@@ -31,13 +21,23 @@ struct DevMenuMainView: View {
31
21
 
32
22
  DevMenuDeveloperTools()
33
23
 
34
- if viewModel.appInfo?.engine == "Hermes" {
24
+ if viewModel.configuration.showDebuggingTip && viewModel.appInfo?.engine == "Hermes" {
35
25
  HermesDebuggerTip()
36
26
  }
37
27
 
28
+ if viewModel.configuration.showHostUrl, let hostUrl = viewModel.appInfo?.hostUrl {
29
+ HostUrl(
30
+ hostUrl: hostUrl,
31
+ onCopy: viewModel.copyToClipboard,
32
+ copiedMessage: viewModel.hostUrlCopiedMessage
33
+ )
34
+ }
35
+
38
36
  DevMenuAppInfo()
39
37
 
40
- DevMenuRNDevMenu(onOpenRNDevMenu: viewModel.openRNDevMenu)
38
+ if viewModel.shouldShowReactNativeDevMenu {
39
+ DevMenuRNDevMenu(onOpenRNDevMenu: viewModel.openRNDevMenu)
40
+ }
41
41
  }
42
42
  .padding()
43
43
  }
@@ -45,6 +45,7 @@ struct DevMenuMainView: View {
45
45
  .navigationTitle("Dev Menu")
46
46
  #if !os(macOS)
47
47
  .navigationBarHidden(true)
48
+ .navigationBarTitleDisplayMode(.inline)
48
49
  #endif
49
50
  }
50
51
  }
@@ -2,6 +2,7 @@ import SwiftUI
2
2
 
3
3
  struct DevMenuOnboardingView: View {
4
4
  let onFinish: () -> Void
5
+ var appName: String = "development builds"
5
6
  @State private var isVisible = true
6
7
 
7
8
  var body: some View {
@@ -16,7 +17,7 @@ struct DevMenuOnboardingView: View {
16
17
  private var onboardingOverlay: some View {
17
18
  VStack(spacing: 24) {
18
19
  VStack(spacing: 16) {
19
- Text("This is the developer menu. It gives you access to useful tools in your development builds.")
20
+ Text("This is the developer menu. It gives you access to useful tools in \(appName).")
20
21
  .frame(maxWidth: .infinity, alignment: .leading)
21
22
  .font(.callout)
22
23
 
@@ -17,7 +17,10 @@ struct DevMenuRootView: View {
17
17
 
18
18
  #if !os(tvOS)
19
19
  if !viewModel.isOnboardingFinished {
20
- DevMenuOnboardingView(onFinish: viewModel.finishOnboarding)
20
+ DevMenuOnboardingView(
21
+ onFinish: viewModel.finishOnboarding,
22
+ appName: viewModel.configuration.onboardingAppName ?? "your development builds"
23
+ )
21
24
  }
22
25
  #endif
23
26
  }
@@ -12,6 +12,7 @@ class DevMenuViewModel: ObservableObject {
12
12
  @Published var clipboardMessage: String?
13
13
  @Published var hostUrlCopiedMessage: String?
14
14
  @Published var isOnboardingFinished: Bool = true
15
+ @Published var showFloatingActionButton: Bool = false
15
16
 
16
17
  private let devMenuManager = DevMenuManager.shared
17
18
  private var cancellables = Set<AnyCancellable>()
@@ -27,6 +28,7 @@ class DevMenuViewModel: ObservableObject {
27
28
  loadAppInfo()
28
29
  loadDevSettings()
29
30
  loadRegisteredCallbacks()
31
+ loadFloatingActionButtonState()
30
32
  }
31
33
 
32
34
  private func loadAppInfo() {
@@ -165,6 +167,14 @@ class DevMenuViewModel: ObservableObject {
165
167
  return devMenuManager.canNavigateHome
166
168
  }
167
169
 
170
+ var shouldShowReactNativeDevMenu: Bool {
171
+ return devMenuManager.shouldShowReactNativeDevMenu
172
+ }
173
+
174
+ var configuration: DevMenuConfiguration {
175
+ return devMenuManager.configuration
176
+ }
177
+
168
178
  private func checkOnboardingStatus() {
169
179
  isOnboardingFinished = devMenuManager.isOnboardingFinished
170
180
  }
@@ -174,6 +184,15 @@ class DevMenuViewModel: ObservableObject {
174
184
  isOnboardingFinished = true
175
185
  }
176
186
 
187
+ private func loadFloatingActionButtonState() {
188
+ showFloatingActionButton = DevMenuPreferences.showFloatingActionButton
189
+ }
190
+
191
+ func toggleFloatingActionButton() {
192
+ showFloatingActionButton.toggle()
193
+ DevMenuPreferences.showFloatingActionButton = showFloatingActionButton
194
+ }
195
+
177
196
  private func observeRegisteredCallbacks() {
178
197
  devMenuManager.callbacksPublisher
179
198
  .map { $0.map { $0.name } }
@@ -186,6 +205,7 @@ class DevMenuViewModel: ObservableObject {
186
205
  .receive(on: DispatchQueue.main)
187
206
  .sink { [weak self] _ in
188
207
  self?.loadAppInfo()
208
+ self?.loadDevSettings()
189
209
  }
190
210
  .store(in: &cancellables)
191
211
  }
@@ -11,7 +11,7 @@ typealias PlatformImage = UIImage
11
11
 
12
12
  struct HeaderView: View {
13
13
  @EnvironmentObject var viewModel: DevMenuViewModel
14
- @State private var appIcon: PlatformImage? = nil
14
+ @State private var appIcon: PlatformImage?
15
15
 
16
16
  var body: some View {
17
17
  HStack(spacing: 12) {
@@ -28,8 +28,9 @@ struct SourceMapExplorerView: View {
28
28
  }
29
29
  }
30
30
  .navigationTitle("Source Code Explorer")
31
- #if !os(macOS)
31
+ #if !os(macOS) && !os(tvOS)
32
32
  .navigationBarTitleDisplayMode(.inline)
33
+ .navigationBarHidden(false)
33
34
  #endif
34
35
  .searchable(text: $viewModel.searchText, placement: .automatic, prompt: "Search files")
35
36
  .task {
@@ -64,7 +65,9 @@ struct SourceMapExplorerView: View {
64
65
  Button("Retry") {
65
66
  Task { await viewModel.loadSourceMap() }
66
67
  }
68
+ #if !os(tvOS)
67
69
  .buttonStyle(.borderedProminent)
70
+ #endif
68
71
  }
69
72
  .padding()
70
73
  .frame(maxWidth: .infinity, maxHeight: .infinity)
@@ -85,43 +88,60 @@ struct FolderListView: View {
85
88
  .foregroundColor(.secondary)
86
89
  } else {
87
90
  ForEach(nodes) { node in
88
- if node.isDirectory {
89
- NavigationLink(destination: FolderListView(
90
- title: node.name,
91
- nodes: node.children,
92
- sourceMap: sourceMap,
93
- stats: nil,
94
- isSearching: false
95
- )) {
96
- FileRow(node: node, showPath: isSearching)
97
- }
98
- } else {
99
- NavigationLink(destination: CodeFileView(node: node, sourceMap: sourceMap)) {
100
- FileRow(node: node, showPath: isSearching)
101
- }
91
+ NavigationLink(destination: destinationView(for: node)) {
92
+ FileRow(node: node, showPath: isSearching)
102
93
  }
103
94
  }
104
95
  }
105
96
  }
106
- #if !os(macOS)
97
+ #if !os(macOS) && !os(tvOS)
107
98
  .listStyle(.insetGrouped)
99
+ #elseif os(tvOS)
100
+ .listStyle(.plain)
108
101
  #endif
109
102
  .navigationTitle(isSearching ? "Search Results" : title)
110
- #if !os(macOS)
103
+ #if !os(macOS) && !os(tvOS)
111
104
  .navigationBarTitleDisplayMode(.inline)
105
+ #endif
112
106
  .toolbar {
113
107
  ToolbarItem(placement: .navigationBarTrailing) {
114
108
  if let stats = stats {
109
+ #if os(tvOS)
110
+ if #available(tvOS 17.0, *) {
111
+ Menu {
112
+ Label("\(stats.files) files", systemImage: "doc.on.doc")
113
+ Label(stats.totalSize, systemImage: "internaldrive")
114
+ } label: {
115
+ Image(systemName: "info.circle")
116
+ }
117
+ }
118
+ // Menu not available on tvOS < 17.0, toolbar item is hidden
119
+ #else
115
120
  Menu {
116
121
  Label("\(stats.files) files", systemImage: "doc.on.doc")
117
122
  Label(stats.totalSize, systemImage: "internaldrive")
118
123
  } label: {
119
124
  Image(systemName: "info.circle")
120
125
  }
126
+ #endif
121
127
  }
122
128
  }
123
129
  }
124
- #endif
130
+ }
131
+
132
+ @ViewBuilder
133
+ private func destinationView(for node: FileTreeNode) -> some View {
134
+ if node.isDirectory {
135
+ FolderListView(
136
+ title: node.name,
137
+ nodes: node.children,
138
+ sourceMap: sourceMap,
139
+ stats: nil,
140
+ isSearching: false
141
+ )
142
+ } else {
143
+ CodeFileView(node: node, sourceMap: sourceMap)
144
+ }
125
145
  }
126
146
  }
127
147
 
@@ -183,8 +203,10 @@ struct CodeFileView: View {
183
203
  let sourceMap: SourceMap?
184
204
  @Environment(\.colorScheme) private var colorScheme
185
205
  @State private var highlightedLines: [AttributedString]?
206
+ @State private var isEditing = false
207
+ @State private var displayContent: String = ""
186
208
 
187
- private var content: String {
209
+ private var originalContent: String {
188
210
  guard let contentIndex = node.contentIndex,
189
211
  let sourceMap,
190
212
  let sourcesContent = sourceMap.sourcesContent,
@@ -196,7 +218,7 @@ struct CodeFileView: View {
196
218
  }
197
219
 
198
220
  private var lines: [String] {
199
- content.components(separatedBy: "\n")
221
+ displayContent.components(separatedBy: "\n")
200
222
  }
201
223
 
202
224
  private var theme: SyntaxHighlighter.Theme {
@@ -209,35 +231,83 @@ struct CodeFileView: View {
209
231
  }
210
232
 
211
233
  var body: some View {
212
- GeometryReader { geometry in
213
- ScrollView(.vertical) {
214
- ScrollView(.horizontal, showsIndicators: false) {
215
- HStack(alignment: .top, spacing: 0) {
216
- LineNumbersColumn(lines: lines, theme: theme, lineNumberWidth: lineNumberWidth)
217
- CodeColumn(lines: lines, highlightedLines: highlightedLines, theme: theme)
218
- }
219
- .frame(minWidth: geometry.size.width, alignment: .leading)
220
- }
234
+ Group {
235
+ if isEditing {
236
+ editingView()
237
+ } else {
238
+ readOnlyView()
221
239
  }
222
240
  }
223
241
  .background(theme.background)
224
242
  .navigationTitle(node.name)
225
- #if !os(macOS)
243
+ #if !os(macOS) && !os(tvOS)
226
244
  .navigationBarTitleDisplayMode(.inline)
245
+ .toolbar {
246
+ ToolbarItem(placement: .navigationBarTrailing) {
247
+ Button(isEditing ? "Done" : "Edit") {
248
+ if isEditing {
249
+ isEditing = false
250
+ Task {
251
+ highlightedLines = await SyntaxHighlighter.highlightLines(lines, theme: theme)
252
+ }
253
+ } else {
254
+ isEditing = true
255
+ }
256
+ }
257
+ }
258
+ }
227
259
  #endif
260
+ .onAppear {
261
+ if displayContent.isEmpty {
262
+ displayContent = originalContent
263
+ }
264
+ }
228
265
  .task(id: colorScheme) {
229
266
  highlightedLines = await SyntaxHighlighter.highlightLines(lines, theme: theme)
230
267
  }
231
268
  }
269
+
270
+ private func readOnlyView() -> some View {
271
+ ScrollView(.vertical) {
272
+ ScrollView(.horizontal, showsIndicators: false) {
273
+ HStack(alignment: .top, spacing: 0) {
274
+ LineNumbersColumn(lines: lines, theme: theme, lineNumberWidth: lineNumberWidth)
275
+ CodeColumn(lines: lines, highlightedLines: highlightedLines, theme: theme)
276
+ }
277
+ }
278
+ }
279
+ }
280
+
281
+ private func editingView() -> some View {
282
+ TextEditor(text: $displayContent)
283
+ .font(.system(size: 13, weight: .regular, design: .monospaced))
284
+ #if os(iOS) || os(tvOS)
285
+ .textInputAutocapitalization(.never)
286
+ #endif
287
+ .autocorrectionDisabled()
288
+ .modifier(ScrollContentBackgroundModifier())
289
+ .background(theme.background)
290
+ .foregroundColor(theme.plain)
291
+ }
292
+ }
293
+
294
+ private struct ScrollContentBackgroundModifier: ViewModifier {
295
+ func body(content: Content) -> some View {
296
+ if #available(iOS 16.0, tvOS 16.0, *) {
297
+ content.scrollContentBackground(.hidden)
298
+ } else {
299
+ content
300
+ }
301
+ }
232
302
  }
233
303
 
234
304
  struct LineNumbersColumn: View {
235
305
  let lines: [String]
236
306
  let theme: SyntaxHighlighter.Theme
237
307
  let lineNumberWidth: CGFloat
238
-
308
+
239
309
  var body: some View {
240
- LazyVStack(alignment: .trailing, spacing: 0) {
310
+ VStack(alignment: .trailing, spacing: 0) {
241
311
  ForEach(0..<lines.count, id: \.self) { index in
242
312
  Text("\(index + 1)")
243
313
  .font(.system(size: 13, weight: .regular, design: .monospaced))
@@ -255,9 +325,9 @@ struct CodeColumn: View {
255
325
  let lines: [String]
256
326
  let highlightedLines: [AttributedString]?
257
327
  let theme: SyntaxHighlighter.Theme
258
-
328
+
259
329
  var body: some View {
260
- LazyVStack(alignment: .leading, spacing: 0) {
330
+ VStack(alignment: .leading, spacing: 0) {
261
331
  ForEach(0..<lines.count, id: \.self) { index in
262
332
  if let highlightedLines, index < highlightedLines.count {
263
333
  Text(highlightedLines[index])
@@ -271,13 +341,8 @@ struct CodeColumn: View {
271
341
  }
272
342
  }
273
343
  }
344
+ .fixedSize(horizontal: true, vertical: false)
274
345
  .padding(.vertical, 12)
275
346
  .padding(.trailing, 16)
276
347
  }
277
348
  }
278
-
279
- #Preview {
280
- NavigationView {
281
- SourceMapExplorerView()
282
- }
283
- }
@@ -184,7 +184,7 @@ class SourceMapService {
184
184
  private func extractInlineSourceMap(from bundleContent: String) throws -> SourceMap {
185
185
  let patterns = [
186
186
  "//# sourceMappingURL=data:application/json;charset=utf-8;base64,",
187
- "//# sourceMappingURL=data:application/json;base64,",
187
+ "//# sourceMappingURL=data:application/json;base64,"
188
188
  ]
189
189
 
190
190
  for pattern in patterns {
@@ -234,7 +234,14 @@ class SourceMapService {
234
234
 
235
235
  let nodes = root.children.values.map { convertToNode($0) }
236
236
  let sorted = sortNodes(nodes)
237
- return collapseSingleChildFolders(sorted)
237
+ let collapsed = collapseSingleChildFolders(sorted)
238
+
239
+ let directories = collapsed.filter { $0.isDirectory }
240
+ if directories.count == 1, let mainDir = directories.first {
241
+ return mainDir.children
242
+ }
243
+
244
+ return collapsed
238
245
  }
239
246
 
240
247
  private func convertToNode(_ builder: Node) -> FileTreeNode {
@@ -255,7 +262,7 @@ class SourceMapService {
255
262
  while current.isDirectory && current.children.count == 1 && current.children[0].isDirectory {
256
263
  let child = current.children[0]
257
264
  current = FileTreeNode(
258
- name: "\(current.name)/\(child.name)",
265
+ name: child.name,
259
266
  path: child.path,
260
267
  isDirectory: true,
261
268
  children: child.children,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-dev-menu",
3
- "version": "55.0.2",
3
+ "version": "55.0.3",
4
4
  "description": "Expo/React Native module with the developer menu.",
5
5
  "main": "build/DevMenu.js",
6
6
  "types": "build/DevMenu.d.ts",
@@ -33,7 +33,7 @@
33
33
  "license": "MIT",
34
34
  "homepage": "https://docs.expo.dev",
35
35
  "dependencies": {
36
- "expo-dev-menu-interface": "55.0.0"
36
+ "expo-dev-menu-interface": "55.0.1"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@babel/preset-typescript": "^7.7.4",
@@ -47,5 +47,5 @@
47
47
  "peerDependencies": {
48
48
  "expo": "*"
49
49
  },
50
- "gitHead": "7d7f6762fc6907c27a329953c682134a84410dea"
50
+ "gitHead": "220594d473a3100248087151004ae4acb7282d5f"
51
51
  }