taler-ios

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

commit c93c70b35ebe749b525fa93ebeb8bdc32b7677c6
parent 7fc8573778758eb930040148518e1316942e1b62
Author: Marc Stibane <marc@taler.net>
Date:   Tue,  1 Sep 2026 16:06:37 +0200

-model, update exchange

Diffstat:
MTalerWallet1/Backend/WalletCore.swift | 7++++++-
MTalerWallet1/Controllers/Controller.swift | 69++++++++++++++++++++++++++++++++++++++++++---------------------------
MTalerWallet1/Controllers/TalerWallet1App.swift | 3+--
MTalerWallet1/Views/Balances/BalancesListView.swift | 2+-
MTalerWallet1/Views/Balances/DiscountPasses.swift | 4++--
MTalerWallet1/Views/Main/WalletEmptyView.swift | 3+--
MTalerWallet1/Views/Main/WalletMain.swift | 6+++---
MTalerWallet1/Views/Sheets/P2P_Sheets/P2pReceiveURIView.swift | 2+-
MTalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawUriScan.swift | 2+-
MTalerWallet1/Views/Transactions/TransactionTypeDetail.swift | 4++--
10 files changed, 60 insertions(+), 42 deletions(-)

diff --git a/TalerWallet1/Backend/WalletCore.swift b/TalerWallet1/Backend/WalletCore.swift @@ -85,7 +85,8 @@ class WalletCore: QuickjsMessageHandler { let isInternal: Bool? let hintTransactionId: String? let event: [String: AnyCodable]? - + let exchangeBaseUrl: String? + let newExchangeState: ExchangeState? let operation: String? // indexeddb-to-native-migration let phase: String? // fixup | copy | verify | complete | failed let error: TalerErrorDetail? // only when phase is "failed" @@ -393,6 +394,10 @@ extension WalletCore { break case Notification.Name.ExchangeStateTransition.rawValue: symLog.log(message) + if let baseUrl = payload.exchangeBaseUrl, + let state = payload.newExchangeState { + Controller.shared.updateExchange(for: baseUrl, state: state) + } break case Notification.Name.RequestProgressError.rawValue: symLog.log(message) diff --git a/TalerWallet1/Controllers/Controller.swift b/TalerWallet1/Controllers/Controller.swift @@ -127,6 +127,7 @@ class Controller: ObservableObject { private let symLog = SymLogC() @Published var haveProdBalance: Bool = false + @Published var exchanges: [Exchange] = [] @Published var balances: [Balance] = [] @Published var discounts: [TalerToken] = [] @Published var subscriptions: [TalerToken] = [] @@ -163,7 +164,6 @@ class Controller: ObservableObject { let player = AVQueuePlayer() let semaphore = AsyncSemaphore(value: 1) private var currencyInfos: [ScopeInfo : CurrencyInfo] - var exchanges: [Exchange] var messageForSheet: String? = nil var isLoadingChoices: String? = nil @@ -347,13 +347,14 @@ class Controller: ObservableObject { // MARK: - @MainActor @discardableResult - func loadBalances(_ stack: CallStack,_ model: WalletModel) async -> Int? { + func loadBalances(_ stack: CallStack) async -> Int? { + let model = WalletModel.shared if let response = try? await model.getBalances(stack.push()) { let reloaded = response.balances if reloaded != balances { for balance in reloaded { let scope = balance.scopeInfo - checkInfo(for: scope, model: model) + checkInfo(for: scope) } self.logger.log("••Got new balances, will redraw") balances = reloaded // redraw @@ -377,7 +378,8 @@ class Controller: ObservableObject { // MARK: - @MainActor @discardableResult - func loadDiscounts(_ stack: CallStack,_ model: WalletModel) async -> Int? { + func loadDiscounts(_ stack: CallStack) async -> Int? { + let model = WalletModel.shared if let response = try? await model.listDiscounts(stack.push()) { let reloaded = response.discounts if reloaded != discounts { @@ -393,7 +395,8 @@ class Controller: ObservableObject { // MARK: - @MainActor @discardableResult - func loadSubscriptions(_ stack: CallStack,_ model: WalletModel) async -> Int? { + func loadSubscriptions(_ stack: CallStack) async -> Int? { + let model = WalletModel.shared if let response = try? await model.listSubscriptions(stack.push()) { let reloaded = response.subscriptions if reloaded != subscriptions { @@ -410,9 +413,9 @@ class Controller: ObservableObject { @MainActor @discardableResult func loadChoicesForPayment(_ stack: CallStack, - _ model: WalletModel, txId: String) async -> Bool { self.logger.log("getChoicesForPayment: \(txId)") + let model = WalletModel.shared if isLoadingChoices != txId { isLoadingChoices = txId if let choiceResponse = try? await model.getChoicesForPayment(txId) { @@ -429,15 +432,6 @@ class Controller: ObservableObject { return false } // MARK: - - func exchange(for baseUrl: String) -> Exchange? { - for exchange in exchanges { - if exchange.exchangeBaseUrl == baseUrl { - return exchange - } - } - return nil - } - func info(for scope: ScopeInfo) -> CurrencyInfo? { // return CurrencyInfo.euro() // Fake EUR instead of the real Currency // return CurrencyInfo.francs() // Fake CHF instead of the real Currency @@ -479,11 +473,12 @@ class Controller: ObservableObject { } @MainActor - func exchange(for baseUrl: String?, model: WalletModel) async -> Exchange? { + func exchange(for baseUrl: String?) async -> Exchange? { if let baseUrl { - if let exchange1 = exchange(for: baseUrl) { - return exchange1 + if let exchange = exchanges.first(where: {$0.exchangeBaseUrl == baseUrl}) { + return exchange } + let model = WalletModel.shared if let exchange2 = try? await model.getExchangeByUrl(url: baseUrl) { // logger.log(" ❗️ will add \(baseUrl)") exchanges.append(exchange2) @@ -492,21 +487,40 @@ class Controller: ObservableObject { } return nil } + @MainActor + func updateExchange(for baseUrl: String, state: ExchangeState) { + Task { + if let exchange = await exchange(for: baseUrl) { + if exchange.tosStatus != state.tosStatus { + logger.log(" ❗️updating exchange: \(baseUrl)") + let model = WalletModel.shared + if let exchange2 = try? await model.getExchangeByUrl(url: baseUrl) { + if let index = exchanges.firstIndex(where: {$0.exchangeBaseUrl == baseUrl}) { + exchanges[index] = exchange2 + } else { + exchanges.append(exchange2) + } + } + } + } + } + } @MainActor - func updateInfo(_ scope: ScopeInfo, model: WalletModel) async { + func updateInfo2(_ scope: ScopeInfo) async { + let model = WalletModel.shared if let info = try? await model.getCurrencyInfo(scope: scope) { await setInfo(info, for: scope) // logger.log(" ❗️info set for \(scope.currency)") } } - func checkCurrencyInfo(for baseUrl: String, model: WalletModel) async -> Exchange? { - if let exchange = await exchange(for: baseUrl, model: model) { + func checkCurrencyInfo(for baseUrl: String) async -> Exchange? { + if let exchange = await exchange(for: baseUrl) { let scope = exchange.scopeInfo if currencyInfos[scope] == nil { logger.log(" ❗️got no info for \(baseUrl.trimURL) \(scope.currency) -> will update") - await updateInfo(scope, model: model) + await updateInfo2(scope) } return exchange } else { @@ -516,14 +530,14 @@ class Controller: ObservableObject { } /// called whenever a new currency pops up - will first load the Exchange and then currencyInfos - func checkInfo(for scope: ScopeInfo, model: WalletModel) { + func checkInfo(for scope: ScopeInfo) { if currencyInfos[scope] == nil { Task { - let exchange = await exchange(for: scope.url, model: model) + let exchange = await exchange(for: scope.url) if let scope2 = exchange?.scopeInfo { let exchangeName = scope2.url ?? "UNKNOWN" logger.log(" ❗️got no info for \(scope.currency) -> will update \(exchangeName.trimURL)") - await updateInfo(scope2, model: model) + await updateInfo2(scope2) } else { logger.error(" ❗️got no info for \(scope.currency), and couldn't load the exchange info❗️") } @@ -553,12 +567,13 @@ class Controller: ObservableObject { } // MARK: - @MainActor - func initWalletCore(_ model: WalletModel, setTesting: Bool, delay: TimeInterval) + func initWalletCore(setTesting: Bool, delay: TimeInterval) async throws { if backendState == .instantiated { backendState = .initing do { let walletCore = WalletCore.shared + let model = WalletModel.shared let response = try await model.initWalletCore(setTesting: setTesting) walletCore.versionInfo = response.versionInfo walletCore.nativeDB = (response.databaseBackend == "sqlite") @@ -603,7 +618,7 @@ class Controller: ObservableObject { // dismiss the launch animation self.backendState = walletCore.nativeDB ? .ready : .update } - await loadBalances(CallStack(), model) + await loadBalances(CallStack()) } catch { // rethrows self.logger.error("\(error.localizedDescription)") backendState = .error(error.toEquatableError()) // ❗️Yikes app cannot continue diff --git a/TalerWallet1/Controllers/TalerWallet1App.swift b/TalerWallet1/Controllers/TalerWallet1App.swift @@ -126,7 +126,7 @@ struct TalerWallet1App: App { let testing = false let delay: TimeInterval = 2.25 #endif - try! await controller.initWalletCore(model, setTesting: testing, delay: delay) // will (and should) crash on failure + try! await controller.initWalletCore(setTesting: testing, delay: delay) // will (and should) crash on failure } if #available(iOS 16.4, *) { return WindowGroup { @@ -173,7 +173,6 @@ struct TalerWallet1App: App { mainView } } - } } // MARK: - diff --git a/TalerWallet1/Views/Balances/BalancesListView.swift b/TalerWallet1/Views/Balances/BalancesListView.swift @@ -38,7 +38,7 @@ struct BalancesListView: View { func refresh() async { controller.hapticNotification(.success) symLog.log("refreshing balances") - await controller.loadBalances(stack.push("refreshing balances"), model) + await controller.loadBalances(stack.push("refreshing balances")) } @ViewBuilder diff --git a/TalerWallet1/Views/Balances/DiscountPasses.swift b/TalerWallet1/Views/Balances/DiscountPasses.swift @@ -145,9 +145,9 @@ struct DiscountsPassesList: View { func refresh() async { // symLog.log("refreshing balances") if isPass { - await controller.loadSubscriptions(stack.push("refreshing passes"), model) + await controller.loadSubscriptions(stack.push("refreshing passes")) } else { - await controller.loadDiscounts(stack.push("refreshing discounts"), model) + await controller.loadDiscounts(stack.push("refreshing discounts")) } } diff --git a/TalerWallet1/Views/Main/WalletEmptyView.swift b/TalerWallet1/Views/Main/WalletEmptyView.swift @@ -129,13 +129,12 @@ struct ProdSectionView: View { struct WalletEmptyHeader: View { let stack: CallStack - @EnvironmentObject private var model: WalletModel @EnvironmentObject private var controller: Controller func refresh() async { controller.hapticNotification(.success) // symLog.log("refreshing balances") - await controller.loadBalances(stack.push("refreshing balances"), model) + await controller.loadBalances(stack.push("refreshing balances")) } var body: some View { diff --git a/TalerWallet1/Views/Main/WalletMain.swift b/TalerWallet1/Views/Main/WalletMain.swift @@ -371,9 +371,9 @@ struct WalletMain: View { } .task(id: shouldReloadBalances) { // runs once at launch, then on each onNotification(.BalanceChange) // symLog.log(".task shouldReloadBalances \(shouldReloadBalances)") - await controller.loadBalances(stack.push("refreshing balances"), model) - await controller.loadDiscounts(stack.push("refreshing discounts"), model) - await controller.loadSubscriptions(stack.push("refreshing passes"), model) + await controller.loadBalances(stack.push("refreshing balances")) + await controller.loadDiscounts(stack.push("refreshing discounts")) + await controller.loadSubscriptions(stack.push("refreshing passes")) } // task } // body } // Content diff --git a/TalerWallet1/Views/Sheets/P2P_Sheets/P2pReceiveURIView.swift b/TalerWallet1/Views/Sheets/P2P_Sheets/P2pReceiveURIView.swift @@ -50,7 +50,7 @@ struct P2pReceiveURIView: View { if let ppResponse = try? await model.preparePeerPushCredit(url.trimmedString) { let baseUrl = ppResponse.exchangeBaseUrl exchange = try? await model.getExchangeByUrl(url: baseUrl) - await controller.checkCurrencyInfo(for: baseUrl, model: model) + await controller.checkCurrencyInfo(for: baseUrl) let oimCurrency = oimCurrency(ppResponse.scopeInfo, oimEuro: oimEuro) cash.setCurrency(oimCurrency) controller.removeURL(url) // tx is now saved by wallet-core diff --git a/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawUriScan.swift b/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawUriScan.swift @@ -123,7 +123,7 @@ struct WithdrawUriScan: View { // await loadExchange(baseUrl) <- checkCurrencyInfo now returns the exchange // symLog.log("\(baseUrl.trimURL) loaded") - if let baseUrl, let someExchange = try? await controller.checkCurrencyInfo(for: baseUrl, model: model) { + if let baseUrl, let someExchange = try? await controller.checkCurrencyInfo(for: baseUrl) { symLog.log("Info(for: \(baseUrl.trimURL) loaded") exchange = someExchange } else { diff --git a/TalerWallet1/Views/Transactions/TransactionTypeDetail.swift b/TalerWallet1/Views/Transactions/TransactionTypeDetail.swift @@ -146,8 +146,8 @@ struct TransactionTypeDetail: View { let choicesForPayment = choicesTuple.1 if choicesTuple.0 != common.transactionId || choicesForPayment == nil { symLog.log("❗️❗️ task: \(common.txState)") - await controller.loadChoicesForPayment(stack.push(),model, - txId: common.transactionId) + await controller.loadChoicesForPayment(stack.push(), + txId: common.transactionId) } else { symLog.log("❗️❗️ choicesForPayment exists, no need for task: \(state)") }