From ab6d110bd5903c6e6360ed1ce66367d96c6ac2a4 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 19 Oct 2026 21:01:07 +0200 Subject: [PATCH 1/8] fix: wrap receive tab swipes around --- Bitkit/Extensions/View+SwipeGestures.swift | 10 ++++ Bitkit/Views/Wallets/Receive/ReceiveQr.swift | 54 ++++++++++++++++++-- Bitkit/Views/Wallets/UsdtReceiveView.swift | 2 +- changelog.d/next/1024.fixed.md | 1 + journeys/README.md | 4 ++ journeys/receive/receive-tabs-wraparound.xml | 29 +++++++++++ journeys/receive/usdt-receive-swipe-tabs.xml | 7 +-- 7 files changed, 99 insertions(+), 8 deletions(-) create mode 100644 changelog.d/next/1024.fixed.md create mode 100644 journeys/receive/receive-tabs-wraparound.xml diff --git a/Bitkit/Extensions/View+SwipeGestures.swift b/Bitkit/Extensions/View+SwipeGestures.swift index e31d8a382..300fcc64f 100644 --- a/Bitkit/Extensions/View+SwipeGestures.swift +++ b/Bitkit/Extensions/View+SwipeGestures.swift @@ -15,9 +15,11 @@ 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. func swipeSegmentedTabs( selection: Binding, tabs visibleTabs: [T]? = nil, + wrapAround: Bool = false, minimumDragDistance: CGFloat = 20, swipeThreshold: CGFloat = 50, animation: Animation = .easeInOut(duration: 0.2) @@ -36,10 +38,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 + } } } ) diff --git a/Bitkit/Views/Wallets/Receive/ReceiveQr.swift b/Bitkit/Views/Wallets/Receive/ReceiveQr.swift index decc07d90..df2e2c873 100644 --- a/Bitkit/Views/Wallets/Receive/ReceiveQr.swift +++ b/Bitkit/Views/Wallets/Receive/ReceiveQr.swift @@ -127,6 +127,35 @@ 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. + private enum ReceivePage: Hashable { + case tab(ReceiveTab) + case wrapToLast + case wrapToFirst + } + + private var pageSelection: Binding { + 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 } @@ -155,7 +184,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) } @@ -167,7 +200,11 @@ struct ReceiveQr: View { } tabContent(for: .spending) - if settings.isUsdtEnabled, cjitInvoice == nil { Color.clear.tag(ReceiveTab.usdt) } + if settings.isUsdtEnabled, cjitInvoice == nil { Color.clear.tag(ReceivePage.tab(.usdt)) } + + if let first = availableTabItems.first?.tab { + wrapPage(showing: first, tag: .wrapToFirst) + } } .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) .indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always)) @@ -332,7 +369,16 @@ 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) + } else { + tabContent(for: tab, tag: tag) + } + } + + private func tabContent(for tab: ReceiveTab, tag: ReceivePage? = nil) -> some View { VStack(spacing: 0) { if needsInvoiceRefresh && tab != .hardware { if invoiceRefreshFailed { @@ -361,7 +407,7 @@ struct ReceiveQr: View { Spacer() } .padding(.horizontal) - .tag(tab) + .tag(tag ?? .tab(tab)) } @ViewBuilder diff --git a/Bitkit/Views/Wallets/UsdtReceiveView.swift b/Bitkit/Views/Wallets/UsdtReceiveView.swift index 5af5d3a6a..9a0633335 100644 --- a/Bitkit/Views/Wallets/UsdtReceiveView.swift +++ b/Bitkit/Views/Wallets/UsdtReceiveView.swift @@ -62,7 +62,7 @@ struct UsdtReceiveView: View { onEdit: { draftAmount = amount; page = .amount }, onNetwork: { page = .networks } ) - .swipeSegmentedTabs(selection: Binding(get: { .usdt }, set: onSelectTab), tabs: tabs.map(\.tab)) + .swipeSegmentedTabs(selection: Binding(get: { .usdt }, set: onSelectTab), tabs: tabs.map(\.tab), wrapAround: true) case .networks: networkList case .amount: diff --git a/changelog.d/next/1024.fixed.md b/changelog.d/next/1024.fixed.md new file mode 100644 index 000000000..1523bc815 --- /dev/null +++ b/changelog.d/next/1024.fixed.md @@ -0,0 +1 @@ +Swiping past the first or last Receive tab now wraps around to the tab at the other end. diff --git a/journeys/README.md b/journeys/README.md index 27489405a..fe19605e4 100644 --- a/journeys/README.md +++ b/journeys/README.md @@ -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. diff --git a/journeys/receive/receive-tabs-wraparound.xml b/journeys/receive/receive-tabs-wraparound.xml new file mode 100644 index 000000000..f364cf19b --- /dev/null +++ b/journeys/receive/receive-tabs-wraparound.xml @@ -0,0 +1,29 @@ + + + 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. 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 CJIT invoice pending and no hardware wallet paired, so the tabs are Savings, + Spending and USDT. 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 ` 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. + + + Tap Receive (id "Receive") and verify the Savings tab (id "Tab-savings") is selected + Swipe right across the Savings QR; verify the USDT tab (id "Tab-usdt") is selected and wait until the USDT QR is visible + Swipe left across the USDT QR; verify the Savings tab is selected + Swipe left across the Savings QR in 600 ms (`axe swipe --start-x 330 --start-y 440 --end-x 80 --end-y 440 --duration 0.6`); verify the Spending tab (id "Tab-spending") is selected + Swipe left across the Spending content in 600 ms; verify the USDT tab is selected and wait until its QR is visible + 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 + 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 + Swipe left across the USDT QR, then tap the Savings QR once; verify the "copied" tooltip appears + + diff --git a/journeys/receive/usdt-receive-swipe-tabs.xml b/journeys/receive/usdt-receive-swipe-tabs.xml index 593b8a3d2..24480ede7 100644 --- a/journeys/receive/usdt-receive-swipe-tabs.xml +++ b/journeys/receive/usdt-receive-swipe-tabs.xml @@ -2,8 +2,9 @@ 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 @@ -24,7 +25,7 @@ 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 + 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 From 6fbf1857380a7e7de01906a6383ee5b51d73353a Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 19 Oct 2026 21:05:26 +0200 Subject: [PATCH 2/8] chore: rename changelog fragment --- changelog.d/next/{1024.fixed.md => 1027.fixed.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/next/{1024.fixed.md => 1027.fixed.md} (100%) diff --git a/changelog.d/next/1024.fixed.md b/changelog.d/next/1027.fixed.md similarity index 100% rename from changelog.d/next/1024.fixed.md rename to changelog.d/next/1027.fixed.md From f9085b4cffe424efd69fca34dd61feca670c9c72 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 19 Oct 2026 21:30:15 +0200 Subject: [PATCH 3/8] test: wrap receive tabs with usdt off in journey --- journeys/receive/receive-tabs-wraparound.xml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/journeys/receive/receive-tabs-wraparound.xml b/journeys/receive/receive-tabs-wraparound.xml index f364cf19b..a1a2db437 100644 --- a/journeys/receive/receive-tabs-wraparound.xml +++ b/journeys/receive/receive-tabs-wraparound.xml @@ -5,9 +5,9 @@ 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 CJIT invoice pending and no hardware wallet paired, so the tabs are Savings, - Spending and USDT. Start on the wallet home screen. The USDT address loads without funding; wait - for the QR before swiping. + 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 ` to the right on a @@ -25,5 +25,11 @@ 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 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 Swipe left across the USDT QR, then tap the Savings QR once; verify the "copied" tooltip appears + 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 + Swipe right across the Savings QR; verify the Spending tab is selected + Swipe right across the Spending content; verify the Savings tab is selected + Swipe left across the Savings QR; verify the Spending tab is selected + Swipe left across the Spending content; verify the Savings tab is selected + Swipe left across the Savings QR; verify the Spending tab is selected From 4934e2884f9f7a16c6a9b81508aae812daf84883 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 19 Oct 2026 21:52:14 +0200 Subject: [PATCH 4/8] fix: hide receive wrap pages from accessibility --- Bitkit/Views/Wallets/Receive/ReceiveQr.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Bitkit/Views/Wallets/Receive/ReceiveQr.swift b/Bitkit/Views/Wallets/Receive/ReceiveQr.swift index df2e2c873..869c52ddd 100644 --- a/Bitkit/Views/Wallets/Receive/ReceiveQr.swift +++ b/Bitkit/Views/Wallets/Receive/ReceiveQr.swift @@ -128,7 +128,8 @@ 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. + /// 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 @@ -375,6 +376,7 @@ struct ReceiveQr: View { Color.clear.tag(tag) } else { tabContent(for: tab, tag: tag) + .accessibilityHidden(true) } } From 364756fb1c9d0086ca61775a375996f8b89c9971 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 20 Oct 2026 04:01:52 +0200 Subject: [PATCH 5/8] test: say the receive wrap slides in journey --- journeys/receive/receive-tabs-wraparound.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/journeys/receive/receive-tabs-wraparound.xml b/journeys/receive/receive-tabs-wraparound.xml index a1a2db437..ead51dd25 100644 --- a/journeys/receive/receive-tabs-wraparound.xml +++ b/journeys/receive/receive-tabs-wraparound.xml @@ -1,7 +1,8 @@ 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. A sideways swipe never closes + 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, From 3fec826559e3c6d964534c3caad11cf241f49590 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 20 Oct 2026 04:34:42 +0200 Subject: [PATCH 6/8] test: swipe the receive pager in 1200 ms in journey --- journeys/receive/receive-tabs-wraparound.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/journeys/receive/receive-tabs-wraparound.xml b/journeys/receive/receive-tabs-wraparound.xml index ead51dd25..1b0bec71d 100644 --- a/journeys/receive/receive-tabs-wraparound.xml +++ b/journeys/receive/receive-tabs-wraparound.xml @@ -21,8 +21,8 @@ Tap Receive (id "Receive") and verify the Savings tab (id "Tab-savings") is selected Swipe right across the Savings QR; verify the USDT tab (id "Tab-usdt") is selected and wait until the USDT QR is visible Swipe left across the USDT QR; verify the Savings tab is selected - Swipe left across the Savings QR in 600 ms (`axe swipe --start-x 330 --start-y 440 --end-x 80 --end-y 440 --duration 0.6`); verify the Spending tab (id "Tab-spending") is selected - Swipe left across the Spending content in 600 ms; verify the USDT tab is selected and wait until its QR is visible + 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 + Swipe left across the Spending content in 1200 ms; verify the USDT tab is selected and wait until its QR is visible 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 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 Swipe left across the USDT QR, then tap the Savings QR once; verify the "copied" tooltip appears From cf51fb29318f6631dc238213435724ba46c463fc Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 20 Oct 2026 11:33:09 +0200 Subject: [PATCH 7/8] test: cover receive wrap after a usdt tab tap in journey --- journeys/receive/receive-tabs-wraparound.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/journeys/receive/receive-tabs-wraparound.xml b/journeys/receive/receive-tabs-wraparound.xml index 1b0bec71d..b5b57ac85 100644 --- a/journeys/receive/receive-tabs-wraparound.xml +++ b/journeys/receive/receive-tabs-wraparound.xml @@ -21,6 +21,8 @@ Tap Receive (id "Receive") and verify the Savings tab (id "Tab-savings") is selected Swipe right across the Savings QR; verify the USDT tab (id "Tab-usdt") is selected and wait until the USDT QR is visible Swipe left across the USDT QR; verify the Savings tab is selected + 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 shows the USDT QR sliding straight on to the Savings QR, with no Auto or Spending QR and no second USDT screen in between + 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 single slide from USDT to Savings 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 Swipe left across the Spending content in 1200 ms; verify the USDT tab is selected and wait until its QR is visible 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 From 912ac7471ee2df63119bd4866ab325d00d633b08 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 20 Oct 2026 12:20:01 +0200 Subject: [PATCH 8/8] test: word the usdt exit check for both platforms in journey --- journeys/receive/receive-tabs-wraparound.xml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/journeys/receive/receive-tabs-wraparound.xml b/journeys/receive/receive-tabs-wraparound.xml index b5b57ac85..9c4dcac6a 100644 --- a/journeys/receive/receive-tabs-wraparound.xml +++ b/journeys/receive/receive-tabs-wraparound.xml @@ -15,14 +15,15 @@ 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. + onto either selects the tab at the other end. Leaving USDT crossfades rather than slides, since USDT replaces + the pager with its own view. Tap Receive (id "Receive") and verify the Savings tab (id "Tab-savings") is selected Swipe right across the Savings QR; verify the USDT tab (id "Tab-usdt") is selected and wait until the USDT QR is visible Swipe left across the USDT QR; verify the Savings tab is selected - 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 shows the USDT QR sliding straight on to the Savings QR, with no Auto or Spending QR and no second USDT screen in between - 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 single slide from USDT to Savings + 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 + 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 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 Swipe left across the Spending content in 1200 ms; verify the USDT tab is selected and wait until its QR is visible 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