commit 52e957693e02115827b8ecb234b9c75e313cb755
parent d54096202c1945281e1bf9919f2611ac22ba7cc2
Author: Marc Stibane <marc@taler.net>
Date: Thu, 12 Feb 2026 22:37:37 +0100
Support for Cyclos
Diffstat:
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,