expo-dev-menu 55.0.7 → 55.0.9

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,14 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 55.0.9 — 2026-02-25
14
+
15
+ _This version does not introduce any user-facing changes._
16
+
17
+ ## 55.0.8 — 2026-02-25
18
+
19
+ _This version does not introduce any user-facing changes._
20
+
13
21
  ## 55.0.7 — 2026-02-20
14
22
 
15
23
  ### 💡 Others
@@ -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.7'
15
+ version = '55.0.9'
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.7'
32
+ versionName '55.0.9'
33
33
  }
34
34
 
35
35
  buildTypes {
@@ -547,9 +547,9 @@ open class DevMenuManager: NSObject {
547
547
  fabWindow = DevMenuFABWindow(manager: self, windowScene: windowScene)
548
548
  }
549
549
 
550
- public func updateFABVisibility(menuDismissing: Bool = false) {
550
+ public func updateFABVisibility() {
551
551
  DispatchQueue.main.async { [weak self] in
552
- guard let self = self else { return }
552
+ guard let self else { return }
553
553
 
554
554
  if self.fabWindow == nil {
555
555
  if let windowScene = UIApplication.shared.connectedScenes
@@ -559,7 +559,7 @@ open class DevMenuManager: NSObject {
559
559
  }
560
560
 
561
561
  let shouldShow = DevMenuPreferences.showFloatingActionButton
562
- && (menuDismissing || !self.isVisible)
562
+ && !self.isVisible
563
563
  && self.currentBridge != nil
564
564
  && !self.isNavigatingHome
565
565
  && DevMenuPreferences.isOnboardingFinished
@@ -567,7 +567,7 @@ open class DevMenuManager: NSObject {
567
567
  }
568
568
  }
569
569
  #else
570
- public func updateFABVisibility(menuDismissing: Bool = false) {
570
+ public func updateFABVisibility() {
571
571
  // FAB not available on macOS/tvOS
572
572
  }
573
573
  #endif
@@ -103,12 +103,11 @@ class DevMenuWindow: UIWindow, PresentationControllerDelegate {
103
103
  self.backgroundColor = .clear
104
104
  }
105
105
 
106
- DevMenuManager.shared.updateFABVisibility(menuDismissing: true)
107
-
108
106
  devMenuViewController.dismiss(animated: true) {
109
107
  self.isDismissing = false
110
108
  self.isHidden = true
111
109
  self.backgroundColor = UIColor(white: 0, alpha: 0.4)
110
+ DevMenuManager.shared.updateFABVisibility()
112
111
  completion?()
113
112
  }
114
113
  }
@@ -100,7 +100,7 @@ class DevMenuWindow: NSObject, AnyObject {
100
100
  ctx.duration = 0.3
101
101
  overlay.animator().alphaValue = 0.0
102
102
  }, completionHandler: { [weak self] in
103
- guard let self = self else { return }
103
+ guard let self else { return }
104
104
  overlay.removeFromSuperview()
105
105
  self.overlayView = nil
106
106
  self.hostingView = nil
@@ -6,8 +6,8 @@ import SwiftUI
6
6
 
7
7
  enum FABConstants {
8
8
  static let iconSize: CGFloat = 44
9
- static let margin: CGFloat = 16
10
- static let verticalPadding: CGFloat = 20
9
+ static let margin: CGFloat = 10
10
+ static let verticalPadding: CGFloat = 0
11
11
  static let dragThreshold: CGFloat = 10
12
12
  static let momentumFactor: CGFloat = 0.35
13
13
  static let labelDismissDelay: TimeInterval = 10
@@ -52,7 +52,7 @@ struct FabPill: View {
52
52
  }
53
53
 
54
54
  if showLabel {
55
- Text("Dev menu")
55
+ Text("Tools")
56
56
  .font(.system(size: 11, weight: .medium))
57
57
  .foregroundStyle(.secondary)
58
58
  .fixedSize()
@@ -77,15 +77,6 @@ struct FabPill: View {
77
77
  }
78
78
  }
79
79
 
80
- @ViewBuilder
81
- private var actionButton: some View {
82
- if #available(iOS 26.0, *) {
83
- liquidGlassButton
84
- } else {
85
- classicButton
86
- }
87
- }
88
-
89
80
  private func startIdleTimer() {
90
81
  idleTask?.cancel()
91
82
  idleTask = Task {
@@ -99,22 +90,7 @@ struct FabPill: View {
99
90
  }
100
91
  }
101
92
 
102
- @available(iOS 26.0, *)
103
- private var liquidGlassButton: some View {
104
- Image(systemName: "gearshape.fill")
105
- .resizable()
106
- .frame(width: FABConstants.imageSize, height: FABConstants.imageSize)
107
- .foregroundStyle(.white)
108
- .frame(width: FABConstants.iconSize, height: FABConstants.iconSize)
109
- .background(
110
- Circle()
111
- .frame(width: FABConstants.iconSize + 10, height: FABConstants.iconSize + 10)
112
- .glassEffect(.clear, in: Circle())
113
- )
114
- .shadow(color: .black.opacity(0.4), radius: 8, x: 0, y: 4)
115
- }
116
-
117
- private var classicButton: some View {
93
+ private var actionButton: some View {
118
94
  Image(systemName: "gearshape.fill")
119
95
  .resizable()
120
96
  .frame(width: FABConstants.imageSize, height: FABConstants.imageSize)
@@ -136,15 +112,32 @@ struct DevMenuFABView: View {
136
112
 
137
113
  private let fabSize = CGSize(width: 72, height: FABConstants.iconSize + 50)
138
114
 
115
+ // UserDefaults keys for persisting position
116
+ private static let positionXKey = "DevMenuFAB.positionX"
117
+ private static let positionYKey = "DevMenuFAB.positionY"
118
+ private static let hasStoredPositionKey = "DevMenuFAB.hasStoredPosition"
119
+
120
+ private static func loadStoredPosition() -> CGPoint? {
121
+ guard UserDefaults.standard.bool(forKey: hasStoredPositionKey) else { return nil }
122
+ let x = UserDefaults.standard.double(forKey: positionXKey)
123
+ let y = UserDefaults.standard.double(forKey: positionYKey)
124
+ return CGPoint(x: x, y: y)
125
+ }
126
+
127
+ private static func savePosition(_ position: CGPoint) {
128
+ UserDefaults.standard.set(position.x, forKey: positionXKey)
129
+ UserDefaults.standard.set(position.y, forKey: positionYKey)
130
+ UserDefaults.standard.set(true, forKey: hasStoredPositionKey)
131
+ }
132
+
139
133
  @State private var position: CGPoint = .zero
140
- @State private var targetPosition: CGPoint = .zero
141
134
  @State private var isDragging = false
142
135
  @State private var isPressed = false
143
136
  @State private var dragStartPosition: CGPoint = .zero
144
137
 
145
138
  private let dragSpring: Animation = .spring(
146
- response: 0.35,
147
- dampingFraction: 0.35,
139
+ response: 0.25,
140
+ dampingFraction: 0.85,
148
141
  blendDuration: 0
149
142
  )
150
143
 
@@ -161,7 +154,21 @@ struct DevMenuFABView: View {
161
154
  .position(x: currentFrame.midX, y: currentFrame.midY)
162
155
  .gesture(dragGesture(bounds: geometry.size, safeArea: safeArea))
163
156
  .onAppear {
164
- let initialPos = defaultPosition(bounds: geometry.size, safeArea: safeArea)
157
+ let initialPos: CGPoint
158
+ if let storedPos = Self.loadStoredPosition() {
159
+ let margin = FABConstants.margin
160
+ let minX = margin / 2
161
+ let maxX = geometry.size.width - fabSize.width - margin / 2
162
+ let minY = safeArea.top + FABConstants.verticalPadding
163
+ let maxY = geometry.size.height - fabSize.height - safeArea.bottom - FABConstants.verticalPadding
164
+
165
+ initialPos = CGPoint(
166
+ x: storedPos.x.clamped(to: minX...maxX),
167
+ y: storedPos.y.clamped(to: minY...maxY)
168
+ )
169
+ } else {
170
+ initialPos = defaultPosition(bounds: geometry.size, safeArea: safeArea)
171
+ }
165
172
  position = initialPos
166
173
  onFrameChange(CGRect(origin: initialPos, size: fabSize))
167
174
  }
@@ -192,19 +199,10 @@ struct DevMenuFABView: View {
192
199
  dragStartPosition = position
193
200
  }
194
201
  if isDragging {
195
- let margin = FABConstants.margin
196
- let minX = margin
197
- let maxX = bounds.width - fabSize.width - margin
198
- let minY = margin + safeArea.top + FABConstants.verticalPadding
199
- let maxY = bounds.height - fabSize.height - margin - safeArea.bottom - FABConstants.verticalPadding
200
-
201
202
  let rawX = dragStartPosition.x + value.translation.width
202
203
  let rawY = dragStartPosition.y + value.translation.height
203
204
 
204
- position = CGPoint(
205
- x: rawX.clamped(to: minX...maxX),
206
- y: rawY.clamped(to: minY...maxY)
207
- )
205
+ position = CGPoint(x: rawX, y: rawY)
208
206
  onFrameChange(currentFrame)
209
207
  }
210
208
  }
@@ -231,6 +229,7 @@ struct DevMenuFABView: View {
231
229
  DispatchQueue.main.async {
232
230
  isDragging = false
233
231
  position = newPos
232
+ Self.savePosition(newPos)
234
233
  onFrameChange(CGRect(origin: newPos, size: fabSize))
235
234
  }
236
235
  }
@@ -239,8 +238,8 @@ struct DevMenuFABView: View {
239
238
 
240
239
  private func defaultPosition(bounds: CGSize, safeArea: EdgeInsets) -> CGPoint {
241
240
  CGPoint(
242
- x: bounds.width - fabSize.width - FABConstants.margin,
243
- y: bounds.height * 0.25
241
+ x: bounds.width - fabSize.width - FABConstants.margin / 2,
242
+ y: safeArea.top + FABConstants.verticalPadding
244
243
  )
245
244
  }
246
245
 
@@ -251,16 +250,17 @@ struct DevMenuFABView: View {
251
250
  safeArea: EdgeInsets
252
251
  ) -> CGPoint {
253
252
  let margin = FABConstants.margin
253
+ let edgeMargin = margin / 2 // Closer to screen edge when snapped
254
254
  let momentumX = velocity.x * FABConstants.momentumFactor
255
255
  let momentumY = velocity.y * FABConstants.momentumFactor
256
256
 
257
257
  let estimatedCenterX = point.x + self.fabSize.width / 2 + momentumX
258
258
  let targetX: CGFloat = estimatedCenterX < bounds.width / 2
259
- ? margin
260
- : bounds.width - self.fabSize.width - margin
259
+ ? edgeMargin
260
+ : bounds.width - self.fabSize.width - edgeMargin
261
261
 
262
- let minY = margin + safeArea.top + FABConstants.verticalPadding
263
- let maxY = bounds.height - self.fabSize.height - margin - safeArea.bottom - FABConstants.verticalPadding
262
+ let minY = safeArea.top + FABConstants.verticalPadding
263
+ let maxY = bounds.height - self.fabSize.height - safeArea.bottom - FABConstants.verticalPadding
264
264
  let targetY = (point.y + momentumY).clamped(to: minY...maxY)
265
265
 
266
266
  return CGPoint(x: targetX, y: targetY)
@@ -10,6 +10,8 @@ class DevMenuFABWindow: UIWindow {
10
10
  private weak var manager: DevMenuManager?
11
11
  private var hostingController: UIHostingController<DevMenuFABView>?
12
12
  var fabFrame: CGRect = .zero
13
+ private var currentAnimator: UIViewPropertyAnimator?
14
+ private var targetVisibility: Bool?
13
15
 
14
16
  init(manager: DevMenuManager, windowScene: UIWindowScene) {
15
17
  self.manager = manager
@@ -51,44 +53,48 @@ class DevMenuFABWindow: UIWindow {
51
53
  }
52
54
 
53
55
  func setVisible(_ visible: Bool, animated: Bool = true) {
56
+ // Skip if already animating to the same state
57
+ if targetVisibility == visible {
58
+ return
59
+ }
60
+
61
+ // Cancel any in-progress animation and reset to clean state
62
+ if currentAnimator != nil {
63
+ currentAnimator?.stopAnimation(true)
64
+ currentAnimator = nil
65
+ transform = .identity
66
+ }
67
+
68
+ targetVisibility = visible
69
+
54
70
  if visible {
55
71
  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
+ alpha = 0
73
+ transform = edgeTranslation
74
+
75
+ let animator = UIViewPropertyAnimator(duration: animated ? 0.5 : 0, dampingRatio: 0.6) {
76
+ self.alpha = 1
77
+ self.transform = .identity
72
78
  }
79
+ animator.addCompletion { [weak self] _ in
80
+ self?.targetVisibility = nil
81
+ }
82
+ currentAnimator = animator
83
+ animator.startAnimation()
73
84
  } 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
85
+ let animator = UIViewPropertyAnimator(duration: animated ? 0.3 : 0, dampingRatio: 0.8) {
86
+ self.alpha = 0
87
+ self.transform = self.edgeTranslation
88
+ }
89
+ animator.addCompletion { [weak self] position in
90
+ self?.targetVisibility = nil
91
+ if position == .end {
92
+ self?.isHidden = true
93
+ self?.transform = .identity
87
94
  }
88
- } else {
89
- alpha = 0
90
- isHidden = true
91
95
  }
96
+ currentAnimator = animator
97
+ animator.startAnimation()
92
98
  }
93
99
  }
94
100
 
@@ -42,7 +42,7 @@ public class DevMenuPreferences: Module {
42
42
  keyCommandsEnabledKey: true,
43
43
  showsAtLaunchKey: false,
44
44
  isOnboardingFinishedKey: false,
45
- showFloatingActionButtonKey: false
45
+ showFloatingActionButtonKey: true
46
46
  ])
47
47
  #endif
48
48
 
@@ -12,8 +12,6 @@ struct DevMenuAppInfo: View {
12
12
  .foregroundColor(.primary.opacity(0.6))
13
13
 
14
14
  VStack(spacing: 0) {
15
- Divider()
16
-
17
15
  InfoRow(title: "Version", value: viewModel.appInfo?.appVersion ?? "Unknown")
18
16
 
19
17
  if let runtimeVersion = viewModel.appInfo?.runtimeVersion {
@@ -44,8 +42,7 @@ struct DevMenuAppInfo: View {
44
42
  }
45
43
  }
46
44
  .padding(.horizontal)
47
- .background(Color.expoSystemBackground)
48
- .cornerRadius(18)
45
+ .background(Color.expoSecondarySystemBackground, in: RoundedRectangle(cornerRadius: 18))
49
46
  }
50
47
  }
51
48
  }
@@ -59,6 +59,8 @@ struct DevMenuToggleButton: View {
59
59
 
60
60
  Text(title)
61
61
  .foregroundColor(disabled ? .secondary : .primary)
62
+ .lineLimit(1)
63
+ .minimumScaleFactor(0.8)
62
64
 
63
65
  Spacer()
64
66
 
@@ -79,15 +79,14 @@ struct DevMenuDeveloperTools: View {
79
79
  Divider()
80
80
 
81
81
  DevMenuToggleButton(
82
- title: "Show dev menu button",
82
+ title: "Tools button",
83
83
  icon: "hand.tap",
84
84
  isEnabled: viewModel.showFloatingActionButton,
85
85
  action: viewModel.toggleFloatingActionButton
86
86
  )
87
87
  #endif
88
88
  }
89
- .background(Color.expoSystemBackground)
90
- .cornerRadius(18)
89
+ .background(Color.expoSecondarySystemBackground, in: RoundedRectangle(cornerRadius: 18))
91
90
  }
92
91
  }
93
92
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-dev-menu",
3
- "version": "55.0.7",
3
+ "version": "55.0.9",
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.6",
42
+ "babel-preset-expo": "~55.0.8",
43
43
  "expo-module-scripts": "^55.0.2",
44
44
  "react": "19.2.0",
45
45
  "react-native": "0.83.2"
@@ -47,5 +47,5 @@
47
47
  "peerDependencies": {
48
48
  "expo": "*"
49
49
  },
50
- "gitHead": "22e2ad4afba05b91f833f8cf07a36637748a1f70"
50
+ "gitHead": "39a7a009e215eb71a112f4a20dba2d471ab21108"
51
51
  }