taler-ios

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

commit f7c55f50bb77ff701ddbd942808231ca71478d65
parent 6cb8d57a9931401bfc89461b45a4b12882507cf1
Author: Iván Ávalos <avalos@disroot.org>
Date:   Mon, 11 Mar 2024 14:10:53 -0600

Implement error handling all around (+refactoring)

Diffstat:
MTalerWallet1/Backend/WalletBackendError.swift | 20++++++++++----------
MTalerWallet1/Backend/WalletCore.swift | 10++++------
MTalerWallet1/Controllers/Controller.swift | 28++++++----------------------
MTalerWallet1/Helper/Encodable+toJSON.swift | 12++++--------
MTalerWallet1/Model/Model+Balances.swift | 13++++---------
MTalerWallet1/Model/Model+Exchange.swift | 27+++++++++------------------
MTalerWallet1/Model/Model+Pending.swift | 12++++--------
MTalerWallet1/Model/Model+Transactions.swift | 18+++++++-----------
MTalerWallet1/Model/WalletModel.swift | 13+------------
MTalerWallet1/Views/Balances/BalancesListView.swift | 24+++++++++++++++++-------
MTalerWallet1/Views/Balances/BalancesSectionView.swift | 28+++++++++++++++++++++++-----
MTalerWallet1/Views/Banking/ExchangeListView.swift | 13+++++++++++--
MTalerWallet1/Views/Banking/ManualWithdraw.swift | 10+++++++---
MTalerWallet1/Views/HelperViews/BarGraph.swift | 13++++++++++---
MTalerWallet1/Views/Main/MainView.swift | 24+++++++++++++++---------
MTalerWallet1/Views/Peer2peer/RequestPayment.swift | 10++++++----
MTalerWallet1/Views/Peer2peer/SendAmount.swift | 10++++++----
MTalerWallet1/Views/Sheets/ErrorSheet.swift | 80++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
MTalerWallet1/Views/Sheets/P2P_Sheets/P2pReceiveURIView.swift | 7+++++--
MTalerWallet1/Views/Sheets/Payment/PaymentView.swift | 5++++-
MTalerWallet1/Views/Sheets/Sheet.swift | 20+++++++++++++++++++-
MTalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawURIView.swift | 8+++++---
MTalerWallet1/Views/Sheets/WithdrawExchangeV.swift | 25+++++++++++++------------
23 files changed, 235 insertions(+), 195 deletions(-)

