taler-ios

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

commit 2fbfa381a92830d15b953ab87da5391981b82005
parent 8074fad762aa570b28016f151be4d4d2c70c5959
Author: Marc Stibane <marc@taler.net>
Date:   Sat, 17 Jun 2023 14:53:31 +0200

Big Model update, removed unneccessary thread-safety code

Diffstat:
MTalerWallet.xcodeproj/project.pbxproj | 8++++++++
MTalerWallet1/Controllers/Controller.swift | 4++--
MTalerWallet1/Model/BalancesModel.swift | 12+++++-------
MTalerWallet1/Model/ExchangeModel.swift | 31++++++++++---------------------
MTalerWallet1/Model/PaymentURIModel.swift | 38+++++++-------------------------------
MTalerWallet1/Model/Peer2peerModel.swift | 11++++++-----
MTalerWallet1/Model/PendingModel.swift | 12++++++------
MTalerWallet1/Model/SettingsModel.swift | 58++++++++++++++++++++++++----------------------------------
MTalerWallet1/Model/TransactionsModel.swift | 58++++++++++++++++++++++------------------------------------
MTalerWallet1/Model/WalletInitModel.swift | 15+++++----------
MTalerWallet1/Model/WalletModel.swift | 38++++++++++----------------------------
MTalerWallet1/Model/WithdrawModel.swift | 98+++++++++++++++++--------------------------------------------------------------
MTalerWallet1/Views/Balances/BalancesListView.swift | 55++++++++++++++++++++++++++++++-------------------------
MTalerWallet1/Views/Balances/BalancesSectionView.swift | 52++++++++++++++++++++++++++++++----------------------
MTalerWallet1/Views/Exchange/ExchangeListView.swift | 157++++++++++++++++++++++++++++++++++++++++++-------------------------------------
MTalerWallet1/Views/Exchange/ExchangeSectionView.swift | 15++++-----------
MTalerWallet1/Views/Exchange/ManualWithdraw.swift | 12+++++++-----
MTalerWallet1/Views/Exchange/ManualWithdrawDone.swift | 19++++++++++---------
ATalerWallet1/Views/HelperViews/QRCodeDetailView.swift | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MTalerWallet1/Views/Main/WalletEmptyView.swift | 8++++----
MTalerWallet1/Views/Payment/PaymentAcceptView.swift | 85+++++++++++++++++++++++++++++++++++++++++--------------------------------------
MTalerWallet1/Views/Payment/PaymentURIView.swift | 70+++++++++++++++++++++++++++++++++++++++++++++++-----------------------
MTalerWallet1/Views/Peer2peer/ReceivePurpose.swift | 4++--
MTalerWallet1/Views/Peer2peer/RequestPayment.swift | 3+--
MTalerWallet1/Views/Peer2peer/SendAmount.swift | 70++++++++++++++++++++++++++++++++++++++++++++++++----------------------
MTalerWallet1/Views/Peer2peer/SendNow.swift | 41++++++++++++++++-------------------------
MTalerWallet1/Views/Peer2peer/SendPurpose.swift | 34+++++++++++++++-------------------
MTalerWallet1/Views/Settings/Pending/PendingOpsListView.swift | 28++++++++++------------------
MTalerWallet1/Views/Settings/SettingsView.swift | 25++++++++++++-------------
MTalerWallet1/Views/Sheets/URLSheet.swift | 5++---
MTalerWallet1/Views/Transactions/TransactionDetailView.swift | 6+++---
MTalerWallet1/Views/Transactions/TransactionsEmptyView.swift | 6+++---
MTalerWallet1/Views/Transactions/TransactionsListView.swift | 10+++++-----
ATalerWallet1/Views/WithdrawBankIntegrated/WithdrawAcceptDone.swift | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MTalerWallet1/Views/WithdrawBankIntegrated/WithdrawAcceptView.swift | 94++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
MTalerWallet1/Views/WithdrawBankIntegrated/WithdrawProgressView.swift | 3++-
MTalerWallet1/Views/WithdrawBankIntegrated/WithdrawTOSView.swift | 55++++++++++++++++++++++++++++---------------------------
MTalerWallet1/Views/WithdrawBankIntegrated/WithdrawURIView.swift | 84++++++++++++++++++++++++++++++++++++++++----------------------------------------
38 files changed, 751 insertions(+), 693 deletions(-)

