taler-ios

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

commit 6d1028c0b4fceeafd3a890850ed4ebc20dde19cf
parent a41e3ea751bf572995a11e81e5aa7065fae2160b
Author: Marc Stibane <marc@taler.net>
Date:   Wed, 21 Jun 2023 09:29:57 +0200

Made Model a Singleton

Diffstat:
MTalerWallet.xcodeproj/project.pbxproj | 104++++++++++++++++++++++++++++++++++++++++++++++----------------------------------
MTalerWallet1/Backend/Transaction.swift | 21++++++++++++---------
MTalerWallet1/Backend/WalletCore.swift | 17++++++++++++-----
MTalerWallet1/Controllers/Controller.swift | 15++++++++++-----
MTalerWallet1/Controllers/DebugViewC.swift | 2++
MTalerWallet1/Controllers/TalerWallet1App.swift | 4+++-
RTalerWallet1/Helper/URL+iban.swift -> TalerWallet1/Helper/URL+id+iban.swift | 0
ATalerWallet1/Helper/View+Notification.swift | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MTalerWallet1/Helper/View+dismissTop.swift | 57---------------------------------------------------------
DTalerWallet1/Model/BalancesModel.swift | 85-------------------------------------------------------------------------------
DTalerWallet1/Model/ExchangeModel.swift | 121-------------------------------------------------------------------------------
ATalerWallet1/Model/Model+Balances.swift | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ATalerWallet1/Model/Model+Exchange.swift | 104+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ATalerWallet1/Model/Model+P2P.swift | 168+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ATalerWallet1/Model/Model+Payment.swift | 143+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ATalerWallet1/Model/Model+Pending.swift | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
ATalerWallet1/Model/Model+Settings.swift | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ATalerWallet1/Model/Model+Transactions.swift | 120+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ATalerWallet1/Model/Model+Withdraw.swift | 188+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
DTalerWallet1/Model/PaymentURIModel.swift | 169-------------------------------------------------------------------------------
DTalerWallet1/Model/Peer2peerModel.swift | 134-------------------------------------------------------------------------------
DTalerWallet1/Model/PendingModel.swift | 78------------------------------------------------------------------------------
DTalerWallet1/Model/SettingsModel.swift | 108-------------------------------------------------------------------------------
DTalerWallet1/Model/TransactionsModel.swift | 143-------------------------------------------------------------------------------
DTalerWallet1/Model/WalletInitModel.swift | 72------------------------------------------------------------------------
MTalerWallet1/Model/WalletModel.swift | 73++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
DTalerWallet1/Model/WithdrawModel.swift | 213-------------------------------------------------------------------------------
MTalerWallet1/Views/Balances/BalancesListView.swift | 20+++++++-------------
MTalerWallet1/Views/Balances/BalancesSectionView.swift | 119+++++++++++++++++++++++++++++++++++++++++++------------------------------------
MTalerWallet1/Views/Exchange/ExchangeListView.swift | 24++++++++----------------
MTalerWallet1/Views/Exchange/ExchangeSectionView.swift | 2--
MTalerWallet1/Views/Exchange/ManualWithdraw.swift | 13++++---------
MTalerWallet1/Views/Exchange/ManualWithdrawDone.swift | 22++++++++--------------
MTalerWallet1/Views/HelperViews/QRCodeDetailView.swift | 20+++++++++-----------
MTalerWallet1/Views/Main/MainView.swift | 2--
MTalerWallet1/Views/Payment/PaymentURIView.swift | 30+++++++++++++++---------------
ATalerWallet1/Views/Peer2peer/PaymentPurpose.swift | 111+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
DTalerWallet1/Views/Peer2peer/ReceivePurpose.swift | 129-------------------------------------------------------------------------------
MTalerWallet1/Views/Peer2peer/RequestPayment.swift | 33+++++++++++++--------------------
MTalerWallet1/Views/Peer2peer/SendAmount.swift | 41++++++++++++++++++-----------------------
MTalerWallet1/Views/Peer2peer/SendNow.swift | 61+++++++++++++++++++++++++++++++++++--------------------------
MTalerWallet1/Views/Peer2peer/SendPurpose.swift | 47++++++++++++++++++++++-------------------------
MTalerWallet1/Views/Settings/Pending/PendingOpsListView.swift | 6++++--
MTalerWallet1/Views/Settings/SettingsView.swift | 7+++----
ATalerWallet1/Views/Sheets/P2P_Sheets/P2pAcceptDone.swift | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ATalerWallet1/Views/Sheets/P2P_Sheets/P2pReceiveURIView.swift | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MTalerWallet1/Views/Sheets/URLSheet.swift | 18+++++++++++++-----
MTalerWallet1/Views/Transactions/ThreeAmounts.swift | 3++-
MTalerWallet1/Views/Transactions/TransactionDetailView.swift | 5+++--
MTalerWallet1/Views/WithdrawBankIntegrated/WithdrawAcceptDone.swift | 12++++--------
MTalerWallet1/Views/WithdrawBankIntegrated/WithdrawTOSView.swift | 19+++++++++++++------
MTalerWallet1/Views/WithdrawBankIntegrated/WithdrawURIView.swift | 5++---
52 files changed, 1685 insertions(+), 1637 deletions(-)

