expo-dev-menu 58.0.3 → 58.0.5

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,16 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 58.0.5 — 2026-09-22
14
+
15
+ ### 🐛 Bug fixes
16
+
17
+ - [iOS] Kept the floating dev menu button clear of system bars, the front camera and the home indicator by clamping the drawn button, rather than its touch frame, to the safe area on every edge. ([#50445](https://github.com/expo/expo/pull/50445) by [@alanjhughes](https://github.com/alanjhughes))
18
+
19
+ ## 58.0.4 — 2026-09-21
20
+
21
+ _This version does not introduce any user-facing changes._
22
+
13
23
  ## 58.0.3 — 2026-09-16
14
24
 
15
25
  _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 = '58.0.3'
15
+ version = '58.0.5'
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 '58.0.3'
32
+ versionName '58.0.5'
33
33
  }
34
34
 
35
35
  buildTypes {
@@ -115,6 +115,9 @@ Pod::Spec.new do |s|
115
115
  test_spec.platforms = {
116
116
  :ios => '16.4'
117
117
  }
118
+ test_spec.pod_target_xcconfig = {
119
+ 'OTHER_LDFLAGS' => '$(inherited) -lc++'
120
+ }
118
121
  end
119
122
 
120
123
  s.test_spec 'UITests' do |test_spec|
@@ -128,6 +131,9 @@ Pod::Spec.new do |s|
128
131
  test_spec.platforms = {
129
132
  :ios => '16.4'
130
133
  }
134
+ test_spec.pod_target_xcconfig = {
135
+ 'OTHER_LDFLAGS' => '$(inherited) -lc++'
136
+ }
131
137
  end
132
138
 
133
139
  s.default_subspec = ['Main']
@@ -22,6 +22,14 @@ enum FABConstants {
22
22
  )
23
23
  }
24
24
 
25
+ struct FABPillHeightKey: PreferenceKey {
26
+ static let defaultValue: CGFloat = FABConstants.iconSize
27
+
28
+ static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
29
+ value = nextValue()
30
+ }
31
+ }
32
+
25
33
  struct FabPill: View {
26
34
  @Binding var isPressed: Bool
27
35
  @Binding var isDragging: Bool
@@ -63,6 +71,11 @@ struct FabPill: View {
63
71
  .transition(.opacity.combined(with: .scale(scale: 0.8)))
64
72
  }
65
73
  }
74
+ .background(
75
+ GeometryReader { proxy in
76
+ Color.clear.preference(key: FABPillHeightKey.self, value: proxy.size.height)
77
+ }
78
+ )
66
79
  .saturation(isIdle ? 0 : 1)
67
80
  .opacity(isIdle ? 0.5 : 1)
68
81
  .animation(.easeInOut(duration: 0.3), value: isIdle)
