expo-dev-menu 58.0.4 → 58.0.6
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 +12 -0
- package/android/build.gradle +2 -2
- package/expo-dev-menu.podspec +9 -0
- package/ios/DevMenuManager.swift +44 -7
- package/ios/FAB/DevMenuFABView.swift +67 -21
- package/ios/FAB/FABPlacement.swift +39 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,18 @@
|
|
|
10
10
|
|
|
11
11
|
### 💡 Others
|
|
12
12
|
|
|
13
|
+
## 58.0.6 — 2026-09-23
|
|
14
|
+
|
|
15
|
+
### 🐛 Bug fixes
|
|
16
|
+
|
|
17
|
+
- [iOS] Fix a data race on `DevMenuManager`'s registered callbacks and app context that could crash the app when one app context registered its dev menu callbacks while another tore down, such as on a dev-client reload. ([#49559](https://github.com/expo/expo/pull/49559) by [@vonovak](https://github.com/vonovak))
|
|
18
|
+
|
|
19
|
+
## 58.0.5 — 2026-09-22
|
|
20
|
+
|
|
21
|
+
### 🐛 Bug fixes
|
|
22
|
+
|
|
23
|
+
- [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))
|
|
24
|
+
|
|
13
25
|
## 58.0.4 — 2026-09-21
|
|
14
26
|
|
|
15
27
|
_This version does not introduce any user-facing changes._
|
package/android/build.gradle
CHANGED
|
@@ -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.
|
|
15
|
+
version = '58.0.6'
|
|
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.
|
|
32
|
+
versionName '58.0.6'
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
buildTypes {
|
package/expo-dev-menu.podspec
CHANGED
|
@@ -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|
|
|
@@ -125,9 +128,15 @@ Pod::Spec.new do |s|
|
|
|
125
128
|
test_spec.dependency 'React'
|
|
126
129
|
# ExpoModulesCore requires React-hermes or React-jsc in tests, add ExpoModulesTestCore for the underlying dependencies
|
|
127
130
|
test_spec.dependency 'ExpoModulesTestCore'
|
|
131
|
+
test_spec.pod_target_xcconfig = {
|
|
132
|
+
'OTHER_LDFLAGS' => '$(inherited) -lc++'
|
|
133
|
+
}
|
|
128
134
|
test_spec.platforms = {
|
|
129
135
|
:ios => '16.4'
|
|
130
136
|
}
|
|
137
|
+
test_spec.pod_target_xcconfig = {
|
|
138
|
+
'OTHER_LDFLAGS' => '$(inherited) -lc++'
|
|
139
|
+
}
|
|
131
140
|
end
|
|
132
141
|
|
|
133
142
|
s.default_subspec = ['Main']
|
package/ios/DevMenuManager.swift
CHANGED
|
@@ -55,6 +55,18 @@ class DevMenuCacheContainer<T> {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
final class DevMenuCallbacksRegistry {
|
|
59
|
+
private let storage = Mutex<[DevMenuManager.Callback]>([])
|
|
60
|
+
|
|
61
|
+
var callbacks: [DevMenuManager.Callback] {
|
|
62
|
+
storage.withLock { $0 }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func replace(with callbacks: [DevMenuManager.Callback]) {
|
|
66
|
+
storage.withLock { $0 = callbacks }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
58
70
|
/**
|
|
59
71
|
Manages the dev menu and provides most of the public API.
|
|
60
72
|
*/
|
|
@@ -74,10 +86,17 @@ open class DevMenuManager: NSObject {
|
|
|
74
86
|
var canLaunchDevMenuOnStart = true
|
|
75
87
|
@objc public var isReactAppRunning = false
|
|
76
88
|
|
|
89
|
+
private let appContextLock = NSLock()
|
|
90
|
+
private weak var _currentAppContext: AppContext?
|
|
91
|
+
|
|
77
92
|
/**
|
|
78
93
|
The AppContext for the currently running React app. Set by DevMenuModule.OnCreate.
|
|
79
94
|
*/
|
|
80
|
-
public
|
|
95
|
+
public var currentAppContext: AppContext? {
|
|
96
|
+
appContextLock.lock()
|
|
97
|
+
defer { appContextLock.unlock() }
|
|
98
|
+
return _currentAppContext
|
|
99
|
+
}
|
|
81
100
|
|
|
82
101
|
@objc public var configuration = DevMenuConfiguration()
|
|
83
102
|
|
|
@@ -179,7 +198,9 @@ open class DevMenuManager: NSObject {
|
|
|
179
198
|
|
|
180
199
|
@objc
|
|
181
200
|
public func setAppContext(_ appContext: AppContext?) {
|
|
182
|
-
|
|
201
|
+
appContextLock.lock()
|
|
202
|
+
_currentAppContext = appContext
|
|
203
|
+
appContextLock.unlock()
|
|
183
204
|
if appContext != nil {
|
|
184
205
|
isReloading = false
|
|
185
206
|
lastReloadEventAt = Date()
|
|
@@ -202,10 +223,17 @@ open class DevMenuManager: NSObject {
|
|
|
202
223
|
*/
|
|
203
224
|
@objc
|
|
204
225
|
public func clearAppContext(current context: AppContext?) {
|
|
205
|
-
|
|
226
|
+
appContextLock.lock()
|
|
227
|
+
let shouldClear = _currentAppContext == nil || _currentAppContext === context
|
|
228
|
+
if shouldClear {
|
|
229
|
+
_currentAppContext = nil
|
|
230
|
+
}
|
|
231
|
+
appContextLock.unlock()
|
|
232
|
+
guard shouldClear else {
|
|
206
233
|
return
|
|
207
234
|
}
|
|
208
|
-
|
|
235
|
+
isReactAppRunning = false
|
|
236
|
+
updateAutoLaunchObserver()
|
|
209
237
|
}
|
|
210
238
|
|
|
211
239
|
@objc
|
|
@@ -470,9 +498,18 @@ open class DevMenuManager: NSObject {
|
|
|
470
498
|
callbacksSubject.eraseToAnyPublisher()
|
|
471
499
|
}
|
|
472
500
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
501
|
+
private let callbacksRegistry = DevMenuCallbacksRegistry()
|
|
502
|
+
|
|
503
|
+
public var registeredCallbacks: [Callback] {
|
|
504
|
+
get {
|
|
505
|
+
callbacksRegistry.callbacks
|
|
506
|
+
}
|
|
507
|
+
set {
|
|
508
|
+
callbacksRegistry.replace(with: newValue)
|
|
509
|
+
// PassthroughSubject doesn't support concurrent sends.
|
|
510
|
+
DispatchQueue.main.async { [weak self] in
|
|
511
|
+
self?.callbacksSubject.send(newValue)
|
|
512
|
+
}
|
|
476
513
|
}
|
|
477
514
|
}
|
|
478
515
|
|
|
@@ -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
|
|
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:
|
|
247
|
-
y: storedPos.y.clamped(to:
|
|
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
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
|
281
|
-
|
|
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
|
+
"version": "58.0.6",
|
|
4
4
|
"description": "Expo/React Native module with the developer menu.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dev",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"expo": "*",
|
|
41
41
|
"react-native": "*"
|
|
42
42
|
},
|
|
43
|
-
"gitHead": "
|
|
43
|
+
"gitHead": "8aeb4368d396d2d71a2fc8c652c4d1d4d53ed858",
|
|
44
44
|
"scripts": {
|
|
45
45
|
"build": "pnpm run build:lib && pnpm run build:plugin",
|
|
46
46
|
"build:lib": "expo-build src",
|