View Issue Details
| ID | Project | Category | View Status | Date Submitted | Last Update |
|---|---|---|---|---|---|
| 0011979 | Taler | wallet (iOS App) | public | 2026-11-09 09:20 | 2026-11-09 09:20 |
| 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 | always |
| Status | new | Resolution | open | ||
| Summary | 0011979: Send / Deposit amount: an amount over the balance is signalled only by turning a heading red -- no message, nothing for VoiceOver, and the disabled button's hint gives the wrong reason (patch attached) | ||||
| Description | Type more than the wallet holds into Send (or Deposit) and the "Next" button goes dead. The only other thing that changes on screen is that the "Available for transfer: X" heading turns red. No message appears, and the app already owns the right words elsewhere. What the user gets ------------------ * **Sighted**: colour, and nothing else. `CurrencyInputView.swift:160-168`: .foregroundColor(insufficient ? WalletColors().errorColor : .primary) That is the whole message. Colour as the sole carrier of meaning is what WCAG 1.4.1 is about, and red-on-grey is the first thing lost to a colour vision deficiency or a dimmed screen. * **VoiceOver**: nothing, and then a wrong reason. The same `Text` carries `.accessibilityLabel(heading.1)`, which is the *unchanged* "Available for transfer: X" -- the colour is not announced, so the only signal a sighted user gets is the one signal a blind user does not. Then the disabled button's hint (`AmountInputV.swift:108`) says "enabled when amount is non-zero" for all three disabling reasons -- `insufficient || isZero || invalid || coinData.tooMany` (`:91`). With an amount of 999 and a balance of 25 the amount is plainly not zero, so the hint is not merely unhelpful, it is false. The app has the right strings already ------------------------------------- `InsufficientBalanceHint.localizedCause` (`WalletModel.swift:13-60`) has eight translated causes, including "Balance insufficient! You don't have enough \(currency)." and "Purchase not possible due to age restriction", and the payment sheet prints them (`PaymentScan.swift:207-211`). None of them reaches the Send/Deposit amount screen. And the omission is specific, not a policy: the *same* screen explains its other disabling reason in words. Too many coins prints "Amount too big for a single withdrawal!" (`QuiteSomeCoins.swift:91`). Insufficiency, the far more common case, prints nothing. It also matters that the verdict is wallet-core's, not arithmetic. `checkAvailable` takes `coreInsufficient` first and says why in a comment (`AmountInputV.swift:70-77`): fees, age restrictions, per-exchange limits and pending refreshes. So the heading can turn red while the typed amount is visibly *below* the balance shown right above it -- and nothing on screen accounts for the difference. Reproducing ----------- Fund a wallet with KUDOS:25, Actions -> Send, type 999. Next is disabled, the heading is red, no text. As a deterministic XCUITest scenario: `scenarios/candidate/send-insufficient-silent.json`, which asserts the absence on purpose and has to be rewritten when this is fixed. Patch ----- Attached (`ios-insufficient-in-words.diff`): `CurrencyInputView` prints the existing `payment_balance_insufficient_max` string under the heading when `insufficient`, and the disabled button's hint names the actual reason. Builds clean (`BUILD SUCCEEDED`, iphonesimulator, Xcode 26). The patch deliberately uses the plainest of the eight causes. Carrying the specific `InsufficientBalanceHint` through from wallet-core's verdict -- so the age-restriction and pending-refresh cases say what they are -- would be the better fix and needs a signature change in `ComputeFeeResult`, which is a decision for whoever owns that type. Related, not duplicates: 11858 (no error message when no denominations are available -- different trigger, same family), 11959 (the subject screen's VoiceOver hint names a condition the button is not gated on -- the same hint-vs-gate mismatch, one screen earlier). | ||||
| Tags | No tags attached. | ||||
| Attached Files | ios-insufficient-in-words.diff (3,422 bytes)
diff --git a/TalerCommon/HelperViews/CurrencyInputView.swift b/TalerCommon/HelperViews/CurrencyInputView.swift
index c3c5697b..485eb622 100644
--- a/TalerCommon/HelperViews/CurrencyInputView.swift
+++ b/TalerCommon/HelperViews/CurrencyInputView.swift
@@ -129,6 +129,13 @@ struct CurrencyInputView: View {
availableString(available?.readableDescription ?? String(localized: "unknown"))
}
+ /// Why the amount cannot be sent. Deliberately the same string the
+ /// payment sheet uses for `wallet-balance-available-insufficient`.
+ var insufficientHint: String {
+ String(localized: "payment_balance_insufficient_max",
+ defaultValue: "Balance insufficient! You don't have enough \(amount.currencyStr).")
+ }
+
func heading() -> (String, String)? {
if let title {
return (title, a11yTitle)
@@ -166,6 +173,20 @@ struct CurrencyInputView: View {
.accessibilityLabel(heading.1)
.padding(.bottom, -6)
}
+ if insufficient {
+ // Turning the heading red was the whole message: no words for
+ // a sighted user, and nothing at all for VoiceOver, which
+ // reads the unchanged `heading.1`. The payment sheet already
+ // says this in words (PaymentScan.swift:207-211 via
+ // InsufficientBalanceHint.localizedCause), so reuse that
+ // string rather than invent a second one -- it is already
+ // translated.
+ Text(insufficientHint)
+ .padding(.horizontal, 4)
+ .frame(maxWidth: .infinity, alignment: title != nil ? .leading : .trailing)
+ .talerFont(.subheadline)
+ .foregroundColor(WalletColors().errorColor)
+ }
currencyField
.frame(maxWidth: .infinity, alignment: .trailing)
.foregroundColor(WalletColors().fieldForeground) // text color
diff --git a/TalerWallet1/Views/HelperViews/AmountInputV.swift b/TalerWallet1/Views/HelperViews/AmountInputV.swift
index 87482845..268c9c7f 100644
--- a/TalerWallet1/Views/HelperViews/AmountInputV.swift
+++ b/TalerWallet1/Views/HelperViews/AmountInputV.swift
@@ -106,7 +106,15 @@ struct AmountInputV: View {
let currency = amountToTransfer.currencyStr
let coinData = CoinData(coins: numCoins, fee: feeAmount)
let flags = checkAvailable(coinData)
- let hint = String(localized: "enabled when amount is non-zero", comment: "a11y")
+ // One hint for three different reasons was wrong twice over: a
+ // VoiceOver user with too high an amount was told the button needs a
+ // non-zero one, and the real reason -- not enough money, or too many
+ // coins -- was never spoken at all.
+ let hint = flags.insufficient
+ ? String(localized: "enabled when the amount fits the available balance", comment: "a11y")
+ : coinData.tooMany
+ ? String(localized: "enabled when the amount is small enough for a single transfer", comment: "a11y")
+ : String(localized: "enabled when amount is non-zero", comment: "a11y")
ScrollView { VStack(alignment: .trailing) {
CurrencyInputView(scope: scope,
amount: $amountToTransfer,
| ||||