diff --git a/TalerWallet1/Backend/WalletBackendError.swift b/TalerWallet1/Backend/WalletBackendError.swift @@ -13,38 +13,38 @@ enum WalletBackendError: Error { case initializationError case serializationError case deserializationError - case walletCoreError + case walletCoreError(WalletBackendResponseError?) } /// Information supplied by the backend describing an error. struct WalletBackendResponseError: Codable { /// Numeric error code defined defined in the GANA gnu-taler-error-codes registry. - var talerErrorCode: Int + var code: Int /// English description of the error code. - var talerErrorHint: String + var hint: String /// English diagnostic message that can give details for the instance of the error. - var message: String - + var message: String? = nil + /// Error details, type depends on `talerErrorCode`. - var details: Data? + var details: Data? = nil } extension WalletCore { static func serializeRequestError() -> WalletBackendResponseError { - return WalletBackendResponseError(talerErrorCode: -1, talerErrorHint: "Could not serialize request.", message: "") + return WalletBackendResponseError(code: -1, hint: "Could not serialize request.", message: "") } static func parseResponseError() -> WalletBackendResponseError { - return WalletBackendResponseError(talerErrorCode: -2, talerErrorHint: "Could not parse response.", message: "") + return WalletBackendResponseError(code: -2, hint: "Could not parse response.", message: "") } static func parseFailureError() -> WalletBackendResponseError { - return WalletBackendResponseError(talerErrorCode: -3, talerErrorHint: "Could not parse error detail.", message: "") + return WalletBackendResponseError(code: -3, hint: "Could not parse error detail.", message: "") } static func walletError() -> WalletBackendResponseError { - return WalletBackendResponseError(talerErrorCode: -4, talerErrorHint: "Error detail.", message: "") + return WalletBackendResponseError(code: -4, hint: "Error detail.", message: "") } } diff --git a/TalerWallet1/Backend/WalletCore.swift b/TalerWallet1/Backend/WalletCore.swift @@ -62,7 +62,7 @@ class WalletCore: QuickjsMessageHandler { let operation: String? let id: UInt? let result: AnyCodable? - let error: AnyCodable? // should be WalletBackendResponseError? + let error: WalletBackendResponseError? let payload: AnyCodable? } @@ -109,12 +109,10 @@ extension WalletCore { let jsonData = try JSONEncoder().encode(walletError) logger.error("wallet-core sent back an error for request \(requestId, privacy: .public)") symLog.log("id:\(requestId) \(walletError)") - // TODO: decode jsonData to WalletBackendResponseError - or HTTPError - completion(requestId, timeSent, jsonData, WalletCore.walletError()) + completion(requestId, timeSent, jsonData, walletError) } catch { // JSON encoding of response.result failed / should never happen symLog.log(decoded) logger.error("cannot encode wallet-core Error") - // TODO: show error alert completion(requestId, timeSent, nil, WalletCore.parseFailureError()) } } else { // JSON decoding of error message failed @@ -409,7 +407,7 @@ extension WalletCore { let millisecs = timeUsed.milliseconds self.logger.info("Request \"id\":\(requestId, privacy: .public) took \(millisecs, privacy: .public) ms") var err: Error? = nil - if let json = result { + if let json = result, error == nil { do { let decoded = try JSONDecoder().decode(T.Response.self, from: json) continuation.resume(returning: (decoded, requestId)) @@ -439,7 +437,7 @@ extension WalletCore { } else { self.lastError = nil } - err = WalletBackendError.walletCoreError + err = WalletBackendError.walletCoreError(error) } continuation.resume(throwing: err ?? TransactionDecodingError.invalidStringValue) } diff --git a/TalerWallet1/Controllers/Controller.swift b/TalerWallet1/Controllers/Controller.swift @@ -87,31 +87,15 @@ class Controller: ObservableObject { } @MainActor - func getInfo(from exchangeBaseUrl: String, model: WalletModel) async -> CurrencyInfo? { - if let exchange = await model.getExchangeByUrl(url: exchangeBaseUrl) { -// let scopeInfo = exchange.scopeInfo -// if let info = hasInfo(for: scopeInfo.currency) { -// return info -// } -// do { -// let info = try await model.getCurrencyInfoM(scope: scopeInfo, delay: 0) -// await setInfo(info) -// return info -// } catch { -// return nil -// } - - let scopeInfo = exchange.scopeInfo + func getInfo(from exchangeBaseUrl: String, model: WalletModel) async throws -> CurrencyInfo? { + let exchange = try await model.getExchangeByUrl(url: exchangeBaseUrl) + if let scopeInfo = exchange.scopeInfo { if let info = hasInfo(for: scopeInfo.currency) { return info } - do { - let info = try await model.getCurrencyInfoM(scope: scopeInfo, delay: 0) - await setInfo(info) - return info - } catch { - return nil - } + let info = try await model.getCurrencyInfoM(scope: scopeInfo, delay: 0) + await setInfo(info) + return info } else { // TODO: Error "Can't get Exchange Info" } diff --git a/TalerWallet1/Helper/Encodable+toJSON.swift b/TalerWallet1/Helper/Encodable+toJSON.swift @@ -1,11 +1,7 @@ -// -// Codable+toJson.swift -// TalerWallet -// -// Created by Ivan Avalos on 08/03/24. -// Copyright © 2024 Taler. All rights reserved. -// - +/* + * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. + * See LICENSE.md + */ import Foundation extension Encodable { diff --git a/TalerWallet1/Model/Model+Balances.swift b/TalerWallet1/Model/Model+Balances.swift @@ -55,18 +55,13 @@ fileprivate struct Balances: WalletBackendFormattedRequest { extension WalletModel { /// fetch Balances from Wallet-Core. No networking involved @MainActor func balancesM(_ stack: CallStack) - async -> [Balance] { // M for MainActor + async throws -> [Balance] { // M for MainActor await semaphore.wait() defer { semaphore.signal() } if cachedBalances == nil { - do { - let request = Balances() - let response = try await sendRequest(request, ASYNCDELAY) - cachedBalances = response.balances - } catch { - logger.error("balancesM failed: \(error)") - // TODO: show error - } + let request = Balances() + let response = try await sendRequest(request, ASYNCDELAY) + cachedBalances = response.balances } else { logger.trace("returning cached Balances") } diff --git a/TalerWallet1/Model/Model+Exchange.swift b/TalerWallet1/Model/Model+Exchange.swift @@ -168,29 +168,20 @@ extension WalletModel { } /// ask wallet-core for its list of known exchanges - @MainActor func listExchangesM() - async -> [Exchange] { // M for MainActor - do { - let request = ListExchanges() - let response = try await sendRequest(request, ASYNCDELAY) - return response.exchanges - } catch { - // TODO: Error - return [] // empty, but not nil - } + @MainActor func listExchangesM(devMode: Bool = false) + async throws -> [Exchange] { // M for MainActor + let request = ListExchanges() + let response = try await sendRequest(request, ASYNCDELAY) + return response.exchanges } /// add a new exchange with URL to the wallet's list of known exchanges func getExchangeByUrl(url: String) - async -> Exchange? { - do { - let request = GetExchangeByUrl(exchangeBaseUrl: url) + async throws -> Exchange { + let request = GetExchangeByUrl(exchangeBaseUrl: url) // logger.info("query for exchange: \(url, privacy: .public)") - let response = try await sendRequest(request) - return response - } catch { - return nil - } + let response = try await sendRequest(request) + return response } /// add a new exchange with URL to the wallet's list of known exchanges diff --git a/TalerWallet1/Model/Model+Pending.swift b/TalerWallet1/Model/Model+Pending.swift @@ -43,13 +43,9 @@ struct PendingOperation: Codable, Hashable { // MARK: - extension WalletModel { @MainActor func getPendingOperationsM() - async -> [PendingOperation] { // M for MainActor - do { - let request = GetPendingOperations() - let response = try await sendRequest(request, ASYNCDELAY) - return response.pendingOperations - } catch { - return [] - } + async throws -> [PendingOperation] { // M for MainActor + let request = GetPendingOperations() + let response = try await sendRequest(request, ASYNCDELAY) + return response.pendingOperations } } diff --git a/TalerWallet1/Model/Model+Transactions.swift b/TalerWallet1/Model/Model+Transactions.swift @@ -103,21 +103,17 @@ struct ResumeTransaction: WalletBackendFormattedRequest { // MARK: - extension WalletModel { /// ask wallet-core for its list of transactions filtered by searchString - func transactionsT(_ stack: CallStack, scopeInfo: ScopeInfo, searchString: String? = nil, + func transactionsT(_ stack: CallStack, scopeInfo: ScopeInfo, searchString: String? = nil, sort: String = "descending", includeRefreshes: Bool = false) - async -> [Transaction] { // might be called from a background thread itself - do { - let request = GetTransactions(scopeInfo: scopeInfo, currency: scopeInfo.currency, search: searchString, sort: sort, includeRefreshes: includeRefreshes) - let response = try await sendRequest(request, ASYNCDELAY) - return response.transactions - } catch { - return [] - } + async throws -> [Transaction] { + let request = GetTransactions(scopeInfo: scopeInfo, currency: scopeInfo.currency, search: searchString, sort: sort, includeRefreshes: includeRefreshes) + let response = try await sendRequest(request, ASYNCDELAY) + return response.transactions } /// fetch transactions from Wallet-Core. No networking involved @MainActor func transactionsMA(_ stack: CallStack, scopeInfo: ScopeInfo, searchString: String? = nil, sort: String = "descending") - async -> [Transaction] { // M for MainActor - return await transactionsT(stack.push(), scopeInfo: scopeInfo, searchString: searchString, sort: sort) + async throws -> [Transaction] { // M for MainActor + return try await transactionsT(stack.push(), scopeInfo: scopeInfo, searchString: searchString, sort: sort) } /// abort the specified transaction from Wallet-Core. No networking involved diff --git a/TalerWallet1/Model/WalletModel.swift b/TalerWallet1/Model/WalletModel.swift @@ -27,12 +27,6 @@ class WalletModel: ObservableObject { let semaphore = AsyncSemaphore(value: 1) var cachedBalances: [Balance]? = nil -#if DEBUG - @AppStorage("developerMode") var developerMode: Bool = true -#else - @AppStorage("developerMode") var developerMode: Bool = false -#endif - @Published var showError: Bool = false @Published var error: ErrorData? = nil { didSet { showError = error != nil } @@ -275,12 +269,7 @@ extension WalletModel { extension WalletModel { @MainActor - func showError(error: ErrorData) { - // Do not show dev errors to users - if case .developer(_) = error, !developerMode { - return - } - + func showError(_ error: ErrorData) { self.error = error } diff --git a/TalerWallet1/Views/Balances/BalancesListView.swift b/TalerWallet1/Views/Balances/BalancesListView.swift @@ -16,6 +16,7 @@ struct BalancesListView: View { @Binding var shouldReloadBalances: Int @EnvironmentObject private var model: WalletModel + @EnvironmentObject private var controller: Controller @State private var lastReloadedBalances = 0 @State private var amountToTransfer = Amount.zero(currency: EMPTYSTRING) // Update currency when used @@ -78,14 +79,23 @@ struct BalancesListView: View { /// runs on MainActor if called in some Task {} @discardableResult - private func reloadBalances(_ stack: CallStack, _ invalidateCache: Bool) async -> Int { + private func reloadBalances(_ stack: CallStack, _ invalidateCache: Bool) async -> Int? { if invalidateCache { model.cachedBalances = nil } - let reloaded = await model.balancesM(stack.push()) - let count = reloaded.count - balances = reloaded // redraw - return count + + do { + let reloaded = try await model.balancesM(stack.push()) + let count = reloaded.count + balances = reloaded // redraw + return count + } catch { + symLog.log(error.localizedDescription) + model.showError(.error(error)) + controller.playSound(0) + } + + return nil } var body: some View { @@ -126,7 +136,7 @@ extension BalancesListView { @Binding var amountToTransfer: Amount @Binding var summary: String @Binding var shouldReloadBalances: Int - var reloadBalances: (_ stack: CallStack, _ invalidateCache: Bool) async -> Int + var reloadBalances: (_ stack: CallStack, _ invalidateCache: Bool) async -> Int? var body: some View { #if PRINT_CHANGES @@ -155,7 +165,7 @@ extension BalancesListView { .refreshable { // already async symLog?.log("refreshing balances") let count = await reloadBalances(stack.push("refreshing balances"), true) - if count > 0 { + if let count, count > 0 { NotificationCenter.default.post(name: .BalanceReloaded, object: nil) } } diff --git a/TalerWallet1/Views/Balances/BalancesSectionView.swift b/TalerWallet1/Views/Balances/BalancesSectionView.swift @@ -48,13 +48,25 @@ struct BalancesSectionView { @State private var shownSectionID = UUID() // guaranteed to be different the first time func reloadCompleted(_ stack: CallStack) async -> () { - transactions = await model.transactionsT(stack.push(), scopeInfo: balance.scopeInfo, includeRefreshes: developerMode) - completedTransactions = WalletModel.completedTransactions(transactions) + do { + transactions = try await model.transactionsT(stack.push(), scopeInfo: balance.scopeInfo, includeRefreshes: developerMode) + completedTransactions = WalletModel.completedTransactions(transactions) + } catch { + symLog.log(error.localizedDescription) + model.showError(.error(error)) + controller.playSound(0) + } } func reloadPending(_ stack: CallStack) async -> () { - transactions = await model.transactionsT(stack.push(), scopeInfo: balance.scopeInfo, includeRefreshes: developerMode) - pendingTransactions = WalletModel.pendingTransactions(transactions) + do { + transactions = try await model.transactionsT(stack.push(), scopeInfo: balance.scopeInfo, includeRefreshes: developerMode) + pendingTransactions = WalletModel.pendingTransactions(transactions) + } catch { + symLog.log(error.localizedDescription) + model.showError(.error(error)) + controller.playSound(0) + } } } @@ -128,11 +140,17 @@ extension BalancesSectionView: View { // if shownSectionID != sectionID { symLog.log(".task for BalancesSectionView - reload Transactions") // TODO: only load the MAXRECENT most recent transactions - let response = await model.transactionsT(stack.push(".task - reload Transactions"), scopeInfo: scopeInfo, includeRefreshes: developerMode) + do { + let response = try await model.transactionsT(stack.push(".task - reload Transactions"), scopeInfo: scopeInfo, includeRefreshes: developerMode) transactions = response pendingTransactions = WalletModel.pendingTransactions(response) completedTransactions = WalletModel.completedTransactions(response) shownSectionID = sectionID + } catch { + symLog.log(error.localizedDescription) + model.showError(.error(error)) + controller.playSound(0) + } // } else { // symLog.log("task for BalancesSectionView \(sectionID) ❗️ skip reloading Transactions") // } diff --git a/TalerWallet1/Views/Banking/ExchangeListView.swift b/TalerWallet1/Views/Banking/ExchangeListView.swift @@ -16,6 +16,7 @@ struct ExchangeListView: View { @Binding var balances: [Balance] let navTitle: String @EnvironmentObject private var model: WalletModel + @EnvironmentObject private var controller: Controller @State var showAlert: Bool = false @State var newExchange: String = TESTEXCHANGE @@ -27,8 +28,10 @@ struct ExchangeListView: View { try await model.addExchange(url: exchange) symLog.log("added: \(exchange)") announce(this: "added: \(exchange)") - } catch { // TODO: error handling - couldn't add exchangeURL + } catch { symLog.log("error: \(error)") + model.showError(.error(error)) + controller.playSound(0) } } } @@ -74,6 +77,7 @@ struct ExchangeListCommonV { @Binding var balances: [Balance] @EnvironmentObject private var model: WalletModel + @EnvironmentObject private var controller: Controller @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic // @AppStorage("depositIBAN") var depositIBAN = EMPTYSTRING // @AppStorage("accountHolder") var accountHolder = EMPTYSTRING @@ -84,7 +88,12 @@ struct ExchangeListCommonV { @State private var amountToTransfer = Amount.zero(currency: EMPTYSTRING) // TODO: Hold different values for different currencies? func reloadExchanges() async -> Void { - exchanges = await model.listExchangesM() + do { + exchanges = try await model.listExchangesM() + } catch { + model.showError(.error(error)) + controller.playSound(0) + } } } // MARK: - diff --git a/TalerWallet1/Views/Banking/ManualWithdraw.swift b/TalerWallet1/Views/Banking/ManualWithdraw.swift @@ -111,11 +111,15 @@ struct ManualWithdraw: View { } .task(id: amountToTransfer.value) { // re-run this whenever amountToTransfer changes if let exchangeBaseUrl = scopeInfo?.url { + symLog.log("getExchangeByUrl(\(exchangeBaseUrl))") if exchange == nil || exchange?.tosStatus != .accepted { - if let exc = await model.getExchangeByUrl(url: exchangeBaseUrl) { - exchange = exc - } else { + do { + exchange = try await model.getExchangeByUrl(url: exchangeBaseUrl) + } catch { // TODO: Error "Can't get Exchange / Payment Service Provider Info" + symLog.log(error.localizedDescription) + model.showError(.error(error)) + controller.playSound(0) } } if !amountToTransfer.isZero { diff --git a/TalerWallet1/Views/HelperViews/BarGraph.swift b/TalerWallet1/Views/HelperViews/BarGraph.swift @@ -15,6 +15,7 @@ struct BarGraphHeader: View { @Binding var shouldReloadBalances: Int @EnvironmentObject private var model: WalletModel + @EnvironmentObject private var controller: Controller @Environment(\.colorScheme) private var colorScheme @Environment(\.colorSchemeContrast) private var colorSchemeContrast @AppStorage("minimalistic") var minimalistic: Bool = false @@ -37,9 +38,15 @@ struct BarGraphHeader: View { if let scopeInfo { symLog.log(".task for BarGraphHeader(\(scopeInfo.currency)) - reload Transactions") // TODO: only load the 10 most recent transactions - let response = await model.transactionsT(stack.push(".task - reload Transactions for \(scopeInfo.currency)"), - scopeInfo: scopeInfo) - completedTransactions = WalletModel.completedTransactions(response) + do { + let response = try await model.transactionsT(stack.push(".task - reload Transactions for \(scopeInfo.currency)"), + scopeInfo: scopeInfo) + completedTransactions = WalletModel.completedTransactions(response) + } catch { + symLog.log(error.localizedDescription) + model.showError(.error(error)) + controller.playSound(0) + } } } } diff --git a/TalerWallet1/Views/Main/MainView.swift b/TalerWallet1/Views/Main/MainView.swift @@ -20,6 +20,12 @@ struct MainView: View { let stack: CallStack @Binding var soundPlayed: Bool +#if DEBUG + @AppStorage("developerMode") var developerMode: Bool = true +#else + @AppStorage("developerMode") var developerMode: Bool = false +#endif + @EnvironmentObject private var controller: Controller @EnvironmentObject private var model: WalletModel @AppStorage("talerFontIndex") var talerFontIndex: Int = 0 // extension mustn't define this, so it must be here @@ -71,6 +77,15 @@ struct MainView: View { let sheet = AnyView(URLSheet(stack: stack.push(), urlToOpen: url)) Sheet(sheetView: sheet) } + .sheet(isPresented: $model.showError) { + model.cleanError() + } content: { + if let error = model.error { + ErrorSheet(data: error, devMode: developerMode) { + model.cleanError() + } + } + } } // body } // MARK: - TabBar @@ -256,15 +271,6 @@ extension MainView { } } } - .sheet(isPresented: $model.showError) { - model.cleanError() - } content: { - if let error = model.error { - ErrorSheet(data: error, developerMode: developerMode) { - model.cleanError() - } - } - } } // body } // Content } diff --git a/TalerWallet1/Views/Peer2peer/RequestPayment.swift b/TalerWallet1/Views/Peer2peer/RequestPayment.swift @@ -105,10 +105,12 @@ struct RequestPayment: View { .task(id: amountToTransfer.value) { if exchange == nil { if let url = scopeInfo.url { - if let exc = await model.getExchangeByUrl(url: url) { - exchange = exc - } else { - // TODO: Error "Can't get Exchange / Payment Service Provider Info" + do { + exchange = try await model.getExchangeByUrl(url: url) + } catch { + symLog.log(error.localizedDescription) + model.showError(.error(error)) + controller.playSound(0) } } } diff --git a/TalerWallet1/Views/Peer2peer/SendAmount.swift b/TalerWallet1/Views/Peer2peer/SendAmount.swift @@ -133,10 +133,12 @@ struct SendAmount: View { .task(id: amountToTransfer.value) { if exchange == nil { if let url = scopeInfo.url { - if let exc = await model.getExchangeByUrl(url: url) { - exchange = exc - } else { - // TODO: Error "Can't get Exchange / Payment Service Provider Info" + do { + exchange = try await model.getExchangeByUrl(url: url) + } catch { + symLog.log(error.localizedDescription) + model.showError(.error(error)) + controller.playSound(0) } } } diff --git a/TalerWallet1/Views/Sheets/ErrorSheet.swift b/TalerWallet1/Views/Sheets/ErrorSheet.swift @@ -1,17 +1,12 @@ -// -// ErrorSheet.swift -// TalerWallet -// -// Created by Ivan Avalos on 08/03/24. -// Copyright © 2024 Taler. All rights reserved. -// - +/* + * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. + * See LICENSE.md + */ import SwiftUI enum ErrorData { -case user(String) -case developer(String) -case taler(WalletBackendResponseError) +case message(String) +case error(Error) } struct ErrorSheet: View { @@ -20,33 +15,50 @@ struct ErrorSheet: View { var onDismiss: () -> Void - let navTitle = String(localized: "Error") - let unknownError = String(localized: "Unknown error") - init(message: String, copyable: Bool, onDismiss: @escaping () -> Void) { self.message = message self.copyable = copyable self.onDismiss = onDismiss } - init(error: WalletBackendResponseError, developerMode: Bool, onDismiss: @escaping () -> Void) { - if let json = error.toJSON(), developerMode { - self.init(message: json, copyable: true, onDismiss: onDismiss) - } else { - self.init(message: error.message, copyable: false, onDismiss: onDismiss) + init(error: Error, devMode: Bool, onDismiss: @escaping () -> Void) { + let walletCoreError = String(localized: "Internal core error") + let initializationError = String(localized: "Initialization error") + let serializationError = String(localized: "Serialization error") + let deserializationError = String(localized: "Deserialization error") + + switch error { + case let walletError as WalletBackendError: + switch walletError { + case .walletCoreError(let error): + if let json = error?.toJSON(), devMode { + self.init(message: json, copyable: true, onDismiss: onDismiss) + } else if let message = error?.message { + self.init(message: message, copyable: false, onDismiss: onDismiss) + } else { + self.init(message: walletCoreError, copyable: false, onDismiss: onDismiss) + } + case .initializationError: + self.init(message: initializationError, copyable: false, onDismiss: onDismiss) + case .serializationError: + self.init(message: serializationError, copyable: false, onDismiss: onDismiss) + case .deserializationError: + self.init(message: deserializationError, copyable: false, onDismiss: onDismiss) + } + default: + self.init(message: error.localizedDescription, copyable: false, onDismiss: onDismiss) } } - init(data: ErrorData, developerMode: Bool, onDismiss: @escaping () -> Void) { + init(data: ErrorData, devMode: Bool, onDismiss: @escaping () -> Void) { + let unknownError = String(localized: "Unknown error") + switch data { - case .user(let message): + case .message(let message): self.init(message: message, copyable: false, onDismiss: onDismiss) return - case .developer(let message): - self.init(message: message, copyable: true, onDismiss: onDismiss) - return - case .taler(let error): - self.init(error: error, developerMode: developerMode, onDismiss: onDismiss) + case .error(let error): + self.init(error: error, devMode: devMode, onDismiss: onDismiss) return } @@ -89,10 +101,8 @@ struct ErrorSheet: View { .frame(minHeight: geometry.size.height) } } - .navigationTitle(navTitle) - .navigationBarTitleDisplayMode(.inline) }.safeAreaInset(edge: .bottom) { - Button("Cancel", role: .cancel) { + Button("Close", role: .cancel) { onDismiss() } .buttonStyle(TalerButtonStyle(type: .bordered)) @@ -103,13 +113,13 @@ struct ErrorSheet: View { } struct ErrorSheet_Previews: PreviewProvider { - static let error = WalletBackendResponseError( - talerErrorCode: 7025, - talerErrorHint: "A KYC step is required before withdrawal can proceed", - message: "A KYC step is required before withdrawal can proceed") + static let error = WalletBackendError.walletCoreError(WalletBackendResponseError( + code: 7025, + hint: "A KYC step is required before withdrawal can proceed", + message: "A KYC step is required before withdrawal can proceed")) static var previews: some View { - ErrorSheet(error: error, developerMode: true, onDismiss: {}) - ErrorSheet(error: error, developerMode: false, onDismiss: {}) + ErrorSheet(error: error, devMode: true, onDismiss: {}) + ErrorSheet(error: error, devMode: false, onDismiss: {}) } } diff --git a/TalerWallet1/Views/Sheets/P2P_Sheets/P2pReceiveURIView.swift b/TalerWallet1/Views/Sheets/P2P_Sheets/P2pReceiveURIView.swift @@ -17,6 +17,7 @@ struct P2pReceiveURIView: View { let url: URL @EnvironmentObject private var model: WalletModel + @EnvironmentObject private var controller: Controller @Environment(\.colorScheme) private var colorScheme @Environment(\.colorSchemeContrast) private var colorSchemeContrast @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic @@ -90,10 +91,12 @@ struct P2pReceiveURIView: View { do { // TODO: cancelled symLog.log(".task") let ppResponse = try await model.preparePeerPushCreditM(url.absoluteString) - exchange = await model.getExchangeByUrl(url: ppResponse.exchangeBaseUrl) + exchange = try await model.getExchangeByUrl(url: ppResponse.exchangeBaseUrl) peerPushCreditResponse = ppResponse - } catch { // TODO: error + } catch { symLog.log(error.localizedDescription) + model.showError(.error(error)) + controller.playSound(0) peerPushCreditResponse = nil } } diff --git a/TalerWallet1/Views/Sheets/Payment/PaymentView.swift b/TalerWallet1/Views/Sheets/Payment/PaymentView.swift @@ -20,6 +20,7 @@ struct PaymentView: View { @Binding var summary: String @EnvironmentObject private var model: WalletModel + @EnvironmentObject private var controller: Controller @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic @State var preparePayResult: PreparePayResult? = nil @@ -105,8 +106,10 @@ struct PaymentView: View { let result = try await model.preparePayForUriM(url.absoluteString) preparePayResult = result } - } catch { // TODO: error + } catch { symLog.log(error.localizedDescription) + model.showError(.error(error)) + controller.playSound(0) } } } diff --git a/TalerWallet1/Views/Sheets/Sheet.swift b/TalerWallet1/Views/Sheets/Sheet.swift @@ -10,8 +10,15 @@ struct Sheet: View { private let symLog = SymLogV(0) @Environment(\.dismiss) var dismiss // call dismiss() to get rid of the sheet @EnvironmentObject private var debugViewC: DebugViewC + @EnvironmentObject private var model: WalletModel @AppStorage("talerFontIndex") var talerFontIndex: Int = 0 +#if DEBUG + @AppStorage("developerMode") var developerMode: Bool = true +#else + @AppStorage("developerMode") var developerMode: Bool = false +#endif + var sheetView: AnyView let logger = Logger(subsystem: "net.taler.gnu", category: "Sheet") @@ -27,7 +34,15 @@ struct Sheet: View { let idString = debugViewC.sheetID > 0 ? String(debugViewC.sheetID) : "" // show nothing if 0 NavigationView { - sheetView + Group { + if let error = model.error { + ErrorSheet(data: error, devMode: developerMode) { + dismissTop() + } + } else { + sheetView + } + } .navigationBarItems(leading: cancelButton) .navigationBarTitleDisplayMode(.automatic) .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all)) @@ -45,5 +60,8 @@ struct Sheet: View { .accessibilityLabel(Text("Sheet.ID.", comment: "AccessibilityLabel")) .accessibilityValue(idString) } + .onDisappear { + model.cleanError() + } } } diff --git a/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawURIView.swift b/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawURIView.swift @@ -98,8 +98,8 @@ struct WithdrawURIView: View { let amount = withdrawUriInfo.amount let baseUrl = withdrawUriInfo.defaultExchangeBaseUrl ?? withdrawUriInfo.possibleExchanges.first?.exchangeBaseUrl - if let baseUrl, let exc = await model.getExchangeByUrl(url: baseUrl) { - exchange = exc + if let baseUrl { + exchange = try await model.getExchangeByUrl(url: baseUrl) let details = try await model.getWithdrawalDetailsForAmountM(baseUrl, amount: amount) withdrawalAmountDetails = details // agePicker.setAges(ages: details?.ageRestrictionOptions) @@ -107,8 +107,10 @@ struct WithdrawURIView: View { symLog.log("no exchangeBaseUrl or no exchange") withdrawalAmountDetails = nil } - } catch { // TODO: error + } catch { symLog.log(error.localizedDescription) + model.showError(.error(error)) + controller.playSound(0) withdrawalAmountDetails = nil } } diff --git a/TalerWallet1/Views/Sheets/WithdrawExchangeV.swift b/TalerWallet1/Views/Sheets/WithdrawExchangeV.swift @@ -25,6 +25,7 @@ struct WithdrawExchangeV: View { let _ = Self._printChanges() let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear #endif + Group { if exchange != nil { ManualWithdraw(stack: stack.push(), @@ -44,22 +45,22 @@ struct WithdrawExchangeV: View { let withdrawExchange = try await model.loadWithdrawalExchangeForUriM(url.absoluteString) let baseUrl = withdrawExchange.exchangeBaseUrl symLog.log("getExchangeByUrl(\(baseUrl))") - if let exc = await model.getExchangeByUrl(url: baseUrl) { - // let the controller collect CurrencyInfo from this formerly unknown exchange - let _ = await controller.getInfo(from: baseUrl, model: model) - if let amount = withdrawExchange.amount { - amountToTransfer = amount - } else { - let currency = exc.scopeInfo.currency - amountToTransfer.setCurrency(currency) - // is already Amount.zero(currency: "") - } - exchange = exc + let exc = try await model.getExchangeByUrl(url: baseUrl) + // let the controller collect CurrencyInfo from this formerly unknown exchange + let _ = try await controller.getInfo(from: baseUrl, model: model) + if let amount = withdrawExchange.amount { + amountToTransfer = amount } else { - // TODO: Error "Can't get Exchange / Payment Service Provider Info" + let currency = exc.scopeInfo.currency + amountToTransfer.setCurrency(currency) + // is already Amount.zero(currency: "") } + exchange = exc } catch { // TODO: error symLog.log(error.localizedDescription) + symLog.log(error.localizedDescription) + model.showError(.error(error)) + controller.playSound(0) exchange = nil } }