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 }