expo-dev-menu 55.0.3 → 55.0.4

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,15 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 55.0.4 — 2026-02-03
14
+
15
+ ### 🐛 Bug fixes
16
+
17
+ - [Android] Fix `null cannot be cast to non-null type expo.modules.devmenu.DevMenuFragment`. ([#42660](https://github.com/expo/expo/pull/42660) by [@lukmccall](https://github.com/lukmccall))
18
+ - [iOS] Fix null current bridge in standalone mode ([#42666](https://github.com/expo/expo/pull/42666) by [@gabrieldonadel](https://github.com/gabrieldonadel))
19
+ - [Android] Fix `cmd + m` not opening the dev menu. ([#42701](https://github.com/expo/expo/pull/42701) by [@lukmccall](https://github.com/lukmccall))
20
+ - [iOS] Restore dev menu on tvOS. ([#42737](https://github.com/expo/expo/pull/42737) by [@douglowder](https://github.com/douglowder))
21
+
13
22
  ## 55.0.3 — 2026-01-27
14
23
 
15
24
  ### 🎉 New features
@@ -21,6 +30,7 @@
21
30
 
22
31
  - [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
32
  - [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))
33
+ - [Quest] Fix Floating action button not responding to presses. ([#42563](https://github.com/expo/expo/pull/42563) by [@behenate](https://github.com/behenate))
24
34
 
25
35
  ### 💡 Others
26
36
 
@@ -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.3'
15
+ version = '55.0.4'
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.3'
32
+ versionName '55.0.4'
33
33
  }
34
34
 
35
35
  buildTypes {
@@ -28,9 +28,9 @@ import expo.modules.devmenu.compose.DevMenuState
28
28
  import expo.modules.devmenu.compose.DevMenuViewModel
29
29
  import expo.modules.devmenu.compose.newtheme.AppTheme
30
30
  import expo.modules.devmenu.compose.ui.DevMenuBottomSheet
31
+ import expo.modules.devmenu.detectors.InterceptingWindowCallback
31
32
  import expo.modules.devmenu.detectors.ShakeDetector
32
33
  import expo.modules.devmenu.detectors.ThreeFingerLongPressDetector
33
- import expo.modules.devmenu.detectors.TouchInterceptingWindowCallback
34
34
  import expo.modules.devmenu.devtools.DevMenuDevToolsDelegate
35
35
  import expo.modules.devmenu.fab.MovableFloatingActionButton
36
36
  import expo.modules.devmenu.helpers.isAcceptingText
@@ -118,23 +118,29 @@ class DevMenuFragment(
118
118
 
119
119
  // Wrap window callback to intercept touch events at the window level
120
120
  activity?.window?.let { window ->
121
- val detector = ThreeFingerLongPressDetector(
121
+ val fingerLongPressDetector = ThreeFingerLongPressDetector(
122
122
  lifecycleScope,
123
123
  ::onThreeFingerLongPressDetected
124
124
  )
125
+ val weakSelf = this.weak()
126
+ val keyEventDispatcher = { event: KeyEvent ->
127
+ weakSelf.get()?.onKeyUp(event.keyCode, event) ?: false
128
+ }
125
129
 
126
130
  val currentCallback = window.callback
127
131
  // Avoid wrapping multiple times
128
- if (currentCallback !is TouchInterceptingWindowCallback) {
132
+ if (currentCallback !is InterceptingWindowCallback) {
129
133
  originalWindowCallback = currentCallback
130
134
  window.callback =
131
- TouchInterceptingWindowCallback(
135
+ InterceptingWindowCallback(
132
136
  currentCallback,
133
- detector
137
+ fingerLongPressDetector,
138
+ keyEventDispatcher
134
139
  )
135
140
  } else {
136
141
  // When user reloads the app, the fragment is restarted but the window callback remains the same
137
- currentCallback.updateDetector(detector)
142
+ currentCallback.updateDetector(fingerLongPressDetector)
143
+ currentCallback.updateKeyEventDispatcher(keyEventDispatcher)
138
144
  }
139
145
  }
140
146
  }
@@ -312,7 +318,7 @@ class DevMenuFragment(
312
318
 
313
319
  internal fun findIn(activity: Activity?): DevMenuFragment? {
314
320
  val activity = activity ?: return null
315
- return (activity as FragmentActivity).supportFragmentManager.findFragmentByTag(TAG) as DevMenuFragment
321
+ return (activity as FragmentActivity).supportFragmentManager.findFragmentByTag(TAG) as? DevMenuFragment
316
322
  }
317
323
 
318
324
  private data class KeyCommand(val code: Int, val withShift: Boolean = false)
@@ -1,6 +1,7 @@
1
1
  package expo.modules.devmenu.detectors
2
2
 
3
3
  import android.os.Build
4
+ import android.view.KeyEvent
4
5
  import android.view.KeyboardShortcutGroup
5
6
  import android.view.Menu
6
7
  import android.view.MotionEvent
@@ -8,23 +9,39 @@ import android.view.Window
8
9
  import androidx.annotation.RequiresApi
9
10
 
10
11
  /**
11
- * A [Window.Callback] that intercepts touch events to detect three-finger long press gestures.
12
+ * A [Window.Callback] that intercepts touch events to detect three-finger long press gestures and
13
+ * dispatchKeyEvent to detect cmd + m.
12
14
  * Delegates all other calls to the [wrapped] callback.
13
15
  */
14
- internal class TouchInterceptingWindowCallback(
16
+ internal class InterceptingWindowCallback(
15
17
  private val wrapped: Window.Callback,
16
- private var detector: ThreeFingerLongPressDetector
18
+ private var threeFingerLongPressDetector: ThreeFingerLongPressDetector,
19
+ private var keyEventDispatcher: (KeyEvent) -> Boolean
17
20
  ) : Window.Callback by wrapped {
18
21
  fun updateDetector(newDetector: ThreeFingerLongPressDetector) {
19
- detector.cancelDetection()
20
- detector = newDetector
22
+ threeFingerLongPressDetector.cancelDetection()
23
+ threeFingerLongPressDetector = newDetector
24
+ }
25
+
26
+ fun updateKeyEventDispatcher(newDispatcher: (KeyEvent) -> Boolean) {
27
+ keyEventDispatcher = newDispatcher
21
28
  }
22
29
 
23
30
  override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
24
- detector.onTouchEvent(event)
31
+ threeFingerLongPressDetector.onTouchEvent(event)
25
32
  return wrapped.dispatchTouchEvent(event)
26
33
  }
27
34
 
35
+ override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
36
+ event?.let { event ->
37
+ val isMenuKey = event.keyCode == KeyEvent.KEYCODE_MENU && event.action == KeyEvent.ACTION_UP
38
+ if (isMenuKey) {
39
+ return keyEventDispatcher(event)
40
+ }
41
+ }
42
+ return wrapped.dispatchKeyEvent(event)
43
+ }
44
+
28
45
  @RequiresApi(Build.VERSION_CODES.O)
29
46
  override fun onPointerCaptureChanged(hasCapture: Boolean) {
30
47
  wrapped.onPointerCaptureChanged(hasCapture)
@@ -86,16 +86,16 @@ fun FloatingActionButtonContent(
86
86
  interactionSource = interactionSource,
87
87
  indication = null,
88
88
  onClick = {
89
- onRefreshPress()
90
89
  scope.launch {
91
90
  animatedRotation.animateTo(
92
91
  targetValue = 360f,
93
92
  animationSpec = spring(
94
93
  dampingRatio = Spring.DampingRatioLowBouncy,
95
94
  stiffness = Spring.StiffnessVeryLow,
96
- visibilityThreshold = 2f
95
+ visibilityThreshold = 5f
97
96
  )
98
97
  )
98
+ onRefreshPress()
99
99
  animatedRotation.snapTo(0f)
100
100
  }
101
101
  }
@@ -123,7 +123,6 @@ fun MovableFloatingActionButton(
123
123
  .coerceIn(maxX = bounds.x, maxY = bounds.y)
124
124
  dragDistance += change.positionChange().getDistance()
125
125
  velocityTracker.registerPosition(dragOffset.x, dragOffset.y)
126
- change.consume()
127
126
 
128
127
  if (dragDistance > ClickDragTolerance) {
129
128
  launch {
@@ -35,6 +35,12 @@ Pod::Spec.new do |s|
35
35
  s.requires_arc = true
36
36
  s.header_dir = 'EXDevMenu'
37
37
 
38
+ s.resource_bundles = {
39
+ 'EXDevMenu' => [
40
+ 'ios/Assets.xcassets',
41
+ ]
42
+ }
43
+
38
44
  s.xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => 'EX_DEV_MENU_ENABLED=1', 'OTHER_SWIFT_FLAGS' => '-DEX_DEV_MENU_ENABLED' }
39
45
 
40
46
  s.user_target_xcconfig = {
@@ -0,0 +1,6 @@
1
+ {
2
+ "info" : {
3
+ "author" : "xcode",
4
+ "version" : 1
5
+ }
6
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "images" : [
3
+ {
4
+ "filename" : "dev-tools.png",
5
+ "idiom" : "universal",
6
+ "scale" : "1x"
7
+ },
8
+ {
9
+ "idiom" : "universal",
10
+ "scale" : "2x"
11
+ },
12
+ {
13
+ "idiom" : "universal",
14
+ "scale" : "3x"
15
+ }
16
+ ],
17
+ "info" : {
18
+ "author" : "xcode",
19
+ "version" : 1
20
+ }
21
+ }
@@ -76,6 +76,8 @@ open class DevMenuManager: NSObject {
76
76
 
77
77
  static public var wasInitilized = false
78
78
 
79
+ private var contentDidAppearObserver: NSObjectProtocol?
80
+
79
81
  /**
80
82
  Shared singleton instance.
81
83
  */
@@ -99,6 +101,8 @@ open class DevMenuManager: NSObject {
99
101
 
100
102
  var currentScreen: String?
101
103
 
104
+ private var isNavigatingHome = false
105
+
102
106
  weak var hostDelegate: DevMenuHostDelegate?
103
107
 
104
108
  @objc
@@ -109,14 +113,32 @@ open class DevMenuManager: NSObject {
109
113
  if let currentBridge {
110
114
  DispatchQueue.main.async {
111
115
  self.disableRNDevMenuHoykeys(for: currentBridge)
112
- self.updateFABVisibility()
113
116
  }
117
+ observeContentDidAppear()
114
118
  } else {
115
119
  updateFABVisibility()
116
120
  }
117
121
  }
118
122
  }
119
123
 
124
+ private func observeContentDidAppear() {
125
+ if let observer = contentDidAppearObserver {
126
+ NotificationCenter.default.removeObserver(observer)
127
+ }
128
+
129
+ contentDidAppearObserver = NotificationCenter.default.addObserver(
130
+ forName: NSNotification.Name.RCTContentDidAppear,
131
+ object: nil,
132
+ queue: .main
133
+ ) { [weak self] _ in
134
+ self?.updateFABVisibility()
135
+ if let observer = self?.contentDidAppearObserver {
136
+ NotificationCenter.default.removeObserver(observer)
137
+ self?.contentDidAppearObserver = nil
138
+ }
139
+ }
140
+ }
141
+
120
142
  private let manifestSubject = PassthroughSubject<Void, Never>()
121
143
  public var manifestPublisher: AnyPublisher<Void, Never> {
122
144
  manifestSubject.eraseToAnyPublisher()
@@ -166,9 +188,17 @@ open class DevMenuManager: NSObject {
166
188
  return DevMenuPreferences.touchGestureEnabled
167
189
  }
168
190
 
191
+ @objc
192
+ public func setShowFloatingActionButton(_ enabled: Bool) {
193
+ DevMenuPreferences.showFloatingActionButton = enabled
194
+ }
195
+
169
196
  @objc
170
197
  public func updateCurrentBridge(_ bridge: RCTBridge?) {
171
198
  currentBridge = bridge
199
+ if bridge != nil {
200
+ isNavigatingHome = false
201
+ }
172
202
  }
173
203
 
174
204
  @objc
@@ -283,11 +313,7 @@ open class DevMenuManager: NSObject {
283
313
  @objc
284
314
  @discardableResult
285
315
  public func hideMenu() -> Bool {
286
- let result = setVisibility(false)
287
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
288
- self?.updateFABVisibility()
289
- }
290
- return result
316
+ return setVisibility(false)
291
317
  }
292
318
 
293
319
  /**
@@ -323,6 +349,12 @@ open class DevMenuManager: NSObject {
323
349
  return
324
350
  }
325
351
 
352
+ isNavigatingHome = true
353
+
354
+ #if !os(macOS) && !os(tvOS)
355
+ fabWindow?.setVisible(false, animated: false)
356
+ #endif
357
+
326
358
  let action: () -> Void = {
327
359
  delegate.devMenuNavigateHome?()
328
360
  }
@@ -414,6 +446,8 @@ open class DevMenuManager: NSObject {
414
446
  DispatchQueue.main.async {
415
447
  #if os(macOS)
416
448
  self.window?.makeKeyAndOrderFront(nil)
449
+ #elseif os(tvOS)
450
+ self.window?.makeKeyAndVisible()
417
451
  #else
418
452
  self.updateFABVisibility()
419
453
 
@@ -428,9 +462,6 @@ open class DevMenuManager: NSObject {
428
462
  } else {
429
463
  DispatchQueue.main.async {
430
464
  self.window?.closeBottomSheet(nil)
431
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
432
- self.updateFABVisibility()
433
- }
434
465
  }
435
466
  }
436
467
  return true
@@ -516,8 +547,7 @@ open class DevMenuManager: NSObject {
516
547
  fabWindow = DevMenuFABWindow(manager: self, windowScene: windowScene)
517
548
  }
518
549
 
519
- @objc
520
- public func updateFABVisibility() {
550
+ public func updateFABVisibility(menuDismissing: Bool = false) {
521
551
  DispatchQueue.main.async { [weak self] in
522
552
  guard let self = self else { return }
523
553
 
@@ -528,13 +558,16 @@ open class DevMenuManager: NSObject {
528
558
  }
529
559
  }
530
560
 
531
- let shouldShow = DevMenuPreferences.showFloatingActionButton && !self.isVisible && self.currentBridge != nil
561
+ let shouldShow = DevMenuPreferences.showFloatingActionButton
562
+ && (menuDismissing || !self.isVisible)
563
+ && self.currentBridge != nil
564
+ && !self.isNavigatingHome
565
+ && DevMenuPreferences.isOnboardingFinished
532
566
  self.fabWindow?.setVisible(shouldShow, animated: true)
533
567
  }
534
568
  }
535
569
  #else
536
- @objc
537
- public func updateFABVisibility() {
570
+ public func updateFABVisibility(menuDismissing: Bool = false) {
538
571
  // FAB not available on macOS/tvOS
539
572
  }
540
573
  #endif
@@ -46,7 +46,12 @@ class DevMenuViewController: UIViewController {
46
46
  let rootView = DevMenuRootView()
47
47
  let hostingController = UIHostingController(rootView: rootView)
48
48
 
49
+ #if os(tvOS)
50
+ hostingController.view.backgroundColor =
51
+ preferredUserInterfaceStyle == .dark ? UIColor.systemGray.withAlphaComponent(0.8) : UIColor.white.withAlphaComponent(0.8)
52
+ #else
49
53
  hostingController.view.backgroundColor = UIColor.clear
54
+ #endif
50
55
 
51
56
  addChild(hostingController)
52
57
  view.addSubview(hostingController.view)
@@ -26,7 +26,7 @@ class DevMenuWindow: UIWindow, PresentationControllerDelegate {
26
26
  self.rootViewController = UIViewController()
27
27
  self.backgroundColor = UIColor(white: 0, alpha: 0.4)
28
28
  #if os(tvOS)
29
- self.windowLevel = .normal
29
+ self.windowLevel = .normal + 1
30
30
  #else
31
31
  self.windowLevel = .statusBar
32
32
  #endif
@@ -103,11 +103,12 @@ class DevMenuWindow: UIWindow, PresentationControllerDelegate {
103
103
  self.backgroundColor = .clear
104
104
  }
105
105
 
106
+ DevMenuManager.shared.updateFABVisibility(menuDismissing: true)
107
+
106
108
  devMenuViewController.dismiss(animated: true) {
107
109
  self.isDismissing = false
108
110
  self.isHidden = true
109
111
  self.backgroundColor = UIColor(white: 0, alpha: 0.4)
110
- DevMenuManager.shared.updateFABVisibility()
111
112
  completion?()
112
113
  }
113
114
  }
@@ -7,9 +7,12 @@ import SwiftUI
7
7
  enum FABConstants {
8
8
  static let iconSize: CGFloat = 44
9
9
  static let margin: CGFloat = 16
10
- static let dragThreshold: CGFloat = 40
10
+ static let verticalPadding: CGFloat = 20
11
+ static let dragThreshold: CGFloat = 10
11
12
  static let momentumFactor: CGFloat = 0.35
12
13
  static let labelDismissDelay: TimeInterval = 10
14
+ static let idleTimeout: UInt64 = 5_000_000_000
15
+ static let imageSize: CGFloat = 26
13
16
 
14
17
  static let snapAnimation: Animation = .spring(
15
18
  response: 0.6,
@@ -20,23 +23,33 @@ enum FABConstants {
20
23
 
21
24
  struct FabPill: View {
22
25
  @Binding var isPressed: Bool
26
+ @Binding var isDragging: Bool
23
27
  @State private var showLabel = true
28
+ @State private var isIdle = false
29
+ @State private var idleTask: Task<Void, Never>?
30
+
31
+ private var isInteracting: Bool {
32
+ isPressed || isDragging
33
+ }
24
34
 
25
35
  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)
36
+ VStack(spacing: 8) {
37
+ actionButton
38
38
  .scaleEffect(isPressed ? 0.9 : 1.0)
39
39
  .animation(.easeInOut(duration: 0.1), value: isPressed)
40
+ .onChange(of: isInteracting) { interacting in
41
+ if interacting {
42
+ idleTask?.cancel()
43
+ withAnimation {
44
+ isIdle = false
45
+ }
46
+ } else {
47
+ startIdleTimer()
48
+ }
49
+ }
50
+ .onAppear {
51
+ startIdleTimer()
52
+ }
40
53
 
41
54
  if showLabel {
42
55
  Text("Dev tools")
@@ -49,7 +62,12 @@ struct FabPill: View {
49
62
  .transition(.opacity.combined(with: .scale(scale: 0.8)))
50
63
  }
51
64
  }
65
+ .saturation(isIdle ? 0 : 1)
66
+ .opacity(isIdle ? 0.5 : 1)
67
+ .animation(.easeInOut(duration: 0.3), value: isIdle)
52
68
  .task {
69
+ // [Alan] This is poor practice but without it, the label is not included in the drag gesture
70
+ // and remains in it's original posistion.
53
71
  try? await Task.sleep(nanoseconds: UInt64(1_000_000_000 * FABConstants.labelDismissDelay))
54
72
  await MainActor.run {
55
73
  withAnimation(.easeOut(duration: 0.3)) {
@@ -58,19 +76,78 @@ struct FabPill: View {
58
76
  }
59
77
  }
60
78
  }
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
+ private func startIdleTimer() {
90
+ idleTask?.cancel()
91
+ idleTask = Task {
92
+ try? await Task.sleep(nanoseconds: FABConstants.idleTimeout)
93
+ guard !Task.isCancelled else { return }
94
+ await MainActor.run {
95
+ withAnimation {
96
+ isIdle = true
97
+ }
98
+ }
99
+ }
100
+ }
101
+
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 {
118
+ Image(systemName: "gearshape.fill")
119
+ .resizable()
120
+ .frame(width: FABConstants.imageSize, height: FABConstants.imageSize)
121
+ .foregroundStyle(.white)
122
+ .frame(width: FABConstants.iconSize, height: FABConstants.iconSize)
123
+ .background(Color.blue, in: Circle())
124
+ .background(
125
+ Circle()
126
+ .stroke(Color.blue.opacity(0.5), lineWidth: 4)
127
+ .frame(width: FABConstants.iconSize + 4, height: FABConstants.iconSize + 4)
128
+ )
129
+ .shadow(color: .black.opacity(0.4), radius: 8, x: 0, y: 4)
130
+ }
61
131
  }
62
132
 
63
133
  struct DevMenuFABView: View {
64
134
  let onOpenMenu: () -> Void
65
135
  let onFrameChange: (CGRect) -> Void
66
136
 
67
- private let fabSize = CGSize(width: 72, height: FABConstants.iconSize + 24)
137
+ private let fabSize = CGSize(width: 72, height: FABConstants.iconSize + 50)
68
138
 
69
139
  @State private var position: CGPoint = .zero
140
+ @State private var targetPosition: CGPoint = .zero
70
141
  @State private var isDragging = false
71
142
  @State private var isPressed = false
72
143
  @State private var dragStartPosition: CGPoint = .zero
73
144
 
145
+ private let dragSpring: Animation = .spring(
146
+ response: 0.35,
147
+ dampingFraction: 0.35,
148
+ blendDuration: 0
149
+ )
150
+
74
151
  private var currentFrame: CGRect {
75
152
  CGRect(origin: position, size: fabSize)
76
153
  }
@@ -78,7 +155,8 @@ struct DevMenuFABView: View {
78
155
  var body: some View {
79
156
  GeometryReader { geometry in
80
157
  let safeArea = geometry.safeAreaInsets
81
- FabPill(isPressed: $isPressed)
158
+
159
+ FabPill(isPressed: $isPressed, isDragging: $isDragging)
82
160
  .frame(width: fabSize.width, height: fabSize.height)
83
161
  .position(x: currentFrame.midX, y: currentFrame.midY)
84
162
  .gesture(dragGesture(bounds: geometry.size, safeArea: safeArea))
@@ -97,7 +175,7 @@ struct DevMenuFABView: View {
97
175
  position = newPos
98
176
  onFrameChange(CGRect(origin: newPos, size: fabSize))
99
177
  }
100
- .animation(isDragging ? nil : FABConstants.snapAnimation, value: position)
178
+ .animation(isDragging ? dragSpring : FABConstants.snapAnimation, value: position)
101
179
  }
102
180
  .ignoresSafeArea()
103
181
  }
@@ -114,9 +192,18 @@ struct DevMenuFABView: View {
114
192
  dragStartPosition = position
115
193
  }
116
194
  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
+ let rawX = dragStartPosition.x + value.translation.width
202
+ let rawY = dragStartPosition.y + value.translation.height
203
+
117
204
  position = CGPoint(
118
- x: dragStartPosition.x + value.translation.width,
119
- y: dragStartPosition.y + value.translation.height
205
+ x: rawX.clamped(to: minX...maxX),
206
+ y: rawY.clamped(to: minY...maxY)
120
207
  )
121
208
  onFrameChange(currentFrame)
122
209
  }
@@ -153,7 +240,7 @@ struct DevMenuFABView: View {
153
240
  private func defaultPosition(bounds: CGSize, safeArea: EdgeInsets) -> CGPoint {
154
241
  CGPoint(
155
242
  x: bounds.width - fabSize.width - FABConstants.margin,
156
- y: (bounds.height / 2) - fabSize.height
243
+ y: bounds.height * 0.25
157
244
  )
158
245
  }
159
246
 
@@ -172,8 +259,8 @@ struct DevMenuFABView: View {
172
259
  ? margin
173
260
  : bounds.width - self.fabSize.width - margin
174
261
 
175
- let minY = margin + safeArea.top
176
- let maxY = bounds.height - self.fabSize.height - margin - safeArea.bottom
262
+ let minY = margin + safeArea.top + FABConstants.verticalPadding
263
+ let maxY = bounds.height - self.fabSize.height - margin - safeArea.bottom - FABConstants.verticalPadding
177
264
  let targetY = (point.y + momentumY).clamped(to: minY...maxY)
178
265
 
179
266
  return CGPoint(x: targetX, y: targetY)
@@ -5,12 +5,7 @@ import ExpoModulesCore
5
5
 
6
6
  @objc
7
7
  public class ExpoDevMenuReactDelegateHandler: ExpoReactDelegateHandler {
8
- public override func createReactRootView(
9
- reactDelegate: ExpoReactDelegate,
10
- moduleName: String,
11
- initialProperties: [AnyHashable: Any]?,
12
- launchOptions: [UIApplication.LaunchOptionsKey: Any]?
13
- ) -> UIView? {
8
+ public override func createRootViewController() -> UIViewController? {
14
9
  if EXAppDefines.APP_DEBUG {
15
10
  DevMenuManager.shared.updateCurrentBridge(RCTBridge.current())
16
11
  }
@@ -0,0 +1,11 @@
1
+ // Copyright 2015-present 650 Industries. All rights reserved.
2
+
3
+ import Foundation
4
+
5
+ func getDevMenuBundle() -> Bundle? {
6
+ if let bundleURL = Bundle.main.url(forResource: "EXDevMenu", withExtension: "bundle"),
7
+ let bundle = Bundle(url: bundleURL) {
8
+ return bundle
9
+ }
10
+ return .main
11
+ }