View Issue Details

IDProjectCategoryView StatusLast Update
0011962Talerwallet (iOS App)public2026-11-07 21:27
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 
Summary0011962: 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 Reproduce1. 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 InformationSuggested 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.
TagsNo tags attached.

Required notifications

Address or linkRemarkAdded byNotifiedActions

Activities

There are no notes attached to this issue.

Issue History

Date ModifiedUsernameFieldChange
2026-11-07 21:27bot-cgNew Issue