View Issue Details
| ID | Project | Category | View Status | Date Submitted | Last Update |
|---|---|---|---|---|---|
| 0011987 | Taler | wallet (iOS App) | public | 2026-11-09 23:38 | 2026-11-09 23:38 |
| 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 | 0011987: Withdrawal wire details offer a choice between two accounts rendered identically ("KUDOS 5.00" twice) -- the chooser is gated on the account count, not on whether the accounts differ | ||||
| Description | Withdrawing KUDOS offers a choice between two wire accounts that the app renders identically -- "KUDOS 5.00" and "KUDOS 5.00". There is nothing to choose between, and the control that asks exists for a case that is not happening here: an account whose currency differs from the withdrawal's, i.e. a conversion. **Where the duplicate comes from.** Asked through wallet-core's own API against `exchange.demo.taler.net` on 2026-11-10 (`getWithdrawalDetailsForAmount`, amount KUDOS:5): withdrawalAccountsList: 2 entries 0 payto://x-taler-bank/sandcastle/exchange ?receiver-name=Sandcastle+Echange+Inc transferAmount KUDOS:5 bankLabel nil priority 0 1 payto://x-taler-bank/sandcastle/exchange ?receiver-name=Sandcastle+Exchange+Inc transferAmount KUDOS:5 bankLabel nil priority 0 The same bank account twice -- same host, same path -- differing only in the `receiver-name` parameter, where the first spells it "Echange". **Why the app then asks.** `ManualDetailsV.swift:99` offers the chooser on a count: if validCount > 1 { if validCount > 3 { AccountPicker(...) } else { SegmentControl(...) } and `SegmentControl.swift:38-60` labels each segment with the formatted transfer amount plus `bankLabel` when wallet-core knows one. Here the amounts are equal and `bankLabel` is nil for both, so the two segments carry the same text. So there are two separate problems, with different owners: * **the deployment**: `exchange.demo.taler.net` publishes one account twice, one copy with a misspelled receiver name. Every wallet that reads its `/keys` sees the duplicate. * **the app**: it offers a choice whenever there is more than one account, which is not the same question as whether the accounts differ. Even with the deployment fixed, two same-currency accounts with no bank label render identically -- and the control's purpose is conversion, which does not arise when every option is in the withdrawal's own currency. This report is about the app; the deployment duplicate is noted here so it is not lost, and I have not filed it separately because I do not know who owns that configuration. | ||||
| Steps To Reproduce | 1. Withdraw KUDOS from exchange.demo.taler.net by the manual route (Balances -> the currency -> Withdraw -> enter an amount), so the wire details screen is shown. 2. Look above the transfer instructions. Observed: a two-segment chooser whose segments both read "KUDOS 5.00". Picking either produces the same wire instructions, because both entries are the same account. Expected: no chooser. One account, one set of instructions. Checkable without the app, which is how the evidence above was produced: taler-wallet-cli --wallet-db=/tmp/w.db api \ getWithdrawalDetailsForAmount \ '{"exchangeBaseUrl":"https://exchange.demo.taler.net/", "amount":"KUDOS:5","restrictAge":null}' and read `withdrawalAccountsList`. | ||||
| Additional Information | Patch attached: `identical-wire-choice.diff`. It builds the set of labels the segments would actually show -- from the same `transferAmount.formatted(...)` and `bankLabel` the control uses -- and offers the chooser only when that set has more than one member. Deriving it from the rendering rather than from the payto means the test cannot drift from what the user sees. var renderedChoices = Set<String>() for i in 0..<validCount { ... renderedChoices.insert(bankLabel + sep + formatted.0) } if validCount > 1 && renderedChoices.count > 1 { ...chooser... } Alternatives I considered and did not take: * **Label the segments distinguishably** (e.g. with the receiver name). That would surface the deployment's typo to every user -- "Sandcastle Echange Inc" beside "Sandcastle Exchange Inc" -- which is worse than hiding a choice that changes nothing. * **Deduplicate by payto in the wallet.** The two paytos are not byte-equal, so this needs a notion of "same account modulo receiver-name", which is a protocol judgment rather than a UI one. * **Gate on conversion specifically** -- show the chooser only when some account's currency differs from the withdrawal's. That is closer to the control's purpose, and it would hide a genuine choice between two same-currency accounts at different banks, which the rendered-label test keeps. **NOT YET COMPILED.** The one machine that builds this app is in the middle of an unattended two-hour measurement and a build would perturb it; I will confirm the build in a follow-up note. (The last patch I attached without building, on 11980, did not compile.) **How this was missed until a user reported it**, which is worth recording: the harness's screen survey (11986) scanned 1,048 recorded episodes for information shown twice and did not find this, for two reasons. The scan looked only at `staticText`/`textView`, so a chooser rendered as segments was invisible to it -- that is now fixed. But the deeper reason is that the corpus contains only screens the harness has actually driven, and the harness has never driven a *manual* withdrawal to its wire-details screen: it funds wallets with `withdrawTestBalance` and bank-integrated URIs. A survey of captured screens cannot find a defect on a screen nothing has visited, and I described that survey as covering "every screen the wallet shows" when it covers every screen the harness reaches. | ||||
| Tags | No tags attached. | ||||
| Attached Files | 11987-two-identical-wire-accounts.json (1,224 bytes)
{
"exchangeBaseUrl": "https://exchange.demo.taler.net/",
"amountRaw": "KUDOS:5",
"withdrawalAccountsList": [
{
"status": "ok",
"paytoUri": "payto://x-taler-bank/sandcastle/exchange?receiver-name=Sandcastle+Echange+Inc",
"transferAmount": "KUDOS:5",
"priority": 0,
"currencySpecification": {
"name": "Kudos",
"num_fractional_input_digits": 2,
"num_fractional_normal_digits": 2,
"num_fractional_trailing_zero_digits": 2,
"alt_unit_names": {
"0": "\u30af"
},
"common_amounts": [
"KUDOS:5",
"KUDOS:10",
"KUDOS:25",
"KUDOS:50"
]
},
"creditRestrictions": [],
"transferOptions": []
},
{
"status": "ok",
"paytoUri": "payto://x-taler-bank/sandcastle/exchange?receiver-name=Sandcastle+Exchange+Inc",
"transferAmount": "KUDOS:5",
"priority": 0,
"currencySpecification": {
"name": "Kudos",
"num_fractional_input_digits": 2,
"num_fractional_normal_digits": 2,
"num_fractional_trailing_zero_digits": 2,
"alt_unit_names": {
"0": "\u30af"
},
"common_amounts": [
"KUDOS:5",
"KUDOS:10",
"KUDOS:25",
"KUDOS:50"
]
},
"creditRestrictions": [],
"transferOptions": []
}
]
} identical-wire-choice.diff (3,040 bytes)
diff --git a/TalerWallet1/Views/Transactions/ManualDetailsV.swift b/TalerWallet1/Views/Transactions/ManualDetailsV.swift
index ea15087e..c4a8f016 100644
--- a/TalerWallet1/Views/Transactions/ManualDetailsV.swift
+++ b/TalerWallet1/Views/Transactions/ManualDetailsV.swift
@@ -96,7 +96,45 @@ struct WireTransferView: View {
.listRowSeparator(.hidden)
.accessibilityLabel(titleA11y)
}
- if validCount > 1 {
+ // A choice the user cannot read is not a choice.
+ //
+ // The chooser existed for accounts that differ -- above all
+ // a *conversion* account, where you wire one currency and
+ // receive another. It was offered whenever there was more
+ // than one account, which is not the same question.
+ //
+ // Measured against exchange.demo.taler.net on 2026-11-10,
+ // through wallet-core's own `getWithdrawalDetailsForAmount`:
+ //
+ // 0 payto://x-taler-bank/sandcastle/exchange
+ // ?receiver-name=Sandcastle+Echange+Inc
+ // transferAmount KUDOS:5 bankLabel nil priority 0
+ // 1 payto://x-taler-bank/sandcastle/exchange
+ // ?receiver-name=Sandcastle+Exchange+Inc
+ // transferAmount KUDOS:5 bankLabel nil priority 0
+ //
+ // The same account twice, differing only by a typo in
+ // `receiver-name` -- and `SegmentControl` labels a segment
+ // with the formatted transfer amount plus the bank label,
+ // so both segments read "KUDOS 5.00" and the user is asked
+ // to pick between them. The deployment's duplicate is worth
+ // fixing too, but the app should not offer a choice whose
+ // options it renders identically whatever the cause.
+ //
+ // Built from exactly what the segments show, so the test
+ // cannot drift from the rendering.
+ var renderedChoices = Set<String>()
+ for i in 0..<validCount {
+ let d = validDetails[i]
+ let f = d.transferAmount?.formatted(
+ specs: d.currencySpecification,
+ isNegative: false,
+ scope: d.scope,
+ useISO: false) ?? (EMPTYSTRING, EMPTYSTRING)
+ renderedChoices.insert((d.bankLabel ?? EMPTYSTRING)
+ + "\u{1F} " + f.0)
+ }
+ if validCount > 1 && renderedChoices.count > 1 {
if validCount > 3 { // too many for SegmentControl
AccountPicker(title: String(localized: "Bank"),
value: $validIndex,
| ||||