diff --git a/TalerWallet.xcodeproj/project.pbxproj b/TalerWallet.xcodeproj/project.pbxproj @@ -15,6 +15,8 @@ 4E40E0BE29F25ABB00B85369 /* SendAmount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E40E0BD29F25ABB00B85369 /* SendAmount.swift */; }; 4E50B3502A1BEE8000F9F01C /* ManualWithdraw.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E50B34F2A1BEE8000F9F01C /* ManualWithdraw.swift */; }; 4E53A33729F50B7B00830EC2 /* CurrencyField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E53A33629F50B7B00830EC2 /* CurrencyField.swift */; }; + 4E5A88F52A38A4FD00072618 /* QRCodeDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5A88F42A38A4FD00072618 /* QRCodeDetailView.swift */; }; + 4E5A88F72A3B9E5B00072618 /* WithdrawAcceptDone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5A88F62A3B9E5B00072618 /* WithdrawAcceptDone.swift */; }; 4E6EDD852A3615BE0031D520 /* ManualDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E6EDD842A3615BE0031D520 /* ManualDetails.swift */; }; 4E6EDD872A363D8D0031D520 /* ListStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E6EDD862A363D8D0031D520 /* ListStyle.swift */; }; 4E753A062A0952F8002D9328 /* DebugViewC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E753A052A0952F7002D9328 /* DebugViewC.swift */; }; @@ -140,6 +142,8 @@ 4E40E0BD29F25ABB00B85369 /* SendAmount.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SendAmount.swift; path = TalerWallet1/Views/Peer2peer/SendAmount.swift; sourceTree = SOURCE_ROOT; }; 4E50B34F2A1BEE8000F9F01C /* ManualWithdraw.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManualWithdraw.swift; sourceTree = "<group>"; }; 4E53A33629F50B7B00830EC2 /* CurrencyField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CurrencyField.swift; sourceTree = "<group>"; }; + 4E5A88F42A38A4FD00072618 /* QRCodeDetailView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRCodeDetailView.swift; sourceTree = "<group>"; }; + 4E5A88F62A3B9E5B00072618 /* WithdrawAcceptDone.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WithdrawAcceptDone.swift; sourceTree = "<group>"; }; 4E6EDD842A3615BE0031D520 /* ManualDetails.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManualDetails.swift; sourceTree = "<group>"; }; 4E6EDD862A363D8D0031D520 /* ListStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListStyle.swift; sourceTree = "<group>"; }; 4E753A042A08E720002D9328 /* transactions.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = transactions.json; sourceTree = "<group>"; }; @@ -433,6 +437,7 @@ children = ( 4EB0953C2989CBFE0043A8A1 /* WithdrawURIView.swift */, 4EB0953E2989CBFE0043A8A1 /* WithdrawAcceptView.swift */, + 4E5A88F62A3B9E5B00072618 /* WithdrawAcceptDone.swift */, 4EB0953F2989CBFE0043A8A1 /* WithdrawProgressView.swift */, 4EB095402989CBFE0043A8A1 /* WithdrawTOSView.swift */, ); @@ -459,6 +464,7 @@ 4E53A33629F50B7B00830EC2 /* CurrencyField.swift */, 4EA551242A2C923600FEC9A8 /* CurrencyInputView.swift */, 4EEC157229F8242800D46A03 /* QRGeneratorView.swift */, + 4E5A88F42A38A4FD00072618 /* QRCodeDetailView.swift */, 4E6EDD862A363D8D0031D520 /* ListStyle.swift */, 4EB095482989CBFE0043A8A1 /* TextFieldAlert.swift */, 4EB095492989CBFE0043A8A1 /* AmountView.swift */, @@ -697,6 +703,7 @@ 4EB0956A2989CBFE0043A8A1 /* Buttons.swift in Sources */, 4EB095602989CBFE0043A8A1 /* BalancesSectionView.swift in Sources */, 4EEC157329F8242800D46A03 /* QRGeneratorView.swift in Sources */, + 4E5A88F72A3B9E5B00072618 /* WithdrawAcceptDone.swift in Sources */, 4EB095222989CBCB0043A8A1 /* Transaction.swift in Sources */, 4E9320432A14F6EA00A87B0E /* WalletColors.swift in Sources */, 4EB0955D2989CBFE0043A8A1 /* BalancesListView.swift in Sources */, @@ -707,6 +714,7 @@ 4EB095632989CBFE0043A8A1 /* WithdrawAcceptView.swift in Sources */, 4EB0956D2989CBFE0043A8A1 /* LoadingView.swift in Sources */, 4E50B3502A1BEE8000F9F01C /* ManualWithdraw.swift in Sources */, + 4E5A88F52A38A4FD00072618 /* QRCodeDetailView.swift in Sources */, 4E87C8732A31CB7F001C6406 /* TransactionsEmptyView.swift in Sources */, 4E87C8752A34B411001C6406 /* UncompletedRowView.swift in Sources */, 4E40E0BE29F25ABB00B85369 /* SendAmount.swift in Sources */, diff --git a/TalerWallet1/Controllers/Controller.swift b/TalerWallet1/Controllers/Controller.swift @@ -43,8 +43,8 @@ class Controller: ObservableObject { WalletCore.shared.versionInfo = versionInfo backendState = .ready // dismiss the launch animation } catch { // rethrows - symLog.log(error.localizedDescription) - backendState = .error // TODO: ❗️Yikes app cannot continue + symLog.log(error.localizedDescription) // TODO: .error + backendState = .error // ❗️Yikes app cannot continue throw error } } else { diff --git a/TalerWallet1/Model/BalancesModel.swift b/TalerWallet1/Model/BalancesModel.swift @@ -8,10 +8,8 @@ fileprivate let ASYNCDELAY: UInt = 0 //set e.g to 6 or 9 seconds for debugging // MARK: - class BalancesModel: WalletModel { - @Published var balances: [Balance] - override init(_ symbol: Int = -1) { - balances = [] // empty, but not nil + override init(_ symbol: Int = -1) { // init with 0 to disable logging for this class super.init(symbol) } } @@ -57,13 +55,13 @@ struct Balance: Decodable, Hashable { extension BalancesModel { /// fetch Balances from Wallet-Core. No networking involved @MainActor func fetchBalancesM() - async { // M for MainActor + async -> [Balance] { // M for MainActor do { let request = GetBalances() - let response = try await sendRequestM(request, ASYNCDELAY) - balances = response.balances // trigger view update in BalancesListView + let response = try await sendRequest(request, ASYNCDELAY) + return response.balances // trigger view update in BalancesListView } catch { - balances = [] + return [] } } } diff --git a/TalerWallet1/Model/ExchangeModel.swift b/TalerWallet1/Model/ExchangeModel.swift @@ -8,10 +8,7 @@ import SymLog fileprivate let ASYNCDELAY: UInt = 0 //set e.g to 6 or 9 seconds for debugging class ExchangeModel: WalletModel { - @Published var exchanges: [Exchange] - override init(_ symbol: Int = -1) { - exchanges = [] // empty, but not nil super.init(symbol) } } @@ -88,30 +85,22 @@ fileprivate struct AddExchange: WalletBackendFormattedRequest { // MARK: - extension ExchangeModel { /// ask wallet-core for its list of known exchanges - @MainActor func updateListM() - async throws { // M for MainActor + @MainActor func listExchangesM() + async -> [Exchange] { // M for MainActor do { let request = ListExchanges() - let response = try await sendRequestM(request, ASYNCDELAY) - exchanges = response.exchanges // trigger view update in ExchangeListView - } catch { // rethrows - symLog?.log(error.localizedDescription) - throw error + let response = try await sendRequest(request, ASYNCDELAY) + return response.exchanges + } catch { + return [] // empty, but not nil } } /// add a new exchange with URL to the wallet's list of known exchanges - func add(url: String) async throws { - do { - symLog?.log("adding exchange: \(url)") // TODO: notice - let request = AddExchange(exchangeBaseUrl: url) - _ = try await sendRequestT(request) // TODO: MainActor? - symLog?.log("added exchange: \(url)") - try await updateListM() - } catch { // rethrows - symLog?.log(error.localizedDescription) - throw error - } + func addExchange(url: String) async throws { + symLog?.log("adding exchange: \(url)") // TODO: .notice + let request = AddExchange(exchangeBaseUrl: url) + _ = try await sendRequest(request) } } diff --git a/TalerWallet1/Model/PaymentURIModel.swift b/TalerWallet1/Model/PaymentURIModel.swift @@ -8,22 +8,12 @@ import AnyCodable //import SymLog fileprivate let ASYNCDELAY: UInt = 0 //set e.g to 6 or 9 seconds for debugging -enum PaymentState { - case error - case waitingForUriDetails - case receivedUriDetails - case waitingForPaymentAck - case receivedPaymentAck -} - class PaymentURIModel: WalletModel { - @Published var paymentState: PaymentState? + override init(_ symbol: Int = -1) { // init with 0 to disable logging for this class super.init(symbol) } } - - // MARK: - ContractTerms struct ContractTerms: Codable { let amount: Amount @@ -149,30 +139,16 @@ extension PaymentURIModel { @MainActor func preparePayForUriM(_ talerPayUri: String) // M for MainActor async throws -> PaymentDetailsForUri { - do { - paymentState = .waitingForUriDetails - let request = PreparePayForUri(talerPayUri: talerPayUri) - let response = try await sendRequestM(request, ASYNCDELAY) // TODO: MainActor ? - paymentState = .receivedUriDetails - return response - } catch { // rethrows - paymentState = .error - throw error - } + let request = PreparePayForUri(talerPayUri: talerPayUri) + let response = try await sendRequest(request, ASYNCDELAY) + return response } @MainActor func confirmPayM(_ proposalId: String) // M for MainActor async throws -> ConfirmPayResult { - do { - paymentState = .waitingForPaymentAck - let request = confirmPayForUri(proposalId: proposalId) - let response = try await sendRequestM(request, ASYNCDELAY) // TODO: MainActor ? - paymentState = .receivedPaymentAck - return response - } catch { // rethrows - paymentState = .error - throw error - } + let request = confirmPayForUri(proposalId: proposalId) + let response = try await sendRequest(request, ASYNCDELAY) + return response } } diff --git a/TalerWallet1/Model/Peer2peerModel.swift b/TalerWallet1/Model/Peer2peerModel.swift @@ -22,9 +22,10 @@ struct PeerContractTerms: Codable { // MARK: - /// The result from CheckPeerPushDebit struct CheckPeerPushDebitResponse: Codable { - let amountEffective: Amount + let exchangeBaseUrl: String let amountRaw: Amount -// let maxExpirationDate: Timestamp // TODO: limit expiration (30 days or 7 days) + let amountEffective: Amount + let maxExpirationDate: Timestamp? // TODO: limit expiration (30 days or 7 days) } /// A request to check fees before sending coins to another wallet. fileprivate struct CheckPeerPushDebit: WalletBackendFormattedRequest { @@ -91,7 +92,7 @@ extension Peer2peerModel { func checkPeerPushDebitM(_ amount: Amount) // M for MainActor async throws -> CheckPeerPushDebitResponse { let request = CheckPeerPushDebit(amount: amount) - let response = try await sendRequestM(request, ASYNCDELAY) + let response = try await sendRequest(request, ASYNCDELAY) return response } /// query exchange for fees (invoice coins). Networking involved @@ -99,7 +100,7 @@ extension Peer2peerModel { func checkPeerPullCreditM(_ amount: Amount, exchangeBaseUrl: String?) // M for MainActor async throws -> CheckPeerPullCreditResponse { let request = CheckPeerPullCredit(exchangeBaseUrl: exchangeBaseUrl, amount: amount) - let response = try await sendRequestM(request, ASYNCDELAY) + let response = try await sendRequest(request, ASYNCDELAY) return response } /// generate peer-push. Networking involved @@ -108,7 +109,7 @@ extension Peer2peerModel { async throws -> PeerPushResponse { let request = InitiatePeerPushDebit(exchangeBaseUrl: baseURL, partialContractTerms: terms) - let response = try await sendRequestM(request, ASYNCDELAY) + let response = try await sendRequest(request, ASYNCDELAY) return response } } diff --git a/TalerWallet1/Model/PendingModel.swift b/TalerWallet1/Model/PendingModel.swift @@ -9,10 +9,8 @@ import SymLog fileprivate let ASYNCDELAY: UInt = 0 //set e.g to 6 or 9 seconds for debugging class PendingModel: WalletModel { - @Published var pendingOperations: [PendingOperation] override init(_ symbol: Int = -1) { - pendingOperations = [] // empty, but not nil super.init(symbol) } } @@ -51,12 +49,14 @@ struct PendingOperation: Codable, Hashable { } // MARK: - extension PendingModel { - @MainActor func updateM() - async throws { // M for MainActor + @MainActor func getPendingOperationsM() + async -> [PendingOperation] { // M for MainActor do { let request = GetPendingOperations() - let response = try await sendRequestM(request, ASYNCDELAY) - pendingOperations = response.pendingOperations + let response = try await sendRequest(request, ASYNCDELAY) + return response.pendingOperations + } catch { + return [] } } } diff --git a/TalerWallet1/Model/SettingsModel.swift b/TalerWallet1/Model/SettingsModel.swift @@ -7,10 +7,10 @@ import taler_swift import SymLog fileprivate let ASYNCDELAY: UInt = 0 //set e.g to 6 or 9 seconds for debugging -fileprivate let DEMO_EXCHANGEBASEURL = DEMOEXCHANGE // "https://exchange.demo.taler.net/" -fileprivate let DEMO_BANKBASEURL = DEMOBANK // "https://bank.demo.taler.net/" +fileprivate let DEMO_EXCHANGEBASEURL = DEMOEXCHANGE +fileprivate let DEMO_BANKBASEURL = DEMOBANK fileprivate let DEMO_BANKAPIBASEURL = DEMOBANK + "/demobanks/default/access-api/" -fileprivate let DEMO_MERCHANTBASEURL = "https://backend.demo.taler.net/" +fileprivate let DEMO_MERCHANTBASEURL = DEMOBACKEND fileprivate let DEMO_MERCHANTAUTHTOKEN = "secret-token:sandbox" // MARK: - @@ -23,42 +23,32 @@ class SettingsModel: WalletModel { extension SettingsModel { @MainActor func loadTestKudosM() async throws { // M for MainActor - do { - 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 sendRequestM(request, ASYNCDELAY) - symLog?.log("received: \(response)") - } catch { // rethrows - symLog?.log(error.localizedDescription) - throw error - } + 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 - do { - 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 sendRequestT(request, ASYNCDELAY) - symLog?.log("runIntegrationTest finished") - } catch { // rethrows - symLog?.log(error.localizedDescription) - throw error - } + 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 @@ -80,7 +70,7 @@ fileprivate struct WalletBackendWithdrawTestBalance: WalletBackendFormattedReque var bankAccessApiBaseUrl: String } } - +// MARK: - /// A request to add a test balance to the wallet. fileprivate struct WalletBackendRunIntegration: WalletBackendFormattedRequest { struct Response: Decodable {} diff --git a/TalerWallet1/Model/TransactionsModel.swift b/TalerWallet1/Model/TransactionsModel.swift @@ -9,18 +9,12 @@ fileprivate let ASYNCDELAY: UInt = 0 //set e.g to 6 or 9 seconds for debugging // MARK: - class TransactionsModel: WalletModel { - @Published var transactions: [Transaction] static func specialTransactions(_ transactions: [Transaction]) -> [Transaction] { transactions.filter { transaction in transaction.isSpecial } } - var specialTransactions: [Transaction] { - transactions.filter { transaction in - transaction.isSpecial - } - } static func completedTransactions(_ transactions: [Transaction]) -> [Transaction] { transactions.filter { transaction in @@ -39,7 +33,6 @@ class TransactionsModel: WalletModel { } override init(_ symbol: Int = -1) { - transactions = [] // empty, but not nil super.init(symbol) } } @@ -92,49 +85,42 @@ struct DeleteTransaction: WalletBackendFormattedRequest { // MARK: - extension TransactionsModel { /// ask wallet-core for its list of transactions filtered by searchString - func fetchTransactions(currency: String?) async { // might be called from a background thread itself - await fetchTransactionsM(currency: currency, searchString: nil) - } - /// fetch transactions from Wallet-Core. No networking involved - @MainActor func fetchTransactionsM(currency: String? = nil, searchString: String? = nil) - async { // M for MainActor + func fetchTransactionsT(currency: String? = nil, searchString: String? = nil) + async -> [Transaction] { // might be called from a background thread itself do { let request = GetTransactions(currency: currency, search: searchString) - let response = try await sendRequestM(request, ASYNCDELAY) - transactions = response.transactions // trigger view update in TransactionsListView + let response = try await sendRequest(request, ASYNCDELAY) + return response.transactions } catch { - transactions = [] + return [] } } + /// fetch transactions from Wallet-Core. No networking involved + @MainActor func fetchTransactionsM(currency: String? = nil, searchString: String? = nil) + async -> [Transaction] { // M for MainActor + await fetchTransactionsT(currency: currency, searchString: searchString) + } - func abortTransaction(transactionId: String) async throws { // might be called from a background thread itself - try await abortTransactionM(transactionId: transactionId) // call deleteTransactionM on main thread + func abortTransactionT(transactionId: String) + async throws { // might be called from a background thread itself + let request = AbortTransaction(transactionId: transactionId) + 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 - do { - let request = AbortTransaction(transactionId: transactionId) - let _ = try await sendRequestT(request, ASYNCDELAY) - } catch { // rethrows - symLog?.log(error.localizedDescription) - throw error - } + async throws { // M for MainActor + try await abortTransactionT(transactionId: transactionId) // call abortTransaction on main thread } - func deleteTransaction(transactionId: String) async throws { // might be called from a background thread itself - try await deleteTransactionM(transactionId: transactionId) // call deleteTransactionM on main thread + func deleteTransactionT(transactionId: String) + async throws { // might be called from a background thread itself + let request = DeleteTransaction(transactionId: transactionId) + 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 - do { - let request = DeleteTransaction(transactionId: transactionId) - let _ = try await sendRequestT(request, ASYNCDELAY) - } catch { // rethrows - symLog?.log(error.localizedDescription) - throw error - } + async throws { // M for MainActor + try await deleteTransactionT(transactionId: transactionId) // call deleteTransaction on main thread } } diff --git a/TalerWallet1/Model/WalletInitModel.swift b/TalerWallet1/Model/WalletInitModel.swift @@ -49,16 +49,11 @@ extension WalletInitModel { /// initalize Wallet-Core. Will do networking func initWalletT() // T for any Thread async throws -> VersionInfo? { - do { - let docPath = try docPath() - let request = WalletBackendInitRequest(persistentStoragePath: docPath) - symLog?.log("info: not main thread") - let response = try await sendRequestT(request, 0) // no Delay - return response.versionInfo - } catch { // rethrows - symLog?.log("error: \(error)") - throw error - } + let docPath = try docPath() + let request = WalletBackendInitRequest(persistentStoragePath: docPath) + symLog?.log("info: not main thread") + let response = try await sendRequest(request, 0) // no Delay + return response.versionInfo } private func docPath () throws -> String { diff --git a/TalerWallet1/Model/WalletModel.swift b/TalerWallet1/Model/WalletModel.swift @@ -10,30 +10,15 @@ 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 { +class WalletModel { static func className() -> String {"\(self)"} var symLog: SymLogC? - @Published var loading: Bool = false // update view - init(_ symbol: Int) { // init with 0 to disable logging for this class self.symLog = SymLogC(symbol == 0 ? 0 : -1, funcName: Self.className()) } - @MainActor func sendRequestM<T: WalletBackendFormattedRequest> (_ request: T, _ delay: UInt = 0) - async throws -> T.Response { // M for MainActor - loading = true // enter progressView - do { - let response = try await sendRequestT(request, delay) - loading = false // exit progressView - return response - } catch { // rethrows - loading = false // exit progressView - throw error - } - } - - func sendRequestT<T: WalletBackendFormattedRequest> (_ request: T, _ delay: UInt = 0) + func sendRequest<T: WalletBackendFormattedRequest> (_ request: T, _ delay: UInt = 0) async throws -> T.Response { // T for any Thread let sendTime = Date.now do { @@ -56,24 +41,21 @@ class WalletModel: ObservableObject { } } - func getTransactionById(transactionId: String) async throws -> Transaction { // might be called from a background thread itself - return try await getTransactionByIdM(transactionId: transactionId) // call deleteTransactionM on main thread + func getTransactionByIdT(_ transactionId: String) + async throws -> Transaction { // T for any Thread + // might be called from a background thread itself + let request = GetTransactionById(transactionId: transactionId) + return try await sendRequest(request, ASYNCDELAY) } /// get the specified transaction from Wallet-Core. No networking involved - @MainActor func getTransactionByIdM(transactionId: String) + @MainActor func getTransactionByIdM(_ transactionId: String) async throws -> Transaction { // M for MainActor - do { - let request = GetTransactionById(transactionId: transactionId) - let response = try await sendRequestT(request, ASYNCDELAY) - return response - } catch { // rethrows - throw error - } + return try await getTransactionByIdT(transactionId) // call GetTransactionById on main thread } } // MARK: - /// A request to get a wallet transaction by ID. -struct GetTransactionById: WalletBackendFormattedRequest { +fileprivate struct GetTransactionById: WalletBackendFormattedRequest { typealias Response = Transaction func operation() -> String { return "getTransactionById" } func args() -> Args { return Args(transactionId: transactionId) } diff --git a/TalerWallet1/Model/WithdrawModel.swift b/TalerWallet1/Model/WithdrawModel.swift @@ -7,22 +7,8 @@ import taler_swift import SymLog fileprivate let ASYNCDELAY: UInt = 0 //set e.g to 6 or 9 seconds for debugging -enum WithdrawState { - case error - case waitingForUriDetails - case receivedUriDetails - case waitingForAmountDetails - case receivedAmountDetails - case waitingForTOS - case receivedTOS - case waitingForTOSAck - case receivedTOSAck - case waitingForWithdrAck - case receivedWithdrAck -} - class WithdrawModel: WalletModel { - @Published var withdrawState: WithdrawState? + override init(_ symbol: Int = -1) { // init with 0 to disable logging for this class super.init(symbol) } @@ -166,87 +152,45 @@ extension WithdrawModel { @MainActor func loadWithdrawalDetailsForUriM(_ talerWithdrawUri: String) // M for MainActor async throws -> WithdrawUriInfoResponse { - do { - withdrawState = .waitingForUriDetails - let request = GetWithdrawalDetailsForURI(talerWithdrawUri: talerWithdrawUri) - let response = try await sendRequestM(request, ASYNCDELAY) // TODO: MainActor ? - withdrawState = .receivedUriDetails - return response - } catch { // rethrows - withdrawState = .error - throw error - } + let request = GetWithdrawalDetailsForURI(talerWithdrawUri: talerWithdrawUri) + let response = try await sendRequest(request, ASYNCDELAY) + return response } @MainActor func loadWithdrawalDetailsForAmountM(_ exchangeBaseUrl: String, amount: Amount) // M for MainActor async throws -> ManualWithdrawalDetails { - do { - withdrawState = .waitingForAmountDetails - let request = GetWithdrawalDetailsForAmount(exchangeBaseUrl: exchangeBaseUrl, - amount: amount) - let response = try await sendRequestM(request, ASYNCDELAY) // TODO: MainActor ? - withdrawState = .receivedAmountDetails - return response - } catch { // rethrows - withdrawState = .error - throw error - } + let request = GetWithdrawalDetailsForAmount(exchangeBaseUrl: exchangeBaseUrl, + amount: amount) + let response = try await sendRequest(request, ASYNCDELAY) + return response } @MainActor func loadExchangeTermsOfServiceM(_ exchangeBaseUrl: String) // M for MainActor async throws -> ExchangeTermsOfService { - do { - withdrawState = .waitingForTOS - let request = GetExchangeTermsOfService(exchangeBaseUrl: exchangeBaseUrl) - let response = try await sendRequestM(request, ASYNCDELAY) // TODO: MainActor ? - withdrawState = .receivedTOS - return response - } catch { // rethrows - withdrawState = .error - throw error - } + let request = GetExchangeTermsOfService(exchangeBaseUrl: exchangeBaseUrl) + let response = try await sendRequest(request, ASYNCDELAY) + return response } @MainActor func setExchangeTOSAcceptedM(_ exchangeBaseUrl: String, etag: String) // M for MainActor async throws -> Decodable { - do { - withdrawState = .waitingForTOSAck - let request = SetExchangeTOSAccepted(exchangeBaseUrl: exchangeBaseUrl, etag: etag) - let response = try await sendRequestM(request, ASYNCDELAY) // TODO: MainActor ? - withdrawState = .receivedTOSAck - return response - } catch { // rethrows - withdrawState = .error - throw error - } + let request = SetExchangeTOSAccepted(exchangeBaseUrl: exchangeBaseUrl, etag: etag) + let response = try await sendRequest(request, ASYNCDELAY) + return response } @MainActor func sendAcceptIntWithdrawalM(_ exchangeBaseUrl: String, withdrawURL: String) // M for MainActor async throws -> String? { - do { - withdrawState = .waitingForWithdrAck - let request = AcceptBankIntegratedWithdrawal(talerWithdrawUri: withdrawURL, exchangeBaseUrl: exchangeBaseUrl) - let response = try await sendRequestM(request, ASYNCDELAY) // TODO: MainActor ? - withdrawState = .receivedWithdrAck - return response.confirmTransferUrl - } catch { // rethrows - withdrawState = .error - throw error - } + let request = AcceptBankIntegratedWithdrawal(talerWithdrawUri: withdrawURL, exchangeBaseUrl: exchangeBaseUrl) + let response = try await sendRequest(request, ASYNCDELAY) + return response.confirmTransferUrl } @MainActor func sendAcceptManualWithdrawalM(_ exchangeBaseUrl: String, amount: Amount, restrictAge: Int?) // M for MainActor - async throws -> AcceptManualWithdrawalResult? { - do { - withdrawState = .waitingForWithdrAck - let request = AcceptManualWithdrawal(exchangeBaseUrl: exchangeBaseUrl, amount: amount, restrictAge: restrictAge) - let response = try await sendRequestM(request, ASYNCDELAY) // TODO: MainActor ? - withdrawState = .receivedWithdrAck - return response - } catch { // rethrows - withdrawState = .error - throw error - } + async throws -> AcceptManualWithdrawalResult? { + let request = AcceptManualWithdrawal(exchangeBaseUrl: exchangeBaseUrl, amount: amount, restrictAge: restrictAge) + let response = try await sendRequest(request, ASYNCDELAY) + return response } } diff --git a/TalerWallet1/Views/Balances/BalancesListView.swift b/TalerWallet1/Views/Balances/BalancesListView.swift @@ -11,13 +11,13 @@ import AVFoundation struct BalancesListView: View { private let symLog = SymLogV() - let navTitle = String(localized: "GNU Taler") // + Wallet - @AppStorage("listStyle") var myListStyle = MyListStyle.automatic + let navTitle: String - @ObservedObject var model: BalancesModel - var hamburgerAction: () -> Void + @State var balances: [Balance] = [] + let model: BalancesModel? + let hamburgerAction: () -> Void - @State private var centsToTransfer: UInt64 = 0 // TODO: maybe Decimal? + @State private var centsToTransfer: UInt64 = 0 @State private var showQRScanner: Bool = false @State private var showCameraAlert: Bool = false @@ -60,22 +60,38 @@ struct BalancesListView: View { }) } + private func reloadAction() async { + if let model { + balances = await model.fetchBalancesM() + } else { + balances = [] + } + } + var body: some View { #if DEBUG let _ = Self._printChanges() let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear #endif - let reloadAction = model.fetchBalancesM - Content(symLog: symLog, model: model, centsToTransfer: $centsToTransfer, - reloadAction: reloadAction, myListStyle: $myListStyle) + Content(symLog: symLog, balances: $balances, centsToTransfer: $centsToTransfer, + reloadAction: reloadAction) .navigationTitle(navTitle) + .navigationBarTitleDisplayMode(.automatic) .navigationBarItems(leading: HamburgerButton(action: hamburgerAction), trailing: QRButton(action: checkCameraAvailable)) + .overlay { + if balances.isEmpty { + WalletEmptyView() + } + } .alert("Scanning QR-codes requires access to the camera", isPresented: $showCameraAlert, actions: { openSettingsButton dismissAlertButton }, message: { Text("Please allow camera access in settings.") }) + .onAppear() { + DebugViewC.shared.setViewID(VIEW_BALANCES) + } .sheet(isPresented: $showQRScanner) { let sheet = AnyView(QRSheet()) Sheet(sheetView: sheet) @@ -90,18 +106,18 @@ struct BalancesListView: View { extension BalancesListView { struct Content: View { let symLog: SymLogV? - @ObservedObject var model: BalancesModel + @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic + @Binding var balances: [Balance] @Binding var centsToTransfer: UInt64 - var reloadAction: () async -> () - @Binding var myListStyle: MyListStyle + var reloadAction: () async -> Void var body: some View { #if DEBUG let _ = Self._printChanges() let _ = symLog?.vlog() // just to get the # to compare it with .onAppear & onDisappear #endif - Group { - List (model.balances, id: \.self) { balance in + Group { // necessary for .backslide transition (bug in SwiftUI) + List(balances, id: \.self) { balance in let model = TransactionsModel.model(currency: balance.available.currencyStr) BalancesSectionView(balance: balance, centsToTransfer: $centsToTransfer, model: model) } @@ -109,18 +125,7 @@ extension BalancesListView { symLog?.log("refreshing") await reloadAction() // this closure is already async, no need for a Task } - .listStyle(myListStyle.style) - .anyView - } - .navigationBarTitleDisplayMode(.automatic) - .onAppear() { - DebugViewC.shared.setViewID(VIEW_BALANCES) - } - .overlay { - if model.balances.isEmpty { - WalletEmptyView() - .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all)) - } + .listStyle(myListStyle.style).anyView } // automatically fetch balances after receiving transaction-state-transition ... .onNotification(.TransactionStateTransition) { notification in diff --git a/TalerWallet1/Views/Balances/BalancesSectionView.swift b/TalerWallet1/Views/Balances/BalancesSectionView.swift @@ -18,37 +18,46 @@ struct BalancesSectionView: View { private let symLog = SymLogV() var balance:Balance @Binding var centsToTransfer: UInt64 - @ObservedObject var model: TransactionsModel + var model: TransactionsModel? @State private var isShowingDetailView = false @State private var buttonSelected: Int? = nil + + @State private var transactions: [Transaction] = [] @State private var completedTransactions: [Transaction] = [] @State private var pendingTransactions: [Transaction] = [] @State private var uncompletedTransactions: [Transaction] = [] + func dummyTransaction (_ transactionId: String) async throws {} var body: some View { let currency = balance.available.currencyStr let reloadCompleted = { - await model.fetchTransactions(currency: currency) - completedTransactions = TransactionsModel.completedTransactions(model.transactions) + if let model { + transactions = await model.fetchTransactionsT(currency: currency) + completedTransactions = TransactionsModel.completedTransactions(transactions) + } } let reloadPending = { - await model.fetchTransactions(currency: currency) - pendingTransactions = TransactionsModel.pendingTransactions(model.transactions) + if let model { + transactions = await model.fetchTransactionsT(currency: currency) + pendingTransactions = TransactionsModel.pendingTransactions(transactions) + } } let reloadUncompleted = { - await model.fetchTransactions(currency: currency) - uncompletedTransactions = TransactionsModel.uncompletedTransactions(model.transactions) + if let model { + transactions = await model.fetchTransactionsT(currency: currency) + uncompletedTransactions = TransactionsModel.uncompletedTransactions(transactions) + } } - let deleteAction = model.deleteTransaction - let abortAction = model.abortTransaction + let deleteAction = model?.deleteTransactionT ?? dummyTransaction + let abortAction = model?.abortTransactionT ?? dummyTransaction Section { - if "KUDOS" == currency && !balance.available.isZero { - Text("You can spend these KUDOS in the [Demo Shop](https://shop.demo.taler.net), or send coins to another wallet.") - .multilineTextAlignment(.leading) - } +// if "KUDOS" == currency && !balance.available.isZero { +// Text("You can spend these KUDOS in the [Demo Shop](https://shop.demo.taler.net), or send coins to another wallet.") +// .multilineTextAlignment(.leading) +// } HStack(spacing: 0) { NavigationLink(destination: LazyView { SendAmount(amountAvailable: balance.available) @@ -56,10 +65,8 @@ struct BalancesSectionView: View { ) { EmptyView() }.frame(width: 0).opacity(0).hidden() NavigationLink(destination: LazyView { - RequestPayment(model: Peer2peerModel.model(), - scopeInfo: balance.scopeInfo, - centsToTransfer: $centsToTransfer) - .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all)) + RequestPayment(scopeInfo: balance.scopeInfo, + centsToTransfer: $centsToTransfer) }, tag: 2, selection: $buttonSelected ) { EmptyView() }.frame(width: 0).opacity(0).hidden() @@ -127,9 +134,11 @@ let _ = print("button: Uncompleted Transactions: \(currency)") Text(currency) .font(.title) } .task { - await model.fetchTransactions(currency: currency) - pendingTransactions = TransactionsModel.pendingTransactions(model.transactions) - uncompletedTransactions = TransactionsModel.uncompletedTransactions(model.transactions) + if let model { + transactions = await model.fetchTransactionsT(currency: currency) + pendingTransactions = TransactionsModel.pendingTransactions(transactions) + uncompletedTransactions = TransactionsModel.uncompletedTransactions(transactions) + } } } // body } @@ -137,7 +146,6 @@ let _ = print("button: Uncompleted Transactions: \(currency)") #if DEBUG fileprivate struct BindingViewContainer : View { @State var centsToTransfer: UInt64 = 333 - let model = TransactionsModel.model(currency: LONGCURRENCY) var body: some View { let scopeInfo = ScopeInfo(type: ScopeInfo.ScopeInfoType.exchange, exchangeBaseUrl: DEMOEXCHANGE, currency: LONGCURRENCY) @@ -148,7 +156,7 @@ fileprivate struct BindingViewContainer : View { requiresUserInput: false, scopeInfo: scopeInfo) List { - BalancesSectionView(balance: balance, centsToTransfer: $centsToTransfer, model: model) + BalancesSectionView(balance: balance, centsToTransfer: $centsToTransfer, model: nil) } } } diff --git a/TalerWallet1/Views/Exchange/ExchangeListView.swift b/TalerWallet1/Views/Exchange/ExchangeListView.swift @@ -9,68 +9,92 @@ import SymLog /// This view shows the list of exchanges struct ExchangeListView: View { private let symLog = SymLogV() - let navTitle = String(localized: "Exchanges") + let navTitle: String - @ObservedObject var model: ExchangeModel + var model: ExchangeModel? var hamburgerAction: () -> Void + @State private var exchanges: [Exchange] = [] - // source of truth for the value the user enters in currencyField - @State private var centsToTransfer: UInt64 = 0 // TODO: different values for different currencies + // source of truth for the value the user enters in currencyField for exchange withdrawals + @State private var centsToTransfer: UInt64 = 0 // TODO: different values for different currencies? + + func reloadAction() async -> Void { + if let model { + exchanges = await model.listExchangesM() + } else { + exchanges = [] + } + } + + func addExchange(_ exUrl: String) -> Void { + Task { + if let model { + symLog.log("adding: \(exUrl)") + do { + try await model.addExchange(url: exUrl) + symLog.log("added: \(exUrl)") + } catch { // TODO: error handling - couldn't add exchangeURL + symLog.log("error: \(error)") + } + } else { + symLog.log("no model, cannot add \(exUrl)") + } + } + } + + @State var showAlert: Bool = false + @State var newExchange: String = "https://exchange-age.taler.ar/" var body: some View { #if DEBUG let _ = Self._printChanges() let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear #endif - let reloadAction = model.updateListM - Content(symLog: symLog, model: model, + let plusAction: () -> Void = { +// withAnimation { showAlert = true } + showAlert = true + } + + //Text("Exchanges...") + Content(symLog: symLog, + exchanges: $exchanges, centsToTransfer: $centsToTransfer, reloadAction: reloadAction) - .navigationBarItems(leading: HamburgerButton(action: hamburgerAction)) - .navigationTitle(navTitle) - .task { - symLog.log(".task") - do { - try await reloadAction() - } catch { // TODO: show error - symLog.log(error.localizedDescription) - } + .navigationTitle(navTitle) + .navigationBarTitleDisplayMode(.automatic) + .navigationBarItems(leading: HamburgerButton(action: hamburgerAction), + trailing: PlusButton(action: plusAction)) + .overlay { + if exchanges.isEmpty { + Text("No Exchanges yet...") } + } + .task { + symLog.log(".task") + await reloadAction() + } + .textFieldAlert(isPresented: $showAlert, title: "Add Exchange", + doneText: "Add", text: $newExchange, action: addExchange) } } // MARK: - -struct ExchangeAmount: Identifiable { - let exchange: Exchange - let amountAvailable: Amount - - var id: String { // needed for Identifiable - exchange.exchangeBaseUrl - } -} +//struct ExchangeAmount: Identifiable { +// let exchange: Exchange +// let amountAvailable: Amount +// +// var id: String { // needed for Identifiable +// exchange.exchangeBaseUrl +// } +//} // MARK: - extension ExchangeListView { struct Content: View { let symLog: SymLogV? - @AppStorage("listStyle") var myListStyle = MyListStyle.automatic - @ObservedObject var model: ExchangeModel + @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic + @Binding var exchanges: [Exchange] @Binding var centsToTransfer: UInt64 - var reloadAction: () async throws -> () - - @State var showAlert: Bool = false - @State var newExchange: String = "https://exchange-age.taler.ar/" - - func addExchange(_ exUrl: String) -> Void { - Task { - do { - symLog?.log("adding: \(exUrl)") - try await model.add(url: exUrl) - symLog?.log("added: \(exUrl)") - } catch { // TODO: error handling - couldn't add exchangeURL - symLog?.log("error: \(error)") - } - } - } + var reloadAction: () async -> Void func currenciesDict(_ exchanges: [Exchange]) -> [String : [Exchange]] { var currencies: [String : [Exchange]] = [:] @@ -86,44 +110,29 @@ extension ExchangeListView { return currencies } - @State private var exchangeAmount: ExchangeAmount? = nil +// @State private var exchangeAmount: ExchangeAmount? = nil var body: some View { - let plusAction: () -> Void = { -// withAnimation { showAlert = true } - showAlert = true + let dict = currenciesDict(exchanges) + let sortedDict = dict.sorted{ $0.key < $1.key} + Group { // necessary for .backslide transition (bug in SwiftUI) + List(sortedDict, id: \.key) { key, value in + ExchangeSectionView(currency: key, exchanges: value, centsToTransfer: $centsToTransfer) + } + .refreshable { + symLog?.log("refreshing") + await reloadAction() + } + .listStyle(myListStyle.style).anyView } - VStack { - if model.exchanges.isEmpty { - Text("No Exchanges yet...") - } else { - ScrollViewReader { scrollView in // - let dict = currenciesDict(model.exchanges) - let sortedDict = dict.sorted{ $0.key < $1.key} - List { - ForEach(sortedDict, id: \.key) { key, value in - ExchangeSectionView(currency: key, exchanges: value, centsToTransfer: $centsToTransfer) - } - } - .refreshable { - do { - symLog?.log("refreshing") - try await reloadAction() - } catch { // TODO: error - symLog?.log(error.localizedDescription) - } - } - .listStyle(myListStyle.style) - .anyView - } - } // else - }.onAppear() { + .onAppear() { DebugViewC.shared.setViewID(VIEW_EXCHANGES) } - .navigationBarTitleDisplayMode(.automatic) - .navigationBarItems(trailing: PlusButton(action: plusAction)) - .textFieldAlert(isPresented: $showAlert, title: "Add Exchange", - doneText: "Add", text: $newExchange, action: addExchange) + .onNotification(.ExchangeAdded) { notification in + // doesn't need to be received on main thread because we just reload in the background anyway + symLog?.log(".onNotification(.ExchangeAdded) ==> reloading exchanges") + Task { await reloadAction() } + } } // body } } diff --git a/TalerWallet1/Views/Exchange/ExchangeSectionView.swift b/TalerWallet1/Views/Exchange/ExchangeSectionView.swift @@ -18,22 +18,15 @@ struct ExchangeRowView: View { Text(baseURL.trimURL()) NavigationLink(destination: LazyView { - EmptyView() + EmptyView() // TODO: Deposit }, tag: 1, selection: $buttonSelected - ) { - EmptyView() - } .frame(width: 0) - .opacity(0) + ) { EmptyView() }.frame(width: 0).opacity(0) NavigationLink(destination: LazyView { ManualWithdraw(exchange: exchange, model: model, centsToTransfer: $centsToTransfer) - .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all)) }, tag: 2, selection: $buttonSelected - ) { - EmptyView() - } .frame(width: 0) - .opacity(0) + ) { EmptyView() }.frame(width: 0).opacity(0) }.listRowSeparator(.hidden) HStack { // buttons just set "buttonSelected" so the NavigationLink will trigger @@ -80,7 +73,7 @@ struct ExchangeSectionView: View { // MARK: - #if DEBUG struct ExchangeRow_Container : View { - @State private var centsToTransfer: UInt64 = 100 // TODO: maybe Decimal? + @State private var centsToTransfer: UInt64 = 100 var body: some View { let exchange1 = Exchange(exchangeBaseUrl: DEMO_AGE_EXCHANGE, diff --git a/TalerWallet1/Views/Exchange/ManualWithdraw.swift b/TalerWallet1/Views/Exchange/ManualWithdraw.swift @@ -11,7 +11,7 @@ struct ManualWithdraw: View { let navTitle = String(localized: "Withdraw Coins") var exchange: Exchange - @ObservedObject var model: WithdrawModel + var model: WithdrawModel? @Binding var centsToTransfer: UInt64 @State var manualWithdrawalDetails: ManualWithdrawalDetails? = nil @@ -45,7 +45,6 @@ struct ManualWithdraw: View { ScrollView { Text("from \(exchange.exchangeBaseUrl.trimURL())") - .padding(.top) .font(.title3) CurrencyInputView(currencyField: currencyField, title: String(localized: "Amount to withdraw:")) @@ -119,8 +118,9 @@ let _ = print(selectedAge, restrictAge) Spacer() } .frame(maxWidth: .infinity, alignment: .leading) - .navigationTitle(navTitle) .padding(.horizontal) + .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all)) + .navigationTitle(navTitle) .onAppear { symLog.log("onAppear") DebugViewC.shared.setViewID(VIEW_WITHDRAWAL) @@ -128,8 +128,10 @@ let _ = print(selectedAge, restrictAge) .task(id: centsToTransfer) { let amount = Amount.amountFromCents(currency, centsToTransfer) do { - manualWithdrawalDetails = try await model.loadWithdrawalDetailsForAmountM(exchange.exchangeBaseUrl, amount: amount) - agePicker.setAges(ages: manualWithdrawalDetails?.ageRestrictionOptions) + if let model { + manualWithdrawalDetails = try await model.loadWithdrawalDetailsForAmountM(exchange.exchangeBaseUrl, amount: amount) +// agePicker.setAges(ages: manualWithdrawalDetails?.ageRestrictionOptions) + } } catch { // TODO: error symLog.log(error.localizedDescription) manualWithdrawalDetails = nil diff --git a/TalerWallet1/Views/Exchange/ManualWithdrawDone.swift b/TalerWallet1/Views/Exchange/ManualWithdrawDone.swift @@ -11,7 +11,7 @@ struct ManualWithdrawDone: View { let navTitle = String(localized: "Wire Transfer") var exchange: Exchange - @ObservedObject var model: WithdrawModel + var model: WithdrawModel? var centsToTransfer: UInt64 var restrictAge: Int? @State var acceptManualWithdrawalResult: AcceptManualWithdrawalResult? @@ -37,13 +37,15 @@ struct ManualWithdrawDone: View { DebugViewC.shared.setViewID(VIEW_WITHDRAW_ACCEPT) }.task { do { - let amount = Amount.amountFromCents(exchange.currency!, centsToTransfer) - let result = try await model.sendAcceptManualWithdrawalM(exchange.exchangeBaseUrl, - amount: amount, restrictAge: restrictAge) + if let model { + let amount = Amount.amountFromCents(exchange.currency!, centsToTransfer) + let result = try await model.sendAcceptManualWithdrawalM(exchange.exchangeBaseUrl, + amount: amount, restrictAge: restrictAge) print(result as Any) - let transaction = try await model.getTransactionById(transactionId: result!.transactionId) - withdrawalTransaction = transaction -// acceptManualWithdrawalResult = result + let transaction = try await model.getTransactionByIdT(result!.transactionId) + withdrawalTransaction = transaction +// acceptManualWithdrawalResult = result + } } catch { // TODO: error symLog.log(error.localizedDescription) } @@ -57,7 +59,6 @@ struct ManualWithdrawDone_Container : View { @State private var centsToTransfer: UInt64 = 510 var body: some View { - let model = WithdrawModel.model(baseURL: DEMOEXCHANGE) let exchange = Exchange(exchangeBaseUrl: DEMOEXCHANGE, currency: LONGCURRENCY, paytoUris: [], @@ -66,7 +67,7 @@ struct ManualWithdrawDone_Container : View { ageRestrictionOptions: [], permanent: false) ManualWithdrawDone(exchange: exchange, - model: model, + model: nil, centsToTransfer: centsToTransfer) } } diff --git a/TalerWallet1/Views/HelperViews/QRCodeDetailView.swift b/TalerWallet1/Views/HelperViews/QRCodeDetailView.swift @@ -0,0 +1,59 @@ +/* + * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. + * See LICENSE.md + */ +import SwiftUI +import taler_swift +import AVFoundation + + +struct QRCodeDetailView: View { + var talerURI: String + + var body: some View { + if talerURI.count > 10 { + VStack { + Text("Let the payee scan this QR code to receive:") + .fixedSize(horizontal: false, vertical: true) + .padding(.top, 30) + .font(.title3) + + QRGeneratorView(text: talerURI) +// Text(talerURI) + + Text("Alternatively, copy and send this URI:") + .fixedSize(horizontal: false, vertical: true) + .font(.title3) + .padding(.vertical) + + Text(talerURI) + .padding(.bottom) + + CopyShare(textToCopy: talerURI, dismissFirst: true) + .disabled(false) + } + } + } +} + + +#if DEBUG +fileprivate struct ContentView: View { + @State var isOn = false + + var body: some View { + VStack { + + } + } +} +struct QRCodeDetailView_Previews: PreviewProvider { + + static var previews: some View { +// ContentView() + List { + QRCodeDetailView(talerURI: "taler://pay-push/exchange.demo.taler.net/95ZG4D1AGFGZQ7CNQ1V49D3FT18HXKA6HQT4X3XME9YSJQVFQ520") + } + } +} +#endif diff --git a/TalerWallet1/Views/Main/WalletEmptyView.swift b/TalerWallet1/Views/Main/WalletEmptyView.swift @@ -10,7 +10,7 @@ import SymLog struct WalletEmptyView: View { private let symLog = SymLogV() - @AppStorage("listStyle") var myListStyle = MyListStyle.automatic + @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic var body: some View { List { @@ -27,10 +27,10 @@ struct WalletEmptyView: View { Text("Just register a test account, then withdraw some coins.") } } - .padding(.vertical) +// .padding(.vertical) .font(.title2) - .listStyle(myListStyle.style) - .anyView + .listStyle(myListStyle.style).anyView + .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all)) .onAppear() { DebugViewC.shared.setViewID(VIEW_EMPTY) // 10 } diff --git a/TalerWallet1/Views/Payment/PaymentAcceptView.swift b/TalerWallet1/Views/Payment/PaymentAcceptView.swift @@ -9,53 +9,24 @@ import SymLog struct PaymentAcceptView: View { private let symLog = SymLogV() - @ObservedObject var model: PaymentURIModel - let detailsForAmount: PaymentDetailsForUri + let detailsForUri: PaymentDetailsForUri + let acceptAction: () -> Void let navTitle = String(localized: "Accept Payment") - @State private var confirmPayResult: ConfirmPayResult? - - func playSound(success: Bool) { -// let url = URL(fileURLWithPath: "/System/Library/Audio/UISounds/PaymentReceived.caf") - let url = URL(fileURLWithPath: "/System/Library/Audio/UISounds/payment_" + (success ? "success.caf" - : "failure.caf")) - var soundID: SystemSoundID = 0 - AudioServicesCreateSystemSoundID(url as CFURL, &soundID) - print(soundID) - AudioServicesPlaySystemSound(soundID); - } - func acceptAction() { - Task { - do { - confirmPayResult = try await model.confirmPayM(detailsForAmount.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) - } - } catch { // TODO: error - symLog.log(error.localizedDescription) - } - dismissTop() - } - } @State private var disabled = false var body: some View { Group { - let raw = detailsForAmount.amountRaw - let effective = detailsForAmount.amountEffective + let raw = detailsForUri.amountRaw + let effective = detailsForUri.amountEffective let fee = try! Amount.diff(raw, effective) // TODO: different currencies ThreeAmountsView(topTitle: "Amount to pay:", topAmount: raw, fee: fee, bottomTitle: "Coins to be spent:", bottomAmount: effective, large: true, pending: false, incoming: false, - baseURL: detailsForAmount.contractTerms.exchanges.first?.url) + baseURL: detailsForUri.contractTerms.exchanges.first?.url) // TODO: payment: popup with all possible exchanges, check fees .safeAreaInset(edge: .bottom) { Button(String(localized: "Accept"), action: acceptAction) @@ -66,10 +37,42 @@ struct PaymentAcceptView: View { .navigationTitle(navTitle) } } - -//struct PaymentAccept_Previews: PreviewProvider { -// static var previews: some View { -// let model: PaymentURIModel = -// PaymentAcceptView(model: <#PaymentURIModel#>, detailsForAmount: <#PaymentDetailsForUri#>) -// } -//} +// MARK: - +struct PaymentAccept_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" + ) + PaymentAcceptView(detailsForUri: details, acceptAction: {}) + } +} diff --git a/TalerWallet1/Views/Payment/PaymentURIView.swift b/TalerWallet1/Views/Payment/PaymentURIView.swift @@ -3,41 +3,65 @@ * See LICENSE.md */ import SwiftUI +import AVFoundation import SymLog struct PaymentURIView: View { private let symLog = SymLogV() var url: URL - @ObservedObject var model: PaymentURIModel - @State var detailsForUri: PaymentDetailsForUri? + var model: PaymentURIModel? - let navTitle = String(localized: "Payment") + @State var detailsForUri: PaymentDetailsForUri? = nil + + func playSound(success: Bool) { +// let url = URL(fileURLWithPath: "/System/Library/Audio/UISounds/PaymentReceived.caf") + let url = URL(fileURLWithPath: "/System/Library/Audio/UISounds/payment_" + (success ? "success.caf" + : "failure.caf")) + var soundID: SystemSoundID = 0 + AudioServicesCreateSystemSoundID(url as CFURL, &soundID) + print(soundID) + AudioServicesPlaySystemSound(soundID); + } + + func acceptAction() { + Task { + do { + if let detailsForUri { + if let model { + 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) + } + } + } + } catch { // TODO: error + symLog.log(error.localizedDescription) + } + dismissTop() + } + } var body: some View { let badURL = "Error in URL: \(url)" VStack { - if model.paymentState == nil { - LoadingView(backButtonHidden: false) - } else { switch model.paymentState { - case .waitingForUriDetails: - let _ = symLog.vlog("waitingForUriDetails") - WithdrawProgressView(message: url.host ?? badURL) - .navigationTitle("Contacting Exchange") - case .receivedUriDetails: - let _ = symLog.vlog("waitingForUser") - PaymentAcceptView(model: model, detailsForAmount: detailsForUri!) - default: - Text("Payment") - .navigationTitle(navTitle) - } } + if let detailsForUri { + PaymentAcceptView(detailsForUri: detailsForUri, acceptAction: acceptAction) + .navigationTitle("Payment") + } else { + WithdrawProgressView(message: url.host ?? badURL) + .navigationTitle("Contacting Exchange") + } }.task { - do { // TODO: cancelled + do { symLog.log(".task") - detailsForUri = try await model.preparePayForUriM(url.absoluteString) -// print(detailsForUri?.status) -// print(detailsForUri?.amountRaw.description) -// print(detailsForUri?.amountEffective.description) -// print(detailsForUri?.proposalId) + if let model { + detailsForUri = try await model.preparePayForUriM(url.absoluteString) + } } catch { // TODO: error symLog.log(error.localizedDescription) } diff --git a/TalerWallet1/Views/Peer2peer/ReceivePurpose.swift b/TalerWallet1/Views/Peer2peer/ReceivePurpose.swift @@ -9,7 +9,7 @@ import SymLog struct ReceivePurpose: View { private let symLog = SymLogV() @FocusState private var isFocused: Bool - @StateObject var model = Peer2peerModel.model() + let model = Peer2peerModel.model() @State var peerPullCheck: CheckPeerPullCreditResponse? var scopeInfo: ScopeInfo @@ -78,7 +78,6 @@ struct ReceivePurpose: View { NavigationLink(destination: LazyView { SendNow(amountToSend: amount, purpose: purpose, expireDays: expireDays, model: model) - .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all)) }) { Text(buttonTitle) .font(buttonFont) @@ -92,6 +91,7 @@ struct ReceivePurpose: View { .padding(.horizontal) } .navigationTitle("Invoice another Wallet") + .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all)) .onAppear { DebugViewC.shared.setSheetID(VIEW_INVOICE_PURPOSE) print("❗️ ReceivePurpose onAppear") diff --git a/TalerWallet1/Views/Peer2peer/RequestPayment.swift b/TalerWallet1/Views/Peer2peer/RequestPayment.swift @@ -10,7 +10,6 @@ struct RequestPayment: View { private let symLog = SymLogV() let navTitle = String(localized: "Request Payment") - @ObservedObject var model: Peer2peerModel var scopeInfo: ScopeInfo @Binding var centsToTransfer: UInt64 @@ -43,7 +42,6 @@ struct RequestPayment: View { ) { deactivateAction() } - .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all)) }) { Text("title2") } @@ -56,6 +54,7 @@ struct RequestPayment: View { .frame(maxWidth: .infinity, alignment: .leading) .navigationTitle(navTitle) .padding(.horizontal) + .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all)) .onAppear { // make CurrencyField show the keyboard DebugViewC.shared.setViewID(VIEW_INVOICE_P2P) print("❗️Yikes \(navTitle) onAppear") diff --git a/TalerWallet1/Views/Peer2peer/SendAmount.swift b/TalerWallet1/Views/Peer2peer/SendAmount.swift @@ -11,48 +11,61 @@ struct SendAmount: View { let navTitle = String(localized: "Send Coins") // @ObservedObject private var keyboardResponder = KeyboardResponder() // @FocusState private var isFocused: Bool + let model = Peer2peerModel.model() + @State var peerPushCheck: CheckPeerPushDebitResponse? let amountAvailable: Amount let buttonFont: Font = .title2 - @State private var centsToTransfer: UInt64 = 0 // TODO: maybe Decimal? + @State private var centsToTransfer: UInt64 = 0 @State private var purpose: String = "" @State private var expireDays: UInt = 0 - var body: some View { - let currencyField = CurrencyField(value: $centsToTransfer, currency: amountAvailable.currencyStr) + private func fee(ppCheck: CheckPeerPushDebitResponse?) -> String { + do { + if let p2pcheck = ppCheck { + let fee = try p2pcheck.amountEffective - p2pcheck.amountRaw + return fee.readableDescription + } + } catch {} + return "" + } - VStack(alignment: .leading, spacing: 6) { - CurrencyInputView(currencyField: currencyField, title: "Amount to send:") + var body: some View { +#if DEBUG + let _ = Self._printChanges() + let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear +#endif + let currency = amountAvailable.currencyStr + let currencyField = CurrencyField(value: $centsToTransfer, currency: currency) + + let fee = fee(ppCheck: peerPushCheck) + ScrollView { let available = amountAvailable.readableDescription Text("Available: \(available)") + .font(.title3) + CurrencyInputView(currencyField: currencyField, title: "Amount to send:") + Text("+ \(fee) payment fee") + .foregroundColor(.red) Text("Choose where to send to:") .padding(.top) .font(.title3) HStack { let kbdShown: Bool = false // keyboardResponder.keyboardHeight > 0 - let title1 = kbdShown ? "To bank" : "To a bank\naccount" let title2 = kbdShown ? "To wallet" : "To another\nwallet" let disabled = centsToTransfer == 0 // TODO: check amountAvailable - // Left button: To bank - NavigationLink(destination: LazyView { - Text("Bank pressed") - }) { - Text(title1) - } - .buttonStyle(TalerButtonStyle(type: .bordered)) - .disabled(disabled) NavigationLink(destination: LazyView { - SendPurpose(amountAvailable: amountAvailable, - centsToSend: centsToTransfer, - purpose: $purpose, - expireDays: $expireDays, - deactivateAction: {}) - .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all)) + SendPurpose(model: model, + amountAvailable: amountAvailable, + centsToSend: centsToTransfer, + fee: fee, + purpose: $purpose, + expireDays: $expireDays, + deactivateAction: {}) }) { Text(title2) } @@ -63,15 +76,28 @@ struct SendAmount: View { Spacer() } .frame(maxWidth: .infinity, alignment: .leading) - .navigationTitle(navTitle) .padding(.horizontal) + .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all)) + .navigationTitle(navTitle) .onAppear { // make CurrencyField show the keyboard - DebugViewC.shared.setSheetID(SHEET_PAY_P2P) + symLog.log("onAppear") + DebugViewC.shared.setViewID(VIEW_SEND_P2P) print("❗️Yikes SendAmount onAppear") } .onDisappear { print("❗️Yikes SendAmount onDisappear") } + .task(id: centsToTransfer) { + let amount = Amount.amountFromCents(currency, centsToTransfer) + do { + peerPushCheck = try await model.checkPeerPushDebitM(amount) + // TODO: set from exchange +// agePicker.setAges(ages: peerPushCheck?.ageRestrictionOptions) + } catch { // TODO: error + symLog.log(error.localizedDescription) + peerPushCheck = nil + } + } } } // MARK: - diff --git a/TalerWallet1/Views/Peer2peer/SendNow.swift b/TalerWallet1/Views/Peer2peer/SendNow.swift @@ -14,7 +14,7 @@ struct SendNow: View { var purpose: String var expireDays: UInt - @ObservedObject var model: Peer2peerModel + var model: Peer2peerModel? @State var peerPushResponse: PeerPushResponse? @State var talerURI: String? = nil @@ -25,23 +25,7 @@ struct SendNow: View { LoadingView(backButtonHidden: true) } else { VStack() { - Text("Let the payee scan this QR code to receive:") - .fixedSize(horizontal: false, vertical: true) - .padding(.top, 30) - .font(.title3) - - QRGeneratorView(text: talerURI!) - - Text("Alternatively, copy and send this URI:") - .fixedSize(horizontal: false, vertical: true) - .font(.title3) - .padding(.vertical) - - Text(talerURI!) - .padding(.bottom) - - CopyShare(textToCopy: talerURI!, dismissFirst: true) - .disabled(false) + QRCodeDetailView(talerURI: talerURI!) Text("The QR code can also be copied and shared from Transactions later") .fixedSize(horizontal: false, vertical: true) @@ -59,17 +43,20 @@ struct SendNow: View { .navigationBarHidden(true) // no back button, no title .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) .padding(.horizontal) + .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all)) } } .task { symLog.log(".task") do { // generate talerURI - let timestamp = Timestamp.inSomeDays(expireDays) - let terms = PeerContractTerms(amount: amountToSend, summary: purpose, purse_expiration: timestamp) - let baseURL = DEMOEXCHANGE // TODO: use correct baseURL - peerPushResponse = try await model.initiatePeerPushDebitM(baseURL, terms: terms) - talerURI = peerPushResponse?.talerUri + if let model { + let timestamp = Timestamp.inSomeDays(expireDays) + let terms = PeerContractTerms(amount: amountToSend, summary: purpose, purse_expiration: timestamp) + let baseURL = DEMOEXCHANGE // TODO: use correct baseURL + peerPushResponse = try await model.initiatePeerPushDebitM(baseURL, terms: terms) + talerURI = peerPushResponse?.talerUri + } } catch { // TODO: error symLog.log(error.localizedDescription) } @@ -80,8 +67,12 @@ struct SendNow: View { //struct SendNow_Previews: PreviewProvider { // static var previews: some View { // Group { -// SendNow() -// SendNow(talerURI: "taler://pay-push/exchange.demo.taler.net") +// SendNow(amountToSend: <#T##Amount#>, +// purpose: <#T##String#>, +// expireDays: <#T##UInt#>, +// model: <#T##Peer2peerModel#>, +// peerPushResponse: <#T##PeerPushResponse?#>, +// talerURI: "taler://pay-push/exchange.demo.taler.net/95ZG4D1AGFGZQ7CNQ1V49D3FT18HXKA6HQT4X3XME9YSJQVFQ520") // } // } //} diff --git a/TalerWallet1/Views/Peer2peer/SendPurpose.swift b/TalerWallet1/Views/Peer2peer/SendPurpose.swift @@ -9,11 +9,11 @@ import SymLog struct SendPurpose: View { private let symLog = SymLogV() @FocusState private var isFocused: Bool - @StateObject var model = Peer2peerModel.model() - @State var peerPushCheck: CheckPeerPushDebitResponse? + var model: Peer2peerModel? var amountAvailable: Amount var centsToSend: UInt64 + var fee: String @Binding var purpose: String @Binding var expireDays: UInt var deactivateAction: () -> Void @@ -26,20 +26,9 @@ struct SendPurpose: View { return formatter.string(for: Decimal(centsToSend) / mag) ?? "" } - private func fee(ppCheck: CheckPeerPushDebitResponse?) -> String { - do { - if let p2pcheck = ppCheck { - let fee = try p2pcheck.amountEffective - p2pcheck.amountRaw - return fee.readableDescription - } - } catch {} - return "" - } - var body: some View { let amount = Amount.amountFromCents(amountAvailable.currencyStr, centsToSend) - let fee = fee(ppCheck: peerPushCheck) VStack (spacing: 6) { Text(amount.readableDescription) Text("+ \(fee) payment fee") @@ -79,7 +68,6 @@ struct SendPurpose: View { NavigationLink(destination: LazyView { SendNow(amountToSend: amount, purpose: purpose, expireDays: expireDays, model: model) - .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all)) }) { Text(buttonTitle) .font(buttonFont) @@ -93,8 +81,9 @@ struct SendPurpose: View { .padding(.horizontal) } .navigationTitle("To another Wallet") + .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all)) .onAppear { - DebugViewC.shared.setSheetID(VIEW_SEND_PURPOSE) + DebugViewC.shared.setViewID(VIEW_SEND_PURPOSE) print("❗️ SendPurpose onAppear") } .onDisappear { @@ -104,7 +93,7 @@ struct SendPurpose: View { .task { symLog.log(".task") do { - peerPushCheck = try await model.checkPeerPushDebitM(amount) +// peerPushCheck = try await model.checkPeerPushDebitM(amount) } catch { // TODO: error symLog.log(error.localizedDescription) } @@ -113,14 +102,21 @@ struct SendPurpose: View { } // MARK: - +#if DEBUG struct SendPurpose_Previews: PreviewProvider { static var previews: some View { @State var purpose: String = "" @State var expireDays: UInt = 0 - let amount = Amount(currency: "TaLeR", integer: 10, fraction: 0) - SendPurpose(amountAvailable: amount, centsToSend: 5, - purpose: $purpose, expireDays: $expireDays) { + let amount = Amount(currency: LONGCURRENCY, integer: 10, fraction: 0) + SendPurpose(model: nil, + amountAvailable: amount, + centsToSend: 543, + fee: "0,43", + purpose: $purpose, + expireDays: $expireDays + ) { print("deactivateAction") } } } +#endif diff --git a/TalerWallet1/Views/Settings/Pending/PendingOpsListView.swift b/TalerWallet1/Views/Settings/Pending/PendingOpsListView.swift @@ -9,23 +9,20 @@ struct PendingOpsListView: View { private let symLog = SymLogV(0) let navTitle = String(localized: "Pending") - @ObservedObject var model: PendingModel + @State var pendingOperations: [PendingOperation] = [] + var model: PendingModel var body: some View { #if DEBUG let _ = Self._printChanges() let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear #endif - let reloadAction = model.updateM - Content(symLog: symLog, model: model, reloadAction: reloadAction) + let reloadAction = model.getPendingOperationsM + Content(symLog: symLog, pendingOperations: $pendingOperations, reloadAction: reloadAction) .navigationTitle(navTitle) .task { symLog.log(".task") - do { - try await reloadAction() - } catch { // TODO: show error - symLog.log(error.localizedDescription) - } + pendingOperations = await reloadAction() } } } @@ -33,16 +30,15 @@ struct PendingOpsListView: View { extension PendingOpsListView { struct Content: View { let symLog: SymLogV? - @ObservedObject var model: PendingModel - var reloadAction: () async throws -> () - + @Binding var pendingOperations: [PendingOperation] + var reloadAction: () async -> [PendingOperation] var body: some View { #if DEBUG let _ = Self._printChanges() let _ = symLog?.vlog() // just to get the # to compare it with .onAppear & onDisappear #endif ScrollViewReader { scrollView in - List(model.pendingOperations, id: \.self) { pendingOp in + List(pendingOperations, id: \.self) { pendingOp in PendingOpView(pendingOp: pendingOp) } .listStyle(SidebarListStyle()) @@ -51,12 +47,8 @@ extension PendingOpsListView { DebugViewC.shared.setViewID(VIEW_PENDING) } .refreshable { - do { - symLog?.log("refreshing") - try await reloadAction() - } catch { // TODO: error - symLog?.log(error.localizedDescription) - } + symLog?.log("refreshing") + pendingOperations = await reloadAction() } } } diff --git a/TalerWallet1/Views/Settings/SettingsView.swift b/TalerWallet1/Views/Settings/SettingsView.swift @@ -19,11 +19,11 @@ import SymLog struct SettingsView: View { private let symLog = SymLogV() - let navTitle = String(localized: "Settings") + let navTitle: String @AppStorage("developerMode") var developerMode: Bool = false @AppStorage("developDelay") var developDelay: Bool = false - @AppStorage("listStyle") var myListStyle = MyListStyle.automatic + @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic var hamburgerAction: () -> Void @@ -50,7 +50,7 @@ struct SettingsView: View { Spacer() Picker(selection: $myListStyle) { ForEach(MyListStyle.allCases, id: \.self) { - Text($0.displayName).tag($0) + Text($0.displayName.capitalized).tag($0) .font(.title2) } } label: {} @@ -159,8 +159,7 @@ struct SettingsView: View { } } } - .listStyle(myListStyle.style) - .anyView + .listStyle(myListStyle.style).anyView } .navigationTitle(navTitle) .navigationBarItems(leading: HamburgerButton(action: hamburgerAction)) @@ -190,11 +189,11 @@ extension Bundle { return "v\(releaseVersionNumber ?? "1.0.0")" } } - -//struct SettingsView_Previews: PreviewProvider { -// static var previews: some View { -// SettingsView { -// -// } -// } -//} +// MARK: - +#if DEBUG +struct SettingsView_Previews: PreviewProvider { + static var previews: some View { + SettingsView(navTitle: "Settings") { } + } +} +#endif diff --git a/TalerWallet1/Views/Sheets/URLSheet.swift b/TalerWallet1/Views/Sheets/URLSheet.swift @@ -15,9 +15,8 @@ struct URLSheet: View { var body: some View { Group { - if urlCommand == UrlCommand.withdraw { - let model = WithdrawModel.model(baseURL: "global") // TODO: get baseURL from command - WithdrawURIView(url: urlToOpen, model: model) + if urlCommand == .withdraw { + WithdrawURIView(url: urlToOpen) } else if urlCommand == UrlCommand.pay { let model = PaymentURIModel.model() PaymentURIView(url: urlToOpen, model: model) diff --git a/TalerWallet1/Views/Transactions/TransactionDetailView.swift b/TalerWallet1/Views/Transactions/TransactionDetailView.swift @@ -8,7 +8,7 @@ import SymLog struct TransactionDetailView: View { private let symLog = SymLogV() - @AppStorage("listStyle") var myListStyle = MyListStyle.automatic + @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic var transaction: Transaction var deleteAction: ((_ transactionId: String) async throws -> Void)? @@ -58,8 +58,7 @@ print(transition.newTxState.major) // ResumeButton(common: common, resumeAction: resumeAction) // } } } - .listStyle(myListStyle.style) - .anyView + .listStyle(myListStyle.style).anyView } .navigationTitle(navTitle) .onAppear { @@ -125,6 +124,7 @@ print(transition.newTxState.major) } } } + struct QRCodeDetails: View { var transaction : Transaction var body: some View { diff --git a/TalerWallet1/Views/Transactions/TransactionsEmptyView.swift b/TalerWallet1/Views/Transactions/TransactionsEmptyView.swift @@ -10,7 +10,7 @@ import SymLog struct TransactionsEmptyView: View { private let symLog = SymLogV() - @AppStorage("listStyle") var myListStyle = MyListStyle.automatic + @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic let currency: String @@ -22,8 +22,8 @@ struct TransactionsEmptyView: View { } .padding(.vertical) .font(.title2) - .listStyle(myListStyle.style) - .anyView + .listStyle(myListStyle.style).anyView + .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all)) .onAppear() { DebugViewC.shared.setViewID(VIEW_EMPTY) // 10 } diff --git a/TalerWallet1/Views/Transactions/TransactionsListView.swift b/TalerWallet1/Views/Transactions/TransactionsListView.swift @@ -7,7 +7,7 @@ import SymLog struct TransactionsListView: View { private let symLog = SymLogV() - @AppStorage("listStyle") var myListStyle = MyListStyle.automatic + @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic let navTitle: String let currency: String @@ -22,11 +22,13 @@ struct TransactionsListView: View { let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear #endif let count = transactions.count + // TODO: Unlock the power of grammatical agreement // let title = AttributedString(localized: "^[\(count) Ticket](inflect: true)") let title: String = "\(count) \(navTitle)" Content(symLog: symLog, currency: currency, transactions: transactions, myListStyle: $myListStyle, reloadAction: reloadAction, deleteAction: deleteAction, abortAction: abortAction) .navigationTitle(title) + .navigationBarTitleDisplayMode(.large) // .inline .onAppear { DebugViewC.shared.setViewID(VIEW_TRANSACTIONLIST) } @@ -94,10 +96,10 @@ extension TransactionsListView { // .onDelete(perform: removeItems) // delete this row from the list } .refreshable { + symLog?.log("refreshing") await reloadAction() } - .listStyle(myListStyle.style) - .anyView + .listStyle(myListStyle.style).anyView .onAppear { upAction = { withAnimation { scrollView.scrollTo(0) }} downAction = { withAnimation { scrollView.scrollTo(transactions.count - 1) }} @@ -106,7 +108,6 @@ extension TransactionsListView { .overlay { if transactions.isEmpty { TransactionsEmptyView(currency: currency) - .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all)) } } } @@ -114,7 +115,6 @@ extension TransactionsListView { ArrowUpButton(action: upAction) ArrowDownButton(action: downAction) }) - .navigationBarTitleDisplayMode(.large) // .inline } } } diff --git a/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawAcceptDone.swift b/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawAcceptDone.swift @@ -0,0 +1,61 @@ +/* + * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. + * See LICENSE.md + */ +import SwiftUI +import taler_swift +import SymLog + +struct WithdrawAcceptDone: View { + private let symLog = SymLogV() + @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic + + let confirmTransferUrl: String? + + let navTitle = String(localized: "Confirm with Bank") + + var body: some View { + VStack { + Text(navTitle) + .font(.title) + .padding() + List { + if let confirmTransferUrl { + let destination = URL(string: confirmTransferUrl)! + // Show Hint that User should Confirm on bank website + Text("Waiting for bank confirmation") + .listRowSeparator(.hidden) + + + Link("Confirm with bank", destination: destination) + .buttonStyle(TalerButtonStyle(type: .prominent, narrow: false, aligned: .center)) + // balances will be updated by TransactionStateTransition + } + } + .listStyle(myListStyle.style).anyView + + } + .interactiveDismissDisabled() // can only use "Done" button to dismiss + .navigationBarHidden(true) // no back button, no title + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) + .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all)) + + .safeAreaInset(edge: .bottom) { + Button("Done", action: { dismissTop() }) + .lineLimit(2) + .disabled(false) + .buttonStyle(TalerButtonStyle(type: .bordered, narrow: false, aligned: .center)) + .padding() + } + .navigationTitle(navTitle) + .onAppear() { + DebugViewC.shared.setSheetID(SHEET_WITHDRAW_CONFIRM) + } + } +} +// MARK: - +struct WithdrawAcceptDone_Previews: PreviewProvider { + static var previews: some View { + WithdrawAcceptDone(confirmTransferUrl: DEMOBANK) + } +} diff --git a/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawAcceptView.swift b/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawAcceptView.swift @@ -9,20 +9,27 @@ import SymLog struct WithdrawAcceptView: View { private let symLog = SymLogV() let url: URL - @ObservedObject var model: WithdrawModel + var model: WithdrawModel? let navTitle = String(localized: "Accept Withdrawal") let detailsForAmount: ManualWithdrawalDetails let baseURL: String + @State private var buttonSelected: Int? = nil + @State private var confirmTransferUrl: String? = nil + func acceptAction() -> () { Task { do { - let confirmTransferUrl = try await model.sendAcceptIntWithdrawalM(baseURL, withdrawURL: url.absoluteString) - symLog.log(confirmTransferUrl as Any) - // TODO: Show Hints that User should Confirm on bank website - // update balances to show pending withdrawal - await BalancesModel.model(currency: "*").fetchBalancesM() + if let model { + if let transferUrl = try await model.sendAcceptIntWithdrawalM(baseURL, withdrawURL: url.absoluteString) { + symLog.log(transferUrl) + confirmTransferUrl = transferUrl + buttonSelected = 1 // trigger NavigationLink + } else { + // TODO: error sendAcceptIntWithdrawal failed + } + } } catch { // TODO: error symLog.log(error.localizedDescription) } @@ -30,43 +37,56 @@ struct WithdrawAcceptView: View { } var body: some View { - Group { - let currState = model.withdrawState - switch currState { - case .receivedAmountDetails, .receivedTOS, .receivedTOSAck: - let raw = detailsForAmount.amountRaw - let effective = detailsForAmount.amountEffective - let fee = try! Amount.diff(raw, effective) // TODO: different currencies - let outColor = WalletColors().transactionColor(false) - let inColor = WalletColors().transactionColor(true) - - ThreeAmountsView(topTitle: String(localized: "Chosen amount to withdraw:"), - topAmount: raw, fee: fee, - bottomTitle: String(localized: "Coins to be withdrawn:"), - bottomAmount: effective, - large: false, pending: false, incoming: true, - baseURL: baseURL) - .safeAreaInset(edge: .bottom) { - Button(String(localized: "Accept"), action: acceptAction) - .buttonStyle(TalerButtonStyle(type: .prominent)) - .padding(.horizontal) - } - case .waitingForWithdrAck, .receivedWithdrAck: - // TODO: SHEET_WITHDRAW_CONFIRM - Text("waiting for bank confirmation") - .navigationTitle("Confirm with Bank") - .onAppear() { - DebugViewC.shared.setSheetID(SHEET_WITHDRAW_CONFIRM) - } + List { + let raw = detailsForAmount.amountRaw + let effective = detailsForAmount.amountEffective + let fee = try! Amount.diff(raw, effective) // TODO: different currencies + let outColor = WalletColors().transactionColor(false) + let inColor = WalletColors().transactionColor(true) - default: - let _ = symLog.vlog(currState as Any) - ErrorView(errortext: "unknown state") // TODO: Error + HStack(spacing: 0) { + NavigationLink(destination: LazyView { + WithdrawAcceptDone(confirmTransferUrl: confirmTransferUrl) + }, tag: 1, selection: $buttonSelected + ) { EmptyView() }.frame(width: 0).opacity(0).hidden() + + ThreeAmountsView(topTitle: String(localized: "Chosen amount to withdraw:"), + topAmount: raw, fee: fee, + bottomTitle: String(localized: "Coins to be withdrawn:"), + bottomAmount: effective, + large: false, pending: false, incoming: true, + baseURL: baseURL) } } + .safeAreaInset(edge: .bottom) { + Button("Confirm Withdrawal", action: acceptAction) + .lineLimit(2) + .disabled(false) + .buttonStyle(TalerButtonStyle(type: .prominent, narrow: false, aligned: .center)) + .padding() + } .navigationTitle(navTitle) +// .overlay { +// VStack { +// ErrorView(errortext: "unknown state") // TODO: Error +// }.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) +// .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all)) +// } .onAppear() { DebugViewC.shared.setSheetID(SHEET_WITHDRAW_ACCEPT) } } } +// MARK: - +struct WithdrawAcceptView_Previews: PreviewProvider { + static var previews: some View { + let details = ManualWithdrawalDetails(amountRaw: try! Amount(fromString: LONGCURRENCY + ":2.4"), + amountEffective: try! Amount(fromString: LONGCURRENCY + ":2.2"), + paytoUris: [], + tosAccepted: true) + WithdrawAcceptView(url: URL(string: DEMOSHOP)!, + model: nil, + detailsForAmount: details, + baseURL: DEMOEXCHANGE) + } +} diff --git a/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawProgressView.swift b/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawProgressView.swift @@ -8,7 +8,7 @@ struct WithdrawProgressView: View { let message: String var body: some View { - VStack { + Form { Spacer() ProgressView() Spacer() @@ -17,6 +17,7 @@ struct WithdrawProgressView: View { Spacer() Spacer() } + .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all)) } } diff --git a/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawTOSView.swift b/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawTOSView.swift @@ -7,12 +7,12 @@ import SymLog struct WithdrawTOSView: View { private let symLog = SymLogV() - @AppStorage("listStyle") var myListStyle = MyListStyle.automatic + @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic let navTitle = String(localized: "Terms of Service") var exchangeBaseUrl: String - @ObservedObject var model: WithdrawModel + var model: WithdrawModel? @State var exchangeTOS: ExchangeTermsOfService? let viewID: Int // either VIEW_WITHDRAW_TOS or SHEET_WITHDRAW_TOS @@ -22,29 +22,29 @@ struct WithdrawTOSView: View { var body: some View { VStack { - switch model.withdrawState { - case .receivedAmountDetails, .waitingForTOS: - WithdrawProgressView(message: exchangeBaseUrl.trimURL()) - .navigationTitle("Loading " + navTitle) - case .receivedTOS, .waitingForTOSAck, .receivedTOSAck: - Content(symLog: symLog, exchangeTOS: exchangeTOS, myListStyle: $myListStyle) { - Task { - do { - _ = try await model.setExchangeTOSAcceptedM(exchangeBaseUrl, etag: exchangeTOS!.currentEtag) - if acceptAction != nil { - acceptAction!() - } else { - self.presentationMode.wrappedValue.dismiss() - } - } catch { // TODO: Show Error - symLog.log(error.localizedDescription) + Content(symLog: symLog, exchangeTOS: exchangeTOS, myListStyle: $myListStyle) { + Task { + do { + if let model { + _ = try await model.setExchangeTOSAcceptedM(exchangeBaseUrl, etag: exchangeTOS!.currentEtag) + if acceptAction != nil { + acceptAction!() + } else { + self.presentationMode.wrappedValue.dismiss() } } + } catch { // TODO: Show Error + symLog.log(error.localizedDescription) } - .navigationBarTitleDisplayMode(.large) // .inline - .navigationTitle(navTitle) - default: - ErrorView(errortext: "unknown state") // TODO: ??? + } + } + .navigationBarTitleDisplayMode(.large) // .inline + .navigationTitle(navTitle) + .overlay { + if exchangeTOS == nil { + WithdrawProgressView(message: exchangeBaseUrl.trimURL()) + .navigationTitle("Loading " + navTitle) + } } }.onAppear() { if viewID > SHEET_WITHDRAWAL { @@ -54,8 +54,10 @@ struct WithdrawTOSView: View { } }.task { do { - let someTOS = try await model.loadExchangeTermsOfServiceM(exchangeBaseUrl) - exchangeTOS = someTOS + if let model { + let someTOS = try await model.loadExchangeTermsOfServiceM(exchangeBaseUrl) + exchangeTOS = someTOS + } } catch { // TODO: error symLog.log(error.localizedDescription) } @@ -77,12 +79,11 @@ extension WithdrawTOSView { List (components, id: \.self) { term in Text(term) }.safeAreaInset(edge: .bottom) { - Button(String(localized: "Accept"), action: acceptAction) + Button(String(localized: "Accept ToS"), action: acceptAction) .buttonStyle(TalerButtonStyle(type: .prominent)) .padding(.horizontal) } - .listStyle(myListStyle.style) - .anyView + .listStyle(myListStyle.style).anyView } else { ErrorView(errortext: String(localized: "unknown ToS")) // TODO: ??? } diff --git a/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawURIView.swift b/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawURIView.swift @@ -5,68 +5,68 @@ import SwiftUI import SymLog +// Will be called either by the user scanning a QR code or tapping the provided link, both from the bank's website +// we show the user the withdrawal details - but first the ToS must be accepted +// after the user confirmed the withdrawal, we remind them to return to the bank website to confirm there, too struct WithdrawURIView: View { private let symLog = SymLogV() + // the URL from the bank website var url: URL - @ObservedObject var model: WithdrawModel -// @State var withdrawUriInfo: WithdrawUriInfoResponse? - @State var exchangeBaseUrl: String? + let model = WithdrawModel.model(baseURL: "global") // TODO: get baseURL from URL + + // the exchange used for this withdrawal. + @State var exchangeBaseUrl: String = "" @State var manualWithdrawalDetails: ManualWithdrawalDetails? @State var didAcceptTOS: Bool = false var body: some View { let badURL = "Error in URL: \(url)" VStack { - let currState = model.withdrawState - if currState == nil { - LoadingView(backButtonHidden: false) - } else { - let _ = symLog.vlog(currState as Any) - switch currState { - case .waitingForUriDetails, .receivedUriDetails: - WithdrawProgressView(message: url.host ?? badURL) - .navigationTitle("Contacting Exchange") - case .waitingForAmountDetails: - WithdrawProgressView(message: exchangeBaseUrl?.trimURL() ?? badURL) - .navigationTitle("Found Exchange") - default: - // .receivedAmountDetails, .waitingForTOS, .receivedTOS, .waitingForTOSAck, .receivedTOSAck - // waitingForWithdrAck, receivedWithdrAck - if !didAcceptTOS { - // user must accept ToS first - WithdrawTOSView(exchangeBaseUrl: exchangeBaseUrl!, - model: model, - viewID: SHEET_WITHDRAW_TOS) { - didAcceptTOS = true - } - } else { - // show Amount details and let user accept - WithdrawAcceptView(url: url, model: model, - detailsForAmount: manualWithdrawalDetails!, - baseURL: exchangeBaseUrl!) - } + if !didAcceptTOS { // user must accept ToS first + WithdrawTOSView(exchangeBaseUrl: exchangeBaseUrl, + model: model, + viewID: SHEET_WITHDRAW_TOS) { + didAcceptTOS = true } + } else { // show Amount details and let user accept + WithdrawAcceptView(url: url, model: model, + detailsForAmount: manualWithdrawalDetails!, + baseURL: exchangeBaseUrl) } - }.onAppear() { + } + .overlay { + if !exchangeBaseUrl.hasPrefix(HTTPS) { + WithdrawProgressView(message: url.host ?? badURL) + .navigationTitle("Contacting Exchange") + } else if manualWithdrawalDetails == nil { + WithdrawProgressView(message: exchangeBaseUrl.trimURL()) + .navigationTitle("Found Exchange") + } + } + .onAppear() { DebugViewC.shared.setSheetID(SHEET_WITHDRAWAL) - }.task { + } + .task { do { // TODO: cancelled symLog.log(".task") let withdrawUriInfo = try await model.loadWithdrawalDetailsForUriM(url.absoluteString) let amount = withdrawUriInfo.amount if let baseURL = withdrawUriInfo.defaultExchangeBaseUrl { exchangeBaseUrl = baseURL - } else { - exchangeBaseUrl = withdrawUriInfo.possibleExchanges.first?.exchangeBaseUrl + } else if let first = withdrawUriInfo.possibleExchanges.first { + exchangeBaseUrl = first.exchangeBaseUrl } - symLog.log("amount: \(amount), baseURL: \(String(describing: exchangeBaseUrl))") - // TODO: let user choose exchange from list - manualWithdrawalDetails = try await model.loadWithdrawalDetailsForAmountM(exchangeBaseUrl!, amount: amount) - - symLog.log("raw: \(manualWithdrawalDetails!.amountRaw), effective: \(manualWithdrawalDetails!.amountEffective)") - if manualWithdrawalDetails!.tosAccepted { - didAcceptTOS = true + if exchangeBaseUrl.hasPrefix(HTTPS) { + symLog.log("amount: \(amount), baseURL: \(String(describing: exchangeBaseUrl))") + // TODO: let user choose exchange from list + manualWithdrawalDetails = try await model.loadWithdrawalDetailsForAmountM(exchangeBaseUrl, amount: amount) + symLog.log("raw: \(manualWithdrawalDetails!.amountRaw), effective: \(manualWithdrawalDetails!.amountEffective)") + if manualWithdrawalDetails!.tosAccepted { + didAcceptTOS = true + } + } else { + // TODO: error no exchange! } } catch { // TODO: error symLog.log(error.localizedDescription)