View Issue Details
| ID | Project | Category | View Status | Date Submitted | Last Update |
|---|---|---|---|---|---|
| 0012065 | Taler | wallet (iOS App) | public | 2026-11-11 01:20 | 2026-11-11 02:25 |
| 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 | major | Reproducibility | have not tried |
| Status | new | Resolution | open | ||
| Summary | 0012065: iOS: bank-integrated withdrawal on a fresh wallet skips the ToS step; "Confirm Withdrawal" then fails with WALLET_EXCHANGE_TOS_NOT_ACCEPTED (stale exchange cache) | ||||
| Description | On a fresh wallet, a bank-integrated withdrawal (taler://withdraw/...) goes straight to "Confirm Withdrawal" without asking for the exchange's terms of service. Confirming then fails with "Internal core error / Error (WALLET_EXCHANGE_TOS_NOT_ACCEPTED)" and the withdrawal cannot continue from that screen. What the user sees (same URI from bank.demo.taler.net, fresh wallets, origin/master builds of 2026-11-11): - Android: amount and a "Terms of Service" button (before-android-1.png). - WebExtension: "Review and accept this exchange's terms of service before withdrawing." with "Review terms"; Withdraw is disabled (before-web-1.png). - iOS: amounts and "Confirm Withdrawal", no ToS step (before-ios-1.png). Tapping it: "Internal core error / Error (WALLET_EXCHANGE_TOS_NOT_ACCEPTED)" (before-ios-2.png). The same happens for an exchange that is not built in (tripwire.taler.net:8445, KUDOS): before-ios-3.png, before-ios-4.png. It is a race: in our runs it happened 3 times out of 3 on fresh wallets; the first run after a simulator/driver restart showed the ToS step (before-ios-0-race.png). Cause: - wallet-core reports tosStatus "missing-tos" for an exchange whose terms it has not downloaded yet (tosCurrentEtag == null; taler-wallet-core/src/common.ts:353-366). That is the state of a built-in or newly discovered exchange before its first update. - prepareBankIntegratedWithdrawal updates the bank's exchange (terms now known: "proposed") and emits ExchangeStateTransition notifications on the way. iOS handles each with Controller.updateExchange(for:state:) (Backend/WalletCore.swift:401-406, Controllers/Controller.swift:566-588), which calls exchange(for:); on a cache miss that fetches the entry and caches it (Controller.swift:543-556), possibly while it still says missing-tos. - WithdrawUriScan.viewDidLoad then takes the exchange from that cache via controller.checkCurrencyInfo(for:) (Views/Sheets/WithdrawBankIntegrated/WithdrawUriScan.swift:130-132; Controller.swift:599-611). If the later notification has not replaced the record yet, the stale missing-tos entry goes to WithdrawAcceptView, whose gate treats missingTos as "nothing to accept" (WithdrawAcceptView.swift:63-71, 119-132), so "Confirm Withdrawal" is offered and acceptBankIntegratedWithdrawal answers 7037. - Android asks wallet-core again after prepare (withdraw/WithdrawManager.kt:381-404 findExchangeByUrl -> getExchangeEntryByUrl, then TosReviewRequired at :434-438); the WebExtension gates on a live ListExchanges query (routes/App.tsx:3564-3568, routes/withdrawal-review-model.ts:54-65). This is the "second defect" suspected in the 2026-11-10 note on 0011934 (cached exchange record disagreeing with wallet-core). That note's patch (offer the ToS when 7037 arrives anyway) is complementary; this one prevents the wrong screen. Patch (ios-bank-withdrawal-tos.patch, against origin/master 674b7915): in WithdrawUriScan.viewDidLoad, keep checkCurrencyInfo (for the currency info) but use wallet-core's current entry, model.getExchangeByUrl(url:viewHandles:true), falling back to the cached one. One statement plus a comment. Verified: GNU_Taler Release build for the simulator (iOS 27.2); three fresh runs against the demo bank all showed the ToS step (after-ios-1.png); after accepting, "Confirm Withdrawal" (after-ios-2.png) leads to "Authorize at Bank" (after-ios-3.png). Same against tripwire :8445 (after-ios-4.png, after-ios-5.png, confirm -> "Authorize at Bank"). Side observation, not part of this report: on tripwire :8445 the confirm screen shows no amounts at all (before-ios-3.png, after-ios-5.png). Assisted by LLM | ||||
| Steps To Reproduce | 1. Fresh install / empty wallet. 2. Start a withdrawal on https://bank.demo.taler.net/ (any amount) and open the taler://withdraw/... link in the app. 3. Observe "Confirm Withdrawal" without a "Terms of Service" step; tap it. 4. "Internal core error / Error (WALLET_EXCHANGE_TOS_NOT_ACCEPTED)". (Automated: wc-stack withdraw-uri --amount KUDOS:5; wc-ios start --fresh; wc-ios uri <uri>.) | ||||
| Tags | No tags attached. | ||||
| Attached Files | ios-bank-withdrawal-tos.patch (1,225 bytes)
diff --git a/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawUriScan.swift b/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawUriScan.swift
index 559dad2a..fa3ebc7a 100755
--- a/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawUriScan.swift
+++ b/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawUriScan.swift
@@ -129,7 +129,11 @@ struct WithdrawUriScan: View {
if let baseUrl, let someExchange = try? await controller.checkCurrencyInfo(for: baseUrl) {
symLog.log("Info(for: \(baseUrl.trimURL) loaded")
- exchange = someExchange
+ // checkCurrencyInfo answers from the Controller's exchange cache, which can predate the
+ // exchange update prepareBankIntegratedWithdrawal just did: a built-in exchange whose ToS
+ // were never downloaded is listed as "missing-tos", and WithdrawAcceptView would then offer
+ // "Confirm Withdrawal" without the ToS step. Use wallet-core's current entry instead.
+ exchange = (try? await model.getExchangeByUrl(url: baseUrl, viewHandles: true)) ?? someExchange
} else {
exchangeError = true
}
| ||||
| related to | 0011934 | reviewing | Withdraw / Loading: expected wallet-core errors are titled "Internal core error" (e.g. 7054 withdrawal limit exceeded), with raw JSON as the body |
| Follow-up: the same stale exchange record also hides the amounts on the confirmation screen, and the patch attached above fixes the ToS step but not that. Combined patch attached (ios-bank-withdrawal-tos-and-amounts.patch). Observed with a bank withdrawal of KUDOS 3 from tripwire.taler.net:8446 (exchange :8445, not built in), fresh wallets: - origin/master, race lost: "Withdrawal / tripwire.taler.net / Confirm Withdrawal" and nothing else, no amount, no fee, no "Using payment service" (before-ios-2-race-base.png). Race won: ToS step plus "Chosen amount to withdraw: KUDOS 3.00" (base-ios-amounts-when-race-not-hit.png). - with only ios-bank-withdrawal-tos.patch: the ToS step is always there, but in 1 of 2 runs the amounts were still missing, also after accepting the ToS (before-ios-3-with-12065-patch-only.png). - Android and WebExtension, same URI: amount shown (before-android-1.png "Total amount ク3.00", before-web-1.png "Send by bank transfer KUDOS:3 / Receive as digital cash KUDOS:3"). Cause: ThreeAmountsSection (Views/Transactions/ThreeAmountsSection.swift:109-114, 130) shows no rows while the currency info for its scope is unknown, and it only looks the info up in Controller's cache. WithdrawUriScan loads currency info via checkCurrencyInfo for the cached exchange's scope; when that record was stale, the fresh exchange's scope never gets currency info, so the section stays empty. Extra hunk: ThreeAmountsSection.viewDidLoad loads the currency info (controller.updateInfo2) when the cache has none for the scope. With both changes: 4 of 4 fresh runs showed the ToS step and the amounts (after-ios-1.png), and the amounts stay after accepting the ToS (after-ios-2-after-tos.png). Built as GNU_Taler Release for the simulator. Assisted by LLM ios-bank-withdrawal-tos-and-amounts.patch (2,065 bytes)
diff --git a/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawUriScan.swift b/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawUriScan.swift
index 559dad2a..fa3ebc7a 100755
--- a/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawUriScan.swift
+++ b/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawUriScan.swift
@@ -129,7 +129,11 @@ struct WithdrawUriScan: View {
if let baseUrl, let someExchange = try? await controller.checkCurrencyInfo(for: baseUrl) {
symLog.log("Info(for: \(baseUrl.trimURL) loaded")
- exchange = someExchange
+ // checkCurrencyInfo answers from the Controller's exchange cache, which can predate the
+ // exchange update prepareBankIntegratedWithdrawal just did: a built-in exchange whose ToS
+ // were never downloaded is listed as "missing-tos", and WithdrawAcceptView would then offer
+ // "Confirm Withdrawal" without the ToS step. Use wallet-core's current entry instead.
+ exchange = (try? await model.getExchangeByUrl(url: baseUrl, viewHandles: true)) ?? someExchange
} else {
exchangeError = true
}
diff --git a/TalerWallet1/Views/Transactions/ThreeAmountsSection.swift b/TalerWallet1/Views/Transactions/ThreeAmountsSection.swift
index ab70daf8..f7f66b40 100644
--- a/TalerWallet1/Views/Transactions/ThreeAmountsSection.swift
+++ b/TalerWallet1/Views/Transactions/ThreeAmountsSection.swift
@@ -109,6 +109,11 @@ struct ThreeAmountsSection: View {
@MainActor
private func viewDidLoad() async {
if let scope {
+ if controller.info(for: scope) == nil {
+ // a scope seen for the first time (e.g. an exchange the wallet just learned
+ // about from a bank withdrawal): without its currency info nothing is shown
+ await controller.updateInfo2(scope)
+ }
currencyInfo = controller.info(for: scope) ?? CurrencyInfo.zero(UNKNOWN)
}
}
| |
| Date Modified | Username | Field | Change |
|---|---|---|---|
| 2026-11-11 01:20 | bot-cg | New Issue | |
| 2026-11-11 01:20 | bot-cg | Relationship added | related to 0011934 |
| 2026-11-11 01:20 | bot-cg | File Added: before-android-1.png | |
| 2026-11-11 01:20 | bot-cg | File Added: before-web-1.png | |
| 2026-11-11 01:20 | bot-cg | File Added: before-ios-0-race.png | |
| 2026-11-11 01:20 | bot-cg | File Added: before-ios-1.png | |
| 2026-11-11 01:20 | bot-cg | File Added: before-ios-2.png | |
| 2026-11-11 01:20 | bot-cg | File Added: before-ios-3.png | |
| 2026-11-11 01:20 | bot-cg | File Added: before-ios-4.png | |
| 2026-11-11 01:20 | bot-cg | File Added: after-ios-1.png | |
| 2026-11-11 01:20 | bot-cg | File Added: after-ios-2.png | |
| 2026-11-11 01:20 | bot-cg | File Added: after-ios-3.png | |
| 2026-11-11 01:20 | bot-cg | File Added: after-ios-4.png | |
| 2026-11-11 01:20 | bot-cg | File Added: after-ios-5.png | |
| 2026-11-11 01:20 | bot-cg | File Added: ios-bank-withdrawal-tos.patch | |
| 2026-11-11 02:25 | bot-cg | Note Added: 0030403 | |
| 2026-11-11 02:25 | bot-cg | File Added: before-ios-2-race-base.png | |
| 2026-11-11 02:25 | bot-cg | File Added: base-ios-amounts-when-race-not-hit.png | |
| 2026-11-11 02:25 | bot-cg | File Added: before-ios-3-with-12065-patch-only.png | |
| 2026-11-11 02:25 | bot-cg | File Added: before-android-1-2.png | |
| 2026-11-11 02:25 | bot-cg | File Added: before-web-1-2.png | |
| 2026-11-11 02:25 | bot-cg | File Added: after-ios-1-2.png | |
| 2026-11-11 02:25 | bot-cg | File Added: after-ios-2-after-tos.png | |
| 2026-11-11 02:25 | bot-cg | File Added: ios-bank-withdrawal-tos-and-amounts.patch |