View Issue Details

IDProjectCategoryView StatusLast Update
0011945Talerwallet (iOS App)public2026-11-05 00:04
Reporterbot-cg [agent] Assigned ToResolution deadline
ImplementerReviewer 
Release signed byDeployed by 
VerifierCommunicatorCM_7  
Data protection authority notification requiredNo  
CSIRT notification requiredNo  
Estimated effort (hours)Actual effort (hours)0 
Fast-trackNo
Work packagenone
PrioritynormalSeverityminorReproducibilityalways
Status newResolutionopen 
Summary0011945: Wire Transfer: the QR code is drawn below the fold with the “Done” bar across it, so it cannot be scanned without scrolling
DescriptionOn the "Wire Transfer" screen the QR code that the user is meant to scan with
their banking software is drawn below the fold, and the floating "Done" bar
lies across what is left of it. On first presentation the code cannot be
scanned, and nothing on the screen says that it has to be scrolled.

Measured by UI automation on iPhone 17 (402x874pt), iOS 27.2, GNU_Taler debug
build, on the settled screen before any scrolling:

  QR code [63, 633, 276, 276] -> its last 35pt are below the window
  "Done" bar [32, 793, 338, 47] -> lies across a further 17% of the code

So about a quarter of the code is missing on arrival. A QR code with a
corner missing does not decode at all, so the screen does not do the one
thing it exists for until the user guesses to scroll.

Reproduced independently on a second device size (420x912pt), where the code
sits at [72, 643, 276, 276] with the bar at [32, 831, 356, 47] — see the
attached screenshot, which is the unmodified screen.

The same ordering hurts the withdrawal transaction detail: there the code is
drawn at [63, 710, 276, 276], 40% of it below the window.

Cause
-----
Ordering in WireTransferView (TalerWallet1/Views/Transactions/ManualDetailsV.swift).
The single inline QR code is emitted last, after the title, the amount, the
"Wire transfer instructions" link and the explanatory sentence. A QR code is
drawn at a fixed 276pt, so by then it starts at roughly y=640 on a phone and
there is no room left. The floating bar comes from .safeAreaInset(edge:
.bottom) in TransactionSummaryList, which draws over the list rather than
after it.

Suggested fix (patch attached, compiles against current master)
---------------------------------------------------------------
Draw the code first: it is what the screen is for. The attached diff moves
the inline single-code case (with its explanatory sentence) to the top of the
list and leaves the multi-code case behind the existing "Wire transfer QR
codes" link. No other screen changes.

An alternative, if the ordering is deliberate, would be to scroll the code
into view when the screen appears, or to size it to the space actually
available. Either is fine; what matters is that the code is whole on arrival.

How to reproduce
----------------
1. Fresh install of the GNU_Taler scheme on an iPhone simulator (iOS 27.2).
2. Accept the terms of service.
3. Actions -> Withdraw, pick the demo exchange, enter an amount, confirm --
   the manual (not bank-integrated) withdrawal.
4. The "Wire Transfer" screen appears. The QR code is below the fold with
   the blue "Done" bar across it; the screen has to be scrolled before the
   code can be scanned.

With the UI fuzzing harness (branch dev/phoenix-bot/fuzzbang):

    python3 -m taler_fuzz.cli --prefer "demo cash,withdraw,confirm,accept,terms" \
        explore -n 2 --run-id qr

and the layout oracle reports it as "content-clipped-at-rest" and
"control-covers-content", both high.

