Skip to content

Commit 2aba6e5

Browse files
Munkhorgilbclaude
andcommitted
fix: deliver launcher button taps + hide launcher while sheet is open
The floating launcher button rendered but was never tappable: the PassthroughWindow hit-test discarded `rootViewController.view`, but SwiftUI flattens the Button into that single hosting view, so button taps fell through the window. Mark the overlay background non-interactive and forward SwiftUI's own hit result instead (empty space passes through, the button is delivered). Also suspend the launcher overlay while the messenger sheet is presented so the FAB no longer floats above it, restoring it on dismiss. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c784756 commit 2aba6e5

3 files changed

Lines changed: 40 additions & 9 deletions

File tree

Sources/MessengerSDK/Messenger/LauncherWindow.swift

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,17 @@ final class LauncherWindow {
4040
window = nil
4141
}
4242

43+
/// Whether the launcher overlay is currently on screen.
44+
var isVisible: Bool { window?.isHidden == false }
45+
46+
/// Temporarily hide the launcher without tearing down the window — used while the
47+
/// messenger sheet is presented so the button doesn't float over it. Pair with
48+
/// `resume()`. No-op when the launcher isn't shown.
49+
func suspend() { window?.isHidden = true }
50+
51+
/// Restore a launcher hidden by `suspend()`.
52+
func resume() { window?.isHidden = false }
53+
4354
private static var activeWindowScene: UIWindowScene? {
4455
UIApplication.shared.connectedScenes
4556
.compactMap { $0 as? UIWindowScene }
@@ -55,7 +66,12 @@ private struct LauncherOverlay: View {
5566
@ObservedObject private var sdk = MessengerSDK.shared
5667

5768
var body: some View {
69+
// The full-screen background must NOT capture touches, otherwise SwiftUI's
70+
// hosting view claims every point and the window can't tell the button apart
71+
// from empty space. With it non-interactive, the hosting view hit-tests to
72+
// `nil` over empty areas and to the button only where the button is drawn.
5873
Color.clear
74+
.allowsHitTesting(false)
5975
.overlay {
6076
if sdk.isReady {
6177
MessengerLaunchButton()
@@ -68,14 +84,21 @@ private struct LauncherOverlay: View {
6884

6985
// MARK: - Pass-through window
7086

71-
/// A window that only "catches" touches on its subviews (the button). Touches that
72-
/// land on the empty root view return `nil` from `hitTest`, so UIKit forwards them
73-
/// to the window underneath — the host app stays fully interactive.
87+
/// A window that only "catches" touches on the launcher button. The hosting view
88+
/// runs SwiftUI's own hit testing — because the background is marked non-interactive
89+
/// it returns `nil` for the transparent areas and the hosting view only where the
90+
/// button is drawn. We forward that result directly: button taps are delivered, and
91+
/// every other touch falls through to the app window underneath.
92+
///
93+
/// NOTE: we must NOT special-case `rootViewController.view` here. SwiftUI flattens
94+
/// the button into the single `_UIHostingView` (which *is* the root view), so doing
95+
/// so would discard the button's own taps.
7496
private final class PassthroughWindow: UIWindow {
7597
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
76-
guard let hit = super.hitTest(point, with: event) else { return nil }
77-
// The root view controller's own view is the transparent background — let
78-
// those touches fall through. Anything else is real launcher UI.
79-
return rootViewController?.view === hit ? nil : hit
98+
let hit = super.hitTest(point, with: event)
99+
// Over empty space the hosting view hit-tests to `nil`, so UIWindow falls
100+
// back to returning itself — treat that as a pass-through. Anywhere SwiftUI
101+
// claims the point (the button) it returns the hosting view, which we deliver.
102+
return hit === self ? nil : hit
80103
}
81104
}

Sources/MessengerSDK/MessengerSDK.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,16 @@ public final class MessengerSDK: ObservableObject {
9393
from viewController: UIViewController,
9494
onDismiss: (() -> Void)?
9595
) {
96+
// Hide the floating launcher while the sheet is up so it doesn't overlay it,
97+
// and restore it on dismiss only if it was visible to begin with.
98+
let launcherWasVisible = LauncherWindow.shared.isVisible
99+
LauncherWindow.shared.suspend()
100+
96101
let root = MessengerContainerView(appVM: appVM)
97-
let hostingVC = DismissCallbackHostingController(rootView: root, onDismiss: onDismiss)
102+
let hostingVC = DismissCallbackHostingController(rootView: root) {
103+
if launcherWasVisible { LauncherWindow.shared.resume() }
104+
onDismiss?()
105+
}
98106
hostingVC.modalPresentationStyle = .pageSheet
99107
if let sheet = hostingVC.sheetPresentationController {
100108
sheet.detents = [.large()]

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-native-erxes-sdk",
3-
"version": "0.30.2",
3+
"version": "0.30.3",
44
"description": "React Native wrapper for the erxes Messenger iOS SDK — headless <ErxesSDK> with a native-owned floating launcher.",
55
"main": "src/index.tsx",
66
"source": "src/index.tsx",

0 commit comments

Comments
 (0)