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
10 changes: 10 additions & 0 deletions Bitkit/Extensions/View+SwipeGestures.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ extension View {

/// 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.
/// With `wrapAround`, a swipe past the first or last tab selects the tab at the other end.
/// 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,
wrapAround: Bool = false,
minimumDragDistance: CGFloat = 20,
swipeThreshold: CGFloat = 50,
animation: Animation = .easeInOut(duration: 0.2),
Expand All @@ -38,10 +40,18 @@ extension View {
withAnimation(animation) {
selection.wrappedValue = tabs[currentIndex + 1]
}
} else if horizontalAmount < -swipeThreshold, wrapAround, let first = tabs.first {
withAnimation(animation) {
selection.wrappedValue = first
}
} else if horizontalAmount > swipeThreshold, currentIndex > 0 {
withAnimation(animation) {
selection.wrappedValue = tabs[currentIndex - 1]
}
} else if horizontalAmount > swipeThreshold, wrapAround, let last = tabs.last {
withAnimation(animation) {
selection.wrappedValue = last
}
}
}
if simultaneous {
Expand Down
56 changes: 52 additions & 4 deletions Bitkit/Views/Wallets/Receive/ReceiveQr.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,36 @@ struct ReceiveQr: View {
)
}

/// A page of the receive `TabView`. The wrap pages sit before the first and after the last tab, so a swipe past
/// either end pages on and then selects the tab at the other end. They are hidden from accessibility, so their
/// copies of the end tab's buttons never match an identifier lookup.
private enum ReceivePage: Hashable {
case tab(ReceiveTab)
case wrapToLast
case wrapToFirst
}

private var pageSelection: Binding<ReceivePage> {
Binding(
get: { .tab(selectedTab) },
set: { page in
let tabs = availableTabItems.map(\.tab)
switch page {
case let .tab(tab):
selectedTabBinding.wrappedValue = tab
case .wrapToLast:
if let last = tabs.last {
selectedTabBinding.wrappedValue = last
}
case .wrapToFirst:
if let first = tabs.first {
selectedTabBinding.wrappedValue = first
}
}
}
)
}

