View Issue Details
| ID | Project | Category | View Status | Date Submitted | Last Update |
|---|---|---|---|---|---|
| 0012142 | Taler | wallet (iOS App) | public | 2026-11-15 15:50 | 2026-11-15 15:54 |
| Reporter | bot-cg [agent] | ||||
| Priority | normal | Severity | major | ||
| Status | new | ||||
| Summary | 0012142: Pay-template sheet stays on "In progress" (claim-proposal) with only "Done": the Pay button never appears when dialog:proposed arrives right after the sheet opens | ||||
| Description | Opening a taler://pay-template/ link can leave the payment sheet on a transaction that says "In progress" (in developer mode: "claim-proposal") with only a "Done" button. The amount, the merchant, the "Pay ... now" button never appear, although wallet-core has had the order ready for confirmation (dialog:proposed) since 40 ms after the sheet opened. The user cannot pay; "Done" closes the sheet. See pay-template-stuck-claim-proposal.jpg, taken 90 s after the link was opened. Reproduced 4 times out of 4 on 2026-11-15 (iOS 27.2 simulator, wallet funded with 25 KUDOS, merchant backend on the same LAN, i.e. a fast /claim): - a template with fixed amount and summary and a minimum_age (2 runs) - a template with editable amount and summary, after entering both (2 runs) It depends on timing, not on the template: two other templates on the same merchant were paid normally on 2026-11-12. The difference visible in the app log is where the sheet's transaction view appears relative to the two state transitions. Stuck (view appears BETWEEN claim-proposal and proposed): 16:17:01.906 handleStateTransition: Pending:claim-proposal txn:payment:46ZZ... 16:17:01.909 waitTransactionState{... "major":"dialog"} 16:17:01.914 switching from 11 to 22 TransactionSummaryList.swift:452 16:17:01.916 getTransactionById{txn:payment:46ZZ...} <- answers claim-proposal 16:17:01.926 current state ... is {"major":"dialog","minor":"proposed"} 16:17:01.949 handleStateTransition: dialog:proposed txn:payment:46ZZ... 16:17:01.954 getTransactionById{txn:payment:46ZZ...} <- should answer proposed 16:17:02.011 Request "id":62 took 59 ms (nothing further; getChoicesForPayment is never sent) Working (both transitions BEFORE the view appears): 00:54:29.523 handleStateTransition: Pending:claim-proposal txn:payment:ZSK8... 00:54:29.524 handleStateTransition: dialog:proposed txn:payment:ZSK8... 00:54:29.537 switching from 11 to 22 TransactionSummaryList.swift:452 00:54:29.539 getTransactionById{...} <- answers proposed 00:54:29.631 getChoicesForPayment{...} 00:54:32.749 confirmPay{...} So when the dialog:proposed transition arrives while the view's first load (which saw claim-proposal) is just completing, the view ends up showing the stale state, even though a second getTransactionById is sent after the transition. Where to look (TransactionSummaryList.swift). I have not established the exact interleaving; these are the pieces involved: - checkReload() answers a transition by resetting talerTX to a dummy and changing viewId, then calling loadTransaction() from a Task; - the .onNotification(.TransactionStateTransition) handlers are attached to the list, which only exists while talerTX is not the dummy; while it is the dummy, the body is Color.clear with its own .task { loadTransaction() }; - loadTransaction() assigns talerTX after an await, so two of them running at once assign in completion order, not in request order. A reload that cannot be overtaken (e.g. a generation counter checked after the await, or a single .task(id:) keyed on the last seen transition) would remove the dependence on timing. With a merchant that is further away (/claim takes longer than the sheet needs to appear) this ordering is rare, which is probably why it has not been seen with the demo merchant. Found by the UI fuzz harness, scenarios template-age-restricted and template-open. | ||||
| Tags | No tags attached. | ||||
| Attached Files | |||||
| Correction and one more data point. The description says "reproduced 4 times out of 4". The count is 3 of 4: the minimum_age template was stuck in 2 of 2 runs, the editable template in 1 of 2 -- its second run (16:54) was paid normally. So this is intermittent on the same template, which fits the timing explanation. The run that worked narrows the window. There the dialog:proposed transition arrived while the view's FIRST getTransactionById was still in flight, so that first load already returned "proposed": 16:54:15.552 handleStateTransition: Pending:claim-proposal 16:54:15.563 switching from 11 to 22 TransactionSummaryList.swift:452 16:54:15.568 getTransactionById{...} (first load, sent) 16:54:15.570 handleStateTransition: dialog:proposed 16:54:15.603 Request "id":63 took 39 ms (first load, answered) 16:54:15.675 getChoicesForPayment{...} In the stuck runs the transition arrived 3 ms AFTER the first load had been answered with claim-proposal (answered .946, transition .949), i.e. at the moment the list is being put on screen for the first time: 16:17:01.916 getTransactionById{...} (first load, sent) 16:17:01.946 Request "id":60 took 32 ms (answered: claim-proposal) 16:17:01.949 handleStateTransition: dialog:proposed 16:17:01.954 getTransactionById{...} (reload, sent) 16:17:02.011 Request "id":62 took 59 ms (no getChoicesForPayment; the sheet keeps showing claim-proposal) So the failing case is specifically: checkReload() running (reset to dummy, new viewId, reload) while the result of the first load is still being applied. The reload is sent and answered, and its result does not end up on screen. | |