View Issue Details

IDProjectCategoryView StatusLast Update
0012105Talerwallet (iOS App)public2026-11-11 02:01
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 
Summary0012105: iOS: no warning before confirming a withdrawal that will need KYC or exceeds the exchange's limit (wallet-core's KYC preview ignored)
DescriptionScenario (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 Reproduce1. 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.
TagsNo tags attached.
Attached Files
before-web-1.png (95,155 bytes)   
before-web-1.png (95,155 bytes)   
before-web-2-outcome-kyc.png (137,983 bytes)   
before-web-2-outcome-kyc.png (137,983 bytes)   
before-android-1.png (37,290 bytes)   
before-android-1.png (37,290 bytes)   
before-ios-1.png (148,064 bytes)   
before-ios-1.png (148,064 bytes)   
before-ios-2-outcome-kyc.png (211,363 bytes)   
before-ios-2-outcome-kyc.png (211,363 bytes)   
after-ios-1.png (180,398 bytes)   
after-ios-1.png (180,398 bytes)   
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 }
ios-withdraw-kyc-preview.patch (11,628 bytes)   

Required notifications

Address or linkRemarkAdded byNotifiedActions

Relationships

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) 

Activities

Issue History

Date ModifiedUsernameFieldChange
2026-11-11 02:01bot-cgNew Issue
2026-11-11 02:01bot-cgRelationship addedrelated to 0012104
2026-11-11 02:01bot-cgFile Added: before-web-1.png
2026-11-11 02:01bot-cgFile Added: before-web-2-outcome-kyc.png
2026-11-11 02:01bot-cgFile Added: before-android-1.png
2026-11-11 02:01bot-cgFile Added: before-android-2-outcome-kyc.png
2026-11-11 02:01bot-cgFile Added: before-ios-1.png
2026-11-11 02:01bot-cgFile Added: before-ios-2-outcome-kyc.png
2026-11-11 02:01bot-cgFile Added: after-ios-1.png
2026-11-11 02:01bot-cgFile Added: ios-withdraw-kyc-preview.patch