taler-ios

iOS apps for GNU Taler (wallet)
Log | Files | Refs | README | LICENSE

commit 0776b681276e4040aa1c586e9b23c5529cc857ac
parent abb65ef5e8a6893d527f5f614d0677d4d888e8b3
Author: Marc Stibane <marc@taler.net>
Date:   Fri, 30 May 2025 20:36:11 +0200

KYCauth

Diffstat:
MTalerWallet1/Model/Transaction.swift | 31+++++++++++++++++++++++++++++--
MTalerWallet1/Views/Settings/Bank/BankEditView.swift | 11++++-------
MTalerWallet1/Views/Settings/Bank/BankSectionView.swift | 18+++++++-----------
MTalerWallet1/Views/Transactions/ManualDetailsV.swift | 35++++++++++++++++-------------------
MTalerWallet1/Views/Transactions/ManualDetailsWireV.swift | 52+++++++++++++++++++++++++++++++++++++++-------------
MTalerWallet1/Views/Transactions/TransactionSummaryV.swift | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
6 files changed, 188 insertions(+), 54 deletions(-)

diff --git a/TalerWallet1/Model/Transaction.swift b/TalerWallet1/Model/Transaction.swift @@ -140,6 +140,31 @@ enum TransactionMajorState: String, Codable { } } +struct PayTo { + var iban: String? + var xTaler: String? + var sender: String? + var receiver: String? + var amountStr: String? + var messageStr: String? + + init(_ string: String) { + let payURL = URL(string: string) + if let queryParameters = payURL?.queryParameters { + iban = payURL?.iban + xTaler = payURL?.xTaler ?? +// payURL?.host() ?? + String(localized: "unknown payment method") + sender = queryParameters["sender-name"] ?? EMPTYSTRING + .replacingOccurrences(of: "+", with: SPACE) + receiver = (queryParameters["receiver-name"] ?? EMPTYSTRING) + .replacingOccurrences(of: "+", with: SPACE) + amountStr = queryParameters["amount"] ?? EMPTYSTRING + messageStr = queryParameters["message"] ?? EMPTYSTRING + } + } +} + struct TransactionState: Codable, Hashable { var major: TransactionMajorState var minor: TransactionMinorState? @@ -147,8 +172,8 @@ struct TransactionState: Codable, Hashable { var isReady: Bool { minor == .ready } var isKYC: Bool { minor == .kyc || minor == .balanceKyc - || minor == .mergeKycRequired - || minor == .kycAuthRequired } + || minor == .mergeKycRequired } + var isKYCauth: Bool { minor == .kycAuthRequired } } struct TransactionTransition: Codable { // Notification @@ -300,6 +325,7 @@ struct KycAuthTransferInfo: Decodable, Sendable { /// The KYC auth transfer will *not* work if it originates from a different account. var debitPaytoUri: String /// Payto URI of the account that must make the transfer var accountPub: String /// Account public key that must be included in the subject + var amount: Amount var creditPaytoUris: [String] /// Possible target payto URIs } @@ -319,6 +345,7 @@ struct TransactionCommon: Decodable, Sendable { var isPending : Bool { txState.major == .pending } var isPendingReady : Bool { isPending && txState.isReady } var isPendingKYC : Bool { isPending && txState.isKYC } + var isPendingKYCauth: Bool { isPending && txState.isKYCauth } var isDone : Bool { txState.major == .done } var isAborting : Bool { txState.major == .aborting } var isAborted : Bool { txState.major == .aborted } diff --git a/TalerWallet1/Views/Settings/Bank/BankEditView.swift b/TalerWallet1/Views/Settings/Bank/BankEditView.swift @@ -58,18 +58,15 @@ struct BankEditView: View { private func viewDidLoad() async { if let accountID { if let account = try? await model.getBankAccountById(accountID) { - let payURL = URL(string: account.paytoUri) - iban = payURL?.iban ?? EMPTYSTRING - xTaler = payURL?.xTaler ?? EMPTYSTRING + let payTo = PayTo(account.paytoUri) + iban = payTo.iban ?? EMPTYSTRING + xTaler = payTo.xTaler ?? EMPTYSTRING if iban.count < 1 && xTaler.count > 1 { paytoType = .xTalerBank } accountLabel = account.label ?? EMPTYSTRING kycCompleted = account.kycCompleted - if let queryParameters = payURL?.queryParameters { - let name = queryParameters["receiver-name"] ?? ownerName - accountHolder = name.replacingOccurrences(of: "+", with: SPACE) - } + accountHolder = payTo.receiver ?? ownerName } } else { diff --git a/TalerWallet1/Views/Settings/Bank/BankSectionView.swift b/TalerWallet1/Views/Settings/Bank/BankSectionView.swift @@ -45,18 +45,14 @@ struct BankSectionView: View { @MainActor private func viewDidLoad() async { - let payURL = URL(string: account.paytoUri) - iban = payURL?.iban ?? EMPTYSTRING - xTaler = payURL?.xTaler ?? EMPTYSTRING - label = account.label ?? EMPTYSTRING - if let queryParameters = payURL?.queryParameters { - let name = if let rcv = queryParameters["receiver-name"] { - rcv.replacingOccurrences(of: "+", with: SPACE) - } else { - ownerName - } - accountHolder = name + let payTo = PayTo(account.paytoUri) + iban = payTo.iban ?? EMPTYSTRING + xTaler = payTo.xTaler ?? EMPTYSTRING + if iban.count < 1 && xTaler.count > 1 { + paytoType = .xTalerBank } + label = account.label ?? EMPTYSTRING + accountHolder = payTo.receiver ?? ownerName } var body: some View { diff --git a/TalerWallet1/Views/Transactions/ManualDetailsV.swift b/TalerWallet1/Views/Transactions/ManualDetailsV.swift @@ -37,8 +37,12 @@ struct SegmentControl: View { ForEach((0..<count), id: \.self) { index in let detail = accountDetails[index] let specs = detail.currencySpecification + let scope = detail.scope let amount = detail.transferAmount - let formatted = amount?.formatted(specs: specs, isNegative: false, useISO: false) + let formatted = amount?.formatted(specs: specs, + isNegative: false, + scope: scope, + useISO: false) ?? (EMPTYSTRING, EMPTYSTRING) let bankName = detail.bankLabel let a11yLabel = bankName != nil ? (bankName! + SPACE + formatted.1) @@ -93,7 +97,9 @@ struct AccountPicker: View { let detail = accountDetails[index] if let amount = detail.transferAmount { let amountStr = amount.formatted(specs: detail.currencySpecification, - isNegative: false, useISO: false) + isNegative: false, + scope: detail.scope, + useISO: false) // let _ = print(amountStr) if let bankName = detail.bankLabel { Text(bankName + ": " + amountStr.0) @@ -187,26 +193,17 @@ struct ManualDetailsV: View { // Text(amountStr) } } - let payto = account.paytoUri - let payURL = URL(string: payto) - if let queryParameters = payURL?.queryParameters { - let iban = payURL?.iban - let xTaler = payURL?.xTaler ?? -// payURL?.host() ?? - String(localized: "unknown payment method") - let receiverStr = (queryParameters["receiver-name"] ?? EMPTYSTRING) - .replacingOccurrences(of: "+", with: SPACE) -// let amountStr = queryParameters["amount"] ?? EMPTYSTRING -// let messageStr = queryParameters["message"] ?? EMPTYSTRING -// let senderStr = queryParameters["sender-name"] ?? EMPTYSTRING + let payto = PayTo(account.paytoUri) + if let receiverStr = payto.receiver { let wireDetails = ManualDetailsWireV(stack: stack.push(), - details: details, + reservePub: details.reservePub, receiverStr: receiverStr, - iban: iban, - xTaler: xTaler, + iban: payto.iban, + xTaler: payto.xTaler ?? EMPTYSTRING, amountValue: amountValue, amountStr: amountStr, obtainStr: obtainStr, + debitIBAN: nil, // only for deposit auth account: account) Group { NavigationLink(destination: wireDetails) { @@ -225,7 +222,7 @@ struct ManualDetailsV: View { .listRowSeparator(.automatic) } #if DEBUG - if let iban { + if let iban = payto.iban { Text(minimalistic ? "**Alternative:** Use this PayTo-Link:" : "**Alternative:** If your bank already supports PayTo, you can use this PayTo-Link instead:") .multilineTextAlignment(.leading) @@ -244,7 +241,7 @@ struct ManualDetailsV: View { #endif }.id(listID) .talerFont(.body) - .task { await viewDidLoad(payto) } + .task { await viewDidLoad(account.paytoUri) } } else { // TODO: Error No payto URL } diff --git a/TalerWallet1/Views/Transactions/ManualDetailsWireV.swift b/TalerWallet1/Views/Transactions/ManualDetailsWireV.swift @@ -11,7 +11,8 @@ import taler_swift struct TransferRestrictionsV: View { let amountStr: (String, String) - let obtainStr: (String, String) + let obtainStr: (String, String)? + let debitIBAN: String? let restrictions: [AccountRestriction]? @AppStorage("minimalistic") var minimalistic: Bool = false @@ -28,14 +29,32 @@ struct TransferRestrictionsV: View { return String(localized: "You need to transfer \(amountNBS) from your regular bank account to the payment service to receive \(obtainNBS) as electronic cash in this wallet.") } + private func authMini(_ amountS: String, _ debitS: String) -> String { + let amountNBS = amountS.nbs + return String(localized: "Transfer \(amountNBS) from \(debitS) to the payment service.") + } + private func authMaxi(_ amountS: String, _ debitS: String) -> String { + let amountNBS = amountS.nbs + return String(localized: "You need to transfer \(amountNBS) from the bank account \(debitS) to the payment service to verify your deposit.") + } + var body: some View { VStack(alignment: .leading) { - Text(minimalistic ? transferMini(amountStr.0) - : transferMaxi(amountStr.0, obtainStr.0)) - .accessibilityLabel(minimalistic ? transferMini(amountStr.1) - : transferMaxi(amountStr.1, obtainStr.1)) - .talerFont(.body) - .multilineTextAlignment(.leading) + if let obtainStr { + Text(minimalistic ? transferMini(amountStr.0) + : transferMaxi(amountStr.0, obtainStr.0)) + .accessibilityLabel(minimalistic ? transferMini(amountStr.1) + : transferMaxi(amountStr.1, obtainStr.1)) + .talerFont(.body) + .multilineTextAlignment(.leading) + } else if let debitIBAN { + Text(minimalistic ? authMini(amountStr.0, debitIBAN) + : authMaxi(amountStr.0, debitIBAN)) + .accessibilityLabel(minimalistic ? authMini(amountStr.1, debitIBAN) + : authMaxi(amountStr.1, debitIBAN)) + .talerFont(.body) + .multilineTextAlignment(.leading) + } if let restrictions { ForEach(restrictions) { restriction in if let hintsI18 = restriction.human_hint_i18n { @@ -58,13 +77,14 @@ struct TransferRestrictionsV: View { // MARK: - struct ManualDetailsWireV: View { let stack: CallStack - let details : WithdrawalDetails + let reservePub: String let receiverStr: String let iban: String? let xTaler: String let amountValue: String // string representation of the value, formatted as "`integer`.`fraction`" let amountStr: (String, String) - let obtainStr: (String, String) + let obtainStr: (String, String)? // only for withdrawal + let debitIBAN: String? // only for deposit auth let account: ExchangeAccountDetails @AppStorage("minimalistic") var minimalistic: Bool = false @@ -72,18 +92,23 @@ struct ManualDetailsWireV: View { private func step3(_ amountS: String) -> String { let amountNBS = amountS.nbs + let bePatient = String(localized: "Depending on your bank the transfer can take from minutes to two working days, please be patient.") + if let debitIBAN { + return minimalistic ? String(localized: "Transfer \(amountNBS) from \(debitIBAN).") + : String(localized: "Finish the wire transfer of \(amountNBS) in your banking app or website to verify your bank account \(debitIBAN).") + "\n" + bePatient + } return minimalistic ? String(localized: "Transfer \(amountNBS).") - : String(localized: "Finish the wire transfer of \(amountNBS) in your banking app or website, then this withdrawal will proceed automatically. Depending on your bank the transfer can take from minutes to two working days, please be patient.") + : String(localized: "Finish the wire transfer of \(amountNBS) in your banking app or website, then this withdrawal will proceed automatically.") + "\n" + bePatient } var body: some View { List { let cryptocode = HStack { - Text(details.reservePub) + Text(reservePub) .monospacedDigit() .accessibilityLabel(Text("Cryptocode", comment: "a11y")) .frame(maxWidth: .infinity, alignment: .leading) - CopyButton(textToCopy: details.reservePub, vertical: true) + CopyButton(textToCopy: reservePub, vertical: true) .accessibilityLabel(Text("Copy the cryptocode", comment: "a11y")) .disabled(false) } .padding(.leading) @@ -173,7 +198,8 @@ struct ManualDetailsWireV: View { Group { TransferRestrictionsV(amountStr: amountStr, obtainStr: obtainStr, - restrictions: account.creditRestrictions) + debitIBAN: debitIBAN, + restrictions: account.creditRestrictions) .listRowSeparator(.visible) step1.listRowSeparator(.hidden) if !minimalistic { diff --git a/TalerWallet1/Views/Transactions/TransactionSummaryV.swift b/TalerWallet1/Views/Transactions/TransactionSummaryV.swift @@ -281,7 +281,97 @@ struct TransactionSummaryV: View { } } } + // MARK: - + struct KYCauth: View { + let stack: CallStack + let common: TransactionCommon + + @AppStorage("minimalistic") var minimalistic: Bool = false + @State private var accountID = 0 + @State private var listID = UUID() + + func redraw(_ newAccount: Int) -> Void { + if newAccount != accountID { + accountID = newAccount + withAnimation { listID = UUID() } + } + } + + func validDetails(_ paytoUris: [String]) -> [ExchangeAccountDetails] { + var details: [ExchangeAccountDetails] = [] + for paytoUri in paytoUris { + let payTo = PayTo(paytoUri) + let amount = common.kycAuthTransferInfo?.amount + let detail = ExchangeAccountDetails(status: "ok", + paytoUri: paytoUri, + transferAmount: amount, + scope: common.scopes[0]) + details.append(detail) + } + return details + } + + var body: some View { + if let info = common.kycAuthTransferInfo { + let debitPayTo = PayTo(info.debitPaytoUri) + let amount = info.amount + let amountStr = amount.formatted(specs: nil, + isNegative: false, + scope: common.scopes[0]) + let amountValue = amount.valueStr + let creditPaytoUris = info.creditPaytoUris + let validDetails = validDetails(creditPaytoUris) + if validDetails.count > 0 { + let countPaytos = creditPaytoUris.count + let account = validDetails[accountID] + Text("You need to verify having control over the bank account for the deposit.") + .bold() + .fixedSize(horizontal: false, vertical: true) // wrap in scrollview + .multilineTextAlignment(.leading) // otherwise + .listRowSeparator(.hidden) + + if countPaytos > 1 { + if countPaytos > 3 { // too many for SegmentControl + AccountPicker(title: String(localized: "Bank"), + value: $accountID, + accountDetails: validDetails, + action: redraw) + .listRowSeparator(.hidden) + .pickerStyle(.menu) + } else { + SegmentControl(value: $accountID, + accountDetails: validDetails, + action: redraw) + .listRowSeparator(.hidden) + } + } else if let creditPaytoUri = creditPaytoUris.first { + if let bankName = account.bankLabel { + Text(bankName + ": " + amountStr.0) + .accessibilityLabel(bankName + ": " + amountStr.1) +// } else { +// Text(amountStr) + } + } + let payto = PayTo(account.paytoUri) + if let receiverStr = payto.receiver { + let wireDetails = ManualDetailsWireV(stack: stack.push(), + reservePub: info.accountPub, + receiverStr: receiverStr, + iban: payto.iban, + xTaler: payto.xTaler ?? EMPTYSTRING, + amountValue: amountValue, + amountStr: amountStr, + obtainStr: nil, // only for withdrawal + debitIBAN: debitPayTo.iban, + account: account) + + } + } + } + } // body + } + // MARK: - struct PendingWithdrawalDetails: View { let stack: CallStack @Binding var transaction: TalerTransaction @@ -390,8 +480,9 @@ struct TransactionSummaryV: View { merchant: nil) } case .deposit(let depositTransaction): Group { - let details = depositTransaction.details - if transaction.isPendingKYC { + if transaction.common.isPendingKYCauth { + KYCauth(stack: stack.push(), common: transaction.common) + } else if transaction.isPendingKYC { KYCbutton(kycUrl: common.kycUrl) } ThreeAmountsSheet(stack: stack.push(),