diff --git a/Bitkit/Extensions/View+SwipeGestures.swift b/Bitkit/Extensions/View+SwipeGestures.swift index b713e9118..46ec555a2 100644 --- a/Bitkit/Extensions/View+SwipeGestures.swift +++ b/Bitkit/Extensions/View+SwipeGestures.swift @@ -14,33 +14,41 @@ extension View { // MARK: Segmented tab swipes /// Swipe left/right to move between adjacent tabs (same order as `T.allCases` / `SegmentedControl`). + /// Pass `tabs` when the control shows only some cases, in its display order. + /// Pass `simultaneous` inside a sheet, so a vertical drag still reaches the sheet's swipe-to-dismiss. + @ViewBuilder func swipeSegmentedTabs( selection: Binding, + tabs visibleTabs: [T]? = nil, minimumDragDistance: CGFloat = 20, swipeThreshold: CGFloat = 50, - animation: Animation = .easeInOut(duration: 0.2) + animation: Animation = .easeInOut(duration: 0.2), + simultaneous: Bool = false ) -> some View { - highPriorityGesture( - DragGesture(minimumDistance: minimumDragDistance, coordinateSpace: .local) - .onEnded { value in - let horizontalAmount = value.translation.width - let verticalAmount = value.translation.height - guard abs(horizontalAmount) > abs(verticalAmount) else { return } - - let tabs = Array(T.allCases) - guard let currentIndex = tabs.firstIndex(of: selection.wrappedValue) else { return } - - if horizontalAmount < -swipeThreshold, currentIndex < tabs.count - 1 { - withAnimation(animation) { - selection.wrappedValue = tabs[currentIndex + 1] - } - } else if horizontalAmount > swipeThreshold, currentIndex > 0 { - withAnimation(animation) { - selection.wrappedValue = tabs[currentIndex - 1] - } + let drag = DragGesture(minimumDistance: minimumDragDistance, coordinateSpace: .local) + .onEnded { value in + let horizontalAmount = value.translation.width + let verticalAmount = value.translation.height + guard abs(horizontalAmount) > abs(verticalAmount) else { return } + + let tabs = visibleTabs ?? Array(T.allCases) + guard let currentIndex = tabs.firstIndex(of: selection.wrappedValue) else { return } + + if horizontalAmount < -swipeThreshold, currentIndex < tabs.count - 1 { + withAnimation(animation) { + selection.wrappedValue = tabs[currentIndex + 1] + } + } else if horizontalAmount > swipeThreshold, currentIndex > 0 { + withAnimation(animation) { + selection.wrappedValue = tabs[currentIndex - 1] } } - ) + } + if simultaneous { + simultaneousGesture(drag) + } else { + highPriorityGesture(drag) + } } } diff --git a/Bitkit/Views/Wallets/UsdtReceiveView.swift b/Bitkit/Views/Wallets/UsdtReceiveView.swift index a1fb480ef..be5871b22 100644 --- a/Bitkit/Views/Wallets/UsdtReceiveView.swift +++ b/Bitkit/Views/Wallets/UsdtReceiveView.swift @@ -70,6 +70,7 @@ struct UsdtReceiveView: View { onEdit: { draftAmount = amount; page = .amount }, onNetwork: { page = .networks } ) + .swipeSegmentedTabs(selection: Binding(get: { .usdt }, set: onSelectTab), tabs: tabs.map(\.tab), simultaneous: true) case .networks: networkList case .amount: diff --git a/changelog.d/next/1020.fixed.md b/changelog.d/next/1020.fixed.md new file mode 100644 index 000000000..9b98106fd --- /dev/null +++ b/changelog.d/next/1020.fixed.md @@ -0,0 +1 @@ +Swiping over the USDT receive QR now moves to the previous tab. diff --git a/journeys/receive/usdt-receive-swipe-tabs.xml b/journeys/receive/usdt-receive-swipe-tabs.xml new file mode 100644 index 000000000..593b8a3d2 --- /dev/null +++ b/journeys/receive/usdt-receive-swipe-tabs.xml @@ -0,0 +1,30 @@ + + + On the Receive sheet, swiping sideways over the QR changes tabs on every tab, USDT included. + With USDT selected, a swipe to the right over its QR selects the tab before it (Spending) and + does not copy the address; a swipe to the left does nothing, since USDT is the last tab. A single + tap on the USDT QR still copies the address and shows the "copied" tooltip. + + Precondition: onboarded dev wallet with USDT enabled (introductory sheet or Settings, General, + Payments), no CJIT invoice pending, start on the wallet home screen. The USDT address loads + without funding; wait for the QR before swiping. + + The copy shows as a tooltip over the QR for about two seconds, so a check for "no copy" reads a + screenshot taken right after the swipe. Swipe with AXe (`axe swipe --start-x 80 --start-y 420 + --end-x 320 --end-y 420 --duration 0.3 --udid ` on a 390 pt wide simulator) across the + middle of the QR, about 60% of its width. On a wallet without a Lightning channel, Spending shows + its setup view instead of a QR. + + iOS: the return from Spending to USDT goes through the paged `TabView`, which moves on a 300 ms + swipe as well; the 600 ms in that step is kept so the two platforms run the same action. + + + Tap Receive (id "Receive") + Tap the USDT tab (id "Tab-usdt") and wait until the USDT QR is visible + Swipe right across the QR and take a screenshot right away + Verify the Spending tab (id "Tab-spending") is selected, the Lightning receive content or the Spending setup is shown, and the screenshot shows no "copied" tooltip + Swipe left across the middle of the Spending content, whether it shows a Lightning QR or the Spending setup, in 600 ms (`axe swipe --start-x 320 --start-y 420 --end-x 80 --end-y 420 --duration 0.6`); verify the USDT tab is selected and wait until its QR is visible + Swipe left across the USDT QR and take a screenshot right away; verify the USDT tab stays selected and the screenshot shows no "copied" tooltip + Tap the USDT QR once and verify the "copied" tooltip appears + +