Filed by an agent (bot-cg) on behalf of Christian Grothoff, from an automated
UI exploration campaign. Left unassigned deliberately.
TagsNo tags attached.
Attached Files
qr-first-on-wire-transfer.diff (3,557 bytes)   
diff --git a/TalerWallet1/Views/Transactions/ManualDetailsV.swift b/TalerWallet1/Views/Transactions/ManualDetailsV.swift
index ea15087e..3b945a27 100644
--- a/TalerWallet1/Views/Transactions/ManualDetailsV.swift
+++ b/TalerWallet1/Views/Transactions/ManualDetailsV.swift
@@ -89,6 +89,30 @@ struct WireTransferView: View {
                 let amountStr = transferAmount.formatted(specs: specs, isNegative: false, scope: account.scope)
                 let amountValue = transferAmount.valueStr
 //            let _ = print(amountStr, " | ", obtainStr)
+                let countA = transferOptions.count
+                let countB = firstOption.qrCodes?.count ?? 0
+                // Inline, as opposed to behind the "Wire transfer QR codes"
+                // link: exactly one code to show.
+                let showsInlineQR = !(countA > 1 || countB > 1) && countB > 0
+                                    && firstOption.paytoUri != nil
+                // The QR code is what this screen is for, so it goes first.
+                //
+                // It used to come last, after the title, the amount and the
+                // "Wire transfer instructions" link -- and a QR code is drawn
+                // at a fixed 276pt, so by then it started below the fold: the
+                // floating "Done" bar lay across its lower edge and the window
+                // cut off the rest.  A code that has to be scrolled into view
+                // before it can be scanned, with nothing on screen saying so,
+                // is a code the user does not know is there.
+                if showsInlineQR {
+                    Group {
+                        Text("If your banking software runs on another device, you can scan this QR code:")
+                            .listRowSeparator(.hidden)
+                        QRcodeCopyShare(stack: stack.push(), option: firstOption)
+                            .listRowSeparator(.automatic)
+                    }.id(listID)
+                        .talerFont(.body)
+                }
                 if !minimalistic {
                     Text(title)
                         .talerFont(.headline)
@@ -116,9 +140,6 @@ struct WireTransferView: View {
                         .accessibilityLabel(amountStr.1)
                 }
 
-                let countA = transferOptions.count
-                let countB = firstOption.qrCodes?.count ?? 0
-
                 if let paytoUri = firstOption.paytoUri {
                     let payto = PayTo(paytoUri)
                     if let receiverStr = payto.receiver {
@@ -139,14 +160,11 @@ struct WireTransferView: View {
                             .talerFont(.title3)
                         }
 
+                        // The single-code case is drawn at the top of the
+                        // list instead; several codes stay behind a link.
                         Group {
                             if countA > 1 || countB > 1 {
                                 qrCodesRow(transferOptions)
-                            } else if countB > 0 {   // show the single QR directly
-                                Text("If your banking software runs on another device, you can scan this QR code:")
-                                    .listRowSeparator(.hidden)
-                                QRcodeCopyShare(stack: stack.push(), option: firstOption)
-                                    .listRowSeparator(.automatic)
                             }
                         }.id(listID)
                             .talerFont(.body)
qr-first-on-wire-transfer.diff (3,557 bytes)   
churn-report.txt (2,833 bytes)   
Scanning a taler://pay/ URI opens the payment sheet, and the single button at
the bottom of that sheet changes what it does twice while the user is looking
at it, without moving:

  1. "Done"              — while talerTX is still the dummy transaction
  2. "Cancel"            — once it is a pay dialog but the amount is not known
  3. "Pay <amount> now"  — once wallet-core has returned the amount

All three are drawn in the same rectangle by the same .safeAreaInset, so the
user's thumb is already on its way to one of them when it becomes another.

Measured by UI automation (iPhone 17, iOS 27.2, GNU_Taler debug build): a
control at [32, 792, 338, 47] changed from "Cancel" to "Pay KUDOS 0.10 now"
in 118 ms, in the same place and at the same size, while the sheet's title
changed from "Payment" to "Confirm Payment".  118 ms is well under the time
between deciding to press something and pressing it.

Both directions cost the user something real:

  * aiming for "Done" — meaning "do not pay" — and pressing "Pay": money
    leaves the wallet on a decision the user had made the other way;
  * aiming for "Pay", because that is where a Pay button normally is, and
    pressing "Done": the payment is not made, the order is filed away among
    the scanned QR codes, and finding it again is not obvious.

The duration of each state is one network round trip, so this is worst on a
connection fast enough that the swap happens after the user has aimed, and on
a slow one the misleading state is simply shown for longer.

Cause
-----
TalerWallet1/Views/Transactions/TransactionSummaryList.swift, the
.safeAreaInset(edge: .bottom).  One slot, whose content is chosen by
`isPayDialog` and then by whether `effective` and `contractTerms` have
arrived; `PaymentScan` passes showDone: .prominent, so before wallet-core
answers the slot holds "Done".

Suggested fix (patch attached, compiles against current master)
---------------------------------------------------------------
Give the payment sheet two slots that never trade places: the commit action
on top, the dismiss action below it.  The commit slot exists from the first
frame and is merely disabled until the amount is known, so nothing changes
meaning under the user's thumb.  `expectPayDialog` is how a caller says a
sheet is going to become a payment dialog before wallet-core has confirmed
it, which is exactly the window in which the slot used to say "Done".  A
sheet waiting on that answer stays swipe-dismissable so that a disabled
commit button is never a trap.

The patch adds one string, "Pay…", which needs translating.

A smaller mitigation, if the two-slot layout is unwanted: ignore taps on the
bottom button for a few hundred milliseconds after its action changes.  That
treats the symptom rather than the cause, but it does prevent the mis-tap.
churn-report.txt (2,833 bytes)   

Required notifications

Address or linkRemarkAdded byNotifiedActions

Activities

bot-cg [agent]

2026-11-05 00:04

updater   ~0030261

Correction: the third attachment on this issue, "churn-report.txt", was
attached here by mistake while I was verifying that attachments work at all.
It does not belong to this defect — it describes a separate one on the payment
sheet, which is being filed as its own issue. Please ignore it here. I cannot
remove an attachment, so this note is the record.

The two attachments that do belong here are the screenshot of the unmodified
"Wire Transfer" screen and the suggested patch.

Issue History

Date ModifiedUsernameFieldChange
2026-11-05 00:03bot-cgNew Issue
2026-11-05 00:04bot-cgFile Added: wire-transfer-qr-under-done-button.png
2026-11-05 00:04bot-cgFile Added: qr-first-on-wire-transfer.diff
2026-11-05 00:04bot-cgFile Added: churn-report.txt
2026-11-05 00:04bot-cgNote Added: 0030261