commit 1a1ed961660b7ebc878d1a1c422c0b67d427cb87
parent 32d6729e955835f9d40ce076f1e6cbbb04ffbb33
Author: Marc Stibane <marc@taler.net>
Date: Tue, 30 Jun 2026 08:01:31 +0200
Let controller handle choicesForPayment
Diffstat:
5 files changed, 107 insertions(+), 99 deletions(-)
diff --git a/TalerWallet1/Controllers/Controller.swift b/TalerWallet1/Controllers/Controller.swift
@@ -139,6 +139,8 @@ class Controller: ObservableObject {
@Published var oimSheetActive: Bool = false
@Published var diagnosticModeEnabled: Bool = false
@Published var talerURI: URL? = nil
+ @Published var choicesForPayment: ChoicesForPayment? = nil
+
@AppStorage("useHaptics") var useHaptics: Bool = true // extension mustn't define this, so it must be here
@AppStorage("playSounds") var playSounds: Bool = false
@AppStorage("talerFontIndex") var talerFontIndex: Int = 0 // extension mustn't define this, so it must be here
@@ -155,6 +157,7 @@ class Controller: ObservableObject {
private var currencyInfos: [ScopeInfo : CurrencyInfo]
var exchanges: [Exchange]
var messageForSheet: String? = nil
+ var isLoadingChoices: String? = nil
private let monitor = NWPathMonitor()
@@ -344,7 +347,7 @@ class Controller: ObservableObject {
}
return nil
}
- // MARK: -
+// MARK: -
@MainActor
@discardableResult
func loadDiscounts(_ stack: CallStack,_ model: WalletModel) async -> Int? {
@@ -360,7 +363,7 @@ class Controller: ObservableObject {
}
return nil
}
- // MARK: -
+// MARK: -
@MainActor
@discardableResult
func loadSubscriptions(_ stack: CallStack,_ model: WalletModel) async -> Int? {
@@ -376,6 +379,28 @@ class Controller: ObservableObject {
}
return nil
}
+ // MARK: -
+ @MainActor
+ @discardableResult
+ func loadChoicesForPayment(_ stack: CallStack,
+ _ model: WalletModel,
+ txId: String) async -> Bool {
+ self.logger.log("getChoicesForPayment: \(txId)")
+ if isLoadingChoices != txId {
+ isLoadingChoices = txId
+ if let choiceResponse = try? await model.getChoicesForPayment(txId) {
+ choicesForPayment = choiceResponse
+ isLoadingChoices = nil
+ return true
+ } else {
+ isLoadingChoices = nil
+ self.logger.log("getChoicesForPayment failed: \(txId)")
+ }
+ } else {
+ self.logger.log("getChoicesForPayment already in progress: \(txId)")
+ }
+ return false
+ }
// MARK: -
func exchange(for baseUrl: String) -> Exchange? {
for exchange in exchanges {
diff --git a/TalerWallet1/Model/Model+Payment.swift b/TalerWallet1/Model/Model+Payment.swift
@@ -350,7 +350,9 @@ struct ChoiceSelectionDetail: Codable, Hashable, Sendable {
var balanceDetails: PaymentInsufficientBalanceDetails? // only if insufficient
}
-struct GetChoicesForPaymentResult: Codable {
+typealias ChoiceTriple = (ChoiceSelectionDetail, ContractChoice, Int)
+
+struct ChoicesForPayment: Codable {
var choices: [ChoiceSelectionDetail]
/**
* Index of the choice in @e choices array to present to the user as default.
@@ -370,11 +372,31 @@ struct GetChoicesForPaymentResult: Codable {
var automaticExecutableIndex: Int?
var contractTerms: MerchantContractTerms
+
+ func choiceTriple() -> ([ChoiceTriple], Bool)? {
+ let terms = self.contractTerms
+ if let ctChoices = terms.choices {
+ let combined = Array(zip(choices, ctChoices, ctChoices.indices))
+ return (combined, true)
+ } else if let amount = terms.amount { // V0
+ let maxFee = terms.maxFee ?? Amount.zero(currency: amount.currencyStr)
+ let ctChoice = ContractChoice(amount: amount,
+ maxFee: maxFee,
+ description: terms.summary,
+ descriptionI18n: terms.summaryI18n,
+ inputs: [],
+ outputs: [])
+ let combined = Array(zip(choices, [ctChoice], [0]))
+ return (combined, false)
+ }
+// symLog.log(" ❗️Yikes, neither choices nor amount in contractTerms!\n\(stack)")
+ return nil
+ }
}
/// A request to get an exchange's payment contract terms.
fileprivate struct GetChoicesForPayment: WalletBackendFormattedRequest {
- typealias Response = GetChoicesForPaymentResult
+ typealias Response = ChoicesForPayment
func operation() -> String { "getChoicesForPayment" }
func args() -> Args { Args(transactionId: transactionId, forcedCoinSel: forcedCoinSel) }
@@ -575,7 +597,7 @@ extension WalletModel {
}
nonisolated func getChoicesForPayment(_ transactionId: String, viewHandles: Bool = false)
- async throws -> GetChoicesForPaymentResult {
+ async throws -> ChoicesForPayment {
let request = GetChoicesForPayment(transactionId: transactionId, forcedCoinSel: nil)
let response = try await sendRequest(request, viewHandles: viewHandles)
return response
diff --git a/TalerWallet1/Views/Sheets/Payment/ChoicesView.swift b/TalerWallet1/Views/Sheets/Payment/ChoicesView.swift
@@ -8,8 +8,6 @@
import SwiftUI
import taler_swift
-typealias ChoiceTriple = (ChoiceSelectionDetail, ContractChoice, Int)
-
struct ChoicesView: View, Sendable {
let stack: CallStack
let choiceTriple: [ChoiceTriple]
diff --git a/TalerWallet1/Views/Transactions/TransactionSummaryList.swift b/TalerWallet1/Views/Transactions/TransactionSummaryList.swift
@@ -81,61 +81,17 @@ struct MerchantHeader: View {
}
// MARK: -
struct PaymentTransactionView: View {
- private let symLog = SymLogV()
+ private let symLog = SymLogV(0)
let stack: CallStack
let common: TransactionCommon
let paymentTransaction: PaymentTransaction
@Binding var scope: ScopeInfo?
@Binding var effective: Amount?
@Binding var payNow: Bool
- @Binding var isLoadingChoices: Bool?
@Binding var selectedChoice: Int
+ @EnvironmentObject private var controller: Controller
@EnvironmentObject private var model: WalletModel
- @State var choicesForPayment: GetChoicesForPaymentResult? = nil
-// @State var isLoadingChoices: Bool? = nil
-
- @MainActor
- func choiceTriple() -> ([ChoiceTriple], Bool)? {
- if let choicesForPayment {
- let choices = choicesForPayment.choices
- let terms = choicesForPayment.contractTerms
- if let ctChoices = terms.choices {
- let combined = Array(zip(choices, ctChoices, ctChoices.indices))
- return (combined, true)
- } else if let amount = terms.amount { // V0
- let maxFee = terms.maxFee ?? Amount.zero(currency: amount.currencyStr)
- let ctChoice = ContractChoice(amount: amount,
- maxFee: maxFee,
- description: terms.summary,
- descriptionI18n: terms.summaryI18n,
- inputs: [],
- outputs: [])
- let combined = Array(zip(choices, [ctChoice], [0]))
- return (combined, false)
- } else {
- symLog.log(" ❗️Yikes, neither choices nor amount in contractTerms!\n\(stack)")
- }
- }
- return nil
- }
-
- private func getChoicesForPayment() async {
- if isLoadingChoices == nil {
- symLog.log("first getChoicesForPayment, stack: \(stack)")
- isLoadingChoices = false
- }
- if isLoadingChoices == false {
- isLoadingChoices = true
- let txId = common.transactionId
- if let choiceResponse = try? await model.getChoicesForPayment(txId) {
- choicesForPayment = choiceResponse
- isLoadingChoices = false
- }
- } else {
- symLog.log("getChoicesForPayment already in progress")
- }
- }
func summary(_ info: OrderShortInfo?) -> String? {
if let i18nDict = info?.summary_i18n {
@@ -154,16 +110,18 @@ struct PaymentTransactionView: View {
}
var body: some View {
+#if PRINT_CHANGES
let _ = Self._printChanges()
let _ = symLog.vlog()
- Group {
- let details = paymentTransaction.details
- if common.isDialog { // show payment confirmation dialog
- MerchantHeader(terms: details.contractTerms)
+#endif
+ let details = paymentTransaction.details
+ if common.isDialog { // show payment confirmation dialog
+ MerchantHeader(terms: details.contractTerms)
- if let (choices, showHeader) = choiceTriple() {
- let hasAutomatic = choicesForPayment?.automaticExecution ?? false
- let automaticIndex = hasAutomatic ? choicesForPayment?.automaticExecutableIndex : nil
+ if let choicesForPayment = controller.choicesForPayment {
+ if let (choices, showHeader) = choicesForPayment.choiceTriple() {
+ let hasAutomatic = choicesForPayment.automaticExecution ?? false
+ let automaticIndex = hasAutomatic ? choicesForPayment.automaticExecutableIndex : nil
ChoicesView(stack: stack.push(),
choiceTriple: choices,
showHeader: showHeader,
@@ -201,33 +159,20 @@ struct PaymentTransactionView: View {
balanceDetails: selectionDetail.balanceDetails)
// }
}
- } else { // show finished payment
- TransactionPayDetailV(paymentTx: paymentTransaction) // TODO: details.info.merchant.name
- ThreeAmountsSheet(stack: stack.push(),
- scope: scope,
- common: common,
- topAbbrev: String(localized: "Price:", comment: "mini"),
- topTitle: String(localized: "Price (net):"),
- baseURL: nil, // TODO: baseURL
- noFees: nil, // TODO: noFees
- feeIsNegative: false,
- large: true,
- summary: details.info?.summary ?? EMPTYSTRING)
- } // show finished payment
- }
- .task(id: common.txState.hashValue) {
- let state = common.txState
- if common.isDialog {
- if choicesForPayment == nil {
- symLog.log("❗️❗️ task: \(common.txState)")
- await getChoicesForPayment()
- } else {
- symLog.log("❗️❗️ choicesForPayment exists, no need for task: \(state)")
- }
- } else {
- symLog.log("❗️❗️ \(common.type.rawValue), no need for task: \(state)")
}
- }
+ } else { // show finished payment
+ TransactionPayDetailV(paymentTx: paymentTransaction) // TODO: details.info.merchant.name
+ ThreeAmountsSheet(stack: stack.push(),
+ scope: scope,
+ common: common,
+ topAbbrev: String(localized: "Price:", comment: "mini"),
+ topTitle: String(localized: "Price (net):"),
+ baseURL: nil, // TODO: baseURL
+ noFees: nil, // TODO: noFees
+ feeIsNegative: false,
+ large: true,
+ summary: details.info?.summary ?? EMPTYSTRING)
+ } // show finished payment
} // body
} // PaymentTransactionView
// MARK: -
@@ -358,14 +303,17 @@ struct TransactionSummaryList: View {
}
func localizedState(_ txState: TransactionState) -> String {
- if let minorState = txState.minor {
- if developerMode { return minorState.localizedDbgState }
- if talerTX.isPayment {
-// return String(localized: "Payment", comment: "TxMajorState heading")
+ let major = txState.major
+ if major != .failed {
+ if let minorState = txState.minor {
+ if developerMode { return minorState.localizedDbgState }
+// if talerTX.isPayment {
+// return String(localized: "Payment", comment: "TxMajorState heading")
+// }
+ return minorState.localizedState ?? major.localizedState
}
- return minorState.localizedState ?? txState.major.localizedState
}
- return txState.major.localizedState
+ return major.localizedState
}
@ViewBuilder
@@ -386,7 +334,7 @@ struct TransactionSummaryList: View {
imageT
Spacer(minLength: 0)
statusT
- }
+ } // TODO: a11y for tx icon
if developerMode {
if !jsonTransaction.isEmpty {
CopyButton(textToCopy: jsonTransaction, isCopied: $isCopied, title: "Copy JSON")
@@ -449,10 +397,10 @@ struct TransactionSummaryList: View {
}
var body: some View {
-//#if PRINT_CHANGES
+#if PRINT_CHANGES
let _ = Self._printChanges()
let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear
-//#endif
+#endif
let common = talerTX.common
// let scope = common.scopes.first // might be nil if scopes == []
let locale = TalerDater.shared.locale
diff --git a/TalerWallet1/Views/Transactions/TransactionTypeDetail.swift b/TalerWallet1/Views/Transactions/TransactionTypeDetail.swift
@@ -19,13 +19,15 @@ struct TransactionTypeDetail: View {
@Binding var scope: ScopeInfo?
@Binding var effective: Amount?
let hasDone: Bool
+
+ @EnvironmentObject private var controller: Controller
+ @EnvironmentObject private var model: WalletModel
@Environment(\.colorScheme) private var colorScheme
@Environment(\.colorSchemeContrast) private var colorSchemeContrast
@AppStorage("minimalistic") var minimalistic: Bool = false
@State private var rotationEnabled = true
@State private var ignoreAccept: Bool = false // accept could be set by OIM to trigger accept
@State private var isCopied: Bool = false
- @State var isLoadingChoices: Bool? = nil
func refreshFee(input: Amount, output: Amount) -> Amount? {
do {
@@ -51,8 +53,10 @@ struct TransactionTypeDetail: View {
}
var body: some View {
+#if PRINT_CHANGES
let _ = Self._printChanges()
let _ = symLog.vlog()
+#endif
let common = transaction.common
let pending = transaction.isPending
let isDialog = transaction.isDialog
@@ -111,16 +115,27 @@ struct TransactionTypeDetail: View {
}
case .payment(let paymentTransaction):
- /// this will always be recreated, thus we need to pass isLoadingChoices as Binding
PaymentTransactionView(stack: stack.push(),
common: common,
paymentTransaction: paymentTransaction,
scope: $scope,
effective: $effective,
payNow: $payNow,
- isLoadingChoices: $isLoadingChoices,
selectedChoice: $selectedChoice)
-
+ .task(id: common.txState.hashValue) {
+ let state = common.txState
+ if common.isDialog {
+ if controller.choicesForPayment == nil {
+ symLog.log("❗️❗️ task: \(common.txState)")
+ await controller.loadChoicesForPayment(stack.push(),model,
+ txId: common.transactionId)
+ } else {
+ symLog.log("❗️❗️ choicesForPayment exists, no need for task: \(state)")
+ }
+ } else {
+ symLog.log("❗️❗️ \(common.type.rawValue), no need for task: \(state)")
+ }
+ }
case .refund(let refundTransaction): Group {
let details = refundTransaction.details // TODO: more details, details.info?.merchant.name
ThreeAmountsSheet(stack: stack.push(),