var showingCjitOnboarding: Bool {
return !wallet.canCreateReceiveLightningInvoice && cjitInvoice == nil && selectedTab == .spending
}
Expand All @@ -155,7 +185,11 @@ struct ReceiveQr: View {
.padding(.horizontal, 16)

VStack(spacing: 0) {
TabView(selection: selectedTabBinding) {
TabView(selection: pageSelection) {
if let last = availableTabItems.last?.tab {
wrapPage(showing: last, tag: .wrapToLast)
}

if selectedHardwareWalletId != nil {
tabContent(for: .hardware)
}
Expand All @@ -168,7 +202,11 @@ struct ReceiveQr: View {

tabContent(for: .spending)
if settings.isUsdtEnabled, cjitInvoice == nil {
Color.clear.tag(ReceiveTab.usdt)
Color.clear.tag(ReceivePage.tab(.usdt))
}

if let first = availableTabItems.first?.tab {
wrapPage(showing: first, tag: .wrapToFirst)
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
Expand Down Expand Up @@ -334,7 +372,17 @@ struct ReceiveQr: View {
)
}

func tabContent(for tab: ReceiveTab) -> some View {
@ViewBuilder
private func wrapPage(showing tab: ReceiveTab, tag: ReceivePage) -> some View {
if tab == .usdt {
Color.clear.tag(tag)
Comment thread
ovitrif marked this conversation as resolved.
} else {
tabContent(for: tab, tag: tag)
.accessibilityHidden(true)
}
}

private func tabContent(for tab: ReceiveTab, tag: ReceivePage? = nil) -> some View {
VStack(spacing: 0) {
if needsInvoiceRefresh && tab != .hardware {
if invoiceRefreshFailed {
Expand Down Expand Up @@ -363,7 +411,7 @@ struct ReceiveQr: View {
Spacer()
}
.padding(.horizontal)
.tag(tab)
.tag(tag ?? .tab(tab))
}

@ViewBuilder
Expand Down
2 changes: 1 addition & 1 deletion Bitkit/Views/Wallets/UsdtReceiveView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +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)
.swipeSegmentedTabs(selection: Binding(get: { .usdt }, set: onSelectTab), tabs: tabs.map(\.tab), wrapAround: true, simultaneous: true)
case .networks:
networkList
case .amount:
Expand Down
1 change: 1 addition & 0 deletions changelog.d/next/1027.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Swiping past the first or last Receive tab now wraps around to the tab at the other end.
4 changes: 4 additions & 0 deletions journeys/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,10 @@ after staging signup. Android also supports this signup flow, but this journey h
`contacts/contacts-entry-points.xml` covers onboarding without an identity and the
authenticated Contacts list. It has not yet been ported to Android.

`receive/receive-tabs-wraparound.xml` has a matching Android journey with the same name and actions.
On Android a swipe that starts at the screen edge is the system back gesture, which closes the sheet on
every tab; start the swipes away from the edge.

`payment-requests/success-dismissal.xml` has a matching Android journey with the same name and
actions. Successful dismissal without a toast or duplicate payment is shared behavior, not an
iOS-only feature.
Expand Down
39 changes: 39 additions & 0 deletions journeys/receive/receive-tabs-wraparound.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<journey name="receive tabs wraparound">
<description>
On the Receive sheet, tab swipes wrap around: a swipe to the right on the first tab selects the
last tab, and a swipe to the left on the last tab selects the first. The wrap slides on to the
other end the way a swipe between neighbouring tabs does. A sideways swipe never closes
the Receive sheet, also when it is released above the sheet, and a tap on a QR still copies it.

Precondition: onboarded dev wallet with USDT enabled (introductory sheet or Settings, General,
Payments), no open Spending channel, no CJIT invoice pending and no hardware wallet paired, so the
tabs are Savings, Spending and USDT; a usable channel adds the Auto tab. Start on the wallet home
screen. The USDT address loads without funding; wait for the QR before swiping.

Swipe with AXe across the middle of the QR, about 60% of its width, in 300 ms (`axe swipe
--start-x 80 --start-y 440 --end-x 330 --end-y 440 --duration 0.3 --udid <udid>` to the right on a
402 pt wide simulator).

iOS: the paged `TabView` holds an extra page before the first tab and after the last one; paging
onto either selects the tab at the other end. Leaving USDT crossfades rather than slides, since USDT replaces
the pager with its own view.
</description>
<actions>
<action>Tap Receive (id "Receive") and verify the Savings tab (id "Tab-savings") is selected</action>
<action>Swipe right across the Savings QR; verify the USDT tab (id "Tab-usdt") is selected and wait until the USDT QR is visible</action>
<action>Swipe left across the USDT QR; verify the Savings tab is selected</action>
<action>Start a screen recording, tap the USDT tab (id "Tab-usdt") and wait until the USDT QR is visible, then swipe left across the USDT QR; verify the Savings tab is selected and the recording goes straight from the USDT QR to the Savings QR, with no Auto or Spending QR and no second USDT screen in between</action>
<action>Start a screen recording, tap the USDT tab and wait until the USDT QR is visible, then tap the Savings tab; verify the Savings tab is selected and the recording shows the same direct change from USDT to Savings</action>
<action>Swipe left across the Savings QR in 1200 ms (`axe swipe --start-x 330 --start-y 440 --end-x 80 --end-y 440 --duration 1.2`); verify the Spending tab (id "Tab-spending") is selected</action>
<action>Swipe left across the Spending content in 1200 ms; verify the USDT tab is selected and wait until its QR is visible</action>
<action>Swipe left from the top right of the USDT QR to above the sheet (`axe swipe --start-x 360 --start-y 300 --end-x 30 --end-y 60 --duration 0.3`); verify the Receive sheet is still open and the Savings tab is selected</action>
<action>Swipe right from the top left of the Savings QR to above the sheet (`axe swipe --start-x 40 --start-y 300 --end-x 380 --end-y 60 --duration 0.3`); verify the Receive sheet is still open and the USDT tab is selected</action>
<action>Swipe left across the USDT QR, then tap the Savings QR once; verify the "copied" tooltip appears</action>
<action>Close Receive, turn USDT off (Settings, General, Payments), return to the wallet home screen and tap Receive; verify the Savings tab is selected and only Savings and Spending show</action>
<action>Swipe right across the Savings QR; verify the Spending tab is selected</action>
<action>Swipe right across the Spending content; verify the Savings tab is selected</action>
<action>Swipe left across the Savings QR; verify the Spending tab is selected</action>
<action>Swipe left across the Spending content; verify the Savings tab is selected</action>
<action>Swipe left across the Savings QR; verify the Spending tab is selected</action>
</actions>
</journey>
7 changes: 4 additions & 3 deletions journeys/receive/usdt-receive-swipe-tabs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
<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.
does not copy the address; a swipe to the left wraps around to the first tab (Savings), 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
Expand All @@ -24,7 +25,7 @@
<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>
<action>Wait until the tooltip is gone, swipe left across the USDT QR and take a screenshot right away; verify the Savings tab (id "Tab-savings") is selected and the screenshot shows no "copied" tooltip</action>
</actions>
</journey>
Loading