View Issue Details
| ID | Project | Category | View Status | Date Submitted | Last Update |
|---|---|---|---|---|---|
| 0011982 | Taler | wallet (iOS App) | public | 2026-11-09 18:50 | 2026-11-09 18:51 |
| 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 | 0011982: 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 | ||||
| Description | The 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 Reproduce | 1. 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 Information | Suggested 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. | ||||
| Tags | No tags attached. | ||||
| 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. | |