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 +8 -0
- package/android/build.gradle +2 -2
- package/ios/DevMenuManager.swift +4 -4
- package/ios/DevMenuWindow-default.swift +1 -2
- package/ios/DevMenuWindow-macOS.swift +1 -1
- package/ios/FAB/DevMenuFABView.swift +48 -48
- package/ios/FAB/DevMenuFABWindow.swift +38 -32
- package/ios/Modules/DevMenuPreferences.swift +1 -1
- package/ios/SwiftUI/DevMenuAppInfo.swift +1 -4
- package/ios/SwiftUI/DevMenuButtons.swift +2 -0
- package/ios/SwiftUI/DevMenuDeveloperTools.swift +2 -3
- package/package.json +3 -3
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
|
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 = '55.0.
|
|
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.
|
|
32
|
+
versionName '55.0.9'
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
buildTypes {
|
package/ios/DevMenuManager.swift
CHANGED
|
@@ -547,9 +547,9 @@ open class DevMenuManager: NSObject {
|
|
|
547
547
|
fabWindow = DevMenuFABWindow(manager: self, windowScene: windowScene)
|
|
548
548
|
}
|
|
549
549
|
|
|
550
|
-
public func updateFABVisibility(
|
|
550
|
+
public func updateFABVisibility() {
|
|
551
551
|
DispatchQueue.main.async { [weak self] in
|
|
552
|
-
guard let self
|
|
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
|
-
&&
|
|
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(
|
|
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
|
|
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 =
|
|
10
|
-
static let verticalPadding: CGFloat =
|
|
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("
|
|
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
|
-
|
|
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.
|
|
147
|
-
dampingFraction: 0.
|
|
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
|
|
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:
|
|
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
|
-
?
|
|
260
|
-
: bounds.width - self.fabSize.width -
|
|
259
|
+
? edgeMargin
|
|
260
|
+
: bounds.width - self.fabSize.width - edgeMargin
|
|
261
261
|
|
|
262
|
-
let minY =
|
|
263
|
-
let maxY = bounds.height - self.fabSize.height -
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
self
|
|
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
|
|
|
@@ -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.
|
|
48
|
-
.cornerRadius(18)
|
|
45
|
+
.background(Color.expoSecondarySystemBackground, in: RoundedRectangle(cornerRadius: 18))
|
|
49
46
|
}
|
|
50
47
|
}
|
|
51
48
|
}
|
|
@@ -79,15 +79,14 @@ struct DevMenuDeveloperTools: View {
|
|
|
79
79
|
Divider()
|
|
80
80
|
|
|
81
81
|
DevMenuToggleButton(
|
|
82
|
-
title: "
|
|
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.
|
|
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.
|
|
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.
|
|
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": "
|
|
50
|
+
"gitHead": "39a7a009e215eb71a112f4a20dba2d471ab21108"
|
|
51
51
|
}
|