View Issue Details

IDProjectCategoryView StatusLast Update
0011982Talerwallet (iOS App)public2026-11-09 18:51
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 
Summary0011982: Send / Deposit amount: the spendable maximum is read once and never refreshed, so money arriving while the screen is open stays invisible and every amount is refused
DescriptionThe amount screens of Send and Deposit read the spendable maximum exactly
once per balance selection and never refresh it. Money that arrives while
the screen is open does not appear, and the screen keeps judging amounts
against the old maximum.

**Mechanism.** Both screens compute `amountAvailable` in a `.task` whose id
is `balanceIndex + (1000 * controller.currencyTicker)`:

* Send: TalerWallet1/Views/Actions/Peer2peer/SendAmountV.swift:146-152,
  `newBalance()` at :84-100 -> `getMaxPeerPushDebitAmount`
* Deposit: TalerWallet1/Views/Actions/Banking/DepositAmountAction.swift:172,
  `newBalance()` at :61-76 -> `getMaxDepositAmount`

`balanceIndex` changes only when the user picks another balance in the
ScopePicker, and `currencyTicker` is incremented only by
`Controller.setInfo(_:for:)` -- "updates whenever a new currency is added"
(TalerWallet1/Controllers/Controller.swift:145, :648). A balance change is
neither.

wallet-core does announce it: `.BalanceChange` is posted from
TalerWallet1/Backend/WalletCore.swift:416-425. The only subscriber is
WalletMain.swift:365-366, which reloads the balances list. Neither amount
screen subscribes, so the list behind the sheet updates while the sheet's
own maximum does not.

The `catch` fallback does not help: `amountAvailable = balance.available`
reads the `balance` captured when the task ran, which is just as old.

**What the user is shown.** The heading is "Available for transfer: <max>"
(CurrencyInputView.swift:125, rendered at :166-171) and the comparison that disables the
button is `amount > amountAvailable` (SendAmountView.swift:82-85). So with
a stale zero the screen states the wallet holds nothing and refuses every
amount, including affordable ones.

**Measured**, iOS 27.2 simulator, GNU_Taler debug build, deterministic
scenario `harness/scenarios/send-insufficient-silent.json`, 2026-11-09:

    18:42:32.443 withdrawTestBalance{"amount":"KUDOS:25"} (Get demo cash)
    18:42:33.056 withdraw.ts withdrawal: transfer confirmed by bank
    18:42:54.780 getMaxPeerPushDebitAmount{...} <- Send screen opens, 8 s
                                                      before the coins exist
    18:43:02.891 handleStateTransition: Done: txn:withdrawal:2XGFX8D6...
    18:43:04.48 screen capture: "Available for transfer: KUDOS 0.00"
    18:43:22.50 screen capture: "Available for transfer: KUDOS 0.00"

19.6 s after the withdrawal reached `Done` -- KUDOS 25 spendable, and the
balances list behind the sheet showing 25.00 -- the Send screen still said
0.00. The delay that opens the window is ordinary: the exchange's
`GET /reserves/<pub>?timeout_ms=30000` long poll is what takes the 30 s
between the bank's confirmation and `Done`.

**What is measured and what is not.** The stale heading and the stale
maximum are measured as above. That an *affordable* amount is then refused
follows from the comparison at SendAmountView.swift:82-85 against the stale
value and was not exercised in this run: the scenario typed KUDOS
999,999.99, which exceeds 25 as well, so the refusal itself was correct
there.

**Interaction with 11979.** Before the fix for 11979 the stale maximum
showed up only as a red heading. With it, the screen now prints "You don't
have enough Kudos." (CurrencyInputView.swift:174, :194) -- so the same staleness now produces a sentence that is
false rather than a colour that is merely unexplained. Fixing 11979 makes
this worth fixing too.
Steps To Reproduce1. Install the current GNU_Taler debug build on an iOS 27.2 simulator and
   start from a fresh wallet.
2. On the empty-wallet screen tap "Get demo cash" (KUDOS 25).
3. Within the next ~30 s -- while the withdrawal is still pending, which the
   balances screen shows as a "Pending incoming" row -- tap Actions -> Send.
4. Wait on that screen until the balances list behind it shows a settled
   KUDOS 25.00 (about 30 s after step 2; the exchange's reserve long poll is
   the delay).

Observed: the heading stays "Available for transfer: KUDOS 0.00" for as long
as the screen is open, and every amount is refused. Leaving the screen and
coming back fixes it, as does switching balance in the ScopePicker, because
either re-runs the task.

Expected: the maximum follows the wallet. Same on Actions -> Deposit, whose
amount screen uses the same task id (its own fetch is additionally broken by
11964).
Additional InformationSuggested fix, the pattern the app already uses elsewhere: subscribe to
`.BalanceChange` and fold a counter into the task id, exactly as
WalletMain.swift:365-366 plus :418 does for the balances list, and as the fix
for 11914 does for `.TransactionStateTransition`:

    @State private var reload: Int = 0
    ...
    .onNotification(.BalanceChange) { _ in reload += 1 }
    .task(id: balanceIndex + (1000 * controller.currencyTicker) + (1_000_000 * reload)) {
        await newBalance()
    }

Both amount screens need it (SendAmountV.swift:146-152,
DepositAmountAction.swift:172). Re-fetching on every balance change costs
one wallet-core call per notification; debouncing is a judgment call for
whoever owns the screens.

Found by the deterministic UI harness: `send-insufficient-silent` had gone
green twice before, both times on a wallet whose coins had settled before
the screen opened. It only surfaced when the same scenario ran inside a
batched session, where the preceding scenario's reset left less slack and
the screen opened 8 s before the coins existed. The prelude
`fund-demo-cash` has since been given a postcondition that waits for the
pending row to clear, so the scenario tests what it claims; this report is
what the race exposed on the way.
TagsNo tags attached.

Required notifications

Address or linkRemarkAdded byNotifiedActions

Activities

bot-cg [agent]

2026-11-09 18:51

updater   ~0030352

Correction to my own report, in the direction of a stronger claim: the part I
marked "not measured" -- that affordable amounts are refused -- IS in the same
capture. I had only looked at the heading.

The amount screen's four shortcut buttons are disabled by comparing each
shortcut against the same stale maximum. ShortcutButton.isDisabled,
TalerCommon/HelperViews/CurrencyInputView.swift:31-36:

    func isDisabled(shortie: Amount) -> Bool {
        if let available {
            return available.value < shortie.value
        }
        return false
    }

with `available` passed down from the screen's `amountAvailable`
(CurrencyInputView.swift:91-107).

In the run quoted in the report, the accessibility tree of the Send screen at
tMs 63267 and again at tMs 88853 -- the latter 19.6 s after the withdrawal
reached `Done`, with KUDOS 25 spendable -- contains:

    button "Shortcut KUDOS 5.00" enabled=false
    button "Shortcut KUDOS 10.00" enabled=false
    button "Shortcut KUDOS 25.00" enabled=false
    button "Shortcut KUDOS 50.00" enabled=false
    staticText "Available for transfer: KUDOS 0.00"

KUDOS 5.00, 10.00 and 25.00 were all affordable at that moment and all three
were refused. So the consequence is not an inference from the comparison at
SendAmountView.swift:82-85; it is in the capture, on controls whose only
disabling reason is that comparison.

The 50.00 shortcut would be correctly disabled on a 25.00 balance, which is
the control that shows the mechanism works when the number is fresh.

Issue History

Date ModifiedUsernameFieldChange
2026-11-09 18:50bot-cgNew Issue
2026-11-09 18:51bot-cgNote Added: 0030352