commit f6cd2c9bf128b6fb59da106668f3a230b63abdbd
parent f3fc3e40f58ecbd4bcd16982e8628552df6aa45c
Author: Marc Stibane <marc@taler.net>
Date: Mon, 8 Jun 2026 16:54:52 +0200
Use V1 for payments
Diffstat:
4 files changed, 148 insertions(+), 254 deletions(-)
diff --git a/TalerWallet1/Model/Model+Payment.swift b/TalerWallet1/Model/Model+Payment.swift
@@ -1,5 +1,5 @@
/*
- * This file is part of GNU Taler, ©2022-25 Taler Systems S.A.
+ * This file is part of GNU Taler, ©2022-26 Taler Systems S.A.
* See LICENSE.md
*/
/**
@@ -13,10 +13,6 @@ import AnyCodable
typealias I18nDict = [String: String] // two-char language code, e.g. "de", "en"
// MARK: - ContractTerms
-// export interface TalerProtocolDuration {
-// readonly d_us: number | "forever";
-// }
-
struct TokenIssuePublicKey: Codable {
let cipher: String // "RSA", "CS"
@@ -192,6 +188,8 @@ struct MerchantContractTerms: Codable {
let extra: Extra? // Extra data, interpreted by the merchant only
let minimumAge: Int?
+ let defaultMoneyPot: Int?
+
// deprecated let wireFeeAmortization: Int? // Share of the wire fee that must be settled with one payment
// let maxWireFee: Amount? // Maximum wire fee that the merchant agrees to pay for
// let auditors: [Auditor]?
@@ -232,6 +230,7 @@ struct MerchantContractTerms: Codable {
case autoRefund = "auto_refund"
case extra
case minimumAge = "minimum_age"
+ case defaultMoneyPot = "default_money_pot"
// case wireFeeAmortization = "wire_fee_amortization"
// case maxWireFee = "max_wire_fee"
@@ -302,25 +301,15 @@ struct ExchangeFeeGapEstimate: Codable {
struct PerScopeDetails: Codable {
let scopeInfo: ScopeInfo
}
-/// The result from PreparePayForUri
-struct PreparePayResult: Codable {
- let status: PreparePayResultType // InsufficientBalance, AlreadyConfirmed, PaymentPossible, ChoiceSelection
- let transactionId: String
- let contractTerms: MerchantContractTerms
- let contractTermsHash: String? // not for InsufficientBalance
- let scopes: [ScopeInfo]? // not for ChoiceSelection
-// let detailsPerScope: [ScopeInfo : PreparePayDetails]
- let amountRaw: Amount // TODO: not for ChoiceSelection
- let amountEffective: Amount? // only if status != insufficientBalance
- let paid: Bool? // only if status == alreadyConfirmed
- let talerUri: String?
- let balanceDetails: PayMerchantInsufficientBalanceDetails? // only if status == insufficientBalance
+/// The result from PreparePayForUri2 and preparePayForTemplate2
+struct PreparePayResult2: Codable {
+ let transactionId: String
}
/// A request to get an exchange's payment contract terms.
fileprivate struct PreparePayForUri: WalletBackendFormattedRequest {
- typealias Response = PreparePayResult
- func operation() -> String { "preparePayForUri" }
+ typealias Response = PreparePayResult2
+ func operation() -> String { "preparePayForUriV2" }
func args() -> Args { Args(talerPayUri: talerPayUri) }
var talerPayUri: String
@@ -418,8 +407,8 @@ struct TemplateParams: Codable {
}
/// A request to get an exchange's payment contract terms.
fileprivate struct PreparePayForTemplateRequest: WalletBackendFormattedRequest {
- typealias Response = PreparePayResult
- func operation() -> String { "preparePayForTemplate" }
+ typealias Response = PreparePayResult2
+ func operation() -> String { "preparePayForTemplateV2" }
func args() -> Args { Args(talerPayTemplateUri: talerPayTemplateUri, templateParams: templateParams) }
var talerPayTemplateUri: String
@@ -560,11 +549,26 @@ struct ConfirmPayResult: Decodable {
fileprivate struct ConfirmPayForUri: WalletBackendFormattedRequest {
typealias Response = ConfirmPayResult
func operation() -> String { "confirmPay" }
- func args() -> Args { Args(transactionId: transactionId) }
+ func args() -> Args { Args(transactionId: transactionId, choiceIndex: choiceIndex) }
var transactionId: String
+ var choiceIndex: Int?
struct Args: Encodable {
var transactionId: String
+ var useDonau: Bool?
+ var sessionId: String?
+ var forcedCoinSel: ForcedCoinSel?
+ /**
+ * Whether token selection should be forced
+ * e.g. use tokens with non-matching `expected_domains'
+ *
+ * Only applies to v1 orders.
+ */
+ var forcedTokenSel: Bool?
+ /**
+ * Only applies to v1 orders.
+ */
+ var choiceIndex: Int?
}
}
// MARK: -
@@ -578,7 +582,7 @@ extension WalletModel {
}
nonisolated func preparePayForTemplate(_ talerPayTemplateUri: String, amount: Amount?, summary: String?, viewHandles: Bool = false)
- async throws -> PreparePayResult {
+ async throws -> PreparePayResult2 {
let templateParams = TemplateParams(amount: amount, summary: summary)
let request = PreparePayForTemplateRequest(talerPayTemplateUri: talerPayTemplateUri, templateParams: templateParams)
let response = try await sendRequest(request, viewHandles: viewHandles)
@@ -593,15 +597,16 @@ extension WalletModel {
}
nonisolated func preparePayForUri(_ talerPayUri: String, viewHandles: Bool = false)
- async throws -> PreparePayResult {
+ async throws -> PreparePayResult2 {
let request = PreparePayForUri(talerPayUri: talerPayUri)
let response = try await sendRequest(request, viewHandles: viewHandles)
return response
}
- nonisolated func confirmPay(_ transactionId: String, viewHandles: Bool = false)
+ nonisolated func confirmPay(_ transactionId: String, choiceIndex: Int?, viewHandles: Bool = false)
async throws -> ConfirmPayResult {
- let request = ConfirmPayForUri(transactionId: transactionId)
+ let request = ConfirmPayForUri(transactionId: transactionId,
+ choiceIndex: choiceIndex)
let response = try await sendRequest(request, viewHandles: viewHandles)
return response
}
diff --git a/TalerWallet1/Views/Sheets/Payment/PayTemplateV.swift b/TalerWallet1/Views/Sheets/Payment/PayTemplateV.swift
@@ -56,31 +56,7 @@ struct PayTemplateV: View {
buttonSelected2 = true
}
- @MainActor
- func acceptAction(preparePayResult: PreparePayResult) {
- Task { // runs on MainActor
- if let confirmPayResult = try? await model.confirmPay(preparePayResult.transactionId) {
-// symLog.log(confirmPayResult as Any)
- if confirmPayResult.type == "done" {
- dismissTop(stack.push())
- } else if confirmPayResult.type == "error" {
- controller.playSound(0)
- // TODO: show error
- }
- }
- }
- }
-
private func computeFeePayTemplate(_ amount: Amount) async -> ComputeFeeResult? {
-// if let result = await preparePayForTemplate(model: model,
-// url: url,
-// amount: amountToTransfer,
-// summary: summaryIsEditable ? summary ?? SPACE
-// : nil,
-// announce: announce)
-// {
-// preparePayResult = result.ppCheck
-// }
return nil
}
diff --git a/TalerWallet1/Views/Sheets/Payment/PaymentDone.swift b/TalerWallet1/Views/Sheets/Payment/PaymentDone.swift
@@ -14,6 +14,7 @@ struct PaymentDone: View {
let url: URL
// let scope: ScopeInfo?
let transactionId: String
+ let choiceIndex: Int
@EnvironmentObject private var controller: Controller
@EnvironmentObject private var model: WalletModel
@@ -24,7 +25,8 @@ struct PaymentDone: View {
@MainActor
private func viewDidLoad() async {
- if let confirmPayResult = try? await model.confirmPay(transactionId) {
+ if let confirmPayResult = try? await model.confirmPay(transactionId,
+ choiceIndex: choiceIndex) {
// symLog.log(confirmPayResult as Any)
if confirmPayResult.type == "done" {
controller.removeURL(url)
diff --git a/TalerWallet1/Views/Sheets/Payment/PaymentView.swift b/TalerWallet1/Views/Sheets/Payment/PaymentView.swift
@@ -15,55 +15,6 @@ fileprivate func feeLabel(_ feeString: String) -> String {
feeString.isEmpty ? EMPTYSTRING : String(localized: "+ \(feeString) fee")
}
-func templateFee(ppCheck: PreparePayResult?) -> Amount? {
- do {
- if let ppCheck {
- // Outgoing: fee = effective - raw
- if let effective = ppCheck.amountEffective { // , let raw = ppCheck.amountRaw {
- let raw = ppCheck.amountRaw
- let fee = try effective - raw
- return fee
- }
- }
- } catch {}
- return nil
-}
-
-/// at the moment the merchant doesn't provide live fee updates, but only after creating the payment. Thus we cannot show life fees...
-//struct PayForTemplateResult {
-// let ppCheck: PreparePayResult
-// let insufficient: Bool
-// let feeAmount: Amount?
-// let feeStr: String
-//}
-//
-//func preparePayForTemplate(model: WalletModel,
-// url: URL,
-// amount: Amount?,
-// summary: String?,
-// announce: Announce)
-// async -> PayForTemplateResult? {
-// if let ppCheck = try? await model.preparePayForTemplateM(url.absoluteString, amount: amount, summary: summary) {
-// let controller = Controller.shared
-// let amountRaw = ppCheck.amountRaw
-// let currency = amountRaw.currencyStr
-// let currencyInfo = controller.info(for: currency, controller.currencyTicker)
-// let amountVoiceOver = amountRaw.formatted(currencyInfo, isNegative: false)
-// let insufficient = ppCheck.status == .insufficientBalance
-// if let feeAmount = templateFee(ppCheck: ppCheck) {
-// let feeStr = feeAmount.formatted(currencyInfo, isNegative: false)
-// let feeLabel = feeLabel(feeStr)
-// announce("\(amountVoiceOver), \(feeLabel)")
-// return PayForTemplateResult(ppCheck: ppCheck, insufficient: insufficient,
-// feeAmount: feeAmount, feeStr: feeStr)
-// }
-// announce(amountVoiceOver)
-// return PayForTemplateResult(ppCheck: ppCheck, insufficient: insufficient,
-// feeAmount: nil, feeStr: EMPTYSTRING)
-// }
-// return nil
-//}
-
// MARK: -
// Will be called either by the user scanning a <pay> QR code or tapping the provided link,
// both from the shop's website - or even from a printed QR code.
@@ -86,37 +37,11 @@ struct PaymentView: View, Sendable {
@AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic
@State private var currencyInfo: CurrencyInfo = CurrencyInfo.zero(UNKNOWN)
- @State var preparePayResult: PreparePayResult? = nil
- @State private var elapsed: Int = 0
+ @State var txId: String? = nil
- @MainActor
- func checkCurrencyInfo(for result: PreparePayResult) async {
- if let scopes = result.scopes {
- if !scopes.isEmpty {
- for scope in scopes {
- controller.checkInfo(for: scope, model: model)
- }
- return
- }
- }
- // else fallback to contractTerms.exchanges
- // TODO: wallet-core should return status==UnknownCurrency. When we get that,
- // we should let the user decide whether they want info about that
-// let exchanges = result.contractTerms.exchanges
-// for exchange in exchanges {
-// let baseUrl = exchange.url
- // Yikes - getExchangeByUrl does NOT query the exchange via network,
- // but returns "exchange entry not found in wallet database"
- // so it doesn't make sense to call it here
-// if let someExchange = try? await model.getExchangeByUrl(url: baseUrl) {
-// symLog.log("\(baseUrl.trimURL) loaded")
-// await controller.checkCurrencyInfo(for: baseUrl, model: model)
-// symLog.log("Info(for: \(baseUrl.trimURL) loaded")
-// return
-// }
-// }
- symLog.log("Couldn't load Info(for: \(result.amountRaw.currencyStr))")
- }
+ @State private var selectedChoice: Int = 0
+ @State private var elapsed: Int = 0
+ @State private var talerTX = TalerTransaction(dummyCurrency: DEMOCURRENCY)
@MainActor
private func viewDidLoad() async {
@@ -125,114 +50,82 @@ struct PaymentView: View, Sendable {
if let templateResponse = try? await model.preparePayForTemplate(url.absoluteString,
amount: amountIsEditable ? amountToTransfer : nil,
summary: summaryIsEditable ? summary : nil) {
- await checkCurrencyInfo(for: templateResponse)
- preparePayResult = templateResponse
- let raw = templateResponse.amountRaw
- controller.updateAmount(raw, forSaved: url)
+ txId = templateResponse.transactionId
+// preparePayResult = templateResponse
+// let raw = templateResponse.amountRaw
+// controller.updateAmount(raw, forSaved: url)
}
} else {
if let payResponse = try? await model.preparePayForUri(url.absoluteString) {
- let raw = payResponse.amountRaw
- amountToTransfer = raw
- await checkCurrencyInfo(for: payResponse)
- preparePayResult = payResponse
- controller.updateAmount(raw, forSaved: url)
+ txId = payResponse.transactionId
+// let raw = payResponse.amountRaw
+// controller.updateAmount(raw, forSaved: url)
}
}
}
var body: some View {
- Group {
- if let preparePayResult {
- let status = preparePayResult.status
- let scopes = preparePayResult.scopes // TODO: might be nil
- let firstScope = scopes?.first
- let raw = preparePayResult.amountRaw
-// let currency = raw?.currencyStr ?? UNKNOWN // TODO: v1 has currencies buried in choices
- let currency = raw.currencyStr
- let effective = preparePayResult.amountEffective
- let terms = preparePayResult.contractTerms
- let exchanges = terms.exchanges
- let baseURL = terms.exchanges.first?.url
- let paid = status == .alreadyConfirmed
- let navTitle = paid ? String(localized: "Already paid", comment:"pay merchant navTitle")
- : String(localized: "Confirm Payment", comment:"pay merchant navTitle")
- List {
- if paid {
- Text("You already paid for this article.")
- .talerFont(.headline)
- if let fulfillmentUrl = terms.fulfillmentURL {
- if let destination = URL(string: fulfillmentUrl) {
- let buttonTitle = terms.fulfillmentMessage ?? String(localized: "Open merchant website")
- Link(buttonTitle, destination: destination)
- .buttonStyle(TalerButtonStyle(type: .bordered))
- .accessibilityHint(String(localized: "Will go to the merchant website.", comment: "a11y"))
- }
- }
- }
-
- PaymentView2(stack: stack.push(),
- paid: paid,
- raw: raw,
- effective: effective,
- firstScope: firstScope,
- baseURL: baseURL,
-// terms: terms,
- summary: terms.summary,
- products: terms.products,
- balanceDetails: preparePayResult.balanceDetails)
+ ZStack {
+ if let txId {
+ TransactionSummaryList(stack: stack.push(),
+ transactionId: txId,
+ talerTX: $talerTX,
+ navTitle: nil,
+ hasDone: true,
+ showDone: .prominent,
+ url: url,
+ withActions: false)
+#if TALER_NIGHTLY2
+// if let preparePayResult {
+ let status = preparePayResult.status
+ let paid = status == .alreadyConfirmed
+ let navTitle = paid ? String(localized: "Already paid", comment:"pay merchant navTitle")
+ : String(localized: "Confirm Payment", comment:"pay merchant navTitle")
+ let list = List {
+ TransactionSummaryList.MerchantHeader(terms: terms)
- }
- .listStyle(myListStyle.style).anyView
- .safeAreaInset(edge: .bottom) {
- if !paid {
- if let effective {
- PaySafeArea(symLog: symLog,
- stack: stack.push(),
- terms: terms,
- url: url,
- effective: effective,
- transactionId: preparePayResult.transactionId,
- currencyInfo: $currencyInfo)
- } else {
- Button("Cancel") {
- dismissTop(stack.push())
+ if paid {
+ Text("You already paid for this article.")
+ .talerFont(.headline)
+ if let fulfillmentUrl = terms.fulfillmentURL {
+ if let destination = URL(string: fulfillmentUrl) {
+ let buttonTitle = terms.fulfillmentMessage ?? String(localized: "Open merchant website")
+ Link(buttonTitle, destination: destination)
+ .buttonStyle(TalerButtonStyle(type: .bordered))
+ .accessibilityHint(String(localized: "Will go to the merchant website.", comment: "a11y"))
+ }
}
- .buttonStyle(TalerButtonStyle(type: .bordered))
- .padding(.horizontal)
- } // Cancel
- }
- }
- .navigationTitle(navTitle)
- .task(id: controller.currencyTicker) {
- let currency = amountToTransfer.currencyStr
- let scopes = preparePayResult.scopes // TODO: might be nil
- if let resultScope = scopes?.first { // TODO: let user choose which currency
- currencyInfo = controller.info(for: resultScope, controller.currencyTicker)
- } else {
- currencyInfo = controller.info2(for: currency, controller.currencyTicker)
+ } // You already paid
+
}
- symLog.log("Info(for: \(currency)) loaded: \(currencyInfo.name)")
- }
+ .listStyle(myListStyle.style).anyView
#if OIM
- .overlay { if #available(iOS 16.4, *) {
- if controller.oimSheetActive {
- OIMpayView(stack: stack.push(),
- amount: effective)
+ .overlay { if #available(iOS 16.4, *) {
+ if controller.oimSheetActive {
+ OIMpayView(stack: stack.push(),
+ amount: effective)
+ }
+ } }
+#endif
+
+ if #available(iOS 17.0, *) {
+ list.toolbarTitleDisplayMode(.inlineLarge)
+ } else {
+ list
}
- } }
#endif
- } else {
- LoadingView(stack: stack.push(), scopeInfo: nil, message: url.host)
- .task { await viewDidLoad() }
+ } else {
+ LoadingView(stack: stack.push(), scopeInfo: nil, message: url.host)
+ .task { await viewDidLoad() }
+ }
+ }.onAppear() {
+ symLog.log("onAppear")
+ DebugViewC.shared.setSheetID(SHEET_PAYMENT)
}
- }.onAppear() {
- symLog.log("onAppear")
- DebugViewC.shared.setSheetID(SHEET_PAYMENT)
- }
}
}
// MARK: -
+// MARK: -
struct PaymentView2: View, Sendable {
let stack: CallStack
let paid: Bool
@@ -243,7 +136,7 @@ struct PaymentView2: View, Sendable {
// let terms: MerchantContractTerms
let summary: String?
let products: [Product]?
- let balanceDetails: PayMerchantInsufficientBalanceDetails?
+ let balanceDetails: PaymentInsufficientBalanceDetails?
func computeFee(raw: Amount?, eff: Amount?) -> Amount? {
if let raw, let eff {
@@ -260,9 +153,9 @@ struct PaymentView2: View, Sendable {
: String(localized: "Pay:", comment: "mini")
let bottomTitle = paid ? String(localized: "Spent amount:")
: String(localized: "Amount to spend:")
- if let effective {
+ if let effective { // payment possible
let fee = computeFee(raw: raw, eff: effective)
- ThreeAmountsSection(stack: stack.push(),
+ ThreeAmountsSection(stack: stack.push("PaymentView2"),
scope: firstScope,
topTitle: topTitle,
topAbbrev: topAbbrev,
@@ -282,9 +175,10 @@ struct PaymentView2: View, Sendable {
products: products)
// TODO: payment: popup with all possible exchanges, check fees
} else if let balanceDetails { // Insufficient
- let localizedCause = balanceDetails.causeHint.localizedCause(raw.currencyStr)
- Text(localizedCause)
- .talerFont(.headline)
+ if let localizedCause = balanceDetails.causeHint?.localizedCause(raw.currencyStr) {
+ Text(localizedCause)
+ .talerFont(.headline)
+ }
ThreeAmountsSection(stack: stack.push(),
scope: firstScope,
topTitle: topTitle,
@@ -319,6 +213,7 @@ struct PaySafeArea: View, Sendable {
let url: URL
let effective: Amount
let transactionId: String
+ let choiceIndex: Int
@Binding var currencyInfo: CurrencyInfo
func timeToPay(_ terms: MerchantContractTerms) -> Int {
@@ -338,37 +233,49 @@ struct PaySafeArea: View, Sendable {
return 0
}
+ @ViewBuilder
+ func timeView(_ timeToPay: Int) -> some View {
+ let startDate = Date()
+ HStack {
+ Text("Time to pay:")
+ TimelineView(.animation) { context in
+ let elapsed = Int(context.date.timeIntervalSince(startDate))
+ let seconds = timeToPay - elapsed
+ let text = Text(verbatim: "\(seconds)")
+ if #available(iOS 17.0, *) {
+ text
+ .contentTransition(.numericText(countsDown: true))
+ .animation(.default, value: elapsed)
+ } else if #available(iOS 16.4, *) {
+ text
+ .animation(.default, value: elapsed)
+ } else {
+ text
+ }
+ }.monospacedDigit()
+ Text("seconds")
+ }.accessibilityElement(children: .combine)
+ }
+
var body: some View {
+ let currency = currencyInfo.currency
let timeToPay = timeToPay(terms)
- VStack {
- if timeToPay > 0 && timeToPay < 300 {
- let startDate = Date()
- HStack {
- Text("Time to pay:")
- TimelineView(.animation) { context in
- let elapsed = Int(context.date.timeIntervalSince(startDate))
- let seconds = timeToPay - elapsed
- let text = Text(verbatim: "\(seconds)")
- if #available(iOS 17.0, *) {
- text
- .contentTransition(.numericText(countsDown: true))
- .animation(.default, value: elapsed)
- } else if #available(iOS 16.4, *) {
- text
- .animation(.default, value: elapsed)
- } else {
- text
- }
- }.monospacedDigit()
- Text("seconds")
- }.accessibilityElement(children: .combine)
+ let showTime = timeToPay > 0 && timeToPay < 300
+ let view = VStack {
+ if showTime {
+ timeView(timeToPay)
+ .padding(.top)
} else {
let _ = symLog?.log("\(timeToPay) not shown")
}
+ Text("Payment is made in \(currency)")
+ .talerFont(.callout)
+ .padding(.top, 4)
let destination = PaymentDone(stack: stack.push(),
url: url,
// scope: firstScope, // TODO: let user choose which currency
- transactionId: transactionId)
+ transactionId: transactionId,
+ choiceIndex: choiceIndex)
NavigationLink(destination: destination) {
let formatted = effective.formatted(currencyInfo, isNegative: false)
Text("Pay \(formatted.0) now")
@@ -376,10 +283,14 @@ struct PaySafeArea: View, Sendable {
}
.buttonStyle(TalerButtonStyle(type: .prominent))
.padding(.horizontal)
- let currency = currencyInfo.currency
-// let currency = amountToTransfer.currencyStr
- Text("Payment is made in \(currency)")
- .talerFont(.callout)
+ .padding(.bottom, 4)
+ }
+ if #available(iOS 26.0, *) {
+ view
+ .glassEffect(in: .rect(cornerRadius: 16.0))
+ .padding(.horizontal)
+ } else {
+ view
}
}
}