@@ -113,6 +126,18 @@ struct DevMenuFABView: View {
113
126
 
114
127
  private let fabSize = CGSize(width: 72, height: FABConstants.iconSize + 50)
115
128
 
129
+ /// Centred in the touch frame, so its top moves as the label appears and dismisses.
130
+ private var drawnFrame: CGRect {
131
+ let ring = FABPlacement.ringOverhang
132
+ let width = FABConstants.iconSize + ring * 2
133
+ return CGRect(
134
+ x: (fabSize.width - width) / 2,
135
+ y: (fabSize.height - pillHeight) / 2 - ring,
136
+ width: width,
137
+ height: pillHeight + ring * 2
138
+ )
139
+ }
140
+
116
141
  // UserDefaults keys for persisting position
117
142
  private static let positionXKey = "DevMenuFAB.positionX"
118
143
  private static let positionYKey = "DevMenuFAB.positionY"
@@ -136,6 +161,7 @@ struct DevMenuFABView: View {
136
161
  @State private var isPressed = false
137
162
  @State private var dragStartPosition: CGPoint = .zero
138
163
  @State private var didPosition = false
164
+ @State private var pillHeight: CGFloat = FABConstants.iconSize
139
165
 
140
166
  // Get safe area from window since .ignoresSafeArea() may zero out geometry values
141
167
  private var windowSafeArea: UIEdgeInsets {
@@ -164,6 +190,21 @@ struct DevMenuFABView: View {
164
190
 
165
191
  FabPill(isPressed: $isPressed, isDragging: $isDragging)
166
192
  .frame(width: fabSize.width, height: fabSize.height)
193
+ .onPreferenceChange(FABPillHeightKey.self) { height in
194
+ pillHeight = height
195
+ let ranges = placementRanges(bounds: geometry.size, safeArea: safeArea)
196
+ let clamped = CGPoint(
197
+ x: position.x.clamped(to: ranges.x),
198
+ y: position.y.clamped(to: ranges.y)
199
+ )
200
+ guard clamped != position else { return }
201
+ var transaction = Transaction()
202
+ transaction.disablesAnimations = true
203
+ withTransaction(transaction) {
204
+ position = clamped
205
+ }
206
+ onFrameChange(CGRect(origin: clamped, size: fabSize))
207
+ }
167
208
  .position(x: currentFrame.midX, y: currentFrame.midY)
168
209
  .gesture(dragGesture(bounds: geometry.size, safeArea: safeArea))
169
210
  .onAppear {
@@ -236,15 +277,10 @@ struct DevMenuFABView: View {
236
277
  private func placeInitially(bounds: CGSize, safeArea: EdgeInsets) {
237
278
  let initialPos: CGPoint
238
279
  if let storedPos = Self.loadStoredPosition() {
239
- let margin = FABConstants.margin
240
- let minX = margin / 2
241
- let maxX = bounds.width - fabSize.width - margin / 2
242
- let minY = safeArea.top + FABConstants.verticalPadding
243
- let maxY = bounds.height - fabSize.height - safeArea.bottom - FABConstants.verticalPadding
244
-
280
+ let ranges = placementRanges(bounds: bounds, safeArea: safeArea)
245
281
  initialPos = CGPoint(
246
- x: storedPos.x.clamped(to: minX...maxX),
247
- y: storedPos.y.clamped(to: minY...maxY)
282
+ x: storedPos.x.clamped(to: ranges.x),
283
+ y: storedPos.y.clamped(to: ranges.y)
248
284
  )
249
285
  } else {
250
286
  initialPos = defaultPosition(bounds: bounds, safeArea: safeArea)
@@ -258,11 +294,27 @@ struct DevMenuFABView: View {
258
294
  }
259
295
  }
260
296
 
261
- private func defaultPosition(bounds: CGSize, safeArea: EdgeInsets) -> CGPoint {
262
- CGPoint(
263
- x: bounds.width - fabSize.width - FABConstants.margin / 2,
264
- y: safeArea.top + FABConstants.verticalPadding
297
+ private func placementRanges(
298
+ bounds: CGSize,
299
+ safeArea: EdgeInsets
300
+ ) -> (x: ClosedRange<CGFloat>, y: ClosedRange<CGFloat>) {
301
+ let ranges = FABPlacement.ranges(
302
+ bounds: bounds,
303
+ safeArea: FABInsets(
304
+ top: safeArea.top,
305
+ leading: safeArea.leading,
306
+ bottom: safeArea.bottom,
307
+ trailing: safeArea.trailing
308
+ ),
309
+ drawnFrame: drawnFrame,
310
+ inset: FABConstants.margin
265
311
  )
312
+ return (ranges.x, ranges.y)
313
+ }
314
+
315
+ private func defaultPosition(bounds: CGSize, safeArea: EdgeInsets) -> CGPoint {
316
+ let ranges = placementRanges(bounds: bounds, safeArea: safeArea)
317
+ return CGPoint(x: ranges.x.upperBound, y: ranges.y.lowerBound)
266
318
  }
267
319
 
268
320
  private func snapToEdge(
@@ -271,19 +323,13 @@ struct DevMenuFABView: View {
271
323
  bounds: CGSize,
272
324
  safeArea: EdgeInsets
273
325
  ) -> CGPoint {
274
- let margin = FABConstants.margin
275
- let edgeMargin = margin / 2 // Closer to screen edge when snapped
276
326
  let momentumX = velocity.x * FABConstants.momentumFactor
277
327
  let momentumY = velocity.y * FABConstants.momentumFactor
328
+ let ranges = placementRanges(bounds: bounds, safeArea: safeArea)
278
329
 
279
330
  let estimatedCenterX = point.x + self.fabSize.width / 2 + momentumX
280
- let targetX: CGFloat = estimatedCenterX < bounds.width / 2
281
- ? edgeMargin
282
- : bounds.width - self.fabSize.width - edgeMargin
283
-
284
- let minY = safeArea.top + FABConstants.verticalPadding
285
- let maxY = bounds.height - self.fabSize.height - safeArea.bottom - FABConstants.verticalPadding
286
- let targetY = (point.y + momentumY).clamped(to: minY...maxY)
331
+ let targetX = estimatedCenterX < bounds.width / 2 ? ranges.x.lowerBound : ranges.x.upperBound
332
+ let targetY = (point.y + momentumY).clamped(to: ranges.y)
287
333
 
288
334
  return CGPoint(x: targetX, y: targetY)
289
335
  }
@@ -0,0 +1,39 @@
1
+ // Copyright 2015-present 650 Industries. All rights reserved.
2
+
3
+ import CoreGraphics
4
+
5
+ struct FABInsets {
6
+ let top: CGFloat
7
+ let leading: CGFloat
8
+ let bottom: CGFloat
9
+ let trailing: CGFloat
10
+ }
11
+
12
+ struct FABPlacementRanges {
13
+ let x: ClosedRange<CGFloat>
14
+ let y: ClosedRange<CGFloat>
15
+ }
16
+
17
+ enum FABPlacement {
18
+ /// The ring is stroked on a path 4pt wider than the icon, so it paints this far outside it.
19
+ static let ringOverhang: CGFloat = 4
20
+
21
+ /// Origins the FAB frame may take, given the painted pill in that frame's coordinate space.
22
+ static func ranges(
23
+ bounds: CGSize,
24
+ safeArea: FABInsets,
25
+ drawnFrame: CGRect,
26
+ inset: CGFloat
27
+ ) -> FABPlacementRanges {
28
+ // Keep `inset` from each edge, but never sit further in than the safe area already requires.
29
+ let minX = max(safeArea.leading, inset) - drawnFrame.minX
30
+ let maxX = bounds.width - max(safeArea.trailing, inset) - drawnFrame.maxX
31
+ let minY = max(safeArea.top, inset) - drawnFrame.minY
32
+ let maxY = bounds.height - max(safeArea.bottom, inset) - drawnFrame.maxY
33
+
34
+ return FABPlacementRanges(
35
+ x: min(minX, maxX)...max(minX, maxX),
36
+ y: min(minY, maxY)...max(minY, maxY)
37
+ )
38
+ }
39
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-dev-menu",
3
- "version": "58.0.3",
3
+ "version": "58.0.5",
4
4
  "description": "Expo/React Native module with the developer menu.",
5
5
  "keywords": [
6
6
  "dev",
@@ -31,16 +31,16 @@
31
31
  "@testing-library/react-native": "^13.3.0",
32
32
  "@types/jest": "^29.2.1",
33
33
  "@types/node": "^22.14.0",
34
- "babel-preset-expo": "58.0.3",
34
+ "babel-preset-expo": "58.0.4",
35
35
  "expo-module-scripts": "56.0.3",
36
- "react": "19.2.3",
37
- "react-native": "0.88.0-rc.0"
36
+ "react": "19.3.0",
37
+ "react-native": "0.88.0-rc.1"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "expo": "*",
41
41
  "react-native": "*"
42
42
  },
43
- "gitHead": "917a61b581baeb852aa4fe331da2c1395d824351",
43
+ "gitHead": "add8cbf49f477cfd2283ae05af35b4713a773c55",
44
44
  "scripts": {
45
45
  "build": "pnpm run build:lib && pnpm run build:plugin",
46
46
  "build:lib": "expo-build src",