diff --git a/TalerWallet.xcodeproj/project.pbxproj b/TalerWallet.xcodeproj/project.pbxproj @@ -8,10 +8,15 @@ /* Begin PBXBuildFile section */ 4E16E12329F3BB99008B9C86 /* CurrencyFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E16E12229F3BB99008B9C86 /* CurrencyFormatter.swift */; }; - 4E363CBC2A237E0900D7E98C /* URL+iban.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E363CBB2A237E0900D7E98C /* URL+iban.swift */; }; + 4E363CBC2A237E0900D7E98C /* URL+id+iban.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E363CBB2A237E0900D7E98C /* URL+id+iban.swift */; }; 4E363CBE2A23CB2100D7E98C /* AnyTransition+backslide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E363CBD2A23CB2100D7E98C /* AnyTransition+backslide.swift */; }; 4E363CC02A24754200D7E98C /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 4E363CBF2A24754200D7E98C /* Settings.bundle */; }; 4E363CC22A2621C200D7E98C /* LocalizedAlertError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E363CC12A2621C200D7E98C /* LocalizedAlertError.swift */; }; + 4E3B4BC12A41E6C200CC88B8 /* P2pReceiveURIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E3B4BC02A41E6C200CC88B8 /* P2pReceiveURIView.swift */; }; + 4E3B4BC32A42252300CC88B8 /* P2pAcceptDone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E3B4BC22A42252300CC88B8 /* P2pAcceptDone.swift */; }; + 4E3B4BC52A428AF700CC88B8 /* Model+Balances.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E3B4BC42A428AF700CC88B8 /* Model+Balances.swift */; }; + 4E3B4BC72A429F2A00CC88B8 /* View+Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E3B4BC62A429F2A00CC88B8 /* View+Notification.swift */; }; + 4E3B4BC92A42BC4800CC88B8 /* Model+Exchange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E3B4BC82A42BC4800CC88B8 /* Model+Exchange.swift */; }; 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 */; }; @@ -27,7 +32,7 @@ 4E8E25332A1CD39700A27BFA /* EqualIconWidthDomain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E8E25322A1CD39700A27BFA /* EqualIconWidthDomain.swift */; }; 4E9320432A14F6EA00A87B0E /* WalletColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9320422A14F6EA00A87B0E /* WalletColors.swift */; }; 4E9320452A1645B600A87B0E /* RequestPayment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9320442A1645B600A87B0E /* RequestPayment.swift */; }; - 4E9320472A164BC700A87B0E /* ReceivePurpose.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9320462A164BC700A87B0E /* ReceivePurpose.swift */; }; + 4E9320472A164BC700A87B0E /* PaymentPurpose.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9320462A164BC700A87B0E /* PaymentPurpose.swift */; }; 4E9796902A3765ED006F73BC /* AgePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E97968F2A3765ED006F73BC /* AgePicker.swift */; }; 4EA1ABBE29A3833A008821EA /* PublicConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA1ABBD29A3833A008821EA /* PublicConstants.swift */; }; 4EA551252A2C923600FEC9A8 /* CurrencyInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA551242A2C923600FEC9A8 /* CurrencyInputView.swift */; }; @@ -47,33 +52,30 @@ 4EB0950A2989CB7C0043A8A1 /* TalerStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095072989CB7C0043A8A1 /* TalerStrings.swift */; }; 4EB0950B2989CB7C0043A8A1 /* View+dismissTop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095082989CB7C0043A8A1 /* View+dismissTop.swift */; }; 4EB0950E2989CB9A0043A8A1 /* quickjs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB0950D2989CB9A0043A8A1 /* quickjs.swift */; }; - 4EB095152989CBB00043A8A1 /* SettingsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095102989CBB00043A8A1 /* SettingsModel.swift */; }; + 4EB095152989CBB00043A8A1 /* Model+Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095102989CBB00043A8A1 /* Model+Settings.swift */; }; 4EB095162989CBB00043A8A1 /* WalletModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095112989CBB00043A8A1 /* WalletModel.swift */; }; - 4EB095192989CBB00043A8A1 /* WalletInitModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095142989CBB00043A8A1 /* WalletInitModel.swift */; }; 4EB0951F2989CBCB0043A8A1 /* WalletBackendRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB0951B2989CBCB0043A8A1 /* WalletBackendRequest.swift */; }; 4EB095202989CBCB0043A8A1 /* WalletCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB0951C2989CBCB0043A8A1 /* WalletCore.swift */; }; 4EB095212989CBCB0043A8A1 /* WalletBackendError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB0951D2989CBCB0043A8A1 /* WalletBackendError.swift */; }; 4EB095222989CBCB0043A8A1 /* Transaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB0951E2989CBCB0043A8A1 /* Transaction.swift */; }; 4EB0954F2989CBFE0043A8A1 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095252989CBFE0043A8A1 /* SettingsView.swift */; }; 4EB095502989CBFE0043A8A1 /* SettingsItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095262989CBFE0043A8A1 /* SettingsItem.swift */; }; - 4EB095512989CBFE0043A8A1 /* ExchangeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095282989CBFE0043A8A1 /* ExchangeModel.swift */; }; 4EB095522989CBFE0043A8A1 /* ExchangeListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095292989CBFE0043A8A1 /* ExchangeListView.swift */; }; 4EB095532989CBFE0043A8A1 /* PaymentAcceptView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB0952B2989CBFE0043A8A1 /* PaymentAcceptView.swift */; }; - 4EB095542989CBFE0043A8A1 /* PaymentURIModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB0952C2989CBFE0043A8A1 /* PaymentURIModel.swift */; }; + 4EB095542989CBFE0043A8A1 /* Model+Payment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB0952C2989CBFE0043A8A1 /* Model+Payment.swift */; }; 4EB095552989CBFE0043A8A1 /* PaymentURIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB0952D2989CBFE0043A8A1 /* PaymentURIView.swift */; }; 4EB095562989CBFE0043A8A1 /* TransactionsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB0952F2989CBFE0043A8A1 /* TransactionsListView.swift */; }; 4EB095572989CBFE0043A8A1 /* TransactionRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095302989CBFE0043A8A1 /* TransactionRowView.swift */; }; 4EB095582989CBFE0043A8A1 /* TransactionDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095312989CBFE0043A8A1 /* TransactionDetailView.swift */; }; - 4EB095592989CBFE0043A8A1 /* TransactionsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095322989CBFE0043A8A1 /* TransactionsModel.swift */; }; + 4EB095592989CBFE0043A8A1 /* Model+Transactions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095322989CBFE0043A8A1 /* Model+Transactions.swift */; }; 4EB0955A2989CBFE0043A8A1 /* URLSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095332989CBFE0043A8A1 /* URLSheet.swift */; }; - 4EB0955B2989CBFE0043A8A1 /* BalancesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095352989CBFE0043A8A1 /* BalancesModel.swift */; }; 4EB0955C2989CBFE0043A8A1 /* BalanceRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095362989CBFE0043A8A1 /* BalanceRowView.swift */; }; 4EB0955D2989CBFE0043A8A1 /* BalancesListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095372989CBFE0043A8A1 /* BalancesListView.swift */; }; 4EB0955E2989CBFE0043A8A1 /* PendingRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095382989CBFE0043A8A1 /* PendingRowView.swift */; }; 4EB0955F2989CBFE0043A8A1 /* WalletEmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095392989CBFE0043A8A1 /* WalletEmptyView.swift */; }; 4EB095602989CBFE0043A8A1 /* BalancesSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB0953A2989CBFE0043A8A1 /* BalancesSectionView.swift */; }; 4EB095612989CBFE0043A8A1 /* WithdrawURIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB0953C2989CBFE0043A8A1 /* WithdrawURIView.swift */; }; - 4EB095622989CBFE0043A8A1 /* WithdrawModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB0953D2989CBFE0043A8A1 /* WithdrawModel.swift */; }; + 4EB095622989CBFE0043A8A1 /* Model+Withdraw.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB0953D2989CBFE0043A8A1 /* Model+Withdraw.swift */; }; 4EB095642989CBFE0043A8A1 /* WithdrawProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB0953F2989CBFE0043A8A1 /* WithdrawProgressView.swift */; }; 4EB095652989CBFE0043A8A1 /* WithdrawTOSView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095402989CBFE0043A8A1 /* WithdrawTOSView.swift */; }; 4EB095662989CBFE0043A8A1 /* SideBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095422989CBFE0043A8A1 /* SideBarView.swift */; }; @@ -84,7 +86,7 @@ 4EB0956B2989CBFE0043A8A1 /* TextFieldAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095482989CBFE0043A8A1 /* TextFieldAlert.swift */; }; 4EB0956C2989CBFE0043A8A1 /* AmountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095492989CBFE0043A8A1 /* AmountView.swift */; }; 4EB0956D2989CBFE0043A8A1 /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB0954A2989CBFE0043A8A1 /* LoadingView.swift */; }; - 4EB0956E2989CBFE0043A8A1 /* PendingModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB0954C2989CBFE0043A8A1 /* PendingModel.swift */; }; + 4EB0956E2989CBFE0043A8A1 /* Model+Pending.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB0954C2989CBFE0043A8A1 /* Model+Pending.swift */; }; 4EB0956F2989CBFE0043A8A1 /* PendingOpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB0954D2989CBFE0043A8A1 /* PendingOpView.swift */; }; 4EB095702989CBFE0043A8A1 /* PendingOpsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB0954E2989CBFE0043A8A1 /* PendingOpsListView.swift */; }; 4EB3136129FEE79B007D68BC /* SendNow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB3136029FEE79B007D68BC /* SendNow.swift */; }; @@ -92,7 +94,7 @@ 4EBA82AB2A3EB2CA00E5F39A /* TransactionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EBA82AA2A3EB2CA00E5F39A /* TransactionButton.swift */; }; 4EBA82AD2A3F580500E5F39A /* QuiteSomeCoins.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EBA82AC2A3F580500E5F39A /* QuiteSomeCoins.swift */; }; 4EC90C782A1B528B0071DC58 /* ExchangeSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EC90C772A1B528B0071DC58 /* ExchangeSectionView.swift */; }; - 4ECB62802A0BA6DF004ABBB7 /* Peer2peerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ECB627F2A0BA6DF004ABBB7 /* Peer2peerModel.swift */; }; + 4ECB62802A0BA6DF004ABBB7 /* Model+P2P.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ECB627F2A0BA6DF004ABBB7 /* Model+P2P.swift */; }; 4ECB62822A0BB01D004ABBB7 /* SelectDays.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ECB62812A0BB01D004ABBB7 /* SelectDays.swift */; }; 4ED2F94B2A278F5100453B40 /* ThreeAmounts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ED2F94A2A278F5100453B40 /* ThreeAmounts.swift */; }; 4EEC157329F8242800D46A03 /* QRGeneratorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EEC157229F8242800D46A03 /* QRGeneratorView.swift */; }; @@ -135,11 +137,16 @@ /* Begin PBXFileReference section */ 4E16E12229F3BB99008B9C86 /* CurrencyFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CurrencyFormatter.swift; sourceTree = "<group>"; }; - 4E363CBB2A237E0900D7E98C /* URL+iban.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URL+iban.swift"; sourceTree = "<group>"; }; + 4E363CBB2A237E0900D7E98C /* URL+id+iban.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URL+id+iban.swift"; sourceTree = "<group>"; }; 4E363CBD2A23CB2100D7E98C /* AnyTransition+backslide.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AnyTransition+backslide.swift"; sourceTree = "<group>"; }; 4E363CBF2A24754200D7E98C /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; }; 4E363CC12A2621C200D7E98C /* LocalizedAlertError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalizedAlertError.swift; sourceTree = "<group>"; }; 4E3AE7EF29A7E8F40070BEC4 /* Taler Wallet.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Taler Wallet.entitlements"; sourceTree = "<group>"; }; + 4E3B4BC02A41E6C200CC88B8 /* P2pReceiveURIView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = P2pReceiveURIView.swift; sourceTree = "<group>"; }; + 4E3B4BC22A42252300CC88B8 /* P2pAcceptDone.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = P2pAcceptDone.swift; sourceTree = "<group>"; }; + 4E3B4BC42A428AF700CC88B8 /* Model+Balances.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Model+Balances.swift"; sourceTree = "<group>"; }; + 4E3B4BC62A429F2A00CC88B8 /* View+Notification.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+Notification.swift"; sourceTree = "<group>"; }; + 4E3B4BC82A42BC4800CC88B8 /* Model+Exchange.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Model+Exchange.swift"; sourceTree = "<group>"; }; 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>"; }; @@ -156,7 +163,7 @@ 4E8E25322A1CD39700A27BFA /* EqualIconWidthDomain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EqualIconWidthDomain.swift; sourceTree = "<group>"; }; 4E9320422A14F6EA00A87B0E /* WalletColors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletColors.swift; sourceTree = "<group>"; }; 4E9320442A1645B600A87B0E /* RequestPayment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestPayment.swift; sourceTree = "<group>"; }; - 4E9320462A164BC700A87B0E /* ReceivePurpose.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReceivePurpose.swift; sourceTree = "<group>"; }; + 4E9320462A164BC700A87B0E /* PaymentPurpose.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaymentPurpose.swift; sourceTree = "<group>"; }; 4E97968F2A3765ED006F73BC /* AgePicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AgePicker.swift; sourceTree = "<group>"; }; 4EA1ABBD29A3833A008821EA /* PublicConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicConstants.swift; sourceTree = "<group>"; }; 4EA551242A2C923600FEC9A8 /* CurrencyInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyInputView.swift; sourceTree = "<group>"; }; @@ -176,33 +183,30 @@ 4EB095072989CB7C0043A8A1 /* TalerStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TalerStrings.swift; sourceTree = "<group>"; }; 4EB095082989CB7C0043A8A1 /* View+dismissTop.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+dismissTop.swift"; sourceTree = "<group>"; }; 4EB0950D2989CB9A0043A8A1 /* quickjs.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = quickjs.swift; sourceTree = "<group>"; }; - 4EB095102989CBB00043A8A1 /* SettingsModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsModel.swift; sourceTree = "<group>"; }; + 4EB095102989CBB00043A8A1 /* Model+Settings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Model+Settings.swift"; sourceTree = "<group>"; }; 4EB095112989CBB00043A8A1 /* WalletModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletModel.swift; sourceTree = "<group>"; }; - 4EB095142989CBB00043A8A1 /* WalletInitModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletInitModel.swift; sourceTree = "<group>"; }; 4EB0951B2989CBCB0043A8A1 /* WalletBackendRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletBackendRequest.swift; sourceTree = "<group>"; }; 4EB0951C2989CBCB0043A8A1 /* WalletCore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletCore.swift; sourceTree = "<group>"; }; 4EB0951D2989CBCB0043A8A1 /* WalletBackendError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletBackendError.swift; sourceTree = "<group>"; }; 4EB0951E2989CBCB0043A8A1 /* Transaction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Transaction.swift; sourceTree = "<group>"; }; 4EB095252989CBFE0043A8A1 /* SettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; }; 4EB095262989CBFE0043A8A1 /* SettingsItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsItem.swift; sourceTree = "<group>"; }; - 4EB095282989CBFE0043A8A1 /* ExchangeModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExchangeModel.swift; sourceTree = "<group>"; }; 4EB095292989CBFE0043A8A1 /* ExchangeListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExchangeListView.swift; sourceTree = "<group>"; }; 4EB0952B2989CBFE0043A8A1 /* PaymentAcceptView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaymentAcceptView.swift; sourceTree = "<group>"; }; - 4EB0952C2989CBFE0043A8A1 /* PaymentURIModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaymentURIModel.swift; sourceTree = "<group>"; }; + 4EB0952C2989CBFE0043A8A1 /* Model+Payment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Model+Payment.swift"; sourceTree = "<group>"; }; 4EB0952D2989CBFE0043A8A1 /* PaymentURIView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaymentURIView.swift; sourceTree = "<group>"; }; 4EB0952F2989CBFE0043A8A1 /* TransactionsListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionsListView.swift; sourceTree = "<group>"; }; 4EB095302989CBFE0043A8A1 /* TransactionRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionRowView.swift; sourceTree = "<group>"; }; 4EB095312989CBFE0043A8A1 /* TransactionDetailView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionDetailView.swift; sourceTree = "<group>"; }; - 4EB095322989CBFE0043A8A1 /* TransactionsModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionsModel.swift; sourceTree = "<group>"; }; + 4EB095322989CBFE0043A8A1 /* Model+Transactions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Model+Transactions.swift"; sourceTree = "<group>"; }; 4EB095332989CBFE0043A8A1 /* URLSheet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSheet.swift; sourceTree = "<group>"; }; - 4EB095352989CBFE0043A8A1 /* BalancesModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BalancesModel.swift; sourceTree = "<group>"; }; 4EB095362989CBFE0043A8A1 /* BalanceRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BalanceRowView.swift; sourceTree = "<group>"; }; 4EB095372989CBFE0043A8A1 /* BalancesListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BalancesListView.swift; sourceTree = "<group>"; }; 4EB095382989CBFE0043A8A1 /* PendingRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PendingRowView.swift; sourceTree = "<group>"; }; 4EB095392989CBFE0043A8A1 /* WalletEmptyView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletEmptyView.swift; sourceTree = "<group>"; }; 4EB0953A2989CBFE0043A8A1 /* BalancesSectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BalancesSectionView.swift; sourceTree = "<group>"; }; 4EB0953C2989CBFE0043A8A1 /* WithdrawURIView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WithdrawURIView.swift; sourceTree = "<group>"; }; - 4EB0953D2989CBFE0043A8A1 /* WithdrawModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WithdrawModel.swift; sourceTree = "<group>"; }; + 4EB0953D2989CBFE0043A8A1 /* Model+Withdraw.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Model+Withdraw.swift"; sourceTree = "<group>"; }; 4EB0953F2989CBFE0043A8A1 /* WithdrawProgressView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WithdrawProgressView.swift; sourceTree = "<group>"; }; 4EB095402989CBFE0043A8A1 /* WithdrawTOSView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WithdrawTOSView.swift; sourceTree = "<group>"; }; 4EB095422989CBFE0043A8A1 /* SideBarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SideBarView.swift; sourceTree = "<group>"; }; @@ -213,7 +217,7 @@ 4EB095482989CBFE0043A8A1 /* TextFieldAlert.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldAlert.swift; sourceTree = "<group>"; }; 4EB095492989CBFE0043A8A1 /* AmountView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AmountView.swift; sourceTree = "<group>"; }; 4EB0954A2989CBFE0043A8A1 /* LoadingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = "<group>"; }; - 4EB0954C2989CBFE0043A8A1 /* PendingModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PendingModel.swift; sourceTree = "<group>"; }; + 4EB0954C2989CBFE0043A8A1 /* Model+Pending.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Model+Pending.swift"; sourceTree = "<group>"; }; 4EB0954D2989CBFE0043A8A1 /* PendingOpView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PendingOpView.swift; sourceTree = "<group>"; }; 4EB0954E2989CBFE0043A8A1 /* PendingOpsListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PendingOpsListView.swift; sourceTree = "<group>"; }; 4EB3136029FEE79B007D68BC /* SendNow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendNow.swift; sourceTree = "<group>"; }; @@ -221,7 +225,7 @@ 4EBA82AA2A3EB2CA00E5F39A /* TransactionButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionButton.swift; sourceTree = "<group>"; }; 4EBA82AC2A3F580500E5F39A /* QuiteSomeCoins.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuiteSomeCoins.swift; sourceTree = "<group>"; }; 4EC90C772A1B528B0071DC58 /* ExchangeSectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExchangeSectionView.swift; sourceTree = "<group>"; }; - 4ECB627F2A0BA6DF004ABBB7 /* Peer2peerModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Peer2peerModel.swift; sourceTree = "<group>"; }; + 4ECB627F2A0BA6DF004ABBB7 /* Model+P2P.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Model+P2P.swift"; sourceTree = "<group>"; }; 4ECB62812A0BB01D004ABBB7 /* SelectDays.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectDays.swift; sourceTree = "<group>"; }; 4ED2F94A2A278F5100453B40 /* ThreeAmounts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThreeAmounts.swift; sourceTree = "<group>"; }; 4EEC157229F8242800D46A03 /* QRGeneratorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRGeneratorView.swift; sourceTree = "<group>"; }; @@ -264,6 +268,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 4E3B4BBF2A41E64000CC88B8 /* P2P_Sheets */ = { + isa = PBXGroup; + children = ( + 4E3B4BC02A41E6C200CC88B8 /* P2pReceiveURIView.swift */, + 4E3B4BC22A42252300CC88B8 /* P2pAcceptDone.swift */, + ); + path = P2P_Sheets; + sourceTree = "<group>"; + }; 4EB094EE298979840043A8A1 /* TalerWallet1 */ = { isa = PBXGroup; children = ( @@ -319,7 +332,8 @@ 4EB095062989CB7C0043A8A1 /* TalerDater.swift */, 4EB095072989CB7C0043A8A1 /* TalerStrings.swift */, 4EB095082989CB7C0043A8A1 /* View+dismissTop.swift */, - 4E363CBB2A237E0900D7E98C /* URL+iban.swift */, + 4E3B4BC62A429F2A00CC88B8 /* View+Notification.swift */, + 4E363CBB2A237E0900D7E98C /* URL+id+iban.swift */, 4E9320422A14F6EA00A87B0E /* WalletColors.swift */, 4E8E25322A1CD39700A27BFA /* EqualIconWidthDomain.swift */, ); @@ -338,15 +352,14 @@ isa = PBXGroup; children = ( 4EB095112989CBB00043A8A1 /* WalletModel.swift */, - 4EB095142989CBB00043A8A1 /* WalletInitModel.swift */, - 4EB095352989CBFE0043A8A1 /* BalancesModel.swift */, - 4EB095282989CBFE0043A8A1 /* ExchangeModel.swift */, - 4ECB627F2A0BA6DF004ABBB7 /* Peer2peerModel.swift */, - 4EB0954C2989CBFE0043A8A1 /* PendingModel.swift */, - 4EB0952C2989CBFE0043A8A1 /* PaymentURIModel.swift */, - 4EB095102989CBB00043A8A1 /* SettingsModel.swift */, - 4EB095322989CBFE0043A8A1 /* TransactionsModel.swift */, - 4EB0953D2989CBFE0043A8A1 /* WithdrawModel.swift */, + 4E3B4BC42A428AF700CC88B8 /* Model+Balances.swift */, + 4E3B4BC82A42BC4800CC88B8 /* Model+Exchange.swift */, + 4ECB627F2A0BA6DF004ABBB7 /* Model+P2P.swift */, + 4EB0954C2989CBFE0043A8A1 /* Model+Pending.swift */, + 4EB0952C2989CBFE0043A8A1 /* Model+Payment.swift */, + 4EB095102989CBB00043A8A1 /* Model+Settings.swift */, + 4EB095322989CBFE0043A8A1 /* Model+Transactions.swift */, + 4EB0953D2989CBFE0043A8A1 /* Model+Withdraw.swift */, ); path = Model; sourceTree = "<group>"; @@ -493,7 +506,7 @@ 4E7940DD29FC307C00A9AEA1 /* SendPurpose.swift */, 4EB3136029FEE79B007D68BC /* SendNow.swift */, 4E9320442A1645B600A87B0E /* RequestPayment.swift */, - 4E9320462A164BC700A87B0E /* ReceivePurpose.swift */, + 4E9320462A164BC700A87B0E /* PaymentPurpose.swift */, ); path = Peer2peer; sourceTree = "<group>"; @@ -505,6 +518,7 @@ 4EEC157929F9427F00D46A03 /* QRSheet.swift */, 4E753A072A0B6A5F002D9328 /* ShareSheet.swift */, 4EB095332989CBFE0043A8A1 /* URLSheet.swift */, + 4E3B4BBF2A41E64000CC88B8 /* P2P_Sheets */, ); path = Sheets; sourceTree = "<group>"; @@ -699,7 +713,6 @@ buildActionMask = 2147483647; files = ( 4ECB62822A0BB01D004ABBB7 /* SelectDays.swift in Sources */, - 4EB095512989CBFE0043A8A1 /* ExchangeModel.swift in Sources */, 4E9796902A3765ED006F73BC /* AgePicker.swift in Sources */, 4EB095032989C9BC0043A8A1 /* Controller.swift in Sources */, 4EB095682989CBFE0043A8A1 /* MainView.swift in Sources */, @@ -714,15 +727,15 @@ 4EB095532989CBFE0043A8A1 /* PaymentAcceptView.swift in Sources */, 4EB095212989CBCB0043A8A1 /* WalletBackendError.swift in Sources */, 4EB0955E2989CBFE0043A8A1 /* PendingRowView.swift in Sources */, - 4EB0955B2989CBFE0043A8A1 /* BalancesModel.swift in Sources */, 4EB0956D2989CBFE0043A8A1 /* LoadingView.swift in Sources */, 4E50B3502A1BEE8000F9F01C /* ManualWithdraw.swift in Sources */, + 4E3B4BC92A42BC4800CC88B8 /* Model+Exchange.swift in Sources */, 4E5A88F52A38A4FD00072618 /* QRCodeDetailView.swift in Sources */, 4E87C8732A31CB7F001C6406 /* TransactionsEmptyView.swift in Sources */, 4E87C8752A34B411001C6406 /* UncompletedRowView.swift in Sources */, 4E40E0BE29F25ABB00B85369 /* SendAmount.swift in Sources */, 4E8E25332A1CD39700A27BFA /* EqualIconWidthDomain.swift in Sources */, - 4EB095542989CBFE0043A8A1 /* PaymentURIModel.swift in Sources */, + 4EB095542989CBFE0043A8A1 /* Model+Payment.swift in Sources */, 4EB0954F2989CBFE0043A8A1 /* SettingsView.swift in Sources */, 4EB095552989CBFE0043A8A1 /* PaymentURIView.swift in Sources */, 4EB095612989CBFE0043A8A1 /* WithdrawURIView.swift in Sources */, @@ -741,24 +754,27 @@ 4EB0956B2989CBFE0043A8A1 /* TextFieldAlert.swift in Sources */, 4EBA82AD2A3F580500E5F39A /* QuiteSomeCoins.swift in Sources */, 4EB431672A1E55C700C5690E /* ManualWithdrawDone.swift in Sources */, - 4E9320472A164BC700A87B0E /* ReceivePurpose.swift in Sources */, + 4E9320472A164BC700A87B0E /* PaymentPurpose.swift in Sources */, 4E753A082A0B6A5F002D9328 /* ShareSheet.swift in Sources */, 4EB0956C2989CBFE0043A8A1 /* AmountView.swift in Sources */, + 4E3B4BC32A42252300CC88B8 /* P2pAcceptDone.swift in Sources */, 4E363CBE2A23CB2100D7E98C /* AnyTransition+backslide.swift in Sources */, - 4EB095592989CBFE0043A8A1 /* TransactionsModel.swift in Sources */, + 4EB095592989CBFE0043A8A1 /* Model+Transactions.swift in Sources */, 4EB0955F2989CBFE0043A8A1 /* WalletEmptyView.swift in Sources */, 4E16E12329F3BB99008B9C86 /* CurrencyFormatter.swift in Sources */, - 4EB095192989CBB00043A8A1 /* WalletInitModel.swift in Sources */, 4EB095092989CB7C0043A8A1 /* TalerDater.swift in Sources */, + 4E3B4BC52A428AF700CC88B8 /* Model+Balances.swift in Sources */, 4E363CC22A2621C200D7E98C /* LocalizedAlertError.swift in Sources */, 4EB0950E2989CB9A0043A8A1 /* quickjs.swift in Sources */, 4E53A33729F50B7B00830EC2 /* CurrencyField.swift in Sources */, - 4EB095152989CBB00043A8A1 /* SettingsModel.swift in Sources */, + 4EB095152989CBB00043A8A1 /* Model+Settings.swift in Sources */, 4EB095692989CBFE0043A8A1 /* ErrorView.swift in Sources */, - 4EB0956E2989CBFE0043A8A1 /* PendingModel.swift in Sources */, + 4E3B4BC72A429F2A00CC88B8 /* View+Notification.swift in Sources */, + 4EB0956E2989CBFE0043A8A1 /* Model+Pending.swift in Sources */, 4EB095522989CBFE0043A8A1 /* ExchangeListView.swift in Sources */, 4EB095642989CBFE0043A8A1 /* WithdrawProgressView.swift in Sources */, 4EEC157A29F9427F00D46A03 /* QRSheet.swift in Sources */, + 4E3B4BC12A41E6C200CC88B8 /* P2pReceiveURIView.swift in Sources */, 4E6EDD872A363D8D0031D520 /* ListStyle.swift in Sources */, 4EB095582989CBFE0043A8A1 /* TransactionDetailView.swift in Sources */, 4EB095202989CBCB0043A8A1 /* WalletCore.swift in Sources */, @@ -769,13 +785,13 @@ 4EB095162989CBB00043A8A1 /* WalletModel.swift in Sources */, 4EB0955A2989CBFE0043A8A1 /* URLSheet.swift in Sources */, 4ED2F94B2A278F5100453B40 /* ThreeAmounts.swift in Sources */, - 4EB095622989CBFE0043A8A1 /* WithdrawModel.swift in Sources */, + 4EB095622989CBFE0043A8A1 /* Model+Withdraw.swift in Sources */, 4EC90C782A1B528B0071DC58 /* ExchangeSectionView.swift in Sources */, 4E7940DE29FC307C00A9AEA1 /* SendPurpose.swift in Sources */, - 4ECB62802A0BA6DF004ABBB7 /* Peer2peerModel.swift in Sources */, + 4ECB62802A0BA6DF004ABBB7 /* Model+P2P.swift in Sources */, 4EB0950A2989CB7C0043A8A1 /* TalerStrings.swift in Sources */, 4EA551252A2C923600FEC9A8 /* CurrencyInputView.swift in Sources */, - 4E363CBC2A237E0900D7E98C /* URL+iban.swift in Sources */, + 4E363CBC2A237E0900D7E98C /* URL+id+iban.swift in Sources */, 4E9320452A1645B600A87B0E /* RequestPayment.swift in Sources */, 4EB095502989CBFE0043A8A1 /* SettingsItem.swift in Sources */, 4EB0955C2989CBFE0043A8A1 /* BalanceRowView.swift in Sources */, diff --git a/TalerWallet1/Backend/Transaction.swift b/TalerWallet1/Backend/Transaction.swift @@ -102,8 +102,8 @@ struct TransactionCommon: Decodable { case payment case refund case refresh - case reward -// case tip + case reward // get paid for e.g. survey participation +// case tip // tip personnel at restaurants case peerPushDebit = "peer-push-debit" // send coins to peer, show QR case scanPushCredit = "peer-push-credit" // scan QR, receive coins from peer case peerPullCredit = "peer-pull-credit" // request payment from peer, show QR @@ -329,13 +329,13 @@ enum Transaction: Decodable, Hashable, Identifiable { case .refund: return String(localized: "Refund") case .refresh: return String(localized: "Refresh") case .reward: return String(localized: "Reward") - case .peerPushDebit: return String(localized: "P2P Send") - case .scanPushCredit: return String(localized: "P2P Receive") - case .peerPullCredit: return String(localized: "P2P Invoice") - case .scanPullDebit: return String(localized: "P2P Payment") + case .peerPushDebit: return String(localized: "P2P Send", comment: "send coins to another wallet") + case .scanPushCredit: return String(localized: "P2P Receive", comment: "scan to receive coins sent from another wallet") + case .peerPullCredit: return String(localized: "P2P Invoice", comment: "send invoice to another wallet") + case .scanPullDebit: return String(localized: "P2P Payment", comment: "scan invoice to pay to another wallet") } } - + static func == (lhs: Transaction, rhs: Transaction) -> Bool { return lhs.id == rhs.id } @@ -360,13 +360,16 @@ enum Transaction: Decodable, Hashable, Identifiable { var isP2pOutgoing: Bool { isSendCoins || isPayInvoice} var isP2pIncoming: Bool { isSendInvoice || isRcvCoins} - var isDone : Bool { common.txState.major == .done } var isPending : Bool { common.txState.major == .pending } + var isDone : Bool { common.txState.major == .done } var isAborting : Bool { common.txState.major == .aborting } var isAborted : Bool { common.txState.major == .aborted } + var isSuspended : Bool { common.txState.major == .suspended } + var isDialog : Bool { common.txState.major == .dialog } + var isAbSuspended: Bool { common.txState.major == .suspendedAborting } var isFailed : Bool { common.txState.major == .failed } var isExpired : Bool { common.txState.major == .expired } - var isSpecial : Bool { isPending || isAborting || isAborted || isFailed || isExpired } + var isSpecial : Bool { !(isDone || isPending) } var isAbortable : Bool { common.txActions.contains(.abort) } var isFailable : Bool { common.txActions.contains(.fail) } diff --git a/TalerWallet1/Backend/WalletCore.swift b/TalerWallet1/Backend/WalletCore.swift @@ -6,6 +6,7 @@ import SwiftUI // FOUNDATION has no AppStorage import AnyCodable import FTalerWalletcore import SymLog +import os /// Delegate for the wallet backend. protocol WalletBackendDelegate { @@ -22,13 +23,14 @@ class WalletCore: QuickjsMessageHandler { private var queue: DispatchQueue private var semaphore: DispatchSemaphore - private var quickjs: Quickjs + private let quickjs: Quickjs private var requestsMade: UInt // counter for array of completion closures private var completions: [UInt : (Date, (UInt, Date, Data?, WalletBackendResponseError?) -> Void)] = [:] var delegate: WalletBackendDelegate? var versionInfo: VersionInfo? // shown in SettingsView var developDelay: Bool? // if set in SettingsView will delay wallet-core after each action + let logger = Logger (subsystem: "net.taler.gnu", category: "wallet-core") private struct FullRequest: Encodable { let operation: String @@ -164,6 +166,9 @@ extension WalletCore { } else if id.hasPrefix("withdraw:") { // TODO: handle withdraw // symLog.log("\(pendingOp): \(id)") + } else if id.hasPrefix("peer-pull-credit:") { + // TODO: handle peer-pull-credit +// symLog.log("\(pendingOp): \(id)") } else if id.hasPrefix("peer-push-debit:") { // TODO: handle peer-push-debit // symLog.log("\(pendingOp): \(id)") @@ -176,8 +181,10 @@ print("\n❗️ \(pendingOp): \(id)\n") // this is a new pendingOp I have private func handleStateTransition(_ jsonData: Data) throws { do { let decoded = try JSONDecoder().decode(TransactionTransition.self, from: jsonData) - postNotification(.TransactionStateTransition, - userInfo: [TRANSACTIONTRANSITION: decoded]) + if decoded.newTxState != decoded.oldTxState { + postNotification(.TransactionStateTransition, + userInfo: [TRANSACTIONTRANSITION: decoded]) + } } catch { // rethrows symLog.log(jsonData) // TODO: .error throw WalletBackendError.deserializationError @@ -315,7 +322,7 @@ extension WalletCore { sendRequest(request: reqData) { requestId, timeSent, result, error in let timeUsed = Date.now - timeSent let millisecs = timeUsed.milliseconds -print("Request \(requestId) took \(millisecs) ms") + self.logger.log("Request \(requestId) took \(millisecs) ms") var err: Error? = nil if let json = result { do { @@ -339,7 +346,7 @@ print("Request \(requestId) took \(millisecs) ms") } else { self.symLog.log(json) // TODO: .error } - err = error // this will be thrown, otherwise keep nil + err = error // this will be thrown in continuation.resume(throwing:), otherwise keep nil } } else { if let error = error { diff --git a/TalerWallet1/Controllers/Controller.swift b/TalerWallet1/Controllers/Controller.swift @@ -14,10 +14,12 @@ enum BackendState { case error } -enum UrlCommand { +enum UrlCommand: String { case unknown case withdraw case pay + case payPull = "pay-pull" + case payPush = "pay-push" } // MARK: - @@ -33,13 +35,12 @@ class Controller: ObservableObject { backendState = .instantiated } - @MainActor func initWalletCoreM() + @MainActor func initWalletCoreM(_ model: WalletModel) async throws { // M for MainActor if backendState == .instantiated { backendState = .initing do { - let walletInitModel = WalletInitModel() - let versionInfo = try await walletInitModel.initWalletT() + let versionInfo = try await model.initWalletCoreT() WalletCore.shared.versionInfo = versionInfo backendState = .ready // dismiss the launch animation } catch { // rethrows @@ -48,7 +49,7 @@ class Controller: ObservableObject { throw error } } else { - symLog.log("Yikes❗️ wallet-core already initialized") // TODO: .warning + symLog.log("Yikes❗️ wallet-core already initialized") // TODO: .fault } } } @@ -93,6 +94,10 @@ extension Controller { return UrlCommand.withdraw case "pay": return UrlCommand.pay + case "pay-pull": + return UrlCommand.payPull + case "pay-push": + return UrlCommand.payPush default: symLog.log("unknown command taler://\(command)") } diff --git a/TalerWallet1/Controllers/DebugViewC.swift b/TalerWallet1/Controllers/DebugViewC.swift @@ -84,6 +84,8 @@ public let SHEET_PAY_P2P = SHEET_RCV_REWARD + 10 // 160 Pay P // MARK: P2P Receive Coins // p2p push credit - openURL (Link or scan QR) public let SHEET_RCV_P2P = SHEET_PAY_P2P + 10 // 170 Receive P2P Coins +public let SHEET_RCV_P2P_TOS = SHEET_RCV_P2P + 1 // 171 Receive P2P TOSView +public let SHEET_RCV_P2P_ACCEPT = SHEET_RCV_P2P_TOS + 1 // 172 Receive P2P AcceptView //public let SHEET_REFUND = diff --git a/TalerWallet1/Controllers/TalerWallet1App.swift b/TalerWallet1/Controllers/TalerWallet1App.swift @@ -22,6 +22,7 @@ struct TalerWallet1App: App { private let walletCore = WalletCore.shared // our main controller private let controller = Controller.shared + private let model = WalletModel.shared private let debugViewC = DebugViewC.shared func scheduleAppRefresh() { @@ -36,12 +37,13 @@ struct TalerWallet1App: App { .environmentObject(debugViewC) // change viewID / sheetID .environmentObject(viewState) // popToRoot .environmentObject(controller) + .environmentObject(model) /// external events are taler:// or payto:// URLs passed to this app /// we handle them in .onOpenURL in MainView.swift .handlesExternalEvents(preferring: ["*"], allowing: ["*"]) .task { symLog.log("task -> initWalletCore") - try! await controller.initWalletCoreM() // will (and should) crash on failure + try! await controller.initWalletCoreM(model) // will (and should) crash on failure symLog.log("task -> initWalletCore done") } .onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification, object: nil)) { _ in diff --git a/TalerWallet1/Helper/URL+iban.swift b/TalerWallet1/Helper/URL+id+iban.swift diff --git a/TalerWallet1/Helper/View+Notification.swift b/TalerWallet1/Helper/View+Notification.swift @@ -0,0 +1,75 @@ +// MIT License +// Copyright © John Sundell +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software +// and associated documentation files (the "Software"), to deal in the Software without restriction, +// including without limitation the rights to use, copy, modify, merge, publish, distribute, +// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +import SwiftUI + +extension View { + func onNotification( + _ notificationName: Notification.Name, + perform action: @escaping () -> Void + ) -> some View { + onReceive(NotificationCenter.default + .publisher(for: notificationName) + ) { _ in + action() + } + } + func onNotification( + _ notificationName: Notification.Name, + perform action: @escaping (_ notification: Notification) -> Void + ) -> some View { + onReceive(NotificationCenter.default + .publisher(for: notificationName) + ) { notification in + action(notification) + } + } + + func onNotificationM( // M for main thread + _ notificationName: Notification.Name, + perform action: @escaping () -> Void + ) -> some View { + onReceive(NotificationCenter.default + .publisher(for: notificationName) + .receive(on: RunLoop.main) + ) { _ in + action() + } + } + + func onNotificationM( // M for main thread + _ notificationName: Notification.Name, + perform action: @escaping (_ notification: Notification) -> Void + ) -> some View { + onReceive(NotificationCenter.default + .publisher(for: notificationName) + .receive(on: RunLoop.main) + ) { notification in + action(notification) + } + } + + func onAppEnteredBackground( + perform action: @escaping () -> Void + ) -> some View { + onNotification( + UIApplication.didEnterBackgroundNotification, + perform: action + ) + } +} diff --git a/TalerWallet1/Helper/View+dismissTop.swift b/TalerWallet1/Helper/View+dismissTop.swift @@ -18,63 +18,6 @@ // import SwiftUI -// John Sundell -extension View { - func onNotification( - _ notificationName: Notification.Name, - perform action: @escaping () -> Void - ) -> some View { - onReceive(NotificationCenter.default - .publisher(for: notificationName) - ) { _ in - action() - } - } - func onNotification( - _ notificationName: Notification.Name, - perform action: @escaping (_ notification: Notification) -> Void - ) -> some View { - onReceive(NotificationCenter.default - .publisher(for: notificationName) - ) { notification in - action(notification) - } - } - - func onNotificationM( // M for main thread - _ notificationName: Notification.Name, - perform action: @escaping () -> Void - ) -> some View { - onReceive(NotificationCenter.default - .publisher(for: notificationName) - .receive(on: RunLoop.main) - ) { _ in - action() - } - } - - func onNotificationM( // M for main thread - _ notificationName: Notification.Name, - perform action: @escaping (_ notification: Notification) -> Void - ) -> some View { - onReceive(NotificationCenter.default - .publisher(for: notificationName) - .receive(on: RunLoop.main) - ) { notification in - action(notification) - } - } - - func onAppEnteredBackground( - perform action: @escaping () -> Void - ) -> some View { - onNotification( - UIApplication.didEnterBackgroundNotification, - perform: action - ) - } -} - /// This is just a workaround for a SwiftUI bug /// A presented sheet (SwiftUI view) doesn't always close when calling "dismiss()" provided by @Environment(\.dismiss), /// so we are walking the view stack to find the top presentedViewController (UIKit) and dismiss it. diff --git a/TalerWallet1/Model/BalancesModel.swift b/TalerWallet1/Model/BalancesModel.swift @@ -1,85 +0,0 @@ -/* - * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. - * See LICENSE.md - */ -import Foundation -import taler_swift -fileprivate let ASYNCDELAY: UInt = 0 //set e.g to 6 or 9 seconds for debugging - -// MARK: - -class BalancesModel: WalletModel { - - override init(_ symbol: Int = -1) { // init with 0 to disable logging for this class - super.init(0)//symbol) - } -} -// MARK: - -/// A request to get the balances held in the wallet. -fileprivate struct GetBalances: WalletBackendFormattedRequest { - func operation() -> String { return "getBalances" } - func args() -> Args { return Args() } - - struct Args: Encodable {} // no arguments needed - - struct Response: Decodable { // list of balances - var balances: [Balance] - } -} -// MARK: - -/// A currency balance -struct Balance: Decodable, Hashable { - var available: Amount - var pendingIncoming: Amount - var pendingOutgoing: Amount - var hasPendingTransactions: Bool - var requiresUserInput: Bool - var scopeInfo: ScopeInfo - - public static func == (lhs: Balance, rhs: Balance) -> Bool { - return lhs.available == rhs.available && - lhs.pendingIncoming == rhs.pendingIncoming && - lhs.pendingOutgoing == rhs.pendingOutgoing && - lhs.hasPendingTransactions == rhs.hasPendingTransactions && - lhs.requiresUserInput == rhs.requiresUserInput - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(available) - hasher.combine(pendingIncoming) - hasher.combine(pendingOutgoing) - hasher.combine(hasPendingTransactions) - hasher.combine(requiresUserInput) - } -} -// MARK: - -extension BalancesModel { - /// fetch Balances from Wallet-Core. No networking involved - @MainActor func fetchBalancesM() - async -> [Balance] { // M for MainActor - do { - let request = GetBalances() - let response = try await sendRequest(request, ASYNCDELAY) - return response.balances // trigger view update in BalancesListView - } catch { - return [] - } - } -} - -// MARK: - -extension BalancesModel { - private static var currencies: [String] = [] // names of currencies - private static var models: [BalancesModel] = [] // one model per currency - - static func model(currency: String) -> BalancesModel { - if let index = BalancesModel.currencies.firstIndex(of:currency) { - let model = BalancesModel.models[index] - return model - } else { // new currency - let model = BalancesModel() - BalancesModel.models.append(model) - BalancesModel.currencies.append(currency) - return model - } - } -} diff --git a/TalerWallet1/Model/ExchangeModel.swift b/TalerWallet1/Model/ExchangeModel.swift @@ -1,121 +0,0 @@ -/* - * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. - * See LICENSE.md - */ -import Foundation -import taler_swift -import SymLog -fileprivate let ASYNCDELAY: UInt = 0 //set e.g to 6 or 9 seconds for debugging - -class ExchangeModel: WalletModel { - override init(_ symbol: Int = -1) { - super.init(0)//symbol) - } -} -// MARK: - -/// The result from wallet-core's ListExchanges -struct Exchange: Codable, Hashable, Identifiable { - static func == (lhs: Exchange, rhs: Exchange) -> Bool { - return lhs.exchangeBaseUrl == rhs.exchangeBaseUrl && - lhs.exchangeStatus == rhs.exchangeStatus && - lhs.permanent == rhs.permanent - } - - var exchangeBaseUrl: String - var currency: String? - var paytoUris: [String] - var tosStatus: String - var exchangeStatus: String - var ageRestrictionOptions: [Int] - var permanent: Bool - var lastUpdateErrorInfo: ExchangeError? - - var id: String { - exchangeBaseUrl - } - var name: String? { - if let url = URL(string: exchangeBaseUrl) { - if let host = url.host { - return host - } - } - return nil - } -} - -struct ExchangeError: Codable, Hashable { - var error: HTTPError -} - -struct HTTPError: Codable, Hashable { - var code: Int - var requestUrl: String? - var hint: String - var requestMethod: String? - var httpStatusCode: Int? - var when: Timestamp? - var stack: String? -} -// MARK: - -/// A request to list exchanges. -fileprivate struct ListExchanges: WalletBackendFormattedRequest { - func operation() -> String { return "listExchanges" } - func args() -> Args { return Args() } - - struct Args: Encodable {} // no arguments needed - - struct Response: Decodable { // list of known exchanges - var exchanges: [Exchange] - } -} - -/// A request to add an exchange. -fileprivate struct AddExchange: WalletBackendFormattedRequest { - func operation() -> String { return "addExchange" } - func args() -> Args { return Args(exchangeBaseUrl: exchangeBaseUrl) } - - var exchangeBaseUrl: String - - struct Args: Encodable { - var exchangeBaseUrl: String - } - - struct Response: Decodable {} -} -// MARK: - -extension ExchangeModel { - /// 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 { - return [] // empty, but not nil - } - } - - /// add a new exchange with URL to the wallet's list of known exchanges - func addExchange(url: String) async throws { - symLog?.log("adding exchange: \(url)") // TODO: .notice - let request = AddExchange(exchangeBaseUrl: url) - _ = try await sendRequest(request) - } -} - -// MARK: - -extension ExchangeModel { - private static var models: [ExchangeModel] = [] // a list of models even though I currently need only one - - static func model() -> ExchangeModel { - if ExchangeModel.models.count > 0 { - let model = ExchangeModel.models[0] - return model - } else { // new model - let model = ExchangeModel() - ExchangeModel.models.append(model) - return model - } - } -} diff --git a/TalerWallet1/Model/Model+Balances.swift b/TalerWallet1/Model/Model+Balances.swift @@ -0,0 +1,58 @@ +/* + * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. + * See LICENSE.md + */ +import Foundation +import taler_swift +fileprivate let ASYNCDELAY: UInt = 0 //set e.g to 6 or 9 seconds for debugging + +// MARK: - +/// A currency balance +struct Balance: Decodable, Hashable { + var available: Amount + var scopeInfo: ScopeInfo + var requiresUserInput: Bool + var hasPendingTransactions: Bool + + public static func == (lhs: Balance, rhs: Balance) -> Bool { + return lhs.available == rhs.available && + lhs.scopeInfo == rhs.scopeInfo && + lhs.requiresUserInput == rhs.requiresUserInput && + lhs.hasPendingTransactions == rhs.hasPendingTransactions + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(available) + hasher.combine(scopeInfo) + hasher.combine(requiresUserInput) + hasher.combine(hasPendingTransactions) + } +} +// MARK: - +/// A request to get the balances held in the wallet. +fileprivate struct Balances: WalletBackendFormattedRequest { + func operation() -> String { return "getBalances" } + func args() -> Args { return Args() } + + struct Args: Encodable {} // no arguments needed + + struct Response: Decodable { // list of balances + var balances: [Balance] + } +} +// MARK: - +extension WalletModel { + /// fetch Balances from Wallet-Core. No networking involved + @MainActor func balancesM() + async -> [Balance] { // M for MainActor + do { + let request = Balances() + logger.info("balancesM") + let response = try await sendRequest(request, ASYNCDELAY) + return response.balances // trigger view update in BalancesListView + } catch { + logger.error("balancesM failed: \(error)") + return [] + } + } +} diff --git a/TalerWallet1/Model/Model+Exchange.swift b/TalerWallet1/Model/Model+Exchange.swift @@ -0,0 +1,104 @@ +/* + * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. + * See LICENSE.md + */ +import Foundation +import taler_swift +import SymLog +fileprivate let ASYNCDELAY: UInt = 0 //set e.g to 6 or 9 seconds for debugging + +// MARK: - +/// The result from wallet-core's ListExchanges +struct Exchange: Codable, Hashable, Identifiable { + static func == (lhs: Exchange, rhs: Exchange) -> Bool { + return lhs.exchangeBaseUrl == rhs.exchangeBaseUrl && + lhs.exchangeStatus == rhs.exchangeStatus && + lhs.permanent == rhs.permanent + } + + var exchangeBaseUrl: String + var currency: String? + var paytoUris: [String] + var tosStatus: String + var exchangeStatus: String + var ageRestrictionOptions: [Int] + var permanent: Bool + var lastUpdateErrorInfo: ExchangeError? + + var id: String { + exchangeBaseUrl + } + var name: String? { + if let url = URL(string: exchangeBaseUrl) { + if let host = url.host { + return host + } + } + return nil + } +} + +struct ExchangeError: Codable, Hashable { + var error: HTTPError +} + +struct HTTPError: Codable, Hashable { + var code: Int + var requestUrl: String? + var hint: String + var requestMethod: String? + var httpStatusCode: Int? + var when: Timestamp? + var stack: String? +} +// MARK: - +/// A request to list exchanges. +fileprivate struct ListExchanges: WalletBackendFormattedRequest { + func operation() -> String { return "listExchanges" } + func args() -> Args { return Args() } + + struct Args: Encodable {} // no arguments needed + + struct Response: Decodable { // list of known exchanges + var exchanges: [Exchange] + } +} + +/// A request to add an exchange. +fileprivate struct AddExchange: WalletBackendFormattedRequest { + func operation() -> String { return "addExchange" } + func args() -> Args { return Args(exchangeBaseUrl: exchangeBaseUrl) } + + var exchangeBaseUrl: String + + struct Args: Encodable { + var exchangeBaseUrl: String + } + + struct Response: Decodable {} +} +// MARK: - +extension WalletModel { + /// ask wallet-core for its list of known exchanges + @MainActor func listExchangesM() + async -> [Exchange] { // M for MainActor + do { + let request = ListExchanges() + logger.info("ListExchanges") + let response = try await sendRequest(request, ASYNCDELAY) + return response.exchanges + } catch { + logger.error("listExchangesM failed: \(error)") + return [] // empty, but not nil + } + } + + /// add a new exchange with URL to the wallet's list of known exchanges + func addExchange(url: String) + async throws { + symLog?.log("adding exchange: \(url)") // TODO: .notice + let request = AddExchange(exchangeBaseUrl: url) + logger.info("adding exchange: \(url)") + _ = try await sendRequest(request) + } +} diff --git a/TalerWallet1/Model/Model+P2P.swift b/TalerWallet1/Model/Model+P2P.swift @@ -0,0 +1,168 @@ +/* + * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. + * See LICENSE.md + */ +import Foundation +import taler_swift +import AnyCodable +//import SymLog +fileprivate let ASYNCDELAY: UInt = 0 //set e.g to 6 or 9 seconds for debugging + +// MARK: - PeerContractTerms +struct PeerContractTerms: Codable { + let amount: Amount + let summary: String + let purse_expiration: Timestamp +} +// MARK: - +/// The result from CheckPeerPushDebit +struct CheckPeerPushDebitResponse: Codable { + let exchangeBaseUrl: String? + let amountRaw: Amount + 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 { + typealias Response = CheckPeerPushDebitResponse + func operation() -> String { return "checkPeerPushDebit" } + func args() -> Args { return Args(amount: amount) } + + var amount: Amount + struct Args: Encodable { + var amount: Amount + } +} +// MARK: - +/// The result from CheckPeerPullCredit +struct CheckPeerPullCreditResponse: Codable { + let scopeInfo: ScopeInfo? + let exchangeBaseUrl: String? + let amountRaw: Amount + let amountEffective: Amount + var numCoins: Int? // Number of coins this amountEffective will create +} +/// A request to check fees before invoicing another wallet. +fileprivate struct CheckPeerPullCredit: WalletBackendFormattedRequest { + typealias Response = CheckPeerPullCreditResponse + func operation() -> String { return "checkPeerPullCredit" } + func args() -> Args { return Args(exchangeBaseUrl: exchangeBaseUrl, scopeInfo: scopeInfo, amount: amount) } + + var exchangeBaseUrl: String? + var scopeInfo: ScopeInfo? + var amount: Amount + struct Args: Encodable { + var exchangeBaseUrl: String? + var scopeInfo: ScopeInfo? + var amount: Amount + } +} +// MARK: - +/// The result from InitiatePeerPushDebit +struct PeerPushResponse: Codable { + let contractPriv: String + let mergePriv: String + let pursePub: String + let exchangeBaseUrl: String + let talerUri: String + let transactionId: String +} +/// A request to send coins to another wallet. +fileprivate struct InitiatePeerPushDebit: WalletBackendFormattedRequest { + typealias Response = PeerPushResponse + func operation() -> String { return "initiatePeerPushDebit" } + func args() -> Args { return Args(exchangeBaseUrl: exchangeBaseUrl, + partialContractTerms: partialContractTerms) } + + var exchangeBaseUrl: String? + var partialContractTerms: PeerContractTerms + struct Args: Encodable { + var exchangeBaseUrl: String? + var partialContractTerms: PeerContractTerms + } +} + +struct PreparePeerPushCreditResponse: Codable { + let contractTerms: PeerContractTerms + let amountRaw: Amount + let amountEffective: Amount + let peerPushPaymentIncomingId: String +} +/// A request to receive coins from another wallet. +fileprivate struct PreparePeerPushCredit: WalletBackendFormattedRequest { + typealias Response = PreparePeerPushCreditResponse + func operation() -> String { return "preparePeerPushCredit" } + func args() -> Args { return Args(talerUri: talerUri) } + + var talerUri: String + struct Args: Encodable { + var talerUri: String + } +} +// MARK: - +/// The result from InitiatePeerPullCredit +struct PeerPullResponse: Codable { + let talerUri: String + let transactionId: String +} +/// A request to send a payment request to another wallet. +fileprivate struct InitiatePeerPullCredit: WalletBackendFormattedRequest { + typealias Response = PeerPullResponse + func operation() -> String { return "initiatePeerPullCredit" } + func args() -> Args { return Args(exchangeBaseUrl: exchangeBaseUrl, + partialContractTerms: partialContractTerms) } + + var exchangeBaseUrl: String? + var partialContractTerms: PeerContractTerms + struct Args: Encodable { + var exchangeBaseUrl: String? + var partialContractTerms: PeerContractTerms + } +} +// MARK: - +extension WalletModel { + /// query exchange for fees (sending coins). Networking involved + @MainActor + func checkPeerPushDebitM(_ amount: Amount) // M for MainActor + async throws -> CheckPeerPushDebitResponse { + let request = CheckPeerPushDebit(amount: amount) + let response = try await sendRequest(request, ASYNCDELAY) + return response + } + /// generate peer-push. Networking involved + @MainActor + func initiatePeerPushDebitM(_ baseURL: String?, terms: PeerContractTerms) // M for MainActor + async throws -> PeerPushResponse { + let request = InitiatePeerPushDebit(exchangeBaseUrl: baseURL, + partialContractTerms: terms) + let response = try await sendRequest(request, ASYNCDELAY) + return response + } + + /// query exchange for fees (invoice coins). Networking involved + @MainActor + func checkPeerPullCreditM(_ amount: Amount, exchangeBaseUrl: String?) // M for MainActor + async throws -> CheckPeerPullCreditResponse { + let request = CheckPeerPullCredit(exchangeBaseUrl: exchangeBaseUrl, amount: amount) + let response = try await sendRequest(request, ASYNCDELAY) + return response + } + /// generate peer-pull. Networking involved + @MainActor + func initiatePeerPullCreditM(_ baseURL: String?, terms: PeerContractTerms) // M for MainActor + async throws -> PeerPullResponse { + let request = InitiatePeerPullCredit(exchangeBaseUrl: baseURL, + partialContractTerms: terms) + let response = try await sendRequest(request, ASYNCDELAY) + return response + } + + /// prepare peer-pull. Networking involved + @MainActor + func preparePeerPushCreditM(_ talerUri: String) // M for MainActor + async throws -> PreparePeerPushCreditResponse { + let request = PreparePeerPushCredit(talerUri: talerUri) + let response = try await sendRequest(request, ASYNCDELAY) + return response + } +} diff --git a/TalerWallet1/Model/Model+Payment.swift b/TalerWallet1/Model/Model+Payment.swift @@ -0,0 +1,143 @@ +/* + * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. + * See LICENSE.md + */ +import Foundation +import taler_swift +import AnyCodable +//import SymLog +fileprivate let ASYNCDELAY: UInt = 0 //set e.g to 6 or 9 seconds for debugging + +// MARK: - ContractTerms +struct ContractTerms: Codable { + let amount: Amount + let maxFee: Amount + let maxWireFee: Amount + let merchant: Merchant + let extra: Extra + let summary: String + let timestamp: Timestamp + let payDeadline: Timestamp + let refundDeadline: Timestamp + let wireTransferDeadline: Timestamp + let merchantBaseURL: String + let fulfillmentURL: String + let publicReorderURL: String + let auditors: [Auditor] + let exchanges: [ExchangeForPay] + let orderID, nonce, merchantPub: String + let products: [Product] + let hWire: String + let wireMethod: String + let wireFeeAmortization: Int + + enum CodingKeys: String, CodingKey { + case amount + case maxFee = "max_fee" + case maxWireFee = "max_wire_fee" + case merchant, extra, summary + case timestamp + case payDeadline = "pay_deadline" + case refundDeadline = "refund_deadline" + case wireTransferDeadline = "wire_transfer_deadline" + case merchantBaseURL = "merchant_base_url" + case fulfillmentURL = "fulfillment_url" + case publicReorderURL = "public_reorder_url" + case auditors, exchanges + case orderID = "order_id" + case nonce + case merchantPub = "merchant_pub" + case products + case hWire = "h_wire" + case wireMethod = "wire_method" + case wireFeeAmortization = "wire_fee_amortization" + } +} +// MARK: - Auditor +struct Auditor: Codable { + let name: String + let auditorPub: String + let url: String + + enum CodingKeys: String, CodingKey { + case name + case auditorPub = "auditor_pub" + case url + } +} +// MARK: - Exchange +struct ExchangeForPay: Codable { + let url: String + let masterPub: String + + enum CodingKeys: String, CodingKey { + case url + case masterPub = "master_pub" + } +} +// MARK: - Extra +struct Extra: Codable { + let articleName: String + + enum CodingKeys: String, CodingKey { + case articleName = "article_name" + } +} +// MARK: - +/// The result from PreparePayForUri +struct PaymentDetailsForUri: Codable { +// let status: String + let amountRaw: Amount + let amountEffective: Amount + let noncePriv: String + let proposalId: String + let contractTerms: ContractTerms + let contractTermsHash: String +} +/// A request to get an exchange's payment contract terms. +fileprivate struct PreparePayForUri: WalletBackendFormattedRequest { + typealias Response = PaymentDetailsForUri + func operation() -> String { return "preparePayForUri" } + func args() -> Args { return Args(talerPayUri: talerPayUri) } + + var talerPayUri: String + struct Args: Encodable { + var talerPayUri: String + } +} +// MARK: - +/// The result from confirmPayForUri +struct ConfirmPayResult: Decodable { + var type: String + var contractTerms: ContractTerms + var transactionId: String +} +/// A request to get an exchange's payment details. +fileprivate struct confirmPayForUri: WalletBackendFormattedRequest { + typealias Response = ConfirmPayResult + func operation() -> String { return "confirmPay" } + func args() -> Args { return Args(proposalId: proposalId) } + + var proposalId: String + struct Args: Encodable { + var proposalId: String + } +} +// MARK: - +extension WalletModel { + /// load payment details. Networking involved + @MainActor + func preparePayForUriM(_ talerPayUri: String) // M for MainActor + async throws -> PaymentDetailsForUri { + 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 { + let request = confirmPayForUri(proposalId: proposalId) + let response = try await sendRequest(request, ASYNCDELAY) + return response + } +} diff --git a/TalerWallet1/Model/Model+Pending.swift b/TalerWallet1/Model/Model+Pending.swift @@ -0,0 +1,55 @@ +/* + * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. + * See LICENSE.md + */ +import Foundation +import AnyCodable +import taler_swift +import SymLog +fileprivate let ASYNCDELAY: UInt = 0 //set e.g to 6 or 9 seconds for debugging + +// MARK: - +/// A request to list the backend's currently pending operations. +fileprivate struct GetPendingOperations: WalletBackendFormattedRequest { + func operation() -> String { return "getPendingOperations" } + func args() -> Args { Args() } + + struct Args: Encodable {} + + struct Response: Decodable { + var pendingOperations: [PendingOperation] + } +} +// MARK: - +struct PendingOperation: Codable, Hashable { + var type: String + var exchangeBaseUrl: String? + var id: String + var isLongpolling: Bool + var givesLifeness: Bool + var isDue: Bool + var timestampDue: Timestamp + + public func hash(into hasher: inout Hasher) { + hasher.combine(type) + hasher.combine(exchangeBaseUrl) + hasher.combine(id) + hasher.combine(isLongpolling) + hasher.combine(givesLifeness) + hasher.combine(isDue) + hasher.combine(timestampDue) + } +} +// 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 [] + } + } +} diff --git a/TalerWallet1/Model/Model+Settings.swift b/TalerWallet1/Model/Model+Settings.swift @@ -0,0 +1,102 @@ +/* + * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. + * See LICENSE.md + */ +import Foundation +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 +fileprivate let DEMO_BANKBASEURL = DEMOBANK +fileprivate let DEMO_BANKAPIBASEURL = DEMOBANK + "/demobanks/default/access-api/" +fileprivate let DEMO_MERCHANTBASEURL = DEMOBACKEND +fileprivate let DEMO_MERCHANTAUTHTOKEN = "secret-token:sandbox" + +// MARK: - +extension WalletModel { + @MainActor func loadTestKudosM() + async throws { // M for MainActor + let amount = Amount(currency: DEMOCURRENCY, integer: 11, fraction: 0) + let request = WalletBackendWithdrawTestBalance(amount: amount, + bankBaseUrl: DEMO_BANKBASEURL, + exchangeBaseUrl: DEMO_EXCHANGEBASEURL, + bankAccessApiBaseUrl: DEMO_BANKAPIBASEURL) + let response = try await sendRequest(request, ASYNCDELAY) + symLog?.log("received: \(response)") + } + + @MainActor func runIntegrationTestM(newVersion: Bool) + async throws { // M for MainActor + let amountW = Amount(currency: DEMOCURRENCY, integer: 3, fraction: 0) + let amountS = Amount(currency: DEMOCURRENCY, integer: 1, fraction: 0) + let request = WalletBackendRunIntegration(newVersion: newVersion, + amountToWithdraw: amountW, + amountToSpend: amountS, + bankBaseUrl: DEMO_BANKAPIBASEURL, + bankAccessApiBaseUrl: DEMO_BANKAPIBASEURL, + exchangeBaseUrl: DEMO_EXCHANGEBASEURL, + merchantBaseUrl: DEMO_MERCHANTBASEURL, + merchantAuthToken: DEMO_MERCHANTAUTHTOKEN) + let _ = try await sendRequest(request, ASYNCDELAY) + symLog?.log("runIntegrationTest finished") + } +} +// MARK: - +/// A request to add a test balance to the wallet. +fileprivate struct WalletBackendWithdrawTestBalance: WalletBackendFormattedRequest { + typealias Response = String + func operation() -> String { return "withdrawTestBalance" } + func args() -> Args { + return Args(amount: amount, bankBaseUrl: bankBaseUrl, + exchangeBaseUrl: exchangeBaseUrl, bankAccessApiBaseUrl: bankAccessApiBaseUrl) + } + + var amount: Amount + var bankBaseUrl: String + var exchangeBaseUrl: String + var bankAccessApiBaseUrl: String + + struct Args: Encodable { + var amount: Amount + var bankBaseUrl: String + var exchangeBaseUrl: String + var bankAccessApiBaseUrl: String + } +} +// MARK: - +/// A request to add a test balance to the wallet. +fileprivate struct WalletBackendRunIntegration: WalletBackendFormattedRequest { + struct Response: Decodable {} + func operation() -> String { return newVersion ? "runIntegrationTestV2" : "runIntegrationTest" } + func args() -> Args { + return Args(amountToWithdraw: amountToWithdraw, + amountToSpend: amountToSpend, + bankBaseUrl: bankBaseUrl, + bankAccessApiBaseUrl: bankAccessApiBaseUrl, + exchangeBaseUrl: exchangeBaseUrl, + merchantBaseUrl: merchantBaseUrl, + merchantAuthToken: merchantAuthToken + ) + } + + let newVersion: Bool + + var amountToWithdraw: Amount + var amountToSpend: Amount + var bankBaseUrl: String + var bankAccessApiBaseUrl: String + var exchangeBaseUrl: String + var merchantBaseUrl: String + var merchantAuthToken: String + + struct Args: Encodable { + var amountToWithdraw: Amount + var amountToSpend: Amount + var bankBaseUrl: String + var bankAccessApiBaseUrl: String + var exchangeBaseUrl: String + var merchantBaseUrl: String + var merchantAuthToken: String + } +} diff --git a/TalerWallet1/Model/Model+Transactions.swift b/TalerWallet1/Model/Model+Transactions.swift @@ -0,0 +1,120 @@ +/* + * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. + * See LICENSE.md + */ +import Foundation +import taler_swift +import SymLog +fileprivate let ASYNCDELAY: UInt = 0 //set e.g to 6 or 9 seconds for debugging + +// MARK: - +extension WalletModel { + static func specialTransactions(_ transactions: [Transaction]) -> [Transaction] { + transactions.filter { transaction in + transaction.isSpecial + } + } + + static func completedTransactions(_ transactions: [Transaction]) -> [Transaction] { + transactions.filter { transaction in + transaction.isDone + } + } + static func pendingTransactions(_ transactions: [Transaction]) -> [Transaction] { + transactions.filter { transaction in + transaction.isPending + } + } + static func uncompletedTransactions(_ transactions: [Transaction]) -> [Transaction] { + transactions.filter { transaction in + !transaction.isDone && !transaction.isPending + } + } +} + +// MARK: - +/// A request to get the transactions in the wallet's history. +fileprivate struct GetTransactions: WalletBackendFormattedRequest { + func operation() -> String { return "getTransactions" } +// func operation() -> String { return "testingGetSampleTransactions" } + func args() -> Args { return Args(currency: currency, search: search) } + + var currency: String? + var search: String? + + struct Args: Encodable { + var currency: String? + var search: String? + } + + struct Response: Decodable { // list of transactions + var transactions: [Transaction] + } +} +/// A request to abort a wallet transaction by ID. +struct AbortTransaction: WalletBackendFormattedRequest { + func operation() -> String { return "abortTransaction" } + func args() -> Args { return Args(transactionId: transactionId) } + + var transactionId: String + + struct Args: Encodable { + var transactionId: String + } + + struct Response: Decodable {} +} +/// A request to delete a wallet transaction by ID. +struct DeleteTransaction: WalletBackendFormattedRequest { + struct Response: Decodable {} + func operation() -> String { return "deleteTransaction" } + func args() -> Args { return Args(transactionId: transactionId) } + + var transactionId: String + + struct Args: Encodable { + var transactionId: String + } +} + +// MARK: - +extension WalletModel { + /// ask wallet-core for its list of transactions filtered by searchString + 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 sendRequest(request, ASYNCDELAY) + return response.transactions + } catch { + 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 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 + try await abortTransactionT(transactionId: transactionId) // call abortTransaction 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 + try await deleteTransactionT(transactionId: transactionId) // call deleteTransaction on main thread + } +} diff --git a/TalerWallet1/Model/Model+Withdraw.swift b/TalerWallet1/Model/Model+Withdraw.swift @@ -0,0 +1,188 @@ +/* + * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. + * See LICENSE.md + */ +import Foundation +import taler_swift +import SymLog +fileprivate let ASYNCDELAY: UInt = 0 //set e.g to 6 or 9 seconds for debugging + +// MARK: - +/// The result from getWithdrawalDetailsForUri +struct WithdrawUriInfoResponse: Decodable { + var amount: Amount + var defaultExchangeBaseUrl: String? // TODO: might be nil ❗️Yikes + var possibleExchanges: [ExchangeListItem] // TODO: query these for fees? +} +struct ExchangeListItem: Codable, Hashable { + var exchangeBaseUrl: String + var currency: String + var paytoUris: [String] + + public static func == (lhs: ExchangeListItem, rhs: ExchangeListItem) -> Bool { + return lhs.exchangeBaseUrl == rhs.exchangeBaseUrl && + lhs.currency == rhs.currency && + lhs.paytoUris == rhs.paytoUris + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(exchangeBaseUrl) + hasher.combine(currency) + hasher.combine(paytoUris) + } +} +/// A request to get an exchange's withdrawal details. +fileprivate struct GetWithdrawalDetailsForURI: WalletBackendFormattedRequest { + typealias Response = WithdrawUriInfoResponse + func operation() -> String { return "getWithdrawalDetailsForUri" } + func args() -> Args { return Args(talerWithdrawUri: talerWithdrawUri) } + + var talerWithdrawUri: String + struct Args: Encodable { + var talerWithdrawUri: String + } +} +// MARK: - +/// The result from getWithdrawalDetailsForAmount +struct ManualWithdrawalDetails: Decodable { + var tosAccepted: Bool // Did the user accept the current version of the exchange's terms of service? + var amountRaw: Amount // Amount that the user will transfer to the exchange + var amountEffective: Amount // Amount that will be added to the user's wallet balance + var paytoUris: [String] // Ways to pay the exchange + var ageRestrictionOptions: [Int]? // Array of ages + var numCoins: Int? // Number of coins this amountEffective will create +} +/// A request to get an exchange's withdrawal details. +fileprivate struct GetWithdrawalDetailsForAmount: WalletBackendFormattedRequest { + typealias Response = ManualWithdrawalDetails + func operation() -> String { return "getWithdrawalDetailsForAmount" } + func args() -> Args { return Args(exchangeBaseUrl: exchangeBaseUrl, amount: amount) } + + var exchangeBaseUrl: String + var amount: Amount + struct Args: Encodable { + var exchangeBaseUrl: String + var amount: Amount + } +} +// MARK: - +struct ExchangeTermsOfService: Decodable { + var content: String + var currentEtag: String + var acceptedEtag: String? +} +/// A request to query an exchange's terms of service. +fileprivate struct GetExchangeTermsOfService: WalletBackendFormattedRequest { + typealias Response = ExchangeTermsOfService + func operation() -> String { return "getExchangeTos" } + func args() -> Args { return Args(exchangeBaseUrl: exchangeBaseUrl) } + + var exchangeBaseUrl: String + struct Args: Encodable { + var exchangeBaseUrl: String + } +} +// MARK: - +/// A request to mark an exchange's terms of service as accepted. +fileprivate struct SetExchangeTOSAccepted: WalletBackendFormattedRequest { + struct Response: Decodable {} + func operation() -> String { return "setExchangeTosAccepted" } + func args() -> Args { return Args(exchangeBaseUrl: exchangeBaseUrl, etag: etag) } + + var exchangeBaseUrl: String + var etag: String + + struct Args: Encodable { + var exchangeBaseUrl: String + var etag: String + } +} +// MARK: - +struct AcceptWithdrawalResponse: Decodable { + var reservePub: String + var confirmTransferUrl: String? + var transactionId: String +} +/// A request to accept a bank-integrated withdrawl. +fileprivate struct AcceptBankIntegratedWithdrawal: WalletBackendFormattedRequest { + typealias Response = AcceptWithdrawalResponse + func operation() -> String { return "acceptBankIntegratedWithdrawal" } + func args() -> Args { return Args(talerWithdrawUri: talerWithdrawUri, exchangeBaseUrl: exchangeBaseUrl) } + + var talerWithdrawUri: String + var exchangeBaseUrl: String + + struct Args: Encodable { + var talerWithdrawUri: String + var exchangeBaseUrl: String + } +} +// MARK: - +struct AcceptManualWithdrawalResult: Decodable { + var reservePub: String + var exchangePaytoUris: [String] + var transactionId: String +} +/// A request to accept a manual withdrawl. +fileprivate struct AcceptManualWithdrawal: WalletBackendFormattedRequest { + typealias Response = AcceptManualWithdrawalResult + func operation() -> String { return "acceptManualWithdrawal" } + func args() -> Args { return Args(exchangeBaseUrl: exchangeBaseUrl, amount: amount, restrictAge: restrictAge) } + + var exchangeBaseUrl: String + var amount: Amount + var restrictAge: Int? + + struct Args: Encodable { + var exchangeBaseUrl: String + var amount: Amount + var restrictAge: Int? + } +} +// MARK: - +extension WalletModel { + /// load withdrawal details. Networking involved + @MainActor + func loadWithdrawalDetailsForUriM(_ talerWithdrawUri: String) // M for MainActor + async throws -> WithdrawUriInfoResponse { + 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 { + 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 { + 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 { + 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 -> AcceptWithdrawalResponse? { + let request = AcceptBankIntegratedWithdrawal(talerWithdrawUri: withdrawURL, exchangeBaseUrl: exchangeBaseUrl) + let response = try await sendRequest(request, ASYNCDELAY) + return response + } + @MainActor + func sendAcceptManualWithdrawalM(_ exchangeBaseUrl: String, amount: Amount, restrictAge: Int?) // M for MainActor + 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/Model/PaymentURIModel.swift b/TalerWallet1/Model/PaymentURIModel.swift @@ -1,169 +0,0 @@ -/* - * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. - * See LICENSE.md - */ -import Foundation -import taler_swift -import AnyCodable -//import SymLog -fileprivate let ASYNCDELAY: UInt = 0 //set e.g to 6 or 9 seconds for debugging - -class PaymentURIModel: WalletModel { - - override init(_ symbol: Int = -1) { // init with 0 to disable logging for this class - super.init(0)//symbol) - } -} -// MARK: - ContractTerms -struct ContractTerms: Codable { - let amount: Amount - let maxFee: Amount - let maxWireFee: Amount - let merchant: Merchant - let extra: Extra - let summary: String - let timestamp: Timestamp - let payDeadline: Timestamp - let refundDeadline: Timestamp - let wireTransferDeadline: Timestamp - let merchantBaseURL: String - let fulfillmentURL: String - let publicReorderURL: String - let auditors: [Auditor] - let exchanges: [ExchangeForPay] - let orderID, nonce, merchantPub: String - let products: [Product] - let hWire: String - let wireMethod: String - let wireFeeAmortization: Int - - enum CodingKeys: String, CodingKey { - case amount - case maxFee = "max_fee" - case maxWireFee = "max_wire_fee" - case merchant, extra, summary - case timestamp - case payDeadline = "pay_deadline" - case refundDeadline = "refund_deadline" - case wireTransferDeadline = "wire_transfer_deadline" - case merchantBaseURL = "merchant_base_url" - case fulfillmentURL = "fulfillment_url" - case publicReorderURL = "public_reorder_url" - case auditors, exchanges - case orderID = "order_id" - case nonce - case merchantPub = "merchant_pub" - case products - case hWire = "h_wire" - case wireMethod = "wire_method" - case wireFeeAmortization = "wire_fee_amortization" - } -} - -// MARK: - Auditor -struct Auditor: Codable { - let name: String - let auditorPub: String - let url: String - - enum CodingKeys: String, CodingKey { - case name - case auditorPub = "auditor_pub" - case url - } -} - -// MARK: - Exchange -struct ExchangeForPay: Codable { - let url: String - let masterPub: String - - enum CodingKeys: String, CodingKey { - case url - case masterPub = "master_pub" - } -} - -// MARK: - Extra -struct Extra: Codable { - let articleName: String - - enum CodingKeys: String, CodingKey { - case articleName = "article_name" - } -} - -// MARK: - -/// The result from PreparePayForUri -struct PaymentDetailsForUri: Codable { -// let status: String - let amountRaw: Amount - let amountEffective: Amount - let noncePriv: String - let proposalId: String - let contractTerms: ContractTerms - let contractTermsHash: String -} -/// A request to get an exchange's payment contract terms. -fileprivate struct PreparePayForUri: WalletBackendFormattedRequest { - typealias Response = PaymentDetailsForUri - func operation() -> String { return "preparePayForUri" } - func args() -> Args { return Args(talerPayUri: talerPayUri) } - - var talerPayUri: String - struct Args: Encodable { - var talerPayUri: String - } -} -// MARK: - -/// The result from confirmPayForUri -struct ConfirmPayResult: Decodable { - var type: String - var contractTerms: ContractTerms - var transactionId: String -} -/// A request to get an exchange's payment details. -fileprivate struct confirmPayForUri: WalletBackendFormattedRequest { - typealias Response = ConfirmPayResult - func operation() -> String { return "confirmPay" } - func args() -> Args { return Args(proposalId: proposalId) } - - var proposalId: String - struct Args: Encodable { - var proposalId: String - } -} -// MARK: - -extension PaymentURIModel { - /// load payment details. Networking involved - @MainActor - func preparePayForUriM(_ talerPayUri: String) // M for MainActor - async throws -> PaymentDetailsForUri { - 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 { - let request = confirmPayForUri(proposalId: proposalId) - let response = try await sendRequest(request, ASYNCDELAY) - return response - } -} - -// MARK: - -extension PaymentURIModel { - private static var models: [PaymentURIModel] = [] // a list of models even though I currently need only one - - static func model() -> PaymentURIModel { - if PaymentURIModel.models.count > 0 { - let model = PaymentURIModel.models[0] - return model - } else { // new model - let model = PaymentURIModel() - PaymentURIModel.models.append(model) - return model - } - } -} diff --git a/TalerWallet1/Model/Peer2peerModel.swift b/TalerWallet1/Model/Peer2peerModel.swift @@ -1,134 +0,0 @@ -/* - * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. - * See LICENSE.md - */ -import Foundation -import taler_swift -import AnyCodable -//import SymLog -fileprivate let ASYNCDELAY: UInt = 0 //set e.g to 6 or 9 seconds for debugging - -class Peer2peerModel: WalletModel { - override init(_ symbol: Int = -1) { // init with 0 to disable logging for this class - super.init(0)//symbol) - } -} -// MARK: - PeerContractTerms -struct PeerContractTerms: Codable { - let amount: Amount - let summary: String - let purse_expiration: Timestamp -} -// MARK: - -/// The result from CheckPeerPushDebit -struct CheckPeerPushDebitResponse: Codable { - let exchangeBaseUrl: String? - let amountRaw: Amount - 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 { - typealias Response = CheckPeerPushDebitResponse - func operation() -> String { return "checkPeerPushDebit" } - func args() -> Args { return Args(amount: amount) } - - var amount: Amount - struct Args: Encodable { - var amount: Amount - } -} -// MARK: - -/// The result from CheckPeerPullCredit -struct CheckPeerPullCreditResponse: Codable { - let scopeInfo: ScopeInfo? - let exchangeBaseUrl: String? - let amountRaw: Amount - let amountEffective: Amount - var numCoins: Int? // Number of coins this amountEffective will create -} -/// A request to check fees before invoicing another wallet. -fileprivate struct CheckPeerPullCredit: WalletBackendFormattedRequest { - typealias Response = CheckPeerPullCreditResponse - func operation() -> String { return "checkPeerPullCredit" } - func args() -> Args { return Args(exchangeBaseUrl: exchangeBaseUrl, scopeInfo: scopeInfo, amount: amount) } - - var exchangeBaseUrl: String? - var scopeInfo: ScopeInfo? - var amount: Amount - struct Args: Encodable { - var exchangeBaseUrl: String? - var scopeInfo: ScopeInfo? - var amount: Amount - } -} -// MARK: - -/// The result from InitiatePeerPushDebit -struct PeerPushResponse: Codable { - let contractPriv: String - let mergePriv: String - let pursePub: String - let exchangeBaseUrl: String - let talerUri: String - let transactionId: String -} -/// A request to send coins to another wallet. -fileprivate struct InitiatePeerPushDebit: WalletBackendFormattedRequest { - typealias Response = PeerPushResponse - func operation() -> String { return "initiatePeerPushDebit" } - func args() -> Args { return Args(exchangeBaseUrl: exchangeBaseUrl, - partialContractTerms: partialContractTerms) } - - var exchangeBaseUrl: String? - var partialContractTerms: PeerContractTerms - struct Args: Encodable { - var exchangeBaseUrl: String? - var partialContractTerms: PeerContractTerms - } -} -// MARK: - -extension Peer2peerModel { - /// query exchange for fees (sending coins). Networking involved - @MainActor - func checkPeerPushDebitM(_ amount: Amount) // M for MainActor - async throws -> CheckPeerPushDebitResponse { - let request = CheckPeerPushDebit(amount: amount) - let response = try await sendRequest(request, ASYNCDELAY) - return response - } - /// query exchange for fees (invoice coins). Networking involved - @MainActor - func checkPeerPullCreditM(_ amount: Amount, exchangeBaseUrl: String?) // M for MainActor - async throws -> CheckPeerPullCreditResponse { - let request = CheckPeerPullCredit(exchangeBaseUrl: exchangeBaseUrl, amount: amount) - let response = try await sendRequest(request, ASYNCDELAY) - return response - } - /// generate peer-push. Networking involved - @MainActor - func initiatePeerPushDebitM(_ baseURL: String?, terms: PeerContractTerms) // M for MainActor - async throws -> PeerPushResponse { - let request = InitiatePeerPushDebit(exchangeBaseUrl: baseURL, - partialContractTerms: terms) - let response = try await sendRequest(request, ASYNCDELAY) - return response - } -} - -// MARK: - -extension Peer2peerModel { - private static var exchanges: [String] = [] // names of exchanges - private static var models: [Peer2peerModel] = [] // one model per exchange - - static func model(baseURL: String) -> Peer2peerModel { - if let index = Peer2peerModel.exchanges.firstIndex(of:baseURL) { - let model = Peer2peerModel.models[index] - return model - } else { // new model - let model = Peer2peerModel() - Peer2peerModel.models.append(model) - Peer2peerModel.exchanges.append(baseURL) - return model - } - } -} diff --git a/TalerWallet1/Model/PendingModel.swift b/TalerWallet1/Model/PendingModel.swift @@ -1,78 +0,0 @@ -/* - * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. - * See LICENSE.md - */ -import Foundation -import AnyCodable -import taler_swift -import SymLog -fileprivate let ASYNCDELAY: UInt = 0 //set e.g to 6 or 9 seconds for debugging - -class PendingModel: WalletModel { - - override init(_ symbol: Int = -1) { - super.init(0)//symbol) - } -} -// MARK: - -/// A request to list the backend's currently pending operations. -fileprivate struct GetPendingOperations: WalletBackendFormattedRequest { - func operation() -> String { return "getPendingOperations" } - func args() -> Args { Args() } - - struct Args: Encodable {} - - struct Response: Decodable { - var pendingOperations: [PendingOperation] - } -} -// MARK: - -struct PendingOperation: Codable, Hashable { - var type: String - var exchangeBaseUrl: String? - var id: String - var isLongpolling: Bool - var givesLifeness: Bool - var isDue: Bool - var timestampDue: Timestamp - - public func hash(into hasher: inout Hasher) { - hasher.combine(type) - hasher.combine(exchangeBaseUrl) - hasher.combine(id) - hasher.combine(isLongpolling) - hasher.combine(givesLifeness) - hasher.combine(isDue) - hasher.combine(timestampDue) - } - -} -// MARK: - -extension PendingModel { - @MainActor func getPendingOperationsM() - async -> [PendingOperation] { // M for MainActor - do { - let request = GetPendingOperations() - let response = try await sendRequest(request, ASYNCDELAY) - return response.pendingOperations - } catch { - return [] - } - } -} - -// MARK: - -extension PendingModel { - private static var models: [PendingModel] = [] // a list of models even though I currently need only one - - static func model() -> PendingModel { - if PendingModel.models.count > 0 { - let model = PendingModel.models[0] - return model - } else { // new model - let model = PendingModel() - PendingModel.models.append(model) - return model - } - } -} diff --git a/TalerWallet1/Model/SettingsModel.swift b/TalerWallet1/Model/SettingsModel.swift @@ -1,108 +0,0 @@ -/* - * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. - * See LICENSE.md - */ -import Foundation -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 -fileprivate let DEMO_BANKBASEURL = DEMOBANK -fileprivate let DEMO_BANKAPIBASEURL = DEMOBANK + "/demobanks/default/access-api/" -fileprivate let DEMO_MERCHANTBASEURL = DEMOBACKEND -fileprivate let DEMO_MERCHANTAUTHTOKEN = "secret-token:sandbox" - -// MARK: - -class SettingsModel: WalletModel { - override init(_ symbol: Int = -1) { // init with 0 to disable logging for this class - super.init(0)//symbol) - } -} -// MARK: - -extension SettingsModel { - @MainActor func loadTestKudosM() - async throws { // M for MainActor - let amount = Amount(currency: DEMOCURRENCY, integer: 11, fraction: 0) - let request = WalletBackendWithdrawTestBalance(amount: amount, - bankBaseUrl: DEMO_BANKBASEURL, - exchangeBaseUrl: DEMO_EXCHANGEBASEURL, - bankAccessApiBaseUrl: DEMO_BANKAPIBASEURL) - let response = try await sendRequest(request, ASYNCDELAY) - symLog?.log("received: \(response)") - } - - @MainActor func runIntegrationTestM(newVersion: Bool) - async throws { // M for MainActor - let amountW = Amount(currency: DEMOCURRENCY, integer: 3, fraction: 0) - let amountS = Amount(currency: DEMOCURRENCY, integer: 1, fraction: 0) - let request = WalletBackendRunIntegration(newVersion: newVersion, - amountToWithdraw: amountW, - amountToSpend: amountS, - bankBaseUrl: DEMO_BANKAPIBASEURL, - bankAccessApiBaseUrl: DEMO_BANKAPIBASEURL, - exchangeBaseUrl: DEMO_EXCHANGEBASEURL, - merchantBaseUrl: DEMO_MERCHANTBASEURL, - merchantAuthToken: DEMO_MERCHANTAUTHTOKEN) - let _ = try await sendRequest(request, ASYNCDELAY) - symLog?.log("runIntegrationTest finished") - } -} -// MARK: - -/// A request to add a test balance to the wallet. -fileprivate struct WalletBackendWithdrawTestBalance: WalletBackendFormattedRequest { - typealias Response = String - func operation() -> String { return "withdrawTestBalance" } - func args() -> Args { - return Args(amount: amount, bankBaseUrl: bankBaseUrl, - exchangeBaseUrl: exchangeBaseUrl, bankAccessApiBaseUrl: bankAccessApiBaseUrl) - } - - var amount: Amount - var bankBaseUrl: String - var exchangeBaseUrl: String - var bankAccessApiBaseUrl: String - - struct Args: Encodable { - var amount: Amount - var bankBaseUrl: String - var exchangeBaseUrl: String - var bankAccessApiBaseUrl: String - } -} -// MARK: - -/// A request to add a test balance to the wallet. -fileprivate struct WalletBackendRunIntegration: WalletBackendFormattedRequest { - struct Response: Decodable {} - func operation() -> String { return newVersion ? "runIntegrationTestV2" : "runIntegrationTest" } - func args() -> Args { - return Args(amountToWithdraw: amountToWithdraw, - amountToSpend: amountToSpend, - bankBaseUrl: bankBaseUrl, - bankAccessApiBaseUrl: bankAccessApiBaseUrl, - exchangeBaseUrl: exchangeBaseUrl, - merchantBaseUrl: merchantBaseUrl, - merchantAuthToken: merchantAuthToken - ) - } - - let newVersion: Bool - - var amountToWithdraw: Amount - var amountToSpend: Amount - var bankBaseUrl: String - var bankAccessApiBaseUrl: String - var exchangeBaseUrl: String - var merchantBaseUrl: String - var merchantAuthToken: String - - struct Args: Encodable { - var amountToWithdraw: Amount - var amountToSpend: Amount - var bankBaseUrl: String - var bankAccessApiBaseUrl: String - var exchangeBaseUrl: String - var merchantBaseUrl: String - var merchantAuthToken: String - } -} diff --git a/TalerWallet1/Model/TransactionsModel.swift b/TalerWallet1/Model/TransactionsModel.swift @@ -1,143 +0,0 @@ -/* - * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. - * See LICENSE.md - */ -import Foundation -import taler_swift -import SymLog -fileprivate let ASYNCDELAY: UInt = 0 //set e.g to 6 or 9 seconds for debugging - -// MARK: - -class TransactionsModel: WalletModel { - - static func specialTransactions(_ transactions: [Transaction]) -> [Transaction] { - transactions.filter { transaction in - transaction.isSpecial - } - } - - static func completedTransactions(_ transactions: [Transaction]) -> [Transaction] { - transactions.filter { transaction in - transaction.isDone - } - } - static func pendingTransactions(_ transactions: [Transaction]) -> [Transaction] { - transactions.filter { transaction in - transaction.isPending - } - } - static func uncompletedTransactions(_ transactions: [Transaction]) -> [Transaction] { - transactions.filter { transaction in - !transaction.isDone && !transaction.isPending - } - } - - override init(_ symbol: Int = -1) { - super.init(0)//symbol) - } -} - -// MARK: - -/// A request to get the transactions in the wallet's history. -fileprivate struct GetTransactions: WalletBackendFormattedRequest { - func operation() -> String { return "getTransactions" } -// func operation() -> String { return "testingGetSampleTransactions" } - func args() -> Args { return Args(currency: currency, search: search) } - - var currency: String? - var search: String? - - struct Args: Encodable { - var currency: String? - var search: String? - } - - struct Response: Decodable { // list of transactions - var transactions: [Transaction] - } -} -/// A request to abort a wallet transaction by ID. -struct AbortTransaction: WalletBackendFormattedRequest { - func operation() -> String { return "abortTransaction" } - func args() -> Args { return Args(transactionId: transactionId) } - - var transactionId: String - - struct Args: Encodable { - var transactionId: String - } - - struct Response: Decodable {} -} -/// A request to delete a wallet transaction by ID. -struct DeleteTransaction: WalletBackendFormattedRequest { - struct Response: Decodable {} - func operation() -> String { return "deleteTransaction" } - func args() -> Args { return Args(transactionId: transactionId) } - - var transactionId: String - - struct Args: Encodable { - var transactionId: String - } -} - -// MARK: - -extension TransactionsModel { - /// ask wallet-core for its list of transactions filtered by searchString - 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 sendRequest(request, ASYNCDELAY) - return response.transactions - } catch { - 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 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 - try await abortTransactionT(transactionId: transactionId) // call abortTransaction 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 - try await deleteTransactionT(transactionId: transactionId) // call deleteTransaction on main thread - } -} - -// MARK: - -extension TransactionsModel { - private static var currencies: [String] = [] // names of currencies - private static var models: [TransactionsModel] = [] // one model per currency - - static func model(currency: String) -> TransactionsModel { - if let index = TransactionsModel.currencies.firstIndex(of:currency) { - let model = TransactionsModel.models[index] - return model - } else { // new currency - let model = TransactionsModel() - TransactionsModel.models.append(model) - TransactionsModel.currencies.append(currency) - return model - } - } -} diff --git a/TalerWallet1/Model/WalletInitModel.swift b/TalerWallet1/Model/WalletInitModel.swift @@ -1,72 +0,0 @@ -/* - * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. - * See LICENSE.md - */ -import Foundation -import SymLog - -private let DATABASE = "talerwalletdb-v30" - -class WalletInitModel: WalletModel { - override init(_ symbol: Int = -1) { // init with 0 to disable logging for this class - super.init(0)//symbol) - } -} -// MARK: - -/// A request to initialize Wallet-core -fileprivate struct WalletBackendInitRequest: WalletBackendFormattedRequest { - func operation() -> String { return "init" } - func args() -> Args { - return Args(persistentStoragePath: persistentStoragePath, -// cryptoWorkerType: "sync", - logLevel: "info") // "trace", "info", "warn", "error", "none" - } - - struct Args: Encodable { - var persistentStoragePath: String -// var cryptoWorkerType: String - var logLevel: String - } - - var persistentStoragePath: String - - struct Response: Decodable { - var versionInfo: VersionInfo - } -} -// MARK: - -/// The info returned from Wallet-core init -struct VersionInfo: Decodable { - var hash: String - var version: String - var exchange: String - var merchant: String - var bank: String - var devMode: Bool -} -// MARK: - -extension WalletInitModel { - /// initalize Wallet-Core. Will do networking - func initWalletT() // T for any Thread - async throws -> VersionInfo? { - 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 { - let documentUrls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) - if (documentUrls.count > 0) { - var storageDir = documentUrls[0] - storageDir.appendPathComponent(DATABASE, isDirectory: false) - storageDir.appendPathExtension("json") - return storageDir.path - } else { // should never happen - symLog?.log("Yikes❗️ documentURLs empty") // TODO: symLog.error - throw WalletBackendError.initializationError - } - } -} - diff --git a/TalerWallet1/Model/WalletModel.swift b/TalerWallet1/Model/WalletModel.swift @@ -4,25 +4,29 @@ */ import Foundation import SymLog +import os +fileprivate let DATABASE = "talerwalletdb-v30" 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 { +class WalletModel: ObservableObject { + public static let shared = WalletModel(-1) static func className() -> String {"\(self)"} var symLog: SymLogC? + let logger = Logger (subsystem: "net.taler.gnu", category: "wallet-core") init(_ symbol: Int) { // init with 0 to disable logging for this class self.symLog = SymLogC(symbol == 0 ? 0 : -1, funcName: Self.className()) } func sendRequest<T: WalletBackendFormattedRequest> (_ request: T, _ delay: UInt = 0) - async throws -> T.Response { // T for any Thread + async throws -> T.Response { // T for any Thread + symLog?.log("sending: \(request)") let sendTime = Date.now do { - symLog?.log("sending: \(request)") let (response, id) = try await WalletCore.shared.sendFormattedRequest(request: request) let timeUsed = Date.now - sendTime let asyncDelay: UInt = delay > 0 ? delay : UInt(ASYNCDELAY) @@ -66,4 +70,67 @@ fileprivate struct GetTransactionById: WalletBackendFormattedRequest { var transactionId: String } } +// MARK: - +/// The info returned from Wallet-core init +struct VersionInfo: Decodable { + var hash: String + var version: String + var exchange: String + var merchant: String + var bank: String + var devMode: Bool +} +// MARK: - +/// A request to initialize Wallet-core +fileprivate struct InitRequest: WalletBackendFormattedRequest { + func operation() -> String { return "init" } + func args() -> Args { + return Args(persistentStoragePath: persistentStoragePath, +// cryptoWorkerType: "sync", + logLevel: "info") // trace, info, warn, error, none + } + + struct Args: Encodable { + var persistentStoragePath: String +// var cryptoWorkerType: String + var logLevel: String + } + + var persistentStoragePath: String + + struct Response: Decodable { + var versionInfo: VersionInfo + } +} +// MARK: - +extension WalletModel { + /// initalize Wallet-Core. Will do networking + func initWalletCoreT() async throws -> VersionInfo { + do { // T for any Thread + let docPath = try docPath() + let request = InitRequest(persistentStoragePath: docPath) + logger.info("initWalletCoreT") + let response = try await sendRequest(request, 0) // no Delay + return response.versionInfo + } catch { + logger.error("initWalletCoreT failed: \(error)") + throw error + } + } + private func docPath () throws -> String { + let documentUrls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) + if (documentUrls.count > 0) { + var storageDir = documentUrls[0] + storageDir.appendPathComponent(DATABASE, isDirectory: false) + storageDir.appendPathExtension("json") + let docPath = storageDir.path + logger.debug("\(docPath)") + return docPath + } else { // should never happen + symLog?.log("Yikes❗️ documentURLs empty") + logger.error("documentURLs empty") + throw WalletBackendError.initializationError + } + } +} diff --git a/TalerWallet1/Model/WithdrawModel.swift b/TalerWallet1/Model/WithdrawModel.swift @@ -1,213 +0,0 @@ -/* - * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. - * See LICENSE.md - */ -import Foundation -import taler_swift -import SymLog -fileprivate let ASYNCDELAY: UInt = 0 //set e.g to 6 or 9 seconds for debugging - -class WithdrawModel: WalletModel { - - override init(_ symbol: Int = -1) { // init with 0 to disable logging for this class - super.init(0)//symbol) - } -} - -// MARK: - -/// The result from getWithdrawalDetailsForUri -struct WithdrawUriInfoResponse: Decodable { - var amount: Amount - var defaultExchangeBaseUrl: String? // TODO: might be nil ❗️Yikes - var possibleExchanges: [ExchangeListItem] // TODO: query these for fees? -} -struct ExchangeListItem: Codable, Hashable { - var exchangeBaseUrl: String - var currency: String - var paytoUris: [String] - - public static func == (lhs: ExchangeListItem, rhs: ExchangeListItem) -> Bool { - return lhs.exchangeBaseUrl == rhs.exchangeBaseUrl && - lhs.currency == rhs.currency && - lhs.paytoUris == rhs.paytoUris - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(exchangeBaseUrl) - hasher.combine(currency) - hasher.combine(paytoUris) - } -} -/// A request to get an exchange's withdrawal details. -fileprivate struct GetWithdrawalDetailsForURI: WalletBackendFormattedRequest { - typealias Response = WithdrawUriInfoResponse - func operation() -> String { return "getWithdrawalDetailsForUri" } - func args() -> Args { return Args(talerWithdrawUri: talerWithdrawUri) } - - var talerWithdrawUri: String - struct Args: Encodable { - var talerWithdrawUri: String - } -} -// MARK: - -/// The result from getWithdrawalDetailsForAmount -struct ManualWithdrawalDetails: Decodable { - var tosAccepted: Bool // Did the user accept the current version of the exchange's terms of service? - var amountRaw: Amount // Amount that the user will transfer to the exchange - var amountEffective: Amount // Amount that will be added to the user's wallet balance - var paytoUris: [String] // Ways to pay the exchange - var ageRestrictionOptions: [Int]? // Array of ages - var numCoins: Int? // Number of coins this amountEffective will create -} -/// A request to get an exchange's withdrawal details. -fileprivate struct GetWithdrawalDetailsForAmount: WalletBackendFormattedRequest { - typealias Response = ManualWithdrawalDetails - func operation() -> String { return "getWithdrawalDetailsForAmount" } - func args() -> Args { return Args(exchangeBaseUrl: exchangeBaseUrl, amount: amount) } - - var exchangeBaseUrl: String - var amount: Amount - struct Args: Encodable { - var exchangeBaseUrl: String - var amount: Amount - } -} -// MARK: - -struct ExchangeTermsOfService: Decodable { - var content: String - var currentEtag: String - var acceptedEtag: String? -} -/// A request to query an exchange's terms of service. -fileprivate struct GetExchangeTermsOfService: WalletBackendFormattedRequest { - typealias Response = ExchangeTermsOfService - func operation() -> String { return "getExchangeTos" } - func args() -> Args { return Args(exchangeBaseUrl: exchangeBaseUrl) } - - var exchangeBaseUrl: String - struct Args: Encodable { - var exchangeBaseUrl: String - } -} -// MARK: - -/// A request to mark an exchange's terms of service as accepted. -fileprivate struct SetExchangeTOSAccepted: WalletBackendFormattedRequest { - struct Response: Decodable {} - func operation() -> String { return "setExchangeTosAccepted" } - func args() -> Args { return Args(exchangeBaseUrl: exchangeBaseUrl, etag: etag) } - - var exchangeBaseUrl: String - var etag: String - - struct Args: Encodable { - var exchangeBaseUrl: String - var etag: String - } -} -// MARK: - -struct AcceptWithdrawalResponse: Decodable { - var reservePub: String - var confirmTransferUrl: String? - var transactionId: String -} -/// A request to accept a bank-integrated withdrawl. -fileprivate struct AcceptBankIntegratedWithdrawal: WalletBackendFormattedRequest { - typealias Response = AcceptWithdrawalResponse - func operation() -> String { return "acceptBankIntegratedWithdrawal" } - func args() -> Args { return Args(talerWithdrawUri: talerWithdrawUri, exchangeBaseUrl: exchangeBaseUrl) } - - var talerWithdrawUri: String - var exchangeBaseUrl: String - - struct Args: Encodable { - var talerWithdrawUri: String - var exchangeBaseUrl: String - } -} -// MARK: - -struct AcceptManualWithdrawalResult: Decodable { - var reservePub: String - var exchangePaytoUris: [String] - var transactionId: String -} -/// A request to accept a manual withdrawl. -fileprivate struct AcceptManualWithdrawal: WalletBackendFormattedRequest { - typealias Response = AcceptManualWithdrawalResult - func operation() -> String { return "acceptManualWithdrawal" } - func args() -> Args { return Args(exchangeBaseUrl: exchangeBaseUrl, amount: amount, restrictAge: restrictAge) } - - var exchangeBaseUrl: String - var amount: Amount - var restrictAge: Int? - - struct Args: Encodable { - var exchangeBaseUrl: String - var amount: Amount - var restrictAge: Int? - } -} -// MARK: - -extension WithdrawModel { - /// load withdrawal details. Networking involved - @MainActor - func loadWithdrawalDetailsForUriM(_ talerWithdrawUri: String) // M for MainActor - async throws -> WithdrawUriInfoResponse { - 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 { - 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 { - 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 { - 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 -> AcceptWithdrawalResponse? { - let request = AcceptBankIntegratedWithdrawal(talerWithdrawUri: withdrawURL, exchangeBaseUrl: exchangeBaseUrl) - let response = try await sendRequest(request, ASYNCDELAY) - return response - } - @MainActor - func sendAcceptManualWithdrawalM(_ exchangeBaseUrl: String, amount: Amount, restrictAge: Int?) // M for MainActor - async throws -> AcceptManualWithdrawalResult? { - let request = AcceptManualWithdrawal(exchangeBaseUrl: exchangeBaseUrl, amount: amount, restrictAge: restrictAge) - let response = try await sendRequest(request, ASYNCDELAY) - return response - } -} - -// MARK: - -extension WithdrawModel { - private static var exchanges: [String] = [] // names of exchanges - private static var models: [WithdrawModel] = [] // one model per exchange - - static func model(baseURL: String) -> WithdrawModel { - if let index = WithdrawModel.exchanges.firstIndex(of:baseURL) { - let model = WithdrawModel.models[index] - return model - } else { // new model - let model = WithdrawModel() - WithdrawModel.models.append(model) - WithdrawModel.exchanges.append(baseURL) - return model - } - } -} diff --git a/TalerWallet1/Views/Balances/BalancesListView.swift b/TalerWallet1/Views/Balances/BalancesListView.swift @@ -12,13 +12,13 @@ import AVFoundation struct BalancesListView: View { private let symLog = SymLogV(0) let navTitle: String + let hamburgerAction: () -> Void @State var balances: [Balance] = [] - let model: BalancesModel? - let hamburgerAction: () -> Void + @EnvironmentObject private var model: WalletModel @State private var centsToTransfer: UInt64 = 0 - @State private var purpose: String = "" + @State private var summary: String = "" @State private var showQRScanner: Bool = false @State private var showCameraAlert: Bool = false @@ -62,11 +62,7 @@ struct BalancesListView: View { } private func reloadAction() async { - if let model { - balances = await model.fetchBalancesM() - } else { - balances = [] - } + balances = await model.balancesM() } var body: some View { @@ -75,7 +71,7 @@ struct BalancesListView: View { let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear #endif Content(symLog: symLog, balances: $balances, - centsToTransfer: $centsToTransfer, purpose: $purpose, + centsToTransfer: $centsToTransfer, summary: $summary, reloadAction: reloadAction) .navigationTitle(navTitle) .navigationBarTitleDisplayMode(.automatic) @@ -111,7 +107,7 @@ extension BalancesListView { @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic @Binding var balances: [Balance] @Binding var centsToTransfer: UInt64 - @Binding var purpose: String + @Binding var summary: String var reloadAction: () async -> Void var body: some View { @@ -121,11 +117,9 @@ extension BalancesListView { #endif 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, - purpose: $purpose, - model: model) + summary: $summary) } .refreshable { symLog?.log("refreshing") diff --git a/TalerWallet1/Views/Balances/BalancesSectionView.swift b/TalerWallet1/Views/Balances/BalancesSectionView.swift @@ -18,8 +18,9 @@ struct BalancesSectionView: View { private let symLog = SymLogV() var balance:Balance @Binding var centsToTransfer: UInt64 - @Binding var purpose: String - var model: TransactionsModel? + @Binding var summary: String + + @EnvironmentObject private var model: WalletModel @State private var isShowingDetailView = false @State private var buttonSelected: Int? = nil @@ -29,15 +30,33 @@ struct BalancesSectionView: View { @State private var pendingTransactions: [Transaction] = [] @State private var uncompletedTransactions: [Transaction] = [] - func dummyTransaction (_ transactionId: String) async throws {} +// func dummyTransaction (_ transactionId: String) async throws {} func reloadOneAction(_ transactionId: String) async throws -> Transaction { - if let model { - return try await model.getTransactionByIdT(transactionId) - } else { - throw WalletBackendError.walletCoreError + return try await model.getTransactionByIdT(transactionId) + } + + func computePending(currency: String) -> (Amount, Amount) { + var incoming = Amount(currency: currency, value: 0) + var outgoing = Amount(currency: currency, value: 0) + for transaction in pendingTransactions { + let effective = transaction.common.amountEffective + if currency == effective.currencyStr { + do { + if transaction.common.incoming() { + incoming = try incoming + effective + } else { + outgoing = try outgoing + effective + } + } catch { + // TODO: log error + symLog.log(error.localizedDescription) + } + } } + return (incoming, outgoing) } + var body: some View { #if DEBUG let _ = Self._printChanges() @@ -45,27 +64,19 @@ struct BalancesSectionView: View { #endif let currency = balance.available.currencyStr let reloadCompleted = { - if let model { - transactions = await model.fetchTransactionsT(currency: currency) - completedTransactions = TransactionsModel.completedTransactions(transactions) - } + transactions = await model.fetchTransactionsT(currency: currency) + completedTransactions = WalletModel.completedTransactions(transactions) } let reloadPending = { - if let model { - transactions = await model.fetchTransactionsT(currency: currency) - pendingTransactions = TransactionsModel.pendingTransactions(transactions) - } + transactions = await model.fetchTransactionsT(currency: currency) + pendingTransactions = WalletModel.pendingTransactions(transactions) } let reloadUncompleted = { - if let model { - transactions = await model.fetchTransactionsT(currency: currency) - uncompletedTransactions = TransactionsModel.uncompletedTransactions(transactions) - } + transactions = await model.fetchTransactionsT(currency: currency) + uncompletedTransactions = WalletModel.uncompletedTransactions(transactions) } - let deleteAction = model?.deleteTransactionT ?? dummyTransaction - let abortAction = model?.abortTransactionT ?? dummyTransaction - - let p2pModel = Peer2peerModel.model(baseURL: currency) + let deleteAction = model.deleteTransactionT // dummyTransaction + let abortAction = model.abortTransactionT Section { // if "KUDOS" == currency && !balance.available.isZero { @@ -74,18 +85,16 @@ struct BalancesSectionView: View { // } HStack(spacing: 0) { NavigationLink(destination: LazyView { - SendAmount(model: p2pModel, - amountAvailable: balance.available, - centsToTransfer: $centsToTransfer, - purpose: $purpose) + SendAmount(amountAvailable: balance.available, + centsToTransfer: $centsToTransfer, + summary: $summary) }, tag: 1, selection: $buttonSelected ) { EmptyView() }.frame(width: 0).opacity(0).hidden() NavigationLink(destination: LazyView { - RequestPayment(model: p2pModel, - scopeInfo: balance.scopeInfo, - centsToTransfer: $centsToTransfer, - purpose: $purpose) + RequestPayment(scopeInfo: balance.scopeInfo, + centsToTransfer: $centsToTransfer, + summary: $summary) }, tag: 2, selection: $buttonSelected ) { EmptyView() }.frame(width: 0).opacity(0).hidden() @@ -100,22 +109,22 @@ struct BalancesSectionView: View { ) { EmptyView() }.frame(width: 0).opacity(0).hidden() BalanceRowView(amount: balance.available, sendAction: { -print("button: Send \(currency)") +//print("button: Send \(currency)") buttonSelected = 1 // will trigger SendAmount NavigationLink }, recvAction: { -print("button: Request Payment: \(currency)") +//print("button: Request Payment: \(currency)") buttonSelected = 2 // will trigger RequestPayment NavigationLink }, rowAction: { -print("button: Transactions: \(currency)") +//print("button: Transactions: \(currency)") buttonSelected = 3 // will trigger TransactionList NavigationLink }) } let hasPending = pendingTransactions.count > 0 if hasPending { - let hasPendingIn = !balance.pendingIncoming.isZero - let hasPendingOut = !balance.pendingOutgoing.isZero + let (pendingIncoming, pendingOutgoing) = computePending(currency: currency) + NavigationLink { -let _ = print("button: Pending Transactions: \(currency)") +//let _ = print("button: Pending Transactions: \(currency)") LazyView { TransactionsListView(navTitle: String(localized: "Pending"), currency: currency, transactions: pendingTransactions, @@ -126,11 +135,17 @@ let _ = print("button: Pending Transactions: \(currency)") } } label: { VStack(spacing: 6) { - if hasPendingIn { - PendingRowView(amount: balance.pendingIncoming, incoming: true) + var rows = 0 + if !pendingIncoming.isZero { + PendingRowView(amount: pendingIncoming, incoming: true) + let _ = (rows+=1) + } + if !pendingOutgoing.isZero { + PendingRowView(amount: pendingOutgoing, incoming: false) + let _ = (rows+=1) } - if hasPendingOut { - PendingRowView(amount: balance.pendingOutgoing, incoming: false) + if rows == 0 { + Text("Some pending transactions") } } } @@ -138,7 +153,7 @@ let _ = print("button: Pending Transactions: \(currency)") let hasUncompleted = uncompletedTransactions.count > 0 if hasUncompleted { NavigationLink { -let _ = print("button: Uncompleted Transactions: \(currency)") +//let _ = print("button: Uncompleted Transactions: \(currency)") LazyView { TransactionsListView(navTitle: String(localized: "Uncompleted"), currency: currency, transactions: uncompletedTransactions, @@ -156,11 +171,10 @@ let _ = print("button: Uncompleted Transactions: \(currency)") Text(currency) .font(.title) } .task { - if let model { - transactions = await model.fetchTransactionsT(currency: currency) - pendingTransactions = TransactionsModel.pendingTransactions(transactions) - uncompletedTransactions = TransactionsModel.uncompletedTransactions(transactions) - } + let response = await model.fetchTransactionsT(currency: currency) + transactions = response + pendingTransactions = WalletModel.pendingTransactions(response) + uncompletedTransactions = WalletModel.uncompletedTransactions(response) } } // body } @@ -168,21 +182,18 @@ let _ = print("button: Uncompleted Transactions: \(currency)") #if DEBUG fileprivate struct BindingViewContainer : View { @State var centsToTransfer: UInt64 = 333 - @State private var purpose: String = "bla-bla" + @State private var summary: String = "bla-bla" var body: some View { let scopeInfo = ScopeInfo(type: ScopeInfo.ScopeInfoType.exchange, exchangeBaseUrl: DEMOEXCHANGE, currency: LONGCURRENCY) let balance = Balance(available: try! Amount(fromString: LONGCURRENCY + ":0.1"), - pendingIncoming: try! Amount(fromString: LONGCURRENCY + ":4.8"), - pendingOutgoing: try! Amount(fromString: LONGCURRENCY + ":3.25"), - hasPendingTransactions: true, + scopeInfo: scopeInfo, requiresUserInput: false, - scopeInfo: scopeInfo) + hasPendingTransactions: true) List { BalancesSectionView(balance: balance, centsToTransfer: $centsToTransfer, - purpose: $purpose, - model: nil) + summary: $summary) } } } diff --git a/TalerWallet1/Views/Exchange/ExchangeListView.swift b/TalerWallet1/Views/Exchange/ExchangeListView.swift @@ -10,35 +10,27 @@ import SymLog struct ExchangeListView: View { private let symLog = SymLogV(0) let navTitle: String - - var model: ExchangeModel? var hamburgerAction: () -> Void + @EnvironmentObject private var model: WalletModel + @State private var exchanges: [Exchange] = [] // 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)") + 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)") } } } diff --git a/TalerWallet1/Views/Exchange/ExchangeSectionView.swift b/TalerWallet1/Views/Exchange/ExchangeSectionView.swift @@ -13,7 +13,6 @@ struct ExchangeRowView: View { @State private var buttonSelected: Int? = nil var body: some View { let baseURL = exchange.exchangeBaseUrl - let model = WithdrawModel.model(baseURL: baseURL) HStack(spacing: 0) { // can't use the built in Label because it adds the accessory arrow Text(baseURL.trimURL()) @@ -24,7 +23,6 @@ struct ExchangeRowView: View { ) { EmptyView() }.frame(width: 0).opacity(0) NavigationLink(destination: LazyView { ManualWithdraw(exchange: exchange, - model: model, centsToTransfer: $centsToTransfer) }, tag: 2, selection: $buttonSelected ) { EmptyView() }.frame(width: 0).opacity(0) diff --git a/TalerWallet1/Views/Exchange/ManualWithdraw.swift b/TalerWallet1/Views/Exchange/ManualWithdraw.swift @@ -11,9 +11,10 @@ struct ManualWithdraw: View { private let symLog = SymLogV() let exchange: Exchange - let model: WithdrawModel? @Binding var centsToTransfer: UInt64 + @EnvironmentObject private var model: WalletModel + @State var manualWithdrawalDetails: ManualWithdrawalDetails? = nil // @State var ageMenuList: [Int] = [] @@ -50,7 +51,6 @@ struct ManualWithdraw: View { //let _ = print(selectedAge, restrictAge) NavigationLink(destination: LazyView { ManualWithdrawDone(exchange: exchange, - model: model, centsToTransfer: centsToTransfer) // restrictAge: restrictAge) }) { @@ -59,7 +59,6 @@ struct ManualWithdraw: View { } else { NavigationLink(destination: LazyView { WithdrawTOSView(exchangeBaseUrl: exchange.exchangeBaseUrl, - model: model, viewID: VIEW_WITHDRAW_TOS, acceptAction: nil) // pop back to here }) { @@ -69,7 +68,6 @@ struct ManualWithdraw: View { } } // tooMany } // invalid - Spacer() } .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal) @@ -82,10 +80,8 @@ struct ManualWithdraw: View { .task(id: centsToTransfer) { let amount = Amount.amountFromCents(currency, centsToTransfer) do { - if let model { - manualWithdrawalDetails = try await model.loadWithdrawalDetailsForAmountM(exchange.exchangeBaseUrl, amount: amount) -// agePicker.setAges(ages: manualWithdrawalDetails?.ageRestrictionOptions) - } + manualWithdrawalDetails = try await model.loadWithdrawalDetailsForAmountM(exchange.exchangeBaseUrl, amount: amount) +// agePicker.setAges(ages: manualWithdrawalDetails?.ageRestrictionOptions) } catch { // TODO: error symLog.log(error.localizedDescription) manualWithdrawalDetails = nil @@ -112,7 +108,6 @@ struct ManualWithdraw_Container : View { ageRestrictionOptions: [], permanent: false) ManualWithdraw(exchange: exchange, - model: nil, centsToTransfer: $centsToTransfer, manualWithdrawalDetails: details) } diff --git a/TalerWallet1/Views/Exchange/ManualWithdrawDone.swift b/TalerWallet1/Views/Exchange/ManualWithdrawDone.swift @@ -11,19 +11,16 @@ struct ManualWithdrawDone: View { let navTitle = String(localized: "Wire Transfer") let exchange: Exchange - let model: WithdrawModel? let centsToTransfer: UInt64 // let restrictAge: Int? + @EnvironmentObject private var model: WalletModel + @State var acceptManualWithdrawalResult: AcceptManualWithdrawalResult? @State var withdrawalTransaction: Transaction? func reloadOneAction(_ transactionId: String) async throws -> Transaction { - if let model { - return try await model.getTransactionByIdT(transactionId) - } else { - throw WalletBackendError.walletCoreError - } + return try await model.getTransactionByIdT(transactionId) } var body: some View { @@ -47,14 +44,12 @@ struct ManualWithdrawDone: View { DebugViewC.shared.setViewID(VIEW_WITHDRAW_ACCEPT) }.task { do { - if let model { - let amount = Amount.amountFromCents(exchange.currency!, centsToTransfer) - let result = try await model.sendAcceptManualWithdrawalM(exchange.exchangeBaseUrl, - amount: amount, restrictAge: 0) + let amount = Amount.amountFromCents(exchange.currency!, centsToTransfer) + let result = try await model.sendAcceptManualWithdrawalM(exchange.exchangeBaseUrl, + amount: amount, restrictAge: 0) print(result as Any) - let transaction = try await model.getTransactionByIdT(result!.transactionId) - withdrawalTransaction = transaction - } + let transaction = try await model.getTransactionByIdT(result!.transactionId) + withdrawalTransaction = transaction } catch { // TODO: error symLog.log(error.localizedDescription) } @@ -76,7 +71,6 @@ struct ManualWithdrawDone_Container : View { ageRestrictionOptions: [], permanent: false) ManualWithdrawDone(exchange: exchange, - model: nil, centsToTransfer: centsToTransfer) } } diff --git a/TalerWallet1/Views/HelperViews/QRCodeDetailView.swift b/TalerWallet1/Views/HelperViews/QRCodeDetailView.swift @@ -8,12 +8,14 @@ import AVFoundation struct QRCodeDetailView: View { - var talerURI: String + @Binding var talerURI: String + let incoming: Bool var body: some View { if talerURI.count > 10 { VStack { - Text("Let the payee scan this QR code to receive:") + Text(incoming ? "Let the payer scan this QR code to pay:" + : "Let the payee scan this QR code to receive:") .fixedSize(horizontal: false, vertical: true) .padding(.top, 30) .font(.title3) @@ -35,25 +37,21 @@ struct QRCodeDetailView: View { } } } - - +// MARK: - #if DEBUG fileprivate struct ContentView: View { - @State var isOn = false + @State var talerURI: String = "taler://pay-push/exchange.demo.taler.net/95ZG4D1AGFGZQ7CNQ1V49D3FT18HXKA6HQT4X3XME9YSJQVFQ520" var body: some View { - VStack { - + List { + QRCodeDetailView(talerURI: $talerURI, incoming: false) } } } struct QRCodeDetailView_Previews: PreviewProvider { static var previews: some View { -// ContentView() - List { - QRCodeDetailView(talerURI: "taler://pay-push/exchange.demo.taler.net/95ZG4D1AGFGZQ7CNQ1V49D3FT18HXKA6HQT4X3XME9YSJQVFQ520") - } + ContentView() } } #endif diff --git a/TalerWallet1/Views/Main/MainView.swift b/TalerWallet1/Views/Main/MainView.swift @@ -75,13 +75,11 @@ extension MainView { SidebarItem(name: balances, sysImage: "creditcard.fill", // TODO: Wallet Icon view: AnyView(BalancesListView(navTitle: balances, - model: BalancesModel.model(currency: "*"), hamburgerAction: hamburgerAction) )), SidebarItem(name: exchanges, sysImage: "building.columns", view: AnyView(ExchangeListView(navTitle: exchanges, - model: ExchangeModel.model(), hamburgerAction: hamburgerAction) )), SidebarItem(name: settings, // TODO: "About"? diff --git a/TalerWallet1/Views/Payment/PaymentURIView.swift b/TalerWallet1/Views/Payment/PaymentURIView.swift @@ -6,10 +6,13 @@ import SwiftUI import AVFoundation import SymLog +// Will be called either by the user scanning a QR code or tapping the provided link, both from the shop's website +// we show the user the payment details struct PaymentURIView: View { private let symLog = SymLogV() - var url: URL - var model: PaymentURIModel? + let url: URL + + @EnvironmentObject private var model: WalletModel @State var detailsForUri: PaymentDetailsForUri? = nil @@ -27,16 +30,14 @@ struct PaymentURIView: View { 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) - } + 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 @@ -59,9 +60,8 @@ struct PaymentURIView: View { }.task { do { symLog.log(".task") - if let model { - detailsForUri = try await model.preparePayForUriM(url.absoluteString) - } + let details = try await model.preparePayForUriM(url.absoluteString) + detailsForUri = details } catch { // TODO: error symLog.log(error.localizedDescription) } diff --git a/TalerWallet1/Views/Peer2peer/PaymentPurpose.swift b/TalerWallet1/Views/Peer2peer/PaymentPurpose.swift @@ -0,0 +1,111 @@ +/* + * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. + * See LICENSE.md + */ +import SwiftUI +import taler_swift +import SymLog + +struct PaymentPurpose: View { + private let symLog = SymLogV() + + let scopeInfo: ScopeInfo + let centsToTransfer: UInt64 + let fee: String + @Binding var summary: String + @Binding var expireDays: UInt +// var deactivateAction: () -> Void + + @FocusState private var isFocused: Bool + + let formatter = CurrencyFormatter.shared // TODO: based on currency + let buttonFont: Font = .title2 + + private var label: String { + let mag = pow(10, formatter.maximumFractionDigits) + return formatter.string(for: Decimal(centsToTransfer) / mag) ?? "" + } + + var body: some View { + let amount = Amount.amountFromCents(scopeInfo.currency, centsToTransfer) + + VStack (spacing: 6) { + Text(amount.readableDescription) + Text("+ \(fee) payment fee") + .foregroundColor(.red) + VStack(alignment: .leading, spacing: 6) { + Text("Purpose:") + .padding(.top) + .font(.title3) + + TextField("Purpose", text: $summary) + .font(.title) + .foregroundColor(WalletColors().fieldForeground) // text color + .background(WalletColors().fieldBackground) + .border(.primary) + .focused($isFocused) + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { + isFocused = true // make first responder - raise keybord + } + } + + HStack{ + Spacer() + Text("\(summary.count)/100") + } // maximum 100 characters + + Text("Expires in:") + .font(.title3) + + SelectDays(selected: $expireDays, maxExpiration: 35) + .disabled(false) + .padding(.bottom) + + let disabled = (expireDays == 0) || (summary.count < 1) + + NavigationLink(destination: LazyView { + SendNow(amountToSend: nil, + amountToReceive: amount, + summary: summary, expireDays: expireDays) + }) { + Text("Invoice \(label) \(scopeInfo.currency)") + .font(buttonFont) + } + .buttonStyle(TalerButtonStyle(type: .prominent)) + .disabled(disabled) + + Spacer() + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal) + } + .navigationTitle("Invoice another Wallet") + .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all)) + .onAppear { + DebugViewC.shared.setSheetID(VIEW_INVOICE_PURPOSE) + print("❗️ PaymentPurpose onAppear") + } + .onDisappear { + print("❗️ PaymentPurpose onDisappear") +// deactivateAction() + } + } + +} +// MARK: - +#if DEBUG +struct PaymentPurpose_Previews: PreviewProvider { + static var previews: some View { + let scopeInfo = ScopeInfo(type: ScopeInfo.ScopeInfoType.exchange, exchangeBaseUrl: DEMOEXCHANGE, currency: LONGCURRENCY) + @State var summary: String = "pUrPoSe" + @State var expireDays: UInt = 0 + PaymentPurpose(scopeInfo: scopeInfo, + centsToTransfer: 5, + fee: "fee", + summary: $summary, + expireDays: $expireDays) +// { print("deactivateAction") } + } +} +#endif diff --git a/TalerWallet1/Views/Peer2peer/ReceivePurpose.swift b/TalerWallet1/Views/Peer2peer/ReceivePurpose.swift @@ -1,129 +0,0 @@ -/* - * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. - * See LICENSE.md - */ -import SwiftUI -import taler_swift -import SymLog - -struct ReceivePurpose: View { - private let symLog = SymLogV() - @FocusState private var isFocused: Bool - @State var peerPullCheck: CheckPeerPullCreditResponse? - - var scopeInfo: ScopeInfo - var centsToTransfer: UInt64 - @Binding var purpose: String - @Binding var expireDays: UInt - var deactivateAction: () -> Void - - let formatter = CurrencyFormatter.shared // TODO: based on currency - - let buttonFont: Font = .title2 - private var label: String { - let mag = pow(10, formatter.maximumFractionDigits) - return formatter.string(for: Decimal(centsToTransfer) / mag) ?? "" - } - - func pullFee(ppCheck: CheckPeerPullCreditResponse?) -> String { - do { - if let p2pcheck = ppCheck { - let fee = try p2pcheck.amountRaw - p2pcheck.amountEffective - return fee.readableDescription - } - } catch {} - return "" - } - - var body: some View { - let amount = Amount.amountFromCents(scopeInfo.currency, centsToTransfer) - let model = Peer2peerModel.model(baseURL: scopeInfo.currency) - - let fee = pullFee(ppCheck: peerPullCheck) - VStack (spacing: 6) { - Text(amount.readableDescription) - Text("+ \(fee) payment fee") - .foregroundColor(.red) - VStack(alignment: .leading, spacing: 6) { - Text("Purpose:") - .padding(.top) - .font(.title3) - - TextField("Purpose", text: $purpose) - .font(.title) - .foregroundColor(WalletColors().fieldForeground) // text color - .background(WalletColors().fieldBackground) - .border(.primary) - .focused($isFocused) - .onAppear { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { - isFocused = true // make first responder - raise keybord - } - } - - HStack{ - Spacer() - Text("\(purpose.count)/100") - } // maximum 100 characters - - Text("Expires in:") - .font(.title3) - - SelectDays(selected: $expireDays, maxExpiration: 35) - .disabled(false) - .padding(.bottom) - - let buttonTitle = "Invoice \(label) \(scopeInfo.currency)" - let disabled = (expireDays == 0) || (purpose.count < 1) - - NavigationLink(destination: LazyView { - SendNow(amountToSend: amount, purpose: purpose, expireDays: expireDays, model: model) - }) { - Text(buttonTitle) - .font(buttonFont) - } - .buttonStyle(TalerButtonStyle(type: .prominent)) - .disabled(disabled) - - Spacer() - } - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.horizontal) - } - .navigationTitle("Invoice another Wallet") - .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all)) - .onAppear { - DebugViewC.shared.setSheetID(VIEW_INVOICE_PURPOSE) - print("❗️ ReceivePurpose onAppear") - } - .onDisappear { - print("❗️ ReceivePurpose onDisappear") - deactivateAction() - } - .task { - symLog.log(".task") - do { - peerPullCheck = try await model.checkPeerPullCreditM(amount, exchangeBaseUrl: scopeInfo.exchangeBaseUrl ?? nil) // TODO: exchangeBaseUrl! - } catch { // TODO: error - symLog.log(error.localizedDescription) - } - } - } - -} -// MARK: - -#if DEBUG -struct ReceivePurpose_Previews: PreviewProvider { - static var previews: some View { - let scopeInfo = ScopeInfo(type: ScopeInfo.ScopeInfoType.exchange, exchangeBaseUrl: DEMOEXCHANGE, currency: LONGCURRENCY) - @State var purpose: String = "pUrPoSe" - @State var expireDays: UInt = 0 - ReceivePurpose(scopeInfo: scopeInfo, - centsToTransfer: 5, - purpose: $purpose, - expireDays: $expireDays) { - print("deactivateAction") - } - } -} -#endif diff --git a/TalerWallet1/Views/Peer2peer/RequestPayment.swift b/TalerWallet1/Views/Peer2peer/RequestPayment.swift @@ -6,13 +6,15 @@ import SwiftUI import taler_swift import SymLog +// Will be called by the user tapping "Request Payment" in the balances list struct RequestPayment: View { private let symLog = SymLogV() - let model: Peer2peerModel? var scopeInfo: ScopeInfo @Binding var centsToTransfer: UInt64 - @Binding var purpose: String + @Binding var summary: String + + @EnvironmentObject private var model: WalletModel @State private var peerPullCheck: CheckPeerPullCreditResponse? = nil @State private var expireDays: UInt = 0 @@ -26,7 +28,7 @@ struct RequestPayment: View { let navTitle = String(localized: "Request \(currency)") let currencyField = CurrencyField(value: $centsToTransfer, currency: currency) - VStack(alignment: .leading, spacing: 6) { + ScrollView { CurrencyInputView(currencyField: currencyField, title: String(localized: "Amount to receive:")) @@ -37,26 +39,19 @@ struct RequestPayment: View { HStack { let disabled = centsToTransfer == 0 - let deactivateAction = { - print("❗️ deactivateAction") - } - NavigationLink(destination: LazyView { - ReceivePurpose(scopeInfo: scopeInfo, + PaymentPurpose(scopeInfo: scopeInfo, centsToTransfer: centsToTransfer, - purpose: $purpose, - expireDays: $expireDays - ) { - deactivateAction() - } + fee: someCoins.fee, + summary: $summary, + expireDays: $expireDays) +// { deactivateAction() } }) { Text("Create invoice") } .buttonStyle(TalerButtonStyle(type: .prominent)) .disabled(disabled) } - - Spacer() } .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal) @@ -72,12 +67,10 @@ struct RequestPayment: View { .task(id: centsToTransfer) { let amount = Amount.amountFromCents(currency, centsToTransfer) do { - if let model { - let ppCheck = try await model.checkPeerPullCreditM(amount, exchangeBaseUrl: nil) - peerPullCheck = ppCheck - // TODO: set from exchange + let ppCheck = try await model.checkPeerPullCreditM(amount, exchangeBaseUrl: nil) + peerPullCheck = ppCheck + // TODO: set from exchange // agePicker.setAges(ages: peerPushCheck?.ageRestrictionOptions) - } } catch { // TODO: error symLog.log(error.localizedDescription) peerPullCheck = nil diff --git a/TalerWallet1/Views/Peer2peer/SendAmount.swift b/TalerWallet1/Views/Peer2peer/SendAmount.swift @@ -10,12 +10,13 @@ import SymLog struct SendAmount: View { private let symLog = SymLogV() - let model: Peer2peerModel? let amountAvailable: Amount @Binding var centsToTransfer: UInt64 - @Binding var purpose: String + @Binding var summary: String - @State var peerPushCheck: CheckPeerPushDebitResponse? + @EnvironmentObject private var model: WalletModel + + @State var peerPushCheck: CheckPeerPushDebitResponse? = nil @State private var expireDays: UInt = 0 private func fee(ppCheck: CheckPeerPushDebitResponse?) -> String { @@ -54,21 +55,18 @@ struct SendAmount: View { let disabled = centsToTransfer == 0 // TODO: check amountAvailable NavigationLink(destination: LazyView { - SendPurpose(model: model, - amountAvailable: amountAvailable, - centsToSend: centsToTransfer, - fee: fee, - purpose: $purpose, - expireDays: $expireDays, - deactivateAction: {}) + SendPurpose(amountAvailable: amountAvailable, + centsToTransfer: centsToTransfer, + fee: fee, + summary: $summary, + expireDays: $expireDays) +// { deactivateAction() } }) { Text("To another wallet") } .buttonStyle(TalerButtonStyle(type: .prominent)) .disabled(disabled) } - - Spacer() } .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal) @@ -84,12 +82,10 @@ struct SendAmount: View { .task(id: centsToTransfer) { let amount = Amount.amountFromCents(currency, centsToTransfer) do { - if let model { - let ppCheck = try await model.checkPeerPushDebitM(amount) - peerPushCheck = ppCheck - // TODO: set from exchange - // agePicker.setAges(ages: peerPushCheck?.ageRestrictionOptions) - } + let ppCheck = try await model.checkPeerPushDebitM(amount) + peerPushCheck = ppCheck + // TODO: set from exchange +// agePicker.setAges(ages: peerPushCheck?.ageRestrictionOptions) } catch { // TODO: error symLog.log(error.localizedDescription) peerPushCheck = nil @@ -101,14 +97,13 @@ struct SendAmount: View { #if DEBUG struct SendAmount_Container : View { @State private var centsToTransfer: UInt64 = 510 - @State private var purpose: String = "" + @State private var summary: String = "" var body: some View { let amount = Amount(currency: LONGCURRENCY, integer: 10, fraction: 0) - SendAmount(model: nil, - amountAvailable: amount, - centsToTransfer: $centsToTransfer, - purpose: $purpose) + SendAmount(amountAvailable: amount, + centsToTransfer: $centsToTransfer, + summary: $summary) } } diff --git a/TalerWallet1/Views/Peer2peer/SendNow.swift b/TalerWallet1/Views/Peer2peer/SendNow.swift @@ -10,22 +10,22 @@ import SymLog struct SendNow: View { private let symLog = SymLogV() - var amountToSend: Amount - var purpose: String - var expireDays: UInt + let amountToSend: Amount? + let amountToReceive: Amount? + let summary: String + let expireDays: UInt - var model: Peer2peerModel? - @State var peerPushResponse: PeerPushResponse? + @EnvironmentObject private var model: WalletModel - @State var talerURI: String? = nil + @State var talerURI: String = "" var body: some View { ScrollView { - if talerURI == nil { + if talerURI.isEmpty { LoadingView(backButtonHidden: true) } else { VStack() { - QRCodeDetailView(talerURI: talerURI!) + QRCodeDetailView(talerURI: $talerURI, incoming: amountToSend == nil) Text("The QR code can also be copied and shared from Transactions later") .fixedSize(horizontal: false, vertical: true) @@ -50,29 +50,38 @@ struct SendNow: View { symLog.log(".task") do { // generate talerURI - if let model { - let timestamp = Timestamp.inSomeDays(expireDays) - let terms = PeerContractTerms(amount: amountToSend, summary: purpose, purse_expiration: timestamp) + let timestamp = Timestamp.inSomeDays(expireDays) + if let amountToSend { + let terms = PeerContractTerms(amount: amountToSend, + summary: summary, + purse_expiration: timestamp) // TODO: user might choose baseURL - peerPushResponse = try await model.initiatePeerPushDebitM(nil, terms: terms) - talerURI = peerPushResponse?.talerUri - } + let response = try await model.initiatePeerPushDebitM(nil, terms: terms) + talerURI = response.talerUri + } else if let amountToReceive { + let terms = PeerContractTerms(amount: amountToReceive, + summary: summary, + purse_expiration: timestamp) + // TODO: user might choose baseURL + let response = try await model.initiatePeerPullCreditM(nil, terms: terms) + talerURI = response.talerUri + } else { talerURI = "" } } catch { // TODO: error symLog.log(error.localizedDescription) + talerURI = "" } } // task } } // MARK: - -//struct SendNow_Previews: PreviewProvider { -// static var previews: some View { -// Group { -// 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") -// } -// } -//} +struct SendNow_Previews: PreviewProvider { + static var previews: some View { + Group { + SendNow(amountToSend: try! Amount(fromString: LONGCURRENCY + ":4.8"), + amountToReceive: nil, + summary: "some purpose", + expireDays: 0, + talerURI: "taler://pay-push/exchange.demo.taler.net/95ZG4D1AGFGZQ7CNQ1V49D3FT18HXKA6HQT4X3XME9YSJQVFQ520") + } + } +} diff --git a/TalerWallet1/Views/Peer2peer/SendPurpose.swift b/TalerWallet1/Views/Peer2peer/SendPurpose.swift @@ -9,25 +9,24 @@ import SymLog struct SendPurpose: View { private let symLog = SymLogV() @FocusState private var isFocused: Bool - var model: Peer2peerModel? - var amountAvailable: Amount - var centsToSend: UInt64 - var fee: String - @Binding var purpose: String + let amountAvailable: Amount + let centsToTransfer: UInt64 + let fee: String + @Binding var summary: String @Binding var expireDays: UInt - var deactivateAction: () -> Void +// var deactivateAction: () -> Void let formatter = CurrencyFormatter.shared // TODO: based on currency let buttonFont: Font = .title2 private var label: String { let mag = pow(10, formatter.maximumFractionDigits) - return formatter.string(for: Decimal(centsToSend) / mag) ?? "" + return formatter.string(for: Decimal(centsToTransfer) / mag) ?? "" } var body: some View { - let amount = Amount.amountFromCents(amountAvailable.currencyStr, centsToSend) + let amount = Amount.amountFromCents(amountAvailable.currencyStr, centsToTransfer) VStack (spacing: 6) { Text(amount.readableDescription) @@ -38,7 +37,7 @@ struct SendPurpose: View { .padding(.top) .font(.title3) - TextField("Purpose", text: $purpose) + TextField("Purpose", text: $summary) .font(.title) .foregroundColor(WalletColors().fieldForeground) // text color .background(WalletColors().fieldBackground) @@ -52,7 +51,7 @@ struct SendPurpose: View { HStack{ Spacer() - Text("\(purpose.count)/100") + Text("\(summary.count)/100") } // maximum 100 characters Text("Expires in:") @@ -63,13 +62,14 @@ struct SendPurpose: View { .disabled(false) .padding(.bottom) - let buttonTitle = "Send \(label) \(amountAvailable.currencyStr) now" - let disabled = (expireDays == 0) || (purpose.count < 1) // TODO: check amountAvailable + let disabled = (expireDays == 0) || (summary.count < 1) // TODO: check amountAvailable NavigationLink(destination: LazyView { - SendNow(amountToSend: amount, purpose: purpose, expireDays: expireDays, model: model) + SendNow(amountToSend: amount, + amountToReceive: nil, + summary: summary, expireDays: expireDays) }) { - Text(buttonTitle) + Text("Send \(label) \(amountAvailable.currencyStr) now") .font(buttonFont) } .buttonStyle(TalerButtonStyle(type: .prominent)) @@ -88,7 +88,7 @@ struct SendPurpose: View { } .onDisappear { print("❗️ SendPurpose onDisappear") - deactivateAction() +// deactivateAction() } .task { symLog.log(".task") @@ -105,18 +105,15 @@ struct SendPurpose: View { #if DEBUG struct SendPurpose_Previews: PreviewProvider { static var previews: some View { - @State var purpose: String = "" + @State var summary: String = "" @State var expireDays: UInt = 0 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") - } + SendPurpose(amountAvailable: amount, + centsToTransfer: 543, + fee: "0,43", + summary: $summary, + expireDays: $expireDays) +// { print("deactivateAction") } } } #endif diff --git a/TalerWallet1/Views/Settings/Pending/PendingOpsListView.swift b/TalerWallet1/Views/Settings/Pending/PendingOpsListView.swift @@ -9,8 +9,9 @@ struct PendingOpsListView: View { private let symLog = SymLogV(0) let navTitle = String(localized: "Pending") + @EnvironmentObject private var model: WalletModel + @State var pendingOperations: [PendingOperation] = [] - var model: PendingModel var body: some View { #if DEBUG @@ -32,6 +33,7 @@ extension PendingOpsListView { let symLog: SymLogV? @Binding var pendingOperations: [PendingOperation] var reloadAction: () async -> [PendingOperation] + @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic var body: some View { #if DEBUG let _ = Self._printChanges() @@ -41,7 +43,7 @@ extension PendingOpsListView { List(pendingOperations, id: \.self) { pendingOp in PendingOpView(pendingOp: pendingOp) } - .listStyle(SidebarListStyle()) + .listStyle(myListStyle.style).anyView .navigationBarTitleDisplayMode(.large) .onAppear() { DebugViewC.shared.setViewID(VIEW_PENDING) diff --git a/TalerWallet1/Views/Settings/SettingsView.swift b/TalerWallet1/Views/Settings/SettingsView.swift @@ -27,6 +27,8 @@ struct SettingsView: View { var hamburgerAction: () -> Void + @EnvironmentObject private var model: WalletModel + @State private var checkDisabled = false @State private var withDrawDisabled = false #if DEBUG @@ -66,7 +68,7 @@ struct SettingsView: View { } if showDevelopItems { // show or hide the following items NavigationLink { // whole row like in a tableView - LazyView { PendingOpsListView(model: PendingModel.model()) } + LazyView { PendingOpsListView() } } label: { SettingsItem(name: String(localized: "Pending Operations"), description: String(localized: "Exchange not yet ready...")) {} } @@ -80,7 +82,6 @@ struct SettingsView: View { Button("Withdraw") { withDrawDisabled = true // don't run twice Task { - let model: SettingsModel = SettingsModel() symLog.log("Withdraw TestKUDOS") do { try await model.loadTestKudosM() @@ -97,7 +98,6 @@ struct SettingsView: View { Button("Test 1") { checkDisabled = true // don't run twice Task { - let model: SettingsModel = SettingsModel() symLog.log("running integration test") do { try await model.runIntegrationTestM(newVersion: false) @@ -114,7 +114,6 @@ struct SettingsView: View { Button("Test 2") { checkDisabled = true // don't run twice Task { - let model: SettingsModel = SettingsModel() symLog.log("running integration test V2") do { try await model.runIntegrationTestM(newVersion: true) diff --git a/TalerWallet1/Views/Sheets/P2P_Sheets/P2pAcceptDone.swift b/TalerWallet1/Views/Sheets/P2P_Sheets/P2pAcceptDone.swift @@ -0,0 +1,64 @@ +/* + * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. + * See LICENSE.md + */ +import SwiftUI +import taler_swift +import SymLog + +struct P2pAcceptDone: View { + private let symLog = SymLogV() + let navTitle = String(localized: "Received P2P") + + let exchangeBaseUrl: String? + let url: URL + + @EnvironmentObject private var model: WalletModel + + @State private var confirmTransferUrl: String? = nil + @State private var transaction: Transaction? = nil + + func reloadOneAction(_ transactionId: String) async throws -> Transaction { + return try await model.getTransactionByIdT(transactionId) + } + + var body: some View { +#if DEBUG + let _ = Self._printChanges() + let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear +#endif + VStack { + if let transaction { + TransactionDetailView(transaction: transaction, + reloadAction: reloadOneAction, + doneAction: { dismissTop() }) + .navigationBarBackButtonHidden(true) + .interactiveDismissDisabled() // can only use "Done" button to dismiss + .navigationTitle(navTitle) + } else { + WithdrawProgressView(message: "Bank Confirmation") + .navigationTitle("Loading...") + } + }.onAppear() { + symLog.log("onAppear") + DebugViewC.shared.setSheetID(SHEET_RCV_P2P_ACCEPT) + }.task { + do { + if let exchangeBaseUrl { + let result = try await model.sendAcceptIntWithdrawalM(exchangeBaseUrl, withdrawURL: url.absoluteString) + confirmTransferUrl = result!.confirmTransferUrl + transaction = try await model.getTransactionByIdT(result!.transactionId) + } + } catch { // TODO: error + symLog.log(error.localizedDescription) + } + } + } +} +// MARK: - +struct P2pAcceptDone_Previews: PreviewProvider { + static var previews: some View { + P2pAcceptDone(exchangeBaseUrl: DEMOEXCHANGE, + url: URL(string: DEMOSHOP)!) + } +} diff --git a/TalerWallet1/Views/Sheets/P2P_Sheets/P2pReceiveURIView.swift b/TalerWallet1/Views/Sheets/P2P_Sheets/P2pReceiveURIView.swift @@ -0,0 +1,80 @@ +/* + * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. + * See LICENSE.md + */ +import SwiftUI +import taler_swift +import SymLog + +// Will be called either by the user scanning a QR code or tapping the provided link, from another user's SendCoins +// we show the user the P2P details - but first the ToS must be accepted +struct P2pReceiveURIView: View { + private let symLog = SymLogV() + let navTitle = String(localized: "Accept P2P Receive") + + // the URL from the bank website + let url: URL + @EnvironmentObject private var model: WalletModel + + @State private var peerPushCreditResponse: PreparePeerPushCreditResponse? + + var body: some View { + let badURL = "Error in URL: \(url)" + VStack { + if let peerPushCreditResponse { + List { + let raw = peerPushCreditResponse.amountRaw + let effective = peerPushCreditResponse.amountEffective + let currency = raw.currencyStr + let fee = try! Amount.diff(raw, effective) + let outColor = WalletColors().transactionColor(false) + let inColor = WalletColors().transactionColor(true) + + ThreeAmountsView(topTitle: String(localized: "Amount to receive:"), + topAmount: raw, fee: fee, + bottomTitle: String(localized: "\(currency) to be obtained:"), + bottomAmount: effective, + large: false, pending: false, incoming: true, + baseURL: nil) + } + .navigationTitle(navTitle) + let tosAccepted = true // TODO: https://bugs.gnunet.org/view.php?id=7869 + if tosAccepted { + NavigationLink(destination: LazyView { + P2pAcceptDone(exchangeBaseUrl: nil, url: url) + }) { + Text("Confirm Withdrawal") // SHEET_WITHDRAW_ACCEPT + }.buttonStyle(TalerButtonStyle(type: .prominent)) + .padding() + } else { + NavigationLink(destination: LazyView { + WithdrawTOSView(exchangeBaseUrl: nil, + viewID: SHEET_RCV_P2P_TOS, + acceptAction: nil) // pop back to here + }) { + Text("Check Terms of Service") + }.buttonStyle(TalerButtonStyle(type: .prominent)) + .padding() + } + } else { + // Yikes no details or no baseURL +// WithdrawProgressView(message: url.host ?? badURL) +// .navigationTitle("Contacting Exchange") + } + } + .onAppear() { + symLog.log("onAppear") + DebugViewC.shared.setSheetID(SHEET_RCV_P2P) + } + .task { + do { // TODO: cancelled + symLog.log(".task") + let ppCreditResponse = try await model.preparePeerPushCreditM(url.absoluteString) + peerPushCreditResponse = ppCreditResponse + } catch { // TODO: error + symLog.log(error.localizedDescription) + peerPushCreditResponse = nil + } + } + } +} diff --git a/TalerWallet1/Views/Sheets/URLSheet.swift b/TalerWallet1/Views/Sheets/URLSheet.swift @@ -15,11 +15,19 @@ struct URLSheet: View { var body: some View { Group { - if urlCommand == .withdraw { - WithdrawURIView(url: urlToOpen) - } else if urlCommand == UrlCommand.pay { - let model = PaymentURIModel.model() - PaymentURIView(url: urlToOpen, model: model) + if let urlCommand { + switch urlCommand { + case .withdraw: + WithdrawURIView(url: urlToOpen) + case .pay: + PaymentURIView(url: urlToOpen) + case .payPull: + Text("payPull not implemented yet") + case .payPush: + P2pReceiveURIView(url: urlToOpen) + case .unknown: + Text("unknown command") + } } else { VStack { // Error view Spacer() diff --git a/TalerWallet1/Views/Transactions/ThreeAmounts.swift b/TalerWallet1/Views/Transactions/ThreeAmounts.swift @@ -79,7 +79,7 @@ struct ThreeAmountsView: View { if let status { HStack { Spacer() - Text("Status: \(status)") // TODO: localize + Text("Status: \(status)") .font(.title2) }.padding() } @@ -103,6 +103,7 @@ struct ThreeAmounts_Previews: PreviewProvider { Button(String(localized: "Accept"), action: {}) .buttonStyle(TalerButtonStyle(type: .prominent)) .padding(.horizontal) + .disabled(true) } } } diff --git a/TalerWallet1/Views/Transactions/TransactionDetailView.swift b/TalerWallet1/Views/Transactions/TransactionDetailView.swift @@ -27,8 +27,9 @@ struct TransactionDetailView: View { let common = transaction.common let pending = transaction.isPending let dateString = TalerDater.dateString(from: common.timestamp) - let navTitle = (pending ? String(localized: "Pending ") : "") + "\(common.type)" // TODO: localize type - + let localizedType = transaction.localizedType + let navTitle = pending ? String(localized: "Pending \(localizedType)") + : localizedType Group { List { Text("\(dateString)") diff --git a/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawAcceptDone.swift b/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawAcceptDone.swift @@ -10,19 +10,16 @@ struct WithdrawAcceptDone: View { private let symLog = SymLogV() let navTitle = String(localized: "Confirm with Bank") - let exchangeBaseUrl: String - let model: WithdrawModel? + let exchangeBaseUrl: String? let url: URL + @EnvironmentObject private var model: WalletModel + @State private var confirmTransferUrl: String? = nil @State private var transaction: Transaction? = nil func reloadOneAction(_ transactionId: String) async throws -> Transaction { - if let model { return try await model.getTransactionByIdT(transactionId) - } else { - throw WalletBackendError.walletCoreError - } } var body: some View { @@ -47,7 +44,7 @@ struct WithdrawAcceptDone: View { DebugViewC.shared.setSheetID(SHEET_WITHDRAW_CONFIRM) }.task { do { - if let model { + if let exchangeBaseUrl { let result = try await model.sendAcceptIntWithdrawalM(exchangeBaseUrl, withdrawURL: url.absoluteString) confirmTransferUrl = result!.confirmTransferUrl transaction = try await model.getTransactionByIdT(result!.transactionId) @@ -62,7 +59,6 @@ struct WithdrawAcceptDone: View { struct WithdrawAcceptDone_Previews: PreviewProvider { static var previews: some View { WithdrawAcceptDone(exchangeBaseUrl: DEMOEXCHANGE, - model: nil, url: URL(string: DEMOSHOP)!) } } diff --git a/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawTOSView.swift b/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawTOSView.swift @@ -11,8 +11,9 @@ struct WithdrawTOSView: View { let navTitle = String(localized: "Terms of Service") - var exchangeBaseUrl: String - var model: WithdrawModel? + var exchangeBaseUrl: String? + + @EnvironmentObject private var model: WalletModel @State var exchangeTOS: ExchangeTermsOfService? let viewID: Int // either VIEW_WITHDRAW_TOS or SHEET_WITHDRAW_TOS @@ -25,7 +26,7 @@ struct WithdrawTOSView: View { Content(symLog: symLog, exchangeTOS: exchangeTOS, myListStyle: $myListStyle) { Task { do { - if let model { + if let exchangeBaseUrl { _ = try await model.setExchangeTOSAcceptedM(exchangeBaseUrl, etag: exchangeTOS!.currentEtag) if acceptAction != nil { acceptAction!() @@ -42,8 +43,14 @@ struct WithdrawTOSView: View { .navigationTitle(navTitle) .overlay { if exchangeTOS == nil { - WithdrawProgressView(message: exchangeBaseUrl.trimURL()) - .navigationTitle("Loading " + navTitle) + if let exchangeBaseUrl { + WithdrawProgressView(message: exchangeBaseUrl.trimURL()) + .navigationTitle("Loading " + navTitle) + } else { + // Yikes! + WithdrawProgressView(message: "No exchangeBaseUrl!") + .navigationTitle("Loading " + navTitle) + } } } }.onAppear() { @@ -54,7 +61,7 @@ struct WithdrawTOSView: View { } }.task { do { - if let model { + if let exchangeBaseUrl { let someTOS = try await model.loadExchangeTermsOfServiceM(exchangeBaseUrl) exchangeTOS = someTOS } diff --git a/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawURIView.swift b/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawURIView.swift @@ -16,7 +16,7 @@ struct WithdrawURIView: View { // the URL from the bank website let url: URL - let model = WithdrawModel.model(baseURL: "global") // TODO: get baseURL from URL + @EnvironmentObject private var model: WalletModel // the exchange used for this withdrawal. @State private var exchangeBaseUrl: String? = nil @@ -48,7 +48,7 @@ struct WithdrawURIView: View { let tosAccepted = manualWithdrawalDetails.tosAccepted if tosAccepted { NavigationLink(destination: LazyView { - WithdrawAcceptDone(exchangeBaseUrl: exchangeBaseUrl, model: model, url: url) + WithdrawAcceptDone(exchangeBaseUrl: exchangeBaseUrl, url: url) }) { Text("Confirm Withdrawal") // SHEET_WITHDRAW_ACCEPT }.buttonStyle(TalerButtonStyle(type: .prominent)) @@ -56,7 +56,6 @@ struct WithdrawURIView: View { } else { NavigationLink(destination: LazyView { WithdrawTOSView(exchangeBaseUrl: exchangeBaseUrl, - model: model, viewID: SHEET_WITHDRAW_TOS, acceptAction: nil) // pop back to here }) {