View Issue Details

IDProjectCategoryView StatusLast Update
0011955Talerwallet (iOS App)public2026-11-05 18:34
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
PrioritynormalSeverityminorReproducibilityhave not tried
Status newResolutionopen 
Summary0011955: Authorize at Bank: the bottom button shows "Cancel" for ~95 ms before becoming "Authorize later"
DescriptionOn **Authorize at Bank**, in the bank-integrated withdrawal flow, the
button in the bottom slot at [16, 792, 370, 47] is labelled **"Cancel"**
when the screen first appears and becomes **"Authorize later"** (or
"Done") some 94-97 ms later, in the same position and at the same size.

It is a label flicker, not a hazard: both branches run the same action.
I am filing it separately rather than leaving it as a note on 11950,
because on that issue the two labels really do different things, and
conflating them would overstate this one.

Cause
-----
`TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawAcceptDone.swift`:

    .safeAreaInset(edge: .bottom) {
        let shouldConfirm = talerTX.shouldConfirm
        if talerTX.common.type != .dummy {
            Button(shouldConfirm ? "Authorize later" : "Done") { dismissTop(stack.push()) }
                .buttonStyle(TalerButtonStyle(type: shouldConfirm ? .bordered : .prominent))
                .padding(.horizontal)
        } else {
            Button("Cancel") { dismissTop(stack.push()) }
                .buttonStyle(TalerButtonStyle(type: .bordered))
                .padding(.horizontal)
        }
    }

The `.dummy` branch is the placeholder shown while the real transaction
loads. Both call `dismissTop(stack.push())`, so the buttons have never
differed in behaviour -- only in wording.

Why it is still worth fixing: "Cancel" on a withdrawal screen reads as
"abandon this withdrawal". A user who sees it, decides, and presses,
actually presses "Authorize later". The outcome happens to be the same
one, but the user cannot know that, and the next change to either branch
would make the mismatch real.

Suggested fix (patch attached)
------------------------------
Hold the slot's height without offering a target until the transaction is
known:

    .padding(.horizontal)
    .hidden()

`.hidden()` keeps the layout stable and removes the control from hit
testing and from the accessibility tree, so nothing is tappable or
announced before its final label exists.

How to reproduce
----------------
1. Start a bank-integrated withdrawal (`taler://withdraw/...`) so that
   Authorize at Bank is presented.
2. Watch the bottom button as the screen appears. At 95 ms the change is
   hard to catch by eye; it is reliably visible by sampling the
   accessibility tree, which is how it was found.

Found by an automated UI exploration campaign: a control whose label
changes while its frame does not. Screenshot of the screen in its settled
state attached.

Filed by an agent (bot-cg) on behalf of Christian Grothoff. Left
unassigned deliberately.
TagsNo tags attached.
Attached Files
authorize-slot.diff (2,316 bytes)   
From 2ca10b6d1c8446a6225f043a345438ce6c25668b Mon Sep 17 00:00:00 2001
From: Christian Grothoff <christian@grothoff.org>
Date: Thu, 5 Nov 2026 19:27:34 +0100
Subject: [PATCH] iOS: do not flicker Cancel into Authorize later while loading

The bottom slot of Authorize at Bank renders a tappable "Cancel" for the
dummy placeholder transaction, which becomes "Authorize later" (or "Done")
roughly 95 ms later in the same place and at the same size.

Both branches already ran dismissTop, so the two buttons never differed in
what they did -- only in what they said.  Hide the placeholder so the slot
keeps its height without offering a target before the transaction is known.

Assisted by LLM
---
 .../WithdrawBankIntegrated/WithdrawAcceptDone.swift      | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawAcceptDone.swift b/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawAcceptDone.swift
index 2b325ee2..90a98b5e 100755
--- a/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawAcceptDone.swift
+++ b/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawAcceptDone.swift
@@ -69,9 +69,18 @@ struct WithdrawAcceptDone: View {
                             .buttonStyle(TalerButtonStyle(type: shouldConfirm ? .bordered : .prominent))
                             .padding(.horizontal)
                     } else {
+                        // A dummy transaction is the placeholder shown while
+                        // the real one loads.  Offering "Cancel" here puts a
+                        // tappable control in the slot that becomes
+                        // "Authorize later" some 95 ms later, in the same
+                        // place and at the same size -- a label flicker under
+                        // the user's finger.  Both ran dismissTop, so nothing
+                        // is lost by holding the space without offering a
+                        // target until the transaction is known.
                         Button("Cancel") { dismissTop(stack.push()) }
                             .buttonStyle(TalerButtonStyle(type: .bordered))
                             .padding(.horizontal)
+                            .hidden()
                     }
                 }
             } else {
-- 
2.47.3

authorize-slot.diff (2,316 bytes)   
authorize-small.png (34,475 bytes)   
authorize-small.png (34,475 bytes)   

Required notifications

Address or linkRemarkAdded byNotifiedActions

Activities

bot-cg [agent]

2026-11-05 18:34

updater   ~0030281

The attached patch has been compiled: applied to a clean checkout of the
app source and built for the simulator with the `GNU_Taler` scheme,
`** BUILD SUCCEEDED **`, exit 0. The checkout was restored afterwards.

One note on the choice of `.hidden()` rather than removing the button:
keeping the view in the hierarchy preserves the slot's height, so the
content above it does not shift when the real transaction arrives.
`.hidden()` also removes it from hit testing and from the accessibility
tree, so for the ~95 ms in question there is no control to press and
VoiceOver announces nothing that is about to change its name.

Issue History

Date ModifiedUsernameFieldChange
2026-11-05 18:28bot-cgNew Issue
2026-11-05 18:28bot-cgFile Added: authorize-slot.diff
2026-11-05 18:29bot-cgFile Added: authorize-small.png
2026-11-05 18:34bot-cgNote Added: 0030281