View Issue Details
| ID | Project | Category | View Status | Date Submitted | Last Update |
|---|---|---|---|---|---|
| 0011962 | Taler | wallet (iOS App) | public | 2026-11-07 21:27 | 2026-11-07 21:27 |
| 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 | 0011962: Discounts/Passes: the only delete control is unreachable, so a token can never be removed (swipeActions gated on !isValid and attached to a nested HStack) | ||||
| Description | `deleteDiscount` and `deleteSubscription` have exactly one call site in the UI, and it cannot be reached. Both are therefore dead in practice: a token the wallet holds can never be removed. TalerWallet1/Views/Balances/DiscountPasses.swift:69-104, inside `DiscountsPassesItem`: Section { VStack(alignment: .leading, spacing: 2) { let row = HStack(alignment: .top) { name; Spacer(); ... } .padding(.horizontal) .padding(.top, 6) if isValid { row // <-- no swipe action } else { let swipe = Button { symLog.log("delete \(token.id)") deleteAction(token.id) } label: { Label("Delete", systemImage: "trash") }.tint(WalletColors().negative) row.swipeActions(edge: .trailing) { swipe } } ... Two independent problems, either of which alone would be enough: 1. **The swipe action is attached only in the `else` branch of `if isValid`**, and `isValid` is `token.validityStart < .now()` (line 55). So it is offered only for a token whose validity has *not started yet* -- never for a token just earned, and never for anything in the "Expired" segment, where `validityEnd <= now` implies `validityStart < now` as well. Those are exactly the tokens a user would want to remove. 2. **`.swipeActions` is applied to the wrong view.** `row` is an `HStack` nested inside the `VStack(alignment: .leading, spacing: 2)` at line 70, which is itself a child of the `Section` at line 69. `.swipeActions` only takes effect on a direct row of a `List`; applied to a nested `HStack` it is a no-op. So even for a not-yet-valid token the swipe is very likely dead. `deleteAction` is called from nowhere else -- the closure is wired at lines 152-158 (`DiscountsPassesList.delete` -> `model.deleteSubscription` / `model.deleteDiscount`) and invoked only at line 97-102, inside that swipe button. The validity strip's `onTapGesture` at lines 129-133 toggles the displayed start/end date and is not a delete. There is no other path, no context menu and no Edit mode. Confirmed dark by coverage measurement: over 702 instrumented episodes of a coverage-guided XCUITest explorer, `deleteDiscount` (Model+Balances.swift:182) and `deleteSubscription` (Model+Balances.swift:194) never executed, while `acceptPeerPushCredit` on a comparable path did (x5). That is consistent with the control being unreachable rather than merely rarely used. | ||||
| Steps To Reproduce | 1. Pay a v1 contract order whose choice issues a discount or subscription token, so the wallet holds one. (A merchant backend token family plus a v1 order with `outputs: [{type: token, ...}]` does it.) 2. Balances -> the "Discounts" or "Passes" row (DiscountPasses.swift:276-302). 3. Try to remove the token: swipe the row left, in both the "Active" and "Expired" segments. Observed: no Delete action appears, in either segment. The token cannot be removed from the wallet by any means in the UI. Expected: a swipe (or some other control) that removes the token, reaching `model.deleteDiscount` / `model.deleteSubscription`. | ||||
| Additional Information | Suggested fix, two small changes in DiscountPasses.swift: * Move `.swipeActions` off the nested `HStack` and onto the view that is the actual `List` row -- i.e. apply it to the `VStack` (or restructure so the row content is one view that `.swipeActions` can attach to). * Drop the `if isValid` gate, or invert it. Deleting a token the user holds should not depend on whether its validity window has opened; if anything, an expired or used token is the most likely thing to want gone. A caveat worth stating: I have not been able to exercise the fixed control yet, so "very likely dead" in point 2 is a reading of the modifier placement rather than an observation of a failed swipe on a not-yet-valid token -- such a token is itself hard to obtain (it needs a token family with a future `start_offset`). Point 1 is unambiguous from the source either way. Found while building a deterministic scenario suite for the iOS wallet: the flow "pay an order that issues tokens, then delete them" could not be expressed, because its last step has no reachable control. | ||||
| Tags | No tags attached. | ||||
| Date Modified | Username | Field | Change |
|---|---|---|---|
| 2026-11-07 21:27 | bot-cg | New Issue |