taler-ios

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

commit 52e957693e02115827b8ecb234b9c75e313cb755
parent d54096202c1945281e1bf9919f2611ac22ba7cc2
Author: Marc Stibane <marc@taler.net>
Date:   Thu, 12 Feb 2026 22:37:37 +0100

Support for Cyclos

Diffstat:
MTalerWallet1/Helper/URL+id+iban.swift | 17+++++++++++++++++
MTalerWallet1/Model/Model+Deposit.swift | 12++++++++++++
MTalerWallet1/Model/Transaction.swift | 7+++++++
MTalerWallet1/Views/Settings/Bank/BankSectionView.swift | 33+++++++++++++++++++++++----------
MTalerWallet1/Views/Settings/Exchange/ExchangeListView.swift | 36+++++++++++++++++++++++++-----------
MTalerWallet1/Views/Settings/Exchange/ExchangeRowView.swift | 4+++-
MTalerWallet1/Views/Settings/Exchange/ExchangeSectionView.swift | 4+++-
MTalerWallet1/Views/Transactions/ManualDetailsV.swift | 1+
MTalerWallet1/Views/Transactions/ManualDetailsWireV.swift | 6+++++-
MTalerWallet1/Views/Transactions/TransactionSummaryV.swift | 1+
10 files changed, 97 insertions(+), 24 deletions(-)

