taler-ios

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

commit 99d5f1ac7ddb4bc76ae9ed398a6af463bdfef4d1
parent 7ded59c6deb9d4646a29c2d00c57751f34fd5be4
Author: Marc Stibane <marc@taler.net>
Date:   Tue, 27 Jun 2023 14:49:53 +0200

Model cleanup

Diffstat:
MTalerWallet1/Model/Model+Settings.swift | 56+++++++++++++++++++++++++++-----------------------------
MTalerWallet1/Model/Model+Transactions.swift | 57+++++++++++++++++++++++++--------------------------------
MTalerWallet1/Model/WalletModel.swift | 7+------
MTalerWallet1/Views/Balances/BalancesListView.swift | 6++++++
MTalerWallet1/Views/Balances/BalancesSectionView.swift | 16++++++++--------
MTalerWallet1/Views/Payment/PaymentURIView.swift | 124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
6 files changed, 160 insertions(+), 106 deletions(-)

diff --git a/TalerWallet1/Model/Model+Settings.swift b/TalerWallet1/Model/Model+Settings.swift @@ -14,35 +14,6 @@ fileprivate let DEMO_MERCHANTBASEURL = DEMOBACKEND fileprivate let DEMO_MERCHANTAUTHTOKEN = "secret-token:sandbox" // MARK: - -extension WalletModel { - @MainActor func loadTestKudosM() - async throws { // M for MainActor - let amount = Amount(currency: DEMOCURRENCY, integer: 11, fraction: 0) - let request = WalletBackendWithdrawTestBalance(amount: amount, - bankBaseUrl: DEMO_BANKBASEURL, - exchangeBaseUrl: DEMO_EXCHANGEBASEURL, - bankAccessApiBaseUrl: DEMO_BANKAPIBASEURL) - let response = try await sendRequest(request, ASYNCDELAY) - symLog?.log("received: \(response)") - } - - @MainActor func runIntegrationTestM(newVersion: Bool) - async throws { // M for MainActor - let amountW = Amount(currency: DEMOCURRENCY, integer: 3, fraction: 0) - let amountS = Amount(currency: DEMOCURRENCY, integer: 1, fraction: 0) - let request = WalletBackendRunIntegration(newVersion: newVersion, - amountToWithdraw: amountW, - amountToSpend: amountS, - bankBaseUrl: DEMO_BANKAPIBASEURL, - bankAccessApiBaseUrl: DEMO_BANKAPIBASEURL, - exchangeBaseUrl: DEMO_EXCHANGEBASEURL, - merchantBaseUrl: DEMO_MERCHANTBASEURL, - merchantAuthToken: DEMO_MERCHANTAUTHTOKEN) - let _ = try await sendRequest(request, ASYNCDELAY) - symLog?.log("runIntegrationTest finished") - } -} -// MARK: - /// A request to add a test balance to the wallet. fileprivate struct WalletBackendWithdrawTestBalance: WalletBackendFormattedRequest { typealias Response = String @@ -64,6 +35,17 @@ fileprivate struct WalletBackendWithdrawTestBalance: WalletBackendFormattedReque var bankAccessApiBaseUrl: String } } +extension WalletModel { + @MainActor func loadTestKudosM() + async throws { // M for MainActor + let amount = Amount(currency: DEMOCURRENCY, integer: 11, fraction: 0) + let request = WalletBackendWithdrawTestBalance(amount: amount, + bankBaseUrl: DEMO_BANKBASEURL, + exchangeBaseUrl: DEMO_EXCHANGEBASEURL, + bankAccessApiBaseUrl: DEMO_BANKAPIBASEURL) + let response = try await sendRequest(request, ASYNCDELAY) + } +} // loadTestKudosM() // MARK: - /// A request to add a test balance to the wallet. fileprivate struct WalletBackendRunIntegration: WalletBackendFormattedRequest { @@ -100,3 +82,19 @@ fileprivate struct WalletBackendRunIntegration: WalletBackendFormattedRequest { var merchantAuthToken: String } } +extension WalletModel { + @MainActor func runIntegrationTestM(newVersion: Bool) + async throws { // M for MainActor + let amountW = Amount(currency: DEMOCURRENCY, integer: 3, fraction: 0) + let amountS = Amount(currency: DEMOCURRENCY, integer: 1, fraction: 0) + let request = WalletBackendRunIntegration(newVersion: newVersion, + amountToWithdraw: amountW, + amountToSpend: amountS, + bankBaseUrl: DEMO_BANKAPIBASEURL, + bankAccessApiBaseUrl: DEMO_BANKAPIBASEURL, + exchangeBaseUrl: DEMO_EXCHANGEBASEURL, + merchantBaseUrl: DEMO_MERCHANTBASEURL, + merchantAuthToken: DEMO_MERCHANTAUTHTOKEN) + let _ = try await sendRequest(request, ASYNCDELAY) + } +} // runIntegrationTestM() diff --git a/TalerWallet1/Model/Model+Transactions.swift b/TalerWallet1/Model/Model+Transactions.swift @@ -72,6 +72,17 @@ struct DeleteTransaction: WalletBackendFormattedRequest { var transactionId: String } } +/// A request to delete a wallet transaction by ID. +struct FailTransaction: WalletBackendFormattedRequest { + struct Response: Decodable {} + func operation() -> String { return "failTransaction" } + func args() -> Args { return Args(transactionId: transactionId) } + + var transactionId: String + struct Args: Encodable { + var transactionId: String + } +} /// A request to suspend a wallet transaction by ID. struct SuspendTransaction: WalletBackendFormattedRequest { struct Response: Decodable {} @@ -97,69 +108,51 @@ struct ResumeTransaction: WalletBackendFormattedRequest { // MARK: - extension WalletModel { /// ask wallet-core for its list of transactions filtered by searchString - func fetchTransactionsT(currency: String? = nil, searchString: String? = nil) + func transactionsT(currency: String? = nil, searchString: String? = nil) async -> [Transaction] { // might be called from a background thread itself do { let request = GetTransactions(currency: currency, search: searchString) - logger.info("getTransactions") let response = try await sendRequest(request, ASYNCDELAY) return response.transactions } catch { - logger.error("getTransactions failed: \(error)") return [] } } /// fetch transactions from Wallet-Core. No networking involved - @MainActor func fetchTransactionsM(currency: String? = nil, searchString: String? = nil) + @MainActor func transactionsMA(currency: String? = nil, searchString: String? = nil) async -> [Transaction] { // M for MainActor - return await fetchTransactionsT(currency: currency, searchString: searchString) + return await transactionsT(currency: currency, searchString: searchString) } - func abortTransactionT(transactionId: String) - async throws { // might be called from a background thread itself + /// abort the specified transaction from Wallet-Core. No networking involved + func abortTransaction(transactionId: String) async throws { let request = AbortTransaction(transactionId: transactionId) logger.info("abortTransaction: \(transactionId, privacy: .private(mask: .hash))") let _ = try await sendRequest(request, ASYNCDELAY) } - /// delete the specified transaction from Wallet-Core. No networking involved - @MainActor func abortTransactionM(transactionId: String) - async throws { // M for MainActor - try await abortTransactionT(transactionId: transactionId) - } - func deleteTransactionT(transactionId: String) - async throws { // might be called from a background thread itself + /// delete the specified transaction from Wallet-Core. No networking involved + func deleteTransaction(transactionId: String) async throws { let request = DeleteTransaction(transactionId: transactionId) logger.info("deleteTransaction: \(transactionId, privacy: .private(mask: .hash))") let _ = try await sendRequest(request, ASYNCDELAY) } - /// delete the specified transaction from Wallet-Core. No networking involved - @MainActor func deleteTransactionM(transactionId: String) - async throws { // M for MainActor - try await deleteTransactionT(transactionId: transactionId) + + func failTransaction(transactionId: String) async throws { + let request = FailTransaction(transactionId: transactionId) + logger.info("failTransaction: \(transactionId, privacy: .private(mask: .hash))") + let _ = try await sendRequest(request, ASYNCDELAY) } - func suspendTransactionT(transactionId: String) - async throws { // might be called from a background thread itself + func suspendTransaction(transactionId: String) async throws { let request = SuspendTransaction(transactionId: transactionId) logger.info("suspendTransaction: \(transactionId, privacy: .private(mask: .hash))") let _ = try await sendRequest(request, ASYNCDELAY) } - /// delete the specified transaction from Wallet-Core. No networking involved - @MainActor func suspendTransactionM(transactionId: String) - async throws { // M for MainActor - try await suspendTransactionT(transactionId: transactionId) - } - func resumeTransactionT(transactionId: String) - async throws { // might be called from a background thread itself + func resumeTransaction(transactionId: String) async throws { let request = ResumeTransaction(transactionId: transactionId) logger.info("resumeTransaction: \(transactionId, privacy: .private(mask: .hash))") let _ = try await sendRequest(request, ASYNCDELAY) } - /// delete the specified transaction from Wallet-Core. No networking involved - @MainActor func resumeTransactionM(transactionId: String) - async throws { // M for MainActor - try await resumeTransactionT(transactionId: transactionId) - } } diff --git a/TalerWallet1/Model/WalletModel.swift b/TalerWallet1/Model/WalletModel.swift @@ -13,13 +13,8 @@ fileprivate let ASYNCDELAY: UInt = 0 //set e.g to 6 or 9 seconds for debugging // MARK: - /// The "virtual" base class for all models class WalletModel: ObservableObject { - public static let shared = WalletModel(0) + public static let shared = WalletModel() static func className() -> String {"\(self)"} - var symLog: SymLogC? - - init(_ symbol: Int) { // init with 0 to disable logging for this class - self.symLog = SymLogC(symbol == 0 ? 0 : -1, funcName: Self.className()) - } let logger = Logger (subsystem: "net.taler.gnu", category: "WalletModel") func sendRequest<T: WalletBackendFormattedRequest> (_ request: T, _ delay: UInt = 0) diff --git a/TalerWallet1/Views/Balances/BalancesListView.swift b/TalerWallet1/Views/Balances/BalancesListView.swift @@ -80,6 +80,12 @@ struct BalancesListView: View { .overlay { if balances.isEmpty { WalletEmptyView() + .refreshable { + symLog.log("refreshing") + await reloadAction() // this closure is already async, no need for a Task + // TODO: reload transactions + } + } } .alert("Scanning QR-codes requires access to the camera", diff --git a/TalerWallet1/Views/Balances/BalancesSectionView.swift b/TalerWallet1/Views/Balances/BalancesSectionView.swift @@ -66,25 +66,25 @@ struct BalancesSectionView: View { #endif let currency = balance.available.currencyStr let reloadCompleted = { - transactions = await model.fetchTransactionsT(currency: currency) + transactions = await model.transactionsT(currency: currency) completedTransactions = WalletModel.completedTransactions(transactions) // sectionID = UUID() } let reloadPending = { - transactions = await model.fetchTransactionsT(currency: currency) + transactions = await model.transactionsT(currency: currency) pendingTransactions = WalletModel.pendingTransactions(transactions) // sectionID = UUID() } let reloadUncompleted = { - transactions = await model.fetchTransactionsT(currency: currency) + transactions = await model.transactionsT(currency: currency) uncompletedTransactions = WalletModel.uncompletedTransactions(transactions) // sectionID = UUID() } - let deleteAction = model.deleteTransactionT // dummyTransaction - let abortAction = model.abortTransactionT - let suspendAction = model.suspendTransactionT // dummyTransaction - let resumeAction = model.resumeTransactionT + let abortAction = model.abortTransaction + let deleteAction = model.deleteTransaction // dummyTransaction let failAction = model.failTransaction + let suspendAction = model.suspendTransaction // dummyTransaction + let resumeAction = model.resumeTransaction Section { // if "KUDOS" == currency && !balance.available.isZero { @@ -190,8 +190,8 @@ struct BalancesSectionView: View { }.id(sectionID) .task { if shownSectionID != sectionID { - let response = await model.fetchTransactionsT(currency: currency) symLog.log("task for BalancesSectionView \(sectionID) - reload Transactions") + let response = await model.transactionsT(currency: currency) transactions = response pendingTransactions = WalletModel.pendingTransactions(response) uncompletedTransactions = WalletModel.uncompletedTransactions(response) diff --git a/TalerWallet1/Views/Payment/PaymentURIView.swift b/TalerWallet1/Views/Payment/PaymentURIView.swift @@ -3,17 +3,20 @@ * See LICENSE.md */ import SwiftUI +import taler_swift import SymLog -// Will be called either by the user scanning a QR code or tapping the provided link, both from the shop's website -// we show the user the payment details +// Will be called either by the user scanning a QR code or tapping the provided link, +// both from the shop's website. We show the payment details struct PaymentURIView: View { private let symLog = SymLogV() + let navTitle = String(localized: "Confirm Payment", comment:"pay merchant") + + // the scanned URL let url: URL @EnvironmentObject private var model: WalletModel - @State var detailsForUri: PaymentDetailsForUri? = nil func playSound(success: Bool) { let url = URL(fileURLWithPath: "/System/Library/Audio/UISounds/payment_" @@ -21,46 +24,105 @@ struct PaymentURIView: View { GNU_Taler.playSound(fileURL: url) } - func acceptAction() { + func acceptAction(detailsForUri: PaymentDetailsForUri) { Task { do { - if let detailsForUri { - let confirmPayResult = try await model.confirmPayM(detailsForUri.proposalId) - symLog.log(confirmPayResult as Any) - if confirmPayResult.type == "done" { - // TODO: Show Hints that Payment was successfull - playSound(success: true) - } else { - // TODO: show error - playSound(success: false) - } + let confirmPayResult = try await model.confirmPayM(detailsForUri.proposalId) + symLog.log(confirmPayResult as Any) + if confirmPayResult.type == "done" { + playSound(success: true) + // TODO: Show Hints that Payment was successfull + } else { + playSound(success: false) + // TODO: show error } - } catch { // TODO: error + } catch { playSound(success: false) + // TODO: error symLog.log(error.localizedDescription) } dismissTop() } } + @State var detailsForUri: PaymentDetailsForUri? = nil + var body: some View { - let badURL = "Error in URL: \(url)" - VStack { - if let detailsForUri { - PaymentAcceptView(detailsForUri: detailsForUri, acceptAction: acceptAction) - .navigationTitle("Payment") - } else { - WithdrawProgressView(message: url.host ?? badURL) - .navigationTitle("Contacting Exchange") - } - }.task { - do { - symLog.log(".task") - let details = try await model.preparePayForUriM(url.absoluteString) - detailsForUri = details - } catch { // TODO: error - symLog.log(error.localizedDescription) + if let detailsForUri { + VStack { + let baseURL = detailsForUri.contractTerms.exchanges.first?.url + let raw = detailsForUri.amountRaw + let effective = detailsForUri.amountEffective + let currency = raw.currencyStr + let fee = try! Amount.diff(raw, effective) // TODO: different currencies + ThreeAmountsView(topTitle: String(localized: "Amount to pay:"), + topAmount: raw, fee: fee, + bottomTitle: String(localized: "\(currency) to be spent:"), + bottomAmount: effective, + large: true, pending: false, incoming: false, + baseURL: baseURL) + // TODO: payment: popup with all possible exchanges, check fees + .safeAreaInset(edge: .bottom) { + Button(navTitle, action: { acceptAction(detailsForUri: detailsForUri) }) + .buttonStyle(TalerButtonStyle(type: .prominent)) + .padding(.horizontal) + } } + .navigationTitle(navTitle) + } else { + let badURL = "Error in URL: \(url)" + WithdrawProgressView(message: url.host ?? badURL) + .navigationTitle("Contacting Exchange") + .task { + do { + symLog.log(".task") + let details = try await model.preparePayForUriM(url.absoluteString) + detailsForUri = details + } catch { // TODO: error + symLog.log(error.localizedDescription) + } + } } } } +// MARK: - +struct PaymentURIView_Previews: PreviewProvider { + static var previews: some View { + let merchant = Merchant(name: "Merchant") + let extra = Extra(articleName: "articleName") + let product = Product(description: "description") + let terms = ContractTerms(amount: try! Amount(fromString: LONGCURRENCY + ":2.2"), + maxFee: try! Amount(fromString: LONGCURRENCY + ":0.2"), + maxWireFee: try! Amount(fromString: LONGCURRENCY + ":0.2"), + merchant: merchant, + extra: extra, + summary: "summary", + timestamp: Timestamp.now(), + payDeadline: Timestamp.tomorrow(), + refundDeadline: Timestamp.tomorrow(), + wireTransferDeadline: Timestamp.tomorrow(), + merchantBaseURL: "merchantBaseURL", + fulfillmentURL: "fulfillmentURL", + publicReorderURL: "publicReorderURL", + auditors: [], + exchanges: [], + orderID: "orderID", + nonce: "nonce", + merchantPub: "merchantPub", + products: [product], + hWire: "hWire", + wireMethod: "wireMethod", + wireFeeAmortization: 0) + let details = PaymentDetailsForUri( + amountRaw: try! Amount(fromString: LONGCURRENCY + ":2.2"), + amountEffective: try! Amount(fromString: LONGCURRENCY + ":2.4"), + noncePriv: "noncePriv", + proposalId: "proposalId", + contractTerms: terms, + contractTermsHash: "termsHash" + ) + let url = URL(string: "taler://pay/some_amount")! + + PaymentURIView(url: url, detailsForUri: details) + } +}