View Issue Details
| ID | Project | Category | View Status | Date Submitted | Last Update |
|---|---|---|---|---|---|
| 0011969 | Taler | wallet (iOS App) | public | 2026-11-08 20:57 | 2026-11-08 20:57 |
| 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 | 0011969: Confirm Payment: the "Pay <amount> now" button shows no currency (and announces "UNKNOWN") while the same screen shows it correctly | ||||
| Description | On the Confirm Payment screen the primary button renders the amount with an unresolved currency, while the same screen shows the currency correctly two rows above it. Observed (screenshot attached), iPhone 17 (402x874 pt), iOS 27.2, dark appearance, GNU_Taler debug build, paying a 0.1 KUDOS order: Payment Options 0.1 KUDOS <- correct KUDOS [ Pay 0.1 now ] <- no currency at all The accessibility label of that button is different again, and names the placeholder explicitly: "Pay UNKNOWN 0.1 now" So a sighted user is asked to confirm a payment whose currency the button does not state, and a VoiceOver user is told the currency is "UNKNOWN". (The fraction digit is rendered as a superscript in both -- see below, it is the same cause.) Cause ----- TalerWallet1/Views/Transactions/TransactionSummaryList.swift :53 @State private var currencyInfo: CurrencyInfo = CurrencyInfo.zero(UNKNOWN) :358 let formatted = effective.formatted(currencyInfo) PaySafeArea(... amountString: formatted.0, amountA11y: formatted.1 ...) :446 .onChange(of: scope) { newVal in if let newVal { currencyInfo = controller.info(for: newVal) ?? CurrencyInfo.zero(UNKNOWN) } } `currencyInfo` is assigned in exactly one place, an `onChange(of: scope)`. SwiftUI's onChange fires only when the value changes *after* the view appears, so a `scope` that was already set when the view was created never reaches it and `currencyInfo` stays `CurrencyInfo.zero(UNKNOWN)`. There is a second route to the same end state: `controller.info(for:)` returns nil until the currency specification has been fetched, so even a scope change that does fire can land before the currency is known, and the `?? zero( UNKNOWN)` takes over. That also explains the superscript. `zero(UNKNOWN)` carries no fraction digits, so every fraction digit is "beyond the currency's precision" and `Amount.formatted` renders it in superscript (taler-swift Amount.swift:16, TalerCommon/Helper/SuperScriptDigits.swift:15). With KUDOS resolved the same amount formats as "0.10". The rest of the screen is unaffected because it does not format through this `currencyInfo`. Not the same as issue 11950, which is about the identity of this bottom button changing between Done / Cancel / Pay in one sheet. This is the content of the Pay title when it is shown. Suggested fix (patch attached) ------------------------------ Resolve `currencyInfo` with the pattern the rest of the codebase already uses for exactly this -- a `.task(id: controller.currencyTicker)` that re-runs when the controller learns a currency: WithdrawUriScan.swift:207 SendAmountV.swift:91 RequestPayment.swift:93 TransactionSummaryList is the one view of these that does not. The two-argument `controller.info(for:_:)` returns a non-optional CurrencyInfo, so the UNKNOWN fallback is not needed on that path. | ||||
| Steps To Reproduce | 1. Fund the wallet with KUDOS. 2. Open a taler://pay/ URI for a small order, e.g. KUDOS:0.1, so the Confirm Payment screen appears. Observed: the body shows "0.1 KUDOS"; the bottom button shows "Pay 0.1 now" with no currency, and its accessibility label is "Pay UNKNOWN 0.1 now". The fraction digit is superscripted. Expected: the button names the same currency as the rest of the screen, and formats the amount to the currency's precision ("Pay 0.10 KUDOS now"). | ||||
| Additional Information | Found by the fuzzing harness during campaign c2h-3, episode 10, screen 10. taler-ios master as of 2026-11-08. A patch is attached; it applies with `git apply` and the patched file passes `swiftc -parse`. I have not been able to build and run the patched app, so the fix is verified by inspection and by matching the established pattern in three sibling views, not by observation. | ||||
| Tags | No tags attached. | ||||
| Attached Files | pay-button-unknown-currency.diff (2,819 bytes)
Fix: the "Pay <amount> now" button renders the currency as UNKNOWN.
`currencyInfo` in TransactionSummaryList is initialised to
CurrencyInfo.zero(UNKNOWN) and assigned in exactly one place -- an
`.onChange(of: scope)`. SwiftUI's onChange fires only when the value
changes after the view appears, so a scope that was already set when the
view was created never reaches it, and the button formats its amount with
an unresolved currency while the rest of the same screen renders the
amount correctly.
This adds the `.task(id: controller.currencyTicker)` resolution that
WithdrawUriScan.swift:207, SendAmountV.swift:91 and RequestPayment.swift:93
already use, which also covers the second case: `controller.info(for:)`
answers nil until the currency specification has been fetched, so even a
scope change that does fire can land before the currency is known. The
two-argument `info(for:_:)` returns a non-optional CurrencyInfo, which is
why there is no `?? zero(UNKNOWN)` on the new call.
The existing onChange is left in place: it is harmless and still covers a
scope change between ticker bumps.
Generated against taler-ios master, 2026-11-08. Apply with `git apply`.
--- a/TalerWallet1/Views/Transactions/TransactionSummaryList.swift
+++ b/TalerWallet1/Views/Transactions/TransactionSummaryList.swift
@@ -446,8 +446,28 @@
if let newVal {
currencyInfo = controller.info(for: newVal) ?? CurrencyInfo.zero(UNKNOWN)
}
}
+ // `onChange` alone is not enough, and the "Pay <amount> now" button
+ // is where that showed. It fires only when the value changes *after*
+ // the view appears, so a `scope` that was already set when this view
+ // was created never reached `currencyInfo`, which stayed
+ // CurrencyInfo.zero(UNKNOWN) -- and that is what `effective
+ // .formatted(currencyInfo)` renders the button's amount with. The
+ // result is a Confirm Payment screen reading "0.1 KUDOS" in the body
+ // and "Pay UNKNOWN 0.1 now" on the button.
+ //
+ // It also has to re-run when the controller learns a currency later:
+ // `controller.info(for:)` answers nil until the currency
+ // specification has been fetched. The same pattern is already used
+ // in WithdrawUriScan.swift:207, SendAmountV.swift:91 and
+ // RequestPayment.swift:93; this view was the one that did not.
+ .task(id: controller.currencyTicker) {
+ if let scope {
+ currencyInfo = controller.info(for: scope,
+ controller.currencyTicker)
+ }
+ }
.onAppear {
symLog.log("onAppear")
DebugViewC.shared.setViewID(VIEW_TRANSACTIONSUMMARY, stack: stack.push())
}
| ||||