diff --git a/TalerWallet1/Helper/URL+id+iban.swift b/TalerWallet1/Helper/URL+id+iban.swift @@ -16,6 +16,23 @@ extension URL { self.init(string: "\(string)")! } + var cyclos: String? { + /// "payto://cyclos/" host ["/" fpath] "/" account-id [ "?" opts ] + if scheme == "payto", let host = host { + let path = path.dropFirst(1) + if host == "cyclos" { + return String(path) + } else if host.hasPrefix("cyclos") { + let cyclos = host.dropFirst(6) + if path.hasPrefix("/") { + return String(cyclos + path) + } + return String(cyclos + "/" + path) + } + } + return nil + } + var iban: String? { /// https://datatracker.ietf.org/doc/rfc8905/ /// payto://iban/DE75512108001245126199?amount=EUR:200.0&message=hello diff --git a/TalerWallet1/Model/Model+Deposit.swift b/TalerWallet1/Model/Model+Deposit.swift @@ -47,6 +47,7 @@ enum PaytoType: String, Codable { case unknown case iban case bitcoin + case cyclos case xTalerBank = "x-taler-bank" } @@ -184,6 +185,17 @@ struct BankAccountsInfo: Decodable, Hashable { var paytoUri: String var kycCompleted: Bool var label: String? + var payToWorkAround: String { + let payto = PayTo(paytoUri) + + if let cyclos = payto.cyclos { + if cyclos.count > 1 { + let receiver = payto.receiver?.replacingOccurrences(of: SPACE, with: "+") ?? DEMO + return String("payto://cyclos/\(cyclos)?receiver-name=\(receiver)") + } + } + return paytoUri + } } struct BankAccounts: Decodable, Hashable { var accounts: [BankAccountsInfo] diff --git a/TalerWallet1/Model/Transaction.swift b/TalerWallet1/Model/Transaction.swift @@ -143,6 +143,7 @@ enum TransactionMajorState: String, Codable { struct PayTo { // receiver-name=Taler+Operations+AG&receiver-postal-code=2502&receiver-town=Biel-Bienne" var iban: String? + var cyclos: String? var xTaler: String? var sender: String? var receiver: String? @@ -150,6 +151,8 @@ struct PayTo { // receiver-name=Taler+Operations+AG&receiver-postal-cod var town: String? var amountStr: String? var messageStr: String? + // payto-cyclos-URI = "payto://cyclos/" host ["/" fpath] "/" account-id [ "?" opts ] + var host: String? // func param(key: String, from params: [String:String]) -> String? { if let param = params[key] { @@ -160,8 +163,12 @@ struct PayTo { // receiver-name=Taler+Operations+AG&receiver-postal-cod init(_ string: String) { let payURL = URL(string: string) + if let host = payURL?.host { + self.host = host + } if let queryParameters = payURL?.queryParameters { iban = payURL?.iban + cyclos = payURL?.cyclos xTaler = payURL?.xTaler ?? // payURL?.host() ?? String(localized: "unknown payment method") diff --git a/TalerWallet1/Views/Settings/Bank/BankSectionView.swift b/TalerWallet1/Views/Settings/Bank/BankSectionView.swift @@ -29,9 +29,9 @@ struct BankSectionView: View { @State private var label: String = EMPTYSTRING @State private var accountHolder: String = EMPTYSTRING @State private var iban: String = EMPTYSTRING + @State private var cyclos: String = EMPTYSTRING @State private var xTaler: String = EMPTYSTRING @State private var paytoType: PaytoType = .iban - @State private var selected = 0 // @MainActor // private func validateIban() async { @@ -47,9 +47,17 @@ struct BankSectionView: View { private func viewDidLoad() async { let payTo = PayTo(account.paytoUri) iban = payTo.iban ?? EMPTYSTRING + cyclos = payTo.cyclos ?? EMPTYSTRING + xTaler = payTo.xTaler ?? EMPTYSTRING - if iban.count < 1 && xTaler.count > 1 { - paytoType = .xTalerBank + if iban.count < 1 { + if cyclos.count > 1 { + paytoType = .cyclos + } else if xTaler.count > 1 { + paytoType = .xTalerBank + } else { + paytoType = .unknown + } } label = account.label ?? EMPTYSTRING accountHolder = payTo.receiver ?? ownerName @@ -62,9 +70,9 @@ struct BankSectionView: View { #endif let kyc = account.kycCompleted ? String(localized: "verified") : String(localized: "not yet verified") - let methods = [PaytoType.iban, PaytoType.xTalerBank] - let methodType = methods[selected].rawValue.uppercased() + let methodType = paytoType.rawValue.uppercased() let methodStr = (paytoType == .iban) ? iban + : (paytoType == .cyclos) ? cyclos : (paytoType == .xTalerBank) ? xTaler : "unknown payment method" let editHint = String(localized: "Double tap to edit the account", comment: "a11y") @@ -80,16 +88,21 @@ struct BankSectionView: View { DepositAmountV(stack: stack.push(), selectedBalance: selectedBalance, amountLastUsed: $amountLastUsed, - paytoUri: account.paytoUri, + paytoUri: account.payToWorkAround, // TODO: paytoUri, label: account.label) .navigationTitle(navTitle) } } label: { VStack(alignment: .leading) { - BankSectionRow(title: String(localized: "Account holder:"), - value: accountHolder) - BankSectionRow(title: String(localized: "\(methodType):", comment: "methodType:"), - value: methodStr) + if paytoType == .cyclos { + BankSectionRow(title: String(localized: "Cyclos user:"), + value: accountHolder) + } else { + BankSectionRow(title: String(localized: "Account holder:"), + value: accountHolder) + BankSectionRow(title: String(localized: "\(methodType):", comment: "methodType:"), + value: methodStr) + } BankSectionRow(title: String(localized: "Status:"), value: kyc) } .talerFont(.body) diff --git a/TalerWallet1/Views/Settings/Exchange/ExchangeListView.swift b/TalerWallet1/Views/Settings/Exchange/ExchangeListView.swift @@ -90,24 +90,33 @@ struct ExchangeListCommonV: View { let _ = Self._printChanges() let _ = symLog?.vlog() // just to get the # to compare it with .onAppear & onDisappear #endif +#if DEBUG + let excSection = Section { + let cyclos = "https://guava.box.fdold.eu" + Button(cyclos) { + addExchange(cyclos, model: model) + } + + let exchanges = ["glsint.fdold.eu", "taler.magnetbank.hu", "stage.taler-ops.ch"] + ForEach(exchanges, id: \.self) { exchange in + let urlStr = "https://exchange." + exchange + Button(urlStr) { + addExchange(urlStr, model: model) + } + // Link(exchange, destination: URL(string: urlStr)!) + } + } +#endif let balanceList = List { ForEach(Array(controller.balances.enumerated()), id: \.1.id) { index, balance in ExchangeSectionView(stack: stack.push(), balance: balance, - thousand: index * MAXEXCHANGES) // unique ID + thousand: index * MAXEXCHANGES, // unique ID + showDevelopItems: showDevelopItems) } #if DEBUG if showDevelopItems { - Section { - let exchanges = ["glsint.fdold.eu", "taler.magnetbank.hu", "stage.taler-ops.ch"] - ForEach(exchanges, id: \.self) { exchange in - let urlStr = "https://exchange." + exchange - Button(urlStr) { - addExchange(urlStr, model: model) - } - // Link(exchange, destination: URL(string: urlStr)!) - } - } + excSection } #endif } @@ -128,6 +137,11 @@ struct ExchangeListCommonV: View { } } .talerFont(.body) +#if DEBUG + if showDevelopItems { + excSection + } +#endif } Group { diff --git a/TalerWallet1/Views/Settings/Exchange/ExchangeRowView.swift b/TalerWallet1/Views/Settings/Exchange/ExchangeRowView.swift @@ -14,6 +14,7 @@ struct ExchangeRowView: View { let stack: CallStack let exchange: Exchange let index: Int + let showDevelopItems: Bool @Environment(\.sizeCategory) var sizeCategory @EnvironmentObject private var controller: Controller @@ -130,7 +131,8 @@ fileprivate struct ExchangeRow_Previews: PreviewProvider { ageRestrictionOptions: []) ExchangeRowView(stack: CallStack("Preview"), exchange: exchange1, - index: 1) + index: 1, + showDevelopItems: true) } } diff --git a/TalerWallet1/Views/Settings/Exchange/ExchangeSectionView.swift b/TalerWallet1/Views/Settings/Exchange/ExchangeSectionView.swift @@ -16,6 +16,7 @@ struct ExchangeSectionView: View { let stack: CallStack let balance: Balance let thousand: Int // index of balance times 1000 (MAXEXCHANGES per currency) + let showDevelopItems: Bool @EnvironmentObject private var model: WalletModel @EnvironmentObject private var controller: Controller @@ -107,7 +108,8 @@ struct ExchangeSectionView: View { let _ = symLog.log("Index: \(thousand + index)") ExchangeRowView(stack: stack.push(), exchange: exchange, - index: thousand + index + 1) + index: thousand + index + 1, + showDevelopItems: showDevelopItems) .listRowSeparator(.hidden) } if developerMode && (DEMOCURRENCY == currency || TESTCURRENCY == currency) { diff --git a/TalerWallet1/Views/Transactions/ManualDetailsV.swift b/TalerWallet1/Views/Transactions/ManualDetailsV.swift @@ -86,6 +86,7 @@ struct ManualDetailsV: View { receiverZip: payto.postalCode, receiverTown: payto.town, iban: payto.iban, + cyclos: payto.cyclos ?? EMPTYSTRING, xTaler: payto.xTaler ?? EMPTYSTRING, amountValue: amountValue, amountStr: amountStr, diff --git a/TalerWallet1/Views/Transactions/ManualDetailsWireV.swift b/TalerWallet1/Views/Transactions/ManualDetailsWireV.swift @@ -81,7 +81,8 @@ struct ManualDetailsWireV: View { let receiverStr: String let receiverZip: String? let receiverTown: String? - let iban: String? + let iban: String? // TODO: BBAN + let cyclos: String let xTaler: String let amountValue: String // string representation of the value, formatted as "`integer`.`fraction`" let amountStr: (String, String) @@ -260,6 +261,9 @@ struct ManualDetailsWireV: View { } } ibanCode + } else if cyclos.count > 0 { + step2x + payeeCode } else { step2x payeeCode diff --git a/TalerWallet1/Views/Transactions/TransactionSummaryV.swift b/TalerWallet1/Views/Transactions/TransactionSummaryV.swift @@ -374,6 +374,7 @@ struct TransactionSummaryV: View { receiverZip: payto.postalCode, receiverTown: payto.town, iban: payto.iban, + cyclos: payto.cyclos ?? EMPTYSTRING, xTaler: payto.xTaler ?? EMPTYSTRING, amountValue: amountValue, amountStr: amountStr,