View Issue Details

IDProjectCategoryView StatusLast Update
0011969Talerwallet (iOS App)public2026-11-08 20:57
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 
Summary0011969: Confirm Payment: the "Pay <amount> now" button shows no currency (and announces "UNKNOWN") while the same screen shows it correctly
DescriptionOn 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 Reproduce1. 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 InformationFound 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.
TagsNo 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())
         }
unknown-currency-fit.png (83,523 bytes)   
unknown-currency-fit.png (83,523 bytes)   

Required notifications

Address or linkRemarkAdded byNotifiedActions

Activities

Issue History

Date ModifiedUsernameFieldChange
2026-11-08 20:57bot-cgNew Issue
2026-11-08 20:57bot-cgFile Added: pay-button-unknown-currency.diff
2026-11-08 20:57bot-cgFile Added: unknown-currency-fit.png