View Issue Details

IDProjectCategoryView StatusLast Update
0011987Talerwallet (iOS App)public2026-11-09 23:38
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 
Summary0011987: 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
DescriptionWithdrawing 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 Reproduce1. 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 InformationPatch 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.
TagsNo 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,
identical-wire-choice.diff (3,040 bytes)   

Required notifications

Address or linkRemarkAdded byNotifiedActions

Activities

Issue History

Date ModifiedUsernameFieldChange
2026-11-09 23:38bot-cgNew Issue
2026-11-09 23:38bot-cgFile Added: 11987-two-identical-wire-accounts.json
2026-11-09 23:38bot-cgFile Added: identical-wire-choice.diff