View Issue Details
| ID | Project | Category | View Status | Date Submitted | Last Update |
|---|---|---|---|---|---|
| 0012105 | Taler | wallet (iOS App) | public | 2026-11-11 02:01 | 2026-11-11 02:01 |
| 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 | 0012105: iOS: no warning before confirming a withdrawal that will need KYC or exceeds the exchange's limit (wallet-core's KYC preview ignored) | ||||
| Description | Scenario (same on all three, tripwire KUDOS exchange https://tripwire.taler.net:8445/, KYC above KUDOS 4 cumulated per originating bank account): a first bank-integrated withdrawal of KUDOS 3 completes; a second one of KUDOS 3 from the same bank account is opened in the wallet. What the user sees on the confirmation screen of the second withdrawal: - WebExtension: "This withdrawal, together with your recent withdrawals, may require identity verification at the exchange." (before-web-1.png). After confirming it does end in identity verification (before-web-2-outcome-kyc.png). - Android: amount and "Confirm withdrawal", no hint (before-android-1.png); after confirming: "This transaction would exceed a limit set by the payment service..." / "Complete KYC" (before-android-2-outcome-kyc.png). - iOS: amount, payment service and "Confirm Withdrawal", no hint (before-ios-1.png); after confirming: "Pending incoming ... Legitimization required" (before-ios-2-outcome-kyc.png). So on Android and iOS the user authorises the bank transfer first and only then learns that the money is held until they pass KYC. For an amount over the exchange's hard limit the web wallet disables Withdraw with "This withdrawal would exceed the exchange's withdrawal limit..."; Android and iOS let the user confirm. wallet-core already returns the preview in getWithdrawalDetailsForAmount: kycRequired, withdrawalKycStatus ("unknown" | "ok" | "kyc-required" | "hard-limit"), kycSoftLimit, kycHardLimit, balanceKyc (taler-util types-taler-wallet.ts WithdrawalKycPreview / WithdrawalDetailsForAmount; computed in taler-wallet-core withdraw.ts internalGetWithdrawalDetailsForAmount). The account-volume check needs the bank-integrated withdrawal's transactionId in the request, which the web wallet passes (wallet-webui routes/withdrawal-review-model.ts:22-34, used at :71-108; screens/WithdrawalReviewScreen.tsx:99-145). iOS code (origin/master 674b7915): TalerWallet1/Model/Model+Withdraw.swift:146-155 (WithdrawalDetailsForAmount without these fields), :157-173 (request without transactionId); Views/Sheets/WithdrawBankIntegrated/WithdrawAcceptView.swift:72-133 and Views/Actions/Banking/ManualWithdraw.swift:238-283 (no hint, button not gated); WithdrawUriScan.swift:110 has the transactionId but does not pass it on. Patch (ios-withdraw-kyc-preview.patch): decode kycRequired and withdrawalKycStatus, add a kycHint/exceedsHardLimit helper; WithdrawUriScan passes the transactionId to WithdrawAcceptView, which passes it to getWithdrawalDetailsForAmount; both withdrawal screens show the hint in the attention colour and disable the confirm button on hard-limit. Three new keys in Localizable.xcstrings (same wording as the Android patch). Verified: GNU_Taler Release simulator build; after-ios-1.png shows the hint for the same scenario. (Step 1 of the scenario was run with the fix for 0012065, because on origin/master the first withdrawal on a fresh wallet skips the ToS step and fails.) Assisted by LLM | ||||
| Steps To Reproduce | 1. Withdraw KUDOS 3 from a bank account at https://tripwire.taler.net:8446/ into the wallet (exchange :8445) and let it complete. 2. Start a second KUDOS 3 withdrawal from the same bank account and open it in the wallet. 3. The confirmation screen gives no KYC hint; after confirming, the withdrawal waits for KYC. | ||||
| Tags | No tags attached. | ||||
| Attached Files | ios-withdraw-kyc-preview.patch (11,628 bytes)
diff --git a/TalerWallet1/Model/Model+Withdraw.swift b/TalerWallet1/Model/Model+Withdraw.swift
index 1083b458..355c054a 100644
--- a/TalerWallet1/Model/Model+Withdraw.swift
+++ b/TalerWallet1/Model/Model+Withdraw.swift
@@ -152,6 +152,24 @@ struct WithdrawalDetailsForAmount: Decodable {
var ageRestrictionOptions: [Int]? // Array of ages
var scopeInfo: ScopeInfo
var unconfirmedKeyChange: ExchangeKeyChangeInfo? // if set, accepting the withdrawal will fail until confirmed
+ var kycRequired: Bool? // wallet-core's preview: this withdrawal will probably need KYC
+ var withdrawalKycStatus: String? // "unknown", "ok", "kyc-required" or "hard-limit" (needs transactionId)
+}
+extension WithdrawalDetailsForAmount {
+ var exceedsHardLimit: Bool { withdrawalKycStatus == "hard-limit" }
+ /// what to tell the user before the withdrawal is confirmed, if anything
+ var kycHint: String? {
+ if exceedsHardLimit {
+ return String(localized: "This withdrawal would exceed the payment service's withdrawal limit. Choose a smaller amount or try again later.")
+ }
+ if withdrawalKycStatus == "kyc-required" {
+ return String(localized: "This withdrawal, together with your recent withdrawals, may require identity verification at the payment service.")
+ }
+ if kycRequired == true {
+ return String(localized: "This amount may require identity verification at the payment service.")
+ }
+ return nil
+ }
}
/// A request to get an exchange's withdrawal details.
fileprivate struct GetWithdrawalDetailsForAmount: WalletBackendFormattedRequest {
@@ -160,14 +178,17 @@ fileprivate struct GetWithdrawalDetailsForAmount: WalletBackendFormattedRequest
func args() -> Args { Args(amount: amount,
exchangeBaseUrl: baseUrl,
restrictScope: scope,
+ transactionId: transactionId,
progressToken: baseUrl) }
var amount: Amount
var baseUrl: String?
var scope: ScopeInfo?
+ var transactionId: String?
struct Args: Encodable {
var amount: Amount
var exchangeBaseUrl: String? // needed for b-i-withdrawals
var restrictScope: ScopeInfo? // only if exchangeBaseUrl is nil
+ var transactionId: String? // b-i-withdrawal: lets wallet-core check the KYC volume rules
var progressToken: String?
}
}
@@ -294,9 +315,11 @@ extension WalletModel {
nonisolated func getWithdrawalDetailsForAmount(_ amount: Amount,
baseUrl: String?,
scope: ScopeInfo?,
+ transactionId: String? = nil,
viewHandles: Bool = false)
async throws -> WithdrawalDetailsForAmount {
- let request = GetWithdrawalDetailsForAmount(amount: amount, baseUrl: baseUrl, scope: scope)
+ let request = GetWithdrawalDetailsForAmount(amount: amount, baseUrl: baseUrl, scope: scope,
+ transactionId: transactionId)
let controller = Controller.shared
controller.progressOperation = request.operation
controller.progressToken = baseUrl
diff --git a/TalerWallet1/Resources/Localizable.xcstrings b/TalerWallet1/Resources/Localizable.xcstrings
index e283ca45..a5aa890a 100755
--- a/TalerWallet1/Resources/Localizable.xcstrings
+++ b/TalerWallet1/Resources/Localizable.xcstrings
@@ -1699,6 +1699,15 @@
">>" : {
"comment" : "A button that changes the padding of the row subview.",
"shouldTranslate" : false
+ },
+ "This amount may require identity verification at the payment service." : {
+
+ },
+ "This withdrawal would exceed the payment service's withdrawal limit. Choose a smaller amount or try again later." : {
+
+ },
+ "This withdrawal, together with your recent withdrawals, may require identity verification at the payment service." : {
+
},
"~ 1 hour ago" : {
"localizations" : {
diff --git a/TalerWallet1/Views/Actions/Banking/ManualWithdraw.swift b/TalerWallet1/Views/Actions/Banking/ManualWithdraw.swift
index 559c2259..59345ce5 100644
--- a/TalerWallet1/Views/Actions/Banking/ManualWithdraw.swift
+++ b/TalerWallet1/Views/Actions/Banking/ManualWithdraw.swift
@@ -251,6 +251,7 @@ struct ManualWithdrawContent: View {
amountToTransfer: amountToTransfer)
// restrictAge: restrictAge)
let disabled = amountToTransfer.isZero || coinData.invalid || coinData.tooMany
+ || detailsForAmount.exceedsHardLimit
if tosAccepted { // Von der Bank abzuhebender Betrag
let a11yLabel = String(localized: "Amount to withdraw from bank", comment: "a11y, no abbreviations")
@@ -273,6 +274,12 @@ struct ManualWithdrawContent: View {
coinData: coinData,
shouldShowFee: true, // TODO: set to false if we never charge withdrawal fees
feeIsNegative: true)
+ if let kycHint = detailsForAmount.kycHint {
+ Text(kycHint)
+ .foregroundColor(WalletColors().attention)
+ .talerFont(.body)
+ .multilineTextAlignment(.leading)
+ }
// agePicker
NavigationLink(destination: destination) { // VIEW_WITHDRAW_ACCEPT
Text(withdrawButtonTitle(currency))
diff --git a/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawAcceptView.swift b/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawAcceptView.swift
index c0cb7a66..431b406c 100644
--- a/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawAcceptView.swift
+++ b/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawAcceptView.swift
@@ -20,6 +20,7 @@ struct WithdrawAcceptView: View {
@Binding var amountToTransfer: Amount
@Binding var wireFee: Amount?
@Binding var exchange: Exchange? // user can select one from possibleExchanges
+ var transactionId: String? = nil // from prepareBankIntegratedWithdrawal
@EnvironmentObject private var controller: Controller
@EnvironmentObject private var model: WalletModel
@@ -42,7 +43,8 @@ struct WithdrawAcceptView: View {
if !amountToTransfer.isZero, let exchange {
if let details = try? await model.getWithdrawalDetailsForAmount(amountToTransfer,
baseUrl: exchange.exchangeBaseUrl,
- scope: nil) { // TODO: scope
+ scope: nil, // TODO: scope
+ transactionId: transactionId) {
withdrawalDetails = details
}
// agePicker.setAges(ages: details?.ageRestrictionOptions)
@@ -108,6 +110,11 @@ struct WithdrawAcceptView: View {
.accessibilityLabel(Text(feeA11y))
}
}
+ if let kycHint = withdrawalDetails.kycHint {
+ Text(kycHint)
+ .foregroundColor(WalletColors().attention)
+ .talerFont(.body)
+ }
} else {
Section {
Text("The amount will be determined by the Cash Acceptor.")
@@ -123,10 +130,12 @@ struct WithdrawAcceptView: View {
exchangeBaseUrl: exchange2.exchangeBaseUrl,
url: url,
amountToTransfer: amountToTransfer.isZero ? nil : amountToTransfer)
+ let overLimit = withdrawalDetails?.exceedsHardLimit ?? false
NavigationLink(destination: destination) {
Text("Confirm Withdrawal") // SHEET_WITHDRAW_ACCEPT
}
- .buttonStyle(TalerButtonStyle(type: .prominent))
+ .buttonStyle(TalerButtonStyle(type: .prominent, disabled: overLimit))
+ .disabled(overLimit)
.accessibilityIdentifier(A11yID.withdrawAcceptButton)
.padding(.horizontal)
}
diff --git a/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawUriScan.swift b/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawUriScan.swift
index 559dad2a..588ba33c 100755
--- a/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawUriScan.swift
+++ b/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawUriScan.swift
@@ -46,6 +46,7 @@ struct WithdrawUriScan: View {
// @State private var feeAmount: Amount? = nil
@State private var withdrawalDetails: WithdrawalDetailsForAmount? = nil
+ @State private var transactionId: String? = nil
let navTitle = String(localized: "Withdrawal")
@@ -108,6 +109,7 @@ struct WithdrawUriScan: View {
do {
let response = try await model.prepareBankIntegratedWithdrawal(url.trimmedString)
let txId = response.transactionId
+ transactionId = txId
let uriInfoResponse = response.info
let amount = uriInfoResponse.amount
let currency = amount?.currencyStr ?? uriInfoResponse.currency
@@ -166,7 +168,8 @@ struct WithdrawUriScan: View {
scope: exchange2.scopeInfo,
amountToTransfer: $amountToTransfer,
wireFee: $wireFee,
- exchange: $exchange) // from possibleExchanges
+ exchange: $exchange, // from possibleExchanges
+ transactionId: transactionId)
if amountIsEditable {
ScrollView {
let shortcutDest = WithdrawAcceptView(stack: stack.push(),
@@ -174,7 +177,8 @@ struct WithdrawUriScan: View {
scope: exchange2.scopeInfo,
amountToTransfer: $amountShortcut,
wireFee: $wireFee,
- exchange: $exchange) // from possibleExchanges
+ exchange: $exchange, // from possibleExchanges
+ transactionId: transactionId)
let actions = Group {
NavLink($shortcutSelected) { shortcutDest }
NavLink($buttonSelected) { acceptDestination }
| ||||
| related to | 0012104 | new | Android: no warning before confirming a withdrawal that will need KYC or exceeds the exchange's limit (wallet-core's KYC preview ignored) |
| Date Modified | Username | Field | Change |
|---|---|---|---|
| 2026-11-11 02:01 | bot-cg | New Issue | |
| 2026-11-11 02:01 | bot-cg | Relationship added | related to 0012104 |
| 2026-11-11 02:01 | bot-cg | File Added: before-web-1.png | |
| 2026-11-11 02:01 | bot-cg | File Added: before-web-2-outcome-kyc.png | |
| 2026-11-11 02:01 | bot-cg | File Added: before-android-1.png | |
| 2026-11-11 02:01 | bot-cg | File Added: before-android-2-outcome-kyc.png | |
| 2026-11-11 02:01 | bot-cg | File Added: before-ios-1.png | |
| 2026-11-11 02:01 | bot-cg | File Added: before-ios-2-outcome-kyc.png | |
| 2026-11-11 02:01 | bot-cg | File Added: after-ios-1.png | |
| 2026-11-11 02:01 | bot-cg | File Added: ios-withdraw-kyc-preview.patch |