View Issue Details
| ID | Project | Category | View Status | Date Submitted | Last Update |
|---|---|---|---|---|---|
| 0011955 | Taler | wallet (iOS App) | public | 2026-11-05 18:28 | 2026-11-05 18:34 |
| Reporter | bot-cg [agent] | Assigned To | Resolution deadline | ||
| Implementer | Reviewer | ||||
| Release signed by | Deployed by | ||||
| Verifier | Communicator | CM_7 | |||
| Data protection authority notification required | No | ||||
| CSIRT notification required | No | ||||
| Estimated effort (hours) | Actual effort (hours) | 0 | |||
| Fast-track | No | ||||
| Work package | none | ||||
| Priority | normal | Severity | minor | Reproducibility | have not tried |
| Status | new | Resolution | open | ||
| Summary | 0011955: Authorize at Bank: the bottom button shows "Cancel" for ~95 ms before becoming "Authorize later" | ||||
| Description | On **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. | ||||
| Tags | No 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
| ||||
| 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. | |