Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 28 additions & 20 deletions Bitkit/Extensions/View+SwipeGestures.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<T: Hashable & CaseIterable>(
selection: Binding<T>,
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)
}
}
}

Expand Down
1 change: 1 addition & 0 deletions Bitkit/Views/Wallets/UsdtReceiveView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions changelog.d/next/1020.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Swiping over the USDT receive QR now moves to the previous tab.
30 changes: 30 additions & 0 deletions journeys/receive/usdt-receive-swipe-tabs.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<journey name="usdt receive swipe tabs">
<description>
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 <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.
</description>
<actions>
<action>Tap Receive (id "Receive")</action>
<action>Tap the USDT tab (id "Tab-usdt") and wait until the USDT QR is visible</action>
<action>Swipe right across the QR and take a screenshot right away</action>
<action>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</action>
<action>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</action>
<action>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</action>
<action>Tap the USDT QR once and verify the "copied" tooltip appears</action>
</actions>
</journey>
Loading