[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[taler-taler-ios] branch master updated (7ce9180 -> f7f01e5)
From: |
gnunet |
Subject: |
[taler-taler-ios] branch master updated (7ce9180 -> f7f01e5) |
Date: |
Fri, 30 Jun 2023 22:33:32 +0200 |
This is an automated email from the git hooks/post-receive script.
marc-stibane pushed a change to branch master
in repository taler-ios.
from 7ce9180 Version 0.9.3 build 2
new c4294a7 Big update after DD37
new d729e1b Moved AgePicker in its own file
new 9101f27 Cleaned up buttons
new a07f109 PopToRoot instead of dismiss sheet
new 24f4ace cleanup, back to Swift 5.8 (for now until Xcode 15 is usable)
new 8074fad Notifications
new 2fbfa38 Big Model update, removed unneccessary thread-safety code
new d30fe6e Preparations for localization + accessability
new fdc7309 Launch animation, SideBarView
new 0222d21 Reduce Logging
new 50a02d3 Accessibility
new 8abb8bd Localization
new a41e3ea Overhaul withdraw + p2p
new 6d1028c Made Model a Singleton
new 185b6c0 Suspend-Resume
new a75849a Dummy
new 8c36914 for debugging time-outs
new 9b92739 remove loaded
new 8a6ec56 remove dismissFirst
new cf4e4fc viewID + comments
new e6fc4a6 Sounds, P2P receive
new 78e7674 cleaned up P2P
new ec19b36 Remove old view
new 8edd36d balance-change
new ef9b59f failTransaction
new 5a206a0 PeerPullDebit
new 3745dae Logging
new 7ded59c playSound
new 99d5f1a Model cleanup
new f513e92 bugfix
new e1008df bump Testflight version
new 536564c Adjust DebugView for Notch
new f309f9c log only release builds
new d62d3f0 sizeCategory, task
new 74dd08d developerMode
new e77028d playSound
new 6154b92 Decodable
new e68c03a actions
new 3acaeac Scrollview
new 3f13752 TransactionType
new 62ed87e playSound
new fbb0aa6 BalanceRow
new fbc5f12 confirmTransferUrl
new 49c0604 BalanceReloaded
new 5747e3c TransactionDetails
new b77c406 ThreeAmountsSheet
new ce7f12e #available(iOS 17.0, *) only with Xcode 15
new 02c2fa9 Demo Shop, reloading
new dab9b30 ScrollViewReader
new 605eefc withdrawalAmountDetails
new a68b2d2 remove debugging
new 974db16 playSound
new aa5f2fc ScrollViewReader needs Spacers if too few items
new f7f01e5 iOS: bump version to 0.9.3 (10)
The 54 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
...rWalletT.entitlements => GNU Taler.entitlements | 0
Info.plist | 11 +-
LICENSE.md | 12 +
TalerTests/WalletBackendTests.swift | 15 +-
TalerUITests/TalerUITests.swift | 16 +-
TalerWallet.xcodeproj/project.pbxproj | 431 +++++++++++++++------
.../Contents.json | 4 +-
.../taler-logo-2023-red.svg | 19 +
TalerWallet1/Backend/Transaction.swift | 424 +++++++++++++++-----
TalerWallet1/Backend/WalletBackendError.swift | 17 +-
TalerWallet1/Backend/WalletBackendRequest.swift | 201 ++++------
TalerWallet1/Backend/WalletCore.swift | 363 +++++++++++------
TalerWallet1/Controllers/Controller.swift | 68 ++--
TalerWallet1/Controllers/DebugViewC.swift | 189 +++++++++
TalerWallet1/Controllers/PublicConstants.swift | 48 +++
TalerWallet1/Controllers/TalerWallet1App.swift | 89 +++--
TalerWallet1/Helper/AgePicker.swift | 49 +++
TalerWallet1/Helper/AnyTransition+backslide.swift | 30 ++
TalerWallet1/Helper/CurrencyFormatter.swift | 27 ++
TalerWallet1/Helper/EqualIconWidthDomain.swift | 141 +++++++
TalerWallet1/Helper/KeyboardResponder.swift | 45 +++
TalerWallet1/Helper/LocalizedAlertError.swift | 54 +++
TalerWallet1/Helper/PublicConstants.swift | 24 --
TalerWallet1/Helper/TalerDater.swift | 37 +-
TalerWallet1/Helper/TalerStrings.swift | 15 +-
TalerWallet1/Helper/URL+id+iban.swift | 37 ++
TalerWallet1/Helper/View+Notification.swift | 75 ++++
TalerWallet1/Helper/View+dismissTop.swift | 33 +-
TalerWallet1/Helper/WalletColors.swift | 56 +++
TalerWallet1/Helper/playSound.swift | 25 ++
TalerWallet1/Model/ExchangeTestModel.swift | 115 ------
TalerWallet1/Model/Model+Balances.swift | 57 +++
.../Model+Exchange.swift} | 115 +++---
TalerWallet1/Model/Model+P2P.swift | 259 +++++++++++++
.../Model+Payment.swift} | 70 +---
TalerWallet1/Model/Model+Pending.swift | 55 +++
TalerWallet1/Model/Model+Settings.swift | 100 +++++
TalerWallet1/Model/Model+Transactions.swift | 158 ++++++++
TalerWallet1/Model/Model+Withdraw.swift | 187 +++++++++
TalerWallet1/Model/WalletInitModel.swift | 88 -----
TalerWallet1/Model/WalletModel.swift | 137 +++++--
TalerWallet1/Preview Content/transactions.json | 300 ++++++++++++++
TalerWallet1/Quickjs/quickjs.swift | 15 +-
TalerWallet1/Settings.bundle/Root.plist | 21 +
TalerWallet1/Settings.bundle/en.lproj/Root.strings | Bin 0 -> 546 bytes
TalerWallet1/Views/Balances/BalanceRow.swift | 46 ---
.../Views/Balances/BalanceRowButtons.swift | 51 +++
TalerWallet1/Views/Balances/BalanceRowView.swift | 98 +++++
TalerWallet1/Views/Balances/BalancesListView.swift | 171 ++++++++
TalerWallet1/Views/Balances/BalancesModel.swift | 73 ----
.../Views/Balances/BalancesSectionView.swift | 206 ++++++++++
.../Views/Balances/CurrenciesListView.swift | 83 ----
TalerWallet1/Views/Balances/CurrencyView.swift | 58 ---
TalerWallet1/Views/Balances/PendingRow.swift | 61 ---
TalerWallet1/Views/Balances/PendingRowView.swift | 48 +++
.../Views/Balances/UncompletedRowView.swift | 34 ++
TalerWallet1/Views/Balances/WalletEmptyView.swift | 44 ---
TalerWallet1/Views/Exchange/ExchangeListView.swift | 177 +++++----
.../Views/Exchange/ExchangeSectionView.swift | 102 +++++
TalerWallet1/Views/Exchange/ManualWithdraw.swift | 124 ++++++
.../Views/Exchange/ManualWithdrawDone.swift | 81 ++++
TalerWallet1/Views/Exchange/QuiteSomeCoins.swift | 102 +++++
TalerWallet1/Views/HelperViews/AmountView.swift | 28 +-
TalerWallet1/Views/HelperViews/Buttons.swift | 231 +++++++++--
TalerWallet1/Views/HelperViews/CopyShare.swift | 80 ++++
TalerWallet1/Views/HelperViews/CurrencyField.swift | 221 +++++++++++
.../Views/HelperViews/CurrencyInputView.swift | 60 +++
.../LaunchAnimationView.swift | 18 +-
TalerWallet1/Views/HelperViews/ListStyle.swift | 97 +++++
TalerWallet1/Views/HelperViews/LoadingView.swift | 39 +-
.../Views/HelperViews/QRCodeDetailView.swift | 57 +++
.../Views/HelperViews/QRGeneratorView.swift | 62 +++
TalerWallet1/Views/HelperViews/SelectDays.swift | 61 +++
.../Views/HelperViews/TextFieldAlert.swift | 15 +-
.../Views/HelperViews/TransactionButton.swift | 89 +++++
TalerWallet1/Views/Main/ContentView.swift | 92 -----
TalerWallet1/Views/Main/ErrorView.swift | 24 +-
TalerWallet1/Views/Main/MainView.swift | 124 ++++++
TalerWallet1/Views/Main/SideBarView.swift | 93 +++--
TalerWallet1/Views/Main/WalletEmptyView.swift | 44 +++
TalerWallet1/Views/Payment/DeleteMe.swift | 79 ++++
TalerWallet1/Views/Payment/PaymentAcceptView.swift | 71 ----
TalerWallet1/Views/Payment/PaymentURIView.swift | 155 +++++---
TalerWallet1/Views/Peer2peer/PaymentPurpose.swift | 111 ++++++
TalerWallet1/Views/Peer2peer/RequestPayment.swift | 93 +++++
TalerWallet1/Views/Peer2peer/SendAmount.swift | 118 ++++++
TalerWallet1/Views/Peer2peer/SendNow.swift | 93 +++++
TalerWallet1/Views/Peer2peer/SendPurpose.swift | 119 ++++++
TalerWallet1/Views/Pending/PendingModel.swift | 82 ----
.../Views/Pending/PendingOpsListView.swift | 75 ----
.../{ => Settings}/Pending/PendingOpView.swift | 31 +-
.../Settings/Pending/PendingOpsListView.swift | 58 +++
TalerWallet1/Views/Settings/SettingsItem.swift | 35 +-
TalerWallet1/Views/Settings/SettingsView.swift | 242 +++++++-----
.../Views/Sheets/P2P_Sheets/P2pAcceptDone.swift | 63 +++
.../Views/Sheets/P2P_Sheets/P2pPayURIView.swift | 71 ++++
.../Sheets/P2P_Sheets/P2pReceiveURIView.swift | 78 ++++
TalerWallet1/Views/Sheets/QRSheet.swift | 52 +++
TalerWallet1/Views/Sheets/ShareSheet.swift | 40 ++
TalerWallet1/Views/Sheets/Sheet.swift | 42 ++
TalerWallet1/Views/Sheets/URLSheet.swift | 52 +++
.../Views/Transactions/ManualDetails.swift | 74 ++++
TalerWallet1/Views/Transactions/ThreeAmounts.swift | 112 ++++++
.../Views/Transactions/TransactionDetail.swift | 136 -------
.../Views/Transactions/TransactionDetailView.swift | 285 ++++++++++++++
.../Views/Transactions/TransactionRow.swift | 92 -----
.../Views/Transactions/TransactionRowView.swift | 121 ++++++
.../Views/Transactions/TransactionsEmptyView.swift | 37 ++
.../Views/Transactions/TransactionsListView.swift | 169 ++++----
.../Views/Transactions/TransactionsModel.swift | 61 ---
TalerWallet1/Views/URLSheet.swift | 64 ---
.../Views/Withdraw/WithdrawAcceptView.swift | 71 ----
.../Views/Withdraw/WithdrawProgressView.swift | 45 ---
TalerWallet1/Views/Withdraw/WithdrawTOSView.swift | 96 -----
TalerWallet1/Views/Withdraw/WithdrawURIModel.swift | 213 ----------
TalerWallet1/Views/Withdraw/WithdrawURIView.swift | 103 -----
.../WithdrawAcceptDone.swift | 68 ++++
.../WithdrawAcceptView.swift | 111 ++++++
.../WithdrawProgressView.swift | 32 ++
.../WithdrawBankIntegrated/WithdrawTOSView.swift | 99 +++++
.../WithdrawBankIntegrated/WithdrawURIView.swift | 100 +++++
taler-swift/Sources/taler-swift/Amount.swift | 43 +-
taler-swift/Sources/taler-swift/Time.swift | 34 +-
.../Tests/taler-swiftTests/AmountTests.swift | 15 +-
taler-swift/Tests/taler-swiftTests/TimeTests.swift | 15 +-
125 files changed, 8050 insertions(+), 3132 deletions(-)
rename TalerWalletT.entitlements => GNU Taler.entitlements (100%)
create mode 100644 LICENSE.md
copy TalerWallet1/Assets.xcassets/{Taler-logo.imageset =>
taler-logo-2023-red.imageset}/Contents.json (68%)
create mode 100644
TalerWallet1/Assets.xcassets/taler-logo-2023-red.imageset/taler-logo-2023-red.svg
create mode 100644 TalerWallet1/Controllers/DebugViewC.swift
create mode 100644 TalerWallet1/Controllers/PublicConstants.swift
create mode 100644 TalerWallet1/Helper/AgePicker.swift
create mode 100644 TalerWallet1/Helper/AnyTransition+backslide.swift
create mode 100644 TalerWallet1/Helper/CurrencyFormatter.swift
create mode 100644 TalerWallet1/Helper/EqualIconWidthDomain.swift
create mode 100644 TalerWallet1/Helper/KeyboardResponder.swift
create mode 100644 TalerWallet1/Helper/LocalizedAlertError.swift
delete mode 100644 TalerWallet1/Helper/PublicConstants.swift
create mode 100644 TalerWallet1/Helper/URL+id+iban.swift
create mode 100644 TalerWallet1/Helper/View+Notification.swift
create mode 100644 TalerWallet1/Helper/WalletColors.swift
create mode 100644 TalerWallet1/Helper/playSound.swift
delete mode 100644 TalerWallet1/Model/ExchangeTestModel.swift
create mode 100644 TalerWallet1/Model/Model+Balances.swift
rename TalerWallet1/{Views/Exchange/ExchangeModel.swift =>
Model/Model+Exchange.swift} (53%)
create mode 100644 TalerWallet1/Model/Model+P2P.swift
rename TalerWallet1/{Views/Payment/PaymentURIModel.swift =>
Model/Model+Payment.swift} (64%)
create mode 100644 TalerWallet1/Model/Model+Pending.swift
create mode 100644 TalerWallet1/Model/Model+Settings.swift
create mode 100644 TalerWallet1/Model/Model+Transactions.swift
create mode 100644 TalerWallet1/Model/Model+Withdraw.swift
delete mode 100644 TalerWallet1/Model/WalletInitModel.swift
create mode 100644 TalerWallet1/Preview Content/transactions.json
create mode 100644 TalerWallet1/Settings.bundle/Root.plist
create mode 100644 TalerWallet1/Settings.bundle/en.lproj/Root.strings
delete mode 100644 TalerWallet1/Views/Balances/BalanceRow.swift
create mode 100644 TalerWallet1/Views/Balances/BalanceRowButtons.swift
create mode 100644 TalerWallet1/Views/Balances/BalanceRowView.swift
create mode 100644 TalerWallet1/Views/Balances/BalancesListView.swift
delete mode 100644 TalerWallet1/Views/Balances/BalancesModel.swift
create mode 100644 TalerWallet1/Views/Balances/BalancesSectionView.swift
delete mode 100644 TalerWallet1/Views/Balances/CurrenciesListView.swift
delete mode 100644 TalerWallet1/Views/Balances/CurrencyView.swift
delete mode 100644 TalerWallet1/Views/Balances/PendingRow.swift
create mode 100644 TalerWallet1/Views/Balances/PendingRowView.swift
create mode 100644 TalerWallet1/Views/Balances/UncompletedRowView.swift
delete mode 100644 TalerWallet1/Views/Balances/WalletEmptyView.swift
create mode 100644 TalerWallet1/Views/Exchange/ExchangeSectionView.swift
create mode 100644 TalerWallet1/Views/Exchange/ManualWithdraw.swift
create mode 100644 TalerWallet1/Views/Exchange/ManualWithdrawDone.swift
create mode 100644 TalerWallet1/Views/Exchange/QuiteSomeCoins.swift
create mode 100644 TalerWallet1/Views/HelperViews/CopyShare.swift
create mode 100644 TalerWallet1/Views/HelperViews/CurrencyField.swift
create mode 100644 TalerWallet1/Views/HelperViews/CurrencyInputView.swift
rename TalerWallet1/Views/{Main => HelperViews}/LaunchAnimationView.swift (67%)
create mode 100644 TalerWallet1/Views/HelperViews/ListStyle.swift
create mode 100644 TalerWallet1/Views/HelperViews/QRCodeDetailView.swift
create mode 100644 TalerWallet1/Views/HelperViews/QRGeneratorView.swift
create mode 100644 TalerWallet1/Views/HelperViews/SelectDays.swift
create mode 100644 TalerWallet1/Views/HelperViews/TransactionButton.swift
delete mode 100644 TalerWallet1/Views/Main/ContentView.swift
create mode 100644 TalerWallet1/Views/Main/MainView.swift
create mode 100644 TalerWallet1/Views/Main/WalletEmptyView.swift
create mode 100644 TalerWallet1/Views/Payment/DeleteMe.swift
delete mode 100644 TalerWallet1/Views/Payment/PaymentAcceptView.swift
create mode 100644 TalerWallet1/Views/Peer2peer/PaymentPurpose.swift
create mode 100644 TalerWallet1/Views/Peer2peer/RequestPayment.swift
create mode 100644 TalerWallet1/Views/Peer2peer/SendAmount.swift
create mode 100644 TalerWallet1/Views/Peer2peer/SendNow.swift
create mode 100644 TalerWallet1/Views/Peer2peer/SendPurpose.swift
delete mode 100644 TalerWallet1/Views/Pending/PendingModel.swift
delete mode 100644 TalerWallet1/Views/Pending/PendingOpsListView.swift
rename TalerWallet1/Views/{ => Settings}/Pending/PendingOpView.swift (62%)
create mode 100644 TalerWallet1/Views/Settings/Pending/PendingOpsListView.swift
create mode 100644 TalerWallet1/Views/Sheets/P2P_Sheets/P2pAcceptDone.swift
create mode 100644 TalerWallet1/Views/Sheets/P2P_Sheets/P2pPayURIView.swift
create mode 100644 TalerWallet1/Views/Sheets/P2P_Sheets/P2pReceiveURIView.swift
create mode 100644 TalerWallet1/Views/Sheets/QRSheet.swift
create mode 100644 TalerWallet1/Views/Sheets/ShareSheet.swift
create mode 100644 TalerWallet1/Views/Sheets/Sheet.swift
create mode 100644 TalerWallet1/Views/Sheets/URLSheet.swift
create mode 100644 TalerWallet1/Views/Transactions/ManualDetails.swift
create mode 100644 TalerWallet1/Views/Transactions/ThreeAmounts.swift
delete mode 100644 TalerWallet1/Views/Transactions/TransactionDetail.swift
create mode 100644 TalerWallet1/Views/Transactions/TransactionDetailView.swift
delete mode 100644 TalerWallet1/Views/Transactions/TransactionRow.swift
create mode 100644 TalerWallet1/Views/Transactions/TransactionRowView.swift
create mode 100644 TalerWallet1/Views/Transactions/TransactionsEmptyView.swift
delete mode 100644 TalerWallet1/Views/Transactions/TransactionsModel.swift
delete mode 100644 TalerWallet1/Views/URLSheet.swift
delete mode 100644 TalerWallet1/Views/Withdraw/WithdrawAcceptView.swift
delete mode 100644 TalerWallet1/Views/Withdraw/WithdrawProgressView.swift
delete mode 100644 TalerWallet1/Views/Withdraw/WithdrawTOSView.swift
delete mode 100644 TalerWallet1/Views/Withdraw/WithdrawURIModel.swift
delete mode 100644 TalerWallet1/Views/Withdraw/WithdrawURIView.swift
create mode 100644
TalerWallet1/Views/WithdrawBankIntegrated/WithdrawAcceptDone.swift
create mode 100644
TalerWallet1/Views/WithdrawBankIntegrated/WithdrawAcceptView.swift
create mode 100644
TalerWallet1/Views/WithdrawBankIntegrated/WithdrawProgressView.swift
create mode 100644
TalerWallet1/Views/WithdrawBankIntegrated/WithdrawTOSView.swift
create mode 100644
TalerWallet1/Views/WithdrawBankIntegrated/WithdrawURIView.swift
diff --git a/TalerWalletT.entitlements b/GNU Taler.entitlements
similarity index 100%
rename from TalerWalletT.entitlements
rename to GNU Taler.entitlements
diff --git a/Info.plist b/Info.plist
index 229b02d..8634a0f 100644
--- a/Info.plist
+++ b/Info.plist
@@ -12,7 +12,7 @@
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>CFBundleURLName</key>
- <string>com.taler-systems.talerwallet15</string>
+ <string>com.taler-systems.gnutalerwallet09</string>
<key>CFBundleURLSchemes</key>
<array>
<string>taler</string>
@@ -24,17 +24,12 @@
</array>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
- <key>UIApplicationSceneManifest</key>
- <dict>
- <key>UIApplicationSupportsMultipleScenes</key>
- <true/>
- <key>UISceneConfigurations</key>
- <dict/>
- </dict>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>processing</string>
</array>
+ <key>UIFileSharingEnabled</key>
+ <true/>
</dict>
</plist>
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..ed661f4
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,12 @@
+Copyright ©2022-23 Taler Systems S.A.
+
+GNU Taler is free software; you can redistribute it and/or modify it under the
+terms of the GNU General Public License as published by the Free Software
+Foundation; either version 3, or (at your option) any later version.
+
+GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License along with
+GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
diff --git a/TalerTests/WalletBackendTests.swift
b/TalerTests/WalletBackendTests.swift
index 4a09133..20c8788 100644
--- a/TalerTests/WalletBackendTests.swift
+++ b/TalerTests/WalletBackendTests.swift
@@ -1,17 +1,6 @@
/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
*/
import XCTest
@testable import Taler
diff --git a/TalerUITests/TalerUITests.swift b/TalerUITests/TalerUITests.swift
index 24b772e..9c57fef 100644
--- a/TalerUITests/TalerUITests.swift
+++ b/TalerUITests/TalerUITests.swift
@@ -1,19 +1,7 @@
/*
- * This file is part of GNU Taler
- * (C) 2021 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
*/
-
import XCTest
class TalerUITests: XCTestCase {
diff --git a/TalerWallet.xcodeproj/project.pbxproj
b/TalerWallet.xcodeproj/project.pbxproj
index 2052f9a..e8fb295 100644
--- a/TalerWallet.xcodeproj/project.pbxproj
+++ b/TalerWallet.xcodeproj/project.pbxproj
@@ -7,7 +7,39 @@
objects = {
/* Begin PBXBuildFile section */
+ 4E16E12329F3BB99008B9C86 /* CurrencyFormatter.swift in Sources
*/ = {isa = PBXBuildFile; fileRef = 4E16E12229F3BB99008B9C86 /*
CurrencyFormatter.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
*/; };
+ 4E578E922A481D8600F21F1C /* playSound.swift in Sources */ =
{isa = PBXBuildFile; fileRef = 4E578E912A481D8600F21F1C /* playSound.swift */;
};
+ 4E578E942A4822D500F21F1C /* P2pPayURIView.swift in Sources */ =
{isa = PBXBuildFile; fileRef = 4E578E932A4822D500F21F1C /* P2pPayURIView.swift
*/; };
+ 4E5A88F52A38A4FD00072618 /* QRCodeDetailView.swift in Sources
*/ = {isa = PBXBuildFile; fileRef = 4E5A88F42A38A4FD00072618 /*
QRCodeDetailView.swift */; };
+ 4E5A88F72A3B9E5B00072618 /* WithdrawAcceptDone.swift in Sources
*/ = {isa = PBXBuildFile; fileRef = 4E5A88F62A3B9E5B00072618 /*
WithdrawAcceptDone.swift */; };
+ 4E6EDD852A3615BE0031D520 /* ManualDetails.swift in Sources */ =
{isa = PBXBuildFile; fileRef = 4E6EDD842A3615BE0031D520 /* ManualDetails.swift
*/; };
+ 4E6EDD872A363D8D0031D520 /* ListStyle.swift in Sources */ =
{isa = PBXBuildFile; fileRef = 4E6EDD862A363D8D0031D520 /* ListStyle.swift */;
};
+ 4E753A062A0952F8002D9328 /* DebugViewC.swift in Sources */ =
{isa = PBXBuildFile; fileRef = 4E753A052A0952F7002D9328 /* DebugViewC.swift */;
};
+ 4E753A082A0B6A5F002D9328 /* ShareSheet.swift in Sources */ =
{isa = PBXBuildFile; fileRef = 4E753A072A0B6A5F002D9328 /* ShareSheet.swift */;
};
+ 4E7940DE29FC307C00A9AEA1 /* SendPurpose.swift in Sources */ =
{isa = PBXBuildFile; fileRef = 4E7940DD29FC307C00A9AEA1 /* SendPurpose.swift
*/; };
+ 4E87C8732A31CB7F001C6406 /* TransactionsEmptyView.swift in
Sources */ = {isa = PBXBuildFile; fileRef = 4E87C8722A31CB7F001C6406 /*
TransactionsEmptyView.swift */; };
+ 4E87C8752A34B411001C6406 /* UncompletedRowView.swift in Sources
*/ = {isa = PBXBuildFile; fileRef = 4E87C8742A34B411001C6406 /*
UncompletedRowView.swift */; };
+ 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 /* 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 */; };
+ 4EAD117629F672FA008EDD0B /* KeyboardResponder.swift in Sources
*/ = {isa = PBXBuildFile; fileRef = 4EAD117529F672FA008EDD0B /*
KeyboardResponder.swift */; };
+ 4EB065442A4CD1A80039B91D /* BalanceRowButtons.swift in Sources
*/ = {isa = PBXBuildFile; fileRef = 4EB065432A4CD1A80039B91D /*
BalanceRowButtons.swift */; };
4EB094D629896CD20043A8A1 /* TalerWalletTests.swift in Sources
*/ = {isa = PBXBuildFile; fileRef = 4EB094D429896CD20043A8A1 /*
TalerWalletTests.swift */; };
4EB094D729896CD20043A8A1 /* WalletBackendTests.swift in Sources
*/ = {isa = PBXBuildFile; fileRef = 4EB094D529896CD20043A8A1 /*
WalletBackendTests.swift */; };
4EB094DC29896D030043A8A1 /* TalerWalletUITestsLaunchTests.swift
in Sources */ = {isa = PBXBuildFile; fileRef = 4EB094D929896D030043A8A1 /*
TalerWalletUITestsLaunchTests.swift */; };
@@ -23,47 +55,55 @@
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 /* ExchangeTestModel.swift in Sources
*/ = {isa = PBXBuildFile; fileRef = 4EB095102989CBB00043A8A1 /*
ExchangeTestModel.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 /* TransactionRow.swift in Sources */
= {isa = PBXBuildFile; fileRef = 4EB095302989CBFE0043A8A1 /*
TransactionRow.swift */; };
- 4EB095582989CBFE0043A8A1 /* TransactionDetail.swift in Sources
*/ = {isa = PBXBuildFile; fileRef = 4EB095312989CBFE0043A8A1 /*
TransactionDetail.swift */; };
- 4EB095592989CBFE0043A8A1 /* TransactionsModel.swift in Sources
*/ = {isa = PBXBuildFile; fileRef = 4EB095322989CBFE0043A8A1 /*
TransactionsModel.swift */; };
+ 4EB095572989CBFE0043A8A1 /* TransactionRowView.swift in Sources
*/ = {isa = PBXBuildFile; fileRef = 4EB095302989CBFE0043A8A1 /*
TransactionRowView.swift */; };
+ 4EB095582989CBFE0043A8A1 /* TransactionDetailView.swift in
Sources */ = {isa = PBXBuildFile; fileRef = 4EB095312989CBFE0043A8A1 /*
TransactionDetailView.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 /* BalanceRow.swift in Sources */ =
{isa = PBXBuildFile; fileRef = 4EB095362989CBFE0043A8A1 /* BalanceRow.swift */;
};
- 4EB0955D2989CBFE0043A8A1 /* CurrenciesListView.swift in Sources
*/ = {isa = PBXBuildFile; fileRef = 4EB095372989CBFE0043A8A1 /*
CurrenciesListView.swift */; };
- 4EB0955E2989CBFE0043A8A1 /* PendingRow.swift in Sources */ =
{isa = PBXBuildFile; fileRef = 4EB095382989CBFE0043A8A1 /* PendingRow.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 /* CurrencyView.swift in Sources */ =
{isa = PBXBuildFile; fileRef = 4EB0953A2989CBFE0043A8A1 /* CurrencyView.swift
*/; };
+ 4EB095602989CBFE0043A8A1 /* BalancesSectionView.swift in
Sources */ = {isa = PBXBuildFile; fileRef = 4EB0953A2989CBFE0043A8A1 /*
BalancesSectionView.swift */; };
4EB095612989CBFE0043A8A1 /* WithdrawURIView.swift in Sources */
= {isa = PBXBuildFile; fileRef = 4EB0953C2989CBFE0043A8A1 /*
WithdrawURIView.swift */; };
- 4EB095622989CBFE0043A8A1 /* WithdrawURIModel.swift in Sources
*/ = {isa = PBXBuildFile; fileRef = 4EB0953D2989CBFE0043A8A1 /*
WithdrawURIModel.swift */; };
- 4EB095632989CBFE0043A8A1 /* WithdrawAcceptView.swift in Sources
*/ = {isa = PBXBuildFile; fileRef = 4EB0953E2989CBFE0043A8A1 /*
WithdrawAcceptView.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
*/; };
4EB095672989CBFE0043A8A1 /* LaunchAnimationView.swift in
Sources */ = {isa = PBXBuildFile; fileRef = 4EB095432989CBFE0043A8A1 /*
LaunchAnimationView.swift */; };
- 4EB095682989CBFE0043A8A1 /* ContentView.swift in Sources */ =
{isa = PBXBuildFile; fileRef = 4EB095442989CBFE0043A8A1 /* ContentView.swift
*/; };
+ 4EB095682989CBFE0043A8A1 /* MainView.swift in Sources */ = {isa
= PBXBuildFile; fileRef = 4EB095442989CBFE0043A8A1 /* MainView.swift */; };
4EB095692989CBFE0043A8A1 /* ErrorView.swift in Sources */ =
{isa = PBXBuildFile; fileRef = 4EB095452989CBFE0043A8A1 /* ErrorView.swift */;
};
4EB0956A2989CBFE0043A8A1 /* Buttons.swift in Sources */ = {isa
= PBXBuildFile; fileRef = 4EB095472989CBFE0043A8A1 /* Buttons.swift */; };
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 */; };
+ 4EB431672A1E55C700C5690E /* ManualWithdrawDone.swift in Sources
*/ = {isa = PBXBuildFile; fileRef = 4EB431662A1E55C700C5690E /*
ManualWithdrawDone.swift */; };
+ 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 /* 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 */; };
+ 4EEC157629F8ECBF00D46A03 /* CodeScanner in Frameworks */ = {isa
= PBXBuildFile; productRef = 4EEC157529F8ECBF00D46A03 /* CodeScanner */; };
+ 4EEC157829F9032900D46A03 /* Sheet.swift in Sources */ = {isa =
PBXBuildFile; fileRef = 4EEC157729F9032900D46A03 /* Sheet.swift */; };
+ 4EEC157A29F9427F00D46A03 /* QRSheet.swift in Sources */ = {isa
= PBXBuildFile; fileRef = 4EEC157929F9427F00D46A03 /* QRSheet.swift */; };
+ 4EF840A72A0B85F400EE0D47 /* CopyShare.swift in Sources */ =
{isa = PBXBuildFile; fileRef = 4EF840A62A0B85F400EE0D47 /* CopyShare.swift */;
};
ABC13AA32859962800D23185 /* taler-swift in Frameworks */ = {isa
= PBXBuildFile; productRef = ABC13AA22859962800D23185 /* taler-swift */; };
ABE97B1D286D82BF00580772 /* AnyCodable in Frameworks */ = {isa
= PBXBuildFile; productRef = ABE97B1C286D82BF00580772 /* AnyCodable */; };
/* End PBXBuildFile section */
@@ -98,8 +138,41 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
- 4E3AE7EF29A7E8F40070BEC4 /* TalerWalletT.entitlements */ = {isa
= PBXFileReference; lastKnownFileType = text.plist.entitlements; path =
TalerWalletT.entitlements; sourceTree = "<group>"; };
+ 4E16E12229F3BB99008B9C86 /* CurrencyFormatter.swift */ = {isa =
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path
= CurrencyFormatter.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>"; };
+ 4E578E912A481D8600F21F1C /* playSound.swift */ = {isa =
PBXFileReference; lastKnownFileType = sourcecode.swift; path = playSound.swift;
sourceTree = "<group>"; };
+ 4E578E932A4822D500F21F1C /* P2pPayURIView.swift */ = {isa =
PBXFileReference; lastKnownFileType = sourcecode.swift; path =
P2pPayURIView.swift; sourceTree = "<group>"; };
+ 4E5A88F42A38A4FD00072618 /* QRCodeDetailView.swift */ = {isa =
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path
= QRCodeDetailView.swift; sourceTree = "<group>"; };
+ 4E5A88F62A3B9E5B00072618 /* WithdrawAcceptDone.swift */ = {isa
= PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift;
path = WithdrawAcceptDone.swift; sourceTree = "<group>"; };
+ 4E6EDD842A3615BE0031D520 /* ManualDetails.swift */ = {isa =
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path
= ManualDetails.swift; sourceTree = "<group>"; };
+ 4E6EDD862A363D8D0031D520 /* ListStyle.swift */ = {isa =
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path
= ListStyle.swift; sourceTree = "<group>"; };
+ 4E753A042A08E720002D9328 /* transactions.json */ = {isa =
PBXFileReference; lastKnownFileType = text.json; path = transactions.json;
sourceTree = "<group>"; };
+ 4E753A052A0952F7002D9328 /* DebugViewC.swift */ = {isa =
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path
= DebugViewC.swift; sourceTree = "<group>"; };
+ 4E753A072A0B6A5F002D9328 /* ShareSheet.swift */ = {isa =
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path
= ShareSheet.swift; sourceTree = "<group>"; };
+ 4E7940DD29FC307C00A9AEA1 /* SendPurpose.swift */ = {isa =
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path
= SendPurpose.swift; sourceTree = "<group>"; };
+ 4E87C8722A31CB7F001C6406 /* TransactionsEmptyView.swift */ =
{isa = PBXFileReference; fileEncoding = 4; lastKnownFileType =
sourcecode.swift; path = TransactionsEmptyView.swift; sourceTree = "<group>"; };
+ 4E87C8742A34B411001C6406 /* UncompletedRowView.swift */ = {isa
= PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift;
path = UncompletedRowView.swift; sourceTree = "<group>"; };
+ 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 /* 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>"; };
+ 4EAD117529F672FA008EDD0B /* KeyboardResponder.swift */ = {isa =
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path
= KeyboardResponder.swift; sourceTree = "<group>"; };
+ 4EB065432A4CD1A80039B91D /* BalanceRowButtons.swift */ = {isa =
PBXFileReference; lastKnownFileType = sourcecode.swift; path =
BalanceRowButtons.swift; sourceTree = "<group>"; };
4EB094D429896CD20043A8A1 /* TalerWalletTests.swift */ = {isa =
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path
= TalerWalletTests.swift; sourceTree = "<group>"; };
4EB094D529896CD20043A8A1 /* WalletBackendTests.swift */ = {isa
= PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift;
path = WalletBackendTests.swift; sourceTree = "<group>"; };
4EB094D929896D030043A8A1 /* TalerWalletUITestsLaunchTests.swift
*/ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType =
sourcecode.swift; path = TalerWalletUITestsLaunchTests.swift; sourceTree =
"<group>"; };
@@ -115,49 +188,56 @@
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 /* ExchangeTestModel.swift */ = {isa =
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path
= ExchangeTestModel.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 /* TransactionRow.swift */ = {isa =
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path
= TransactionRow.swift; sourceTree = "<group>"; };
- 4EB095312989CBFE0043A8A1 /* TransactionDetail.swift */ = {isa =
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path
= TransactionDetail.swift; sourceTree = "<group>"; };
- 4EB095322989CBFE0043A8A1 /* TransactionsModel.swift */ = {isa =
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path
= TransactionsModel.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 /* 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 /* BalanceRow.swift */ = {isa =
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path
= BalanceRow.swift; sourceTree = "<group>"; };
- 4EB095372989CBFE0043A8A1 /* CurrenciesListView.swift */ = {isa
= PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift;
path = CurrenciesListView.swift; sourceTree = "<group>"; };
- 4EB095382989CBFE0043A8A1 /* PendingRow.swift */ = {isa =
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path
= PendingRow.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 /* CurrencyView.swift */ = {isa =
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path
= CurrencyView.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 /* WithdrawURIModel.swift */ = {isa =
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path
= WithdrawURIModel.swift; sourceTree = "<group>"; };
- 4EB0953E2989CBFE0043A8A1 /* WithdrawAcceptView.swift */ = {isa
= PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift;
path = WithdrawAcceptView.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>"; };
4EB095432989CBFE0043A8A1 /* LaunchAnimationView.swift */ = {isa
= PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift;
path = LaunchAnimationView.swift; sourceTree = "<group>"; };
- 4EB095442989CBFE0043A8A1 /* ContentView.swift */ = {isa =
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path
= ContentView.swift; sourceTree = "<group>"; };
+ 4EB095442989CBFE0043A8A1 /* MainView.swift */ = {isa =
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path
= MainView.swift; sourceTree = "<group>"; };
4EB095452989CBFE0043A8A1 /* ErrorView.swift */ = {isa =
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path
= ErrorView.swift; sourceTree = "<group>"; };
4EB095472989CBFE0043A8A1 /* Buttons.swift */ = {isa =
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path
= Buttons.swift; sourceTree = "<group>"; };
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>"; };
+ 4EB431662A1E55C700C5690E /* ManualWithdrawDone.swift */ = {isa
= PBXFileReference; lastKnownFileType = sourcecode.swift; path =
ManualWithdrawDone.swift; sourceTree = "<group>"; };
+ 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 /* 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>"; };
+ 4EEC157729F9032900D46A03 /* Sheet.swift */ = {isa =
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path
= Sheet.swift; sourceTree = "<group>"; };
+ 4EEC157929F9427F00D46A03 /* QRSheet.swift */ = {isa =
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path
= QRSheet.swift; sourceTree = "<group>"; };
+ 4EF840A62A0B85F400EE0D47 /* CopyShare.swift */ = {isa =
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path
= CopyShare.swift; sourceTree = "<group>"; };
AB710490285995B6008B04F0 /* taler-swift */ = {isa =
PBXFileReference; lastKnownFileType = text; path = "taler-swift"; sourceTree =
SOURCE_ROOT; };
- D14AFD1D24D232B300C51073 /* TalerWalletT.app */ = {isa =
PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0;
path = TalerWalletT.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ D14AFD1D24D232B300C51073 /* GNU Taler.app */ = {isa =
PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0;
path = "GNU Taler.app"; sourceTree = BUILT_PRODUCTS_DIR; };
D14AFD3324D232B500C51073 /* TalerTests.xctest */ = {isa =
PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path
= TalerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
D14AFD3E24D232B500C51073 /* TalerUITests.xctest */ = {isa =
PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path
= TalerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
@@ -171,6 +251,7 @@
4EB094FD29897D280043A8A1 /* SymLog in
Frameworks */,
4EB094F829897CA20043A8A1 /*
FTalerWalletcore.framework in Frameworks */,
ABC13AA32859962800D23185 /* taler-swift in
Frameworks */,
+ 4EEC157629F8ECBF00D46A03 /* CodeScanner in
Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -191,6 +272,16 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
+ 4E3B4BBF2A41E64000CC88B8 /* P2P_Sheets */ = {
+ isa = PBXGroup;
+ children = (
+ 4E3B4BC02A41E6C200CC88B8 /*
P2pReceiveURIView.swift */,
+ 4E3B4BC22A42252300CC88B8 /* P2pAcceptDone.swift
*/,
+ 4E578E932A4822D500F21F1C /* P2pPayURIView.swift
*/,
+ );
+ path = P2P_Sheets;
+ sourceTree = "<group>";
+ };
4EB094EE298979840043A8A1 /* TalerWallet1 */ = {
isa = PBXGroup;
children = (
@@ -198,9 +289,10 @@
4EB095232989CBFE0043A8A1 /* Views */,
4EB0950F2989CBB00043A8A1 /* Model */,
4EB0951A2989CBCB0043A8A1 /* Backend */,
- 4EB0950C2989CB9A0043A8A1 /* Quickjs */,
4EB095052989CB7C0043A8A1 /* Helper */,
+ 4EB0950C2989CB9A0043A8A1 /* Quickjs */,
4EB094EF298979D30043A8A1 /* Assets.xcassets */,
+ 4E363CBF2A24754200D7E98C /* Settings.bundle */,
4EB094F529897A9A0043A8A1 /* Preview Content */,
);
path = TalerWallet1;
@@ -210,6 +302,7 @@
isa = PBXGroup;
children = (
4EB094F329897A510043A8A1 /* Preview
Assets.xcassets */,
+ 4E753A042A08E720002D9328 /* transactions.json
*/,
);
path = "Preview Content";
sourceTree = "<group>";
@@ -227,6 +320,8 @@
children = (
4EB094EC298979620043A8A1 /*
TalerWallet1App.swift */,
4EB095012989C9BC0043A8A1 /* Controller.swift */,
+ 4EA1ABBD29A3833A008821EA /*
PublicConstants.swift */,
+ 4E753A052A0952F7002D9328 /* DebugViewC.swift */,
);
path = Controllers;
sourceTree = "<group>";
@@ -234,10 +329,19 @@
4EB095052989CB7C0043A8A1 /* Helper */ = {
isa = PBXGroup;
children = (
+ 4E97968F2A3765ED006F73BC /* AgePicker.swift */,
+ 4E363CBD2A23CB2100D7E98C /*
AnyTransition+backslide.swift */,
+ 4E16E12229F3BB99008B9C86 /*
CurrencyFormatter.swift */,
+ 4EAD117529F672FA008EDD0B /*
KeyboardResponder.swift */,
+ 4E363CC12A2621C200D7E98C /*
LocalizedAlertError.swift */,
+ 4E578E912A481D8600F21F1C /* playSound.swift */,
4EB095062989CB7C0043A8A1 /* TalerDater.swift */,
4EB095072989CB7C0043A8A1 /* TalerStrings.swift
*/,
4EB095082989CB7C0043A8A1 /*
View+dismissTop.swift */,
- 4EA1ABBD29A3833A008821EA /*
PublicConstants.swift */,
+ 4E3B4BC62A429F2A00CC88B8 /*
View+Notification.swift */,
+ 4E363CBB2A237E0900D7E98C /* URL+id+iban.swift
*/,
+ 4E9320422A14F6EA00A87B0E /* WalletColors.swift
*/,
+ 4E8E25322A1CD39700A27BFA /*
EqualIconWidthDomain.swift */,
);
path = Helper;
sourceTree = "<group>";
@@ -253,9 +357,15 @@
4EB0950F2989CBB00043A8A1 /* Model */ = {
isa = PBXGroup;
children = (
- 4EB095102989CBB00043A8A1 /*
ExchangeTestModel.swift */,
4EB095112989CBB00043A8A1 /* WalletModel.swift
*/,
- 4EB095142989CBB00043A8A1 /*
WalletInitModel.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>";
@@ -275,15 +385,15 @@
isa = PBXGroup;
children = (
4EB095412989CBFE0043A8A1 /* Main */,
- 4EB095242989CBFE0043A8A1 /* Settings */,
+ 4EB095342989CBFE0043A8A1 /* Balances */,
+ 4EB0952E2989CBFE0043A8A1 /* Transactions */,
4EB095272989CBFE0043A8A1 /* Exchange */,
+ 4EB095242989CBFE0043A8A1 /* Settings */,
+ 4ECB627E2A0BA4DA004ABBB7 /* Peer2peer */,
+ 4EEC157129F7188B00D46A03 /* Sheets */,
+ 4EB0953B2989CBFE0043A8A1 /*
WithdrawBankIntegrated */,
4EB0952A2989CBFE0043A8A1 /* Payment */,
- 4EB0952E2989CBFE0043A8A1 /* Transactions */,
- 4EB095332989CBFE0043A8A1 /* URLSheet.swift */,
- 4EB095342989CBFE0043A8A1 /* Balances */,
- 4EB0953B2989CBFE0043A8A1 /* Withdraw */,
4EB095462989CBFE0043A8A1 /* HelperViews */,
- 4EB0954B2989CBFE0043A8A1 /* Pending */,
);
path = Views;
sourceTree = "<group>";
@@ -293,6 +403,7 @@
children = (
4EB095252989CBFE0043A8A1 /* SettingsView.swift
*/,
4EB095262989CBFE0043A8A1 /* SettingsItem.swift
*/,
+ 4EB0954B2989CBFE0043A8A1 /* Pending */,
);
path = Settings;
sourceTree = "<group>";
@@ -300,8 +411,11 @@
4EB095272989CBFE0043A8A1 /* Exchange */ = {
isa = PBXGroup;
children = (
- 4EB095282989CBFE0043A8A1 /* ExchangeModel.swift
*/,
4EB095292989CBFE0043A8A1 /*
ExchangeListView.swift */,
+ 4EC90C772A1B528B0071DC58 /*
ExchangeSectionView.swift */,
+ 4E50B34F2A1BEE8000F9F01C /*
ManualWithdraw.swift */,
+ 4EBA82AC2A3F580500E5F39A /*
QuiteSomeCoins.swift */,
+ 4EB431662A1E55C700C5690E /*
ManualWithdrawDone.swift */,
);
path = Exchange;
sourceTree = "<group>";
@@ -309,8 +423,6 @@
4EB0952A2989CBFE0043A8A1 /* Payment */ = {
isa = PBXGroup;
children = (
- 4EB0952B2989CBFE0043A8A1 /*
PaymentAcceptView.swift */,
- 4EB0952C2989CBFE0043A8A1 /*
PaymentURIModel.swift */,
4EB0952D2989CBFE0043A8A1 /*
PaymentURIView.swift */,
);
path = Payment;
@@ -320,9 +432,11 @@
isa = PBXGroup;
children = (
4EB0952F2989CBFE0043A8A1 /*
TransactionsListView.swift */,
- 4EB095302989CBFE0043A8A1 /*
TransactionRow.swift */,
- 4EB095312989CBFE0043A8A1 /*
TransactionDetail.swift */,
- 4EB095322989CBFE0043A8A1 /*
TransactionsModel.swift */,
+ 4EB095302989CBFE0043A8A1 /*
TransactionRowView.swift */,
+ 4EB095312989CBFE0043A8A1 /*
TransactionDetailView.swift */,
+ 4E87C8722A31CB7F001C6406 /*
TransactionsEmptyView.swift */,
+ 4E6EDD842A3615BE0031D520 /* ManualDetails.swift
*/,
+ 4ED2F94A2A278F5100453B40 /* ThreeAmounts.swift
*/,
);
path = Transactions;
sourceTree = "<group>";
@@ -330,35 +444,34 @@
4EB095342989CBFE0043A8A1 /* Balances */ = {
isa = PBXGroup;
children = (
- 4EB095352989CBFE0043A8A1 /* BalancesModel.swift
*/,
- 4EB095362989CBFE0043A8A1 /* BalanceRow.swift */,
- 4EB095372989CBFE0043A8A1 /*
CurrenciesListView.swift */,
- 4EB095382989CBFE0043A8A1 /* PendingRow.swift */,
- 4EB095392989CBFE0043A8A1 /*
WalletEmptyView.swift */,
- 4EB0953A2989CBFE0043A8A1 /* CurrencyView.swift
*/,
+ 4EB095372989CBFE0043A8A1 /*
BalancesListView.swift */,
+ 4EB0953A2989CBFE0043A8A1 /*
BalancesSectionView.swift */,
+ 4EB095362989CBFE0043A8A1 /*
BalanceRowView.swift */,
+ 4EB065432A4CD1A80039B91D /*
BalanceRowButtons.swift */,
+ 4EB095382989CBFE0043A8A1 /*
PendingRowView.swift */,
+ 4E87C8742A34B411001C6406 /*
UncompletedRowView.swift */,
);
path = Balances;
sourceTree = "<group>";
};
- 4EB0953B2989CBFE0043A8A1 /* Withdraw */ = {
+ 4EB0953B2989CBFE0043A8A1 /* WithdrawBankIntegrated */ = {
isa = PBXGroup;
children = (
4EB0953C2989CBFE0043A8A1 /*
WithdrawURIView.swift */,
- 4EB0953D2989CBFE0043A8A1 /*
WithdrawURIModel.swift */,
- 4EB0953E2989CBFE0043A8A1 /*
WithdrawAcceptView.swift */,
+ 4E5A88F62A3B9E5B00072618 /*
WithdrawAcceptDone.swift */,
4EB0953F2989CBFE0043A8A1 /*
WithdrawProgressView.swift */,
4EB095402989CBFE0043A8A1 /*
WithdrawTOSView.swift */,
);
- path = Withdraw;
+ path = WithdrawBankIntegrated;
sourceTree = "<group>";
};
4EB095412989CBFE0043A8A1 /* Main */ = {
isa = PBXGroup;
children = (
- 4EB095442989CBFE0043A8A1 /* ContentView.swift
*/,
+ 4EB095442989CBFE0043A8A1 /* MainView.swift */,
4EB095422989CBFE0043A8A1 /* SideBarView.swift
*/,
- 4EB095432989CBFE0043A8A1 /*
LaunchAnimationView.swift */,
4EB095452989CBFE0043A8A1 /* ErrorView.swift */,
+ 4EB095392989CBFE0043A8A1 /*
WalletEmptyView.swift */,
);
path = Main;
sourceTree = "<group>";
@@ -367,9 +480,18 @@
isa = PBXGroup;
children = (
4EB095472989CBFE0043A8A1 /* Buttons.swift */,
+ 4EF840A62A0B85F400EE0D47 /* CopyShare.swift */,
+ 4ECB62812A0BB01D004ABBB7 /* SelectDays.swift */,
+ 4E53A33629F50B7B00830EC2 /* CurrencyField.swift
*/,
+ 4EA551242A2C923600FEC9A8 /*
CurrencyInputView.swift */,
+ 4EEC157229F8242800D46A03 /*
QRGeneratorView.swift */,
+ 4E5A88F42A38A4FD00072618 /*
QRCodeDetailView.swift */,
+ 4E6EDD862A363D8D0031D520 /* ListStyle.swift */,
4EB095482989CBFE0043A8A1 /*
TextFieldAlert.swift */,
+ 4EBA82AA2A3EB2CA00E5F39A /*
TransactionButton.swift */,
4EB095492989CBFE0043A8A1 /* AmountView.swift */,
4EB0954A2989CBFE0043A8A1 /* LoadingView.swift
*/,
+ 4EB095432989CBFE0043A8A1 /*
LaunchAnimationView.swift */,
);
path = HelperViews;
sourceTree = "<group>";
@@ -377,19 +499,42 @@
4EB0954B2989CBFE0043A8A1 /* Pending */ = {
isa = PBXGroup;
children = (
- 4EB0954C2989CBFE0043A8A1 /* PendingModel.swift
*/,
- 4EB0954D2989CBFE0043A8A1 /* PendingOpView.swift
*/,
4EB0954E2989CBFE0043A8A1 /*
PendingOpsListView.swift */,
+ 4EB0954D2989CBFE0043A8A1 /* PendingOpView.swift
*/,
);
path = Pending;
sourceTree = "<group>";
};
+ 4ECB627E2A0BA4DA004ABBB7 /* Peer2peer */ = {
+ isa = PBXGroup;
+ children = (
+ 4E40E0BD29F25ABB00B85369 /* SendAmount.swift */,
+ 4E7940DD29FC307C00A9AEA1 /* SendPurpose.swift
*/,
+ 4EB3136029FEE79B007D68BC /* SendNow.swift */,
+ 4E9320442A1645B600A87B0E /*
RequestPayment.swift */,
+ 4E9320462A164BC700A87B0E /*
PaymentPurpose.swift */,
+ );
+ path = Peer2peer;
+ sourceTree = "<group>";
+ };
+ 4EEC157129F7188B00D46A03 /* Sheets */ = {
+ isa = PBXGroup;
+ children = (
+ 4EEC157729F9032900D46A03 /* Sheet.swift */,
+ 4EEC157929F9427F00D46A03 /* QRSheet.swift */,
+ 4E753A072A0B6A5F002D9328 /* ShareSheet.swift */,
+ 4EB095332989CBFE0043A8A1 /* URLSheet.swift */,
+ 4E3B4BBF2A41E64000CC88B8 /* P2P_Sheets */,
+ );
+ path = Sheets;
+ sourceTree = "<group>";
+ };
D14AFD1424D232B300C51073 = {
isa = PBXGroup;
children = (
- 4E3AE7EF29A7E8F40070BEC4 /*
TalerWalletT.entitlements */,
4EB094EE298979840043A8A1 /* TalerWallet1 */,
4EB094E129896FED0043A8A1 /* Info.plist */,
+ 4E3AE7EF29A7E8F40070BEC4 /* Taler
Wallet.entitlements */,
AB710490285995B6008B04F0 /* taler-swift */,
D14AFD3624D232B500C51073 /* TalerTests */,
D14AFD4124D232B500C51073 /* TalerUITests */,
@@ -401,7 +546,7 @@
D14AFD1E24D232B300C51073 /* Products */ = {
isa = PBXGroup;
children = (
- D14AFD1D24D232B300C51073 /* TalerWalletT.app */,
+ D14AFD1D24D232B300C51073 /* GNU Taler.app */,
D14AFD3324D232B500C51073 /* TalerTests.xctest
*/,
D14AFD3E24D232B500C51073 /* TalerUITests.xctest
*/,
);
@@ -430,9 +575,9 @@
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
- D14AFD1C24D232B300C51073 /* TalerWalletT */ = {
+ D14AFD1C24D232B300C51073 /* Taler_Wallet */ = {
isa = PBXNativeTarget;
- buildConfigurationList = D14AFD4724D232B500C51073 /*
Build configuration list for PBXNativeTarget "TalerWalletT" */;
+ buildConfigurationList = D14AFD4724D232B500C51073 /*
Build configuration list for PBXNativeTarget "Taler_Wallet" */;
buildPhases = (
D14AFD1924D232B300C51073 /* Sources */,
D14AFD1A24D232B300C51073 /* Frameworks */,
@@ -443,14 +588,15 @@
);
dependencies = (
);
- name = TalerWalletT;
+ name = Taler_Wallet;
packageProductDependencies = (
ABC13AA22859962800D23185 /* taler-swift */,
ABE97B1C286D82BF00580772 /* AnyCodable */,
4EB094FC29897D280043A8A1 /* SymLog */,
+ 4EEC157529F8ECBF00D46A03 /* CodeScanner */,
);
productName = Taler;
- productReference = D14AFD1D24D232B300C51073 /*
TalerWalletT.app */;
+ productReference = D14AFD1D24D232B300C51073 /* GNU
Taler.app */;
productType = "com.apple.product-type.application";
};
D14AFD3224D232B500C51073 /* TalerTests */ = {
@@ -527,12 +673,13 @@
packageReferences = (
ABE97B1B286D82BF00580772 /*
XCRemoteSwiftPackageReference "AnyCodable" */,
4EB094FB29897D280043A8A1 /*
XCRemoteSwiftPackageReference "SymLog" */,
+ 4EEC157429F8ECBF00D46A03 /*
XCRemoteSwiftPackageReference "CodeScanner" */,
);
productRefGroup = D14AFD1E24D232B300C51073 /* Products
*/;
projectDirPath = "";
projectRoot = "";
targets = (
- D14AFD1C24D232B300C51073 /* TalerWalletT */,
+ D14AFD1C24D232B300C51073 /* Taler_Wallet */,
D14AFD3224D232B500C51073 /* TalerTests */,
D14AFD3D24D232B500C51073 /* TalerUITests */,
);
@@ -545,6 +692,7 @@
buildActionMask = 2147483647;
files = (
4EB094F429897A510043A8A1 /* Preview
Assets.xcassets in Resources */,
+ 4E363CC02A24754200D7E98C /* Settings.bundle in
Resources */,
4EB094F0298979D30043A8A1 /* Assets.xcassets in
Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -570,43 +718,73 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- 4EB095512989CBFE0043A8A1 /* ExchangeModel.swift
in Sources */,
+ 4ECB62822A0BB01D004ABBB7 /* SelectDays.swift in
Sources */,
+ 4E9796902A3765ED006F73BC /* AgePicker.swift in
Sources */,
4EB095032989C9BC0043A8A1 /* Controller.swift in
Sources */,
- 4EB095682989CBFE0043A8A1 /* ContentView.swift
in Sources */,
+ 4EB095682989CBFE0043A8A1 /* MainView.swift in
Sources */,
4EB0956A2989CBFE0043A8A1 /* Buttons.swift in
Sources */,
- 4EB095602989CBFE0043A8A1 /* CurrencyView.swift
in Sources */,
+ 4EBA82AB2A3EB2CA00E5F39A /*
TransactionButton.swift in Sources */,
+ 4EB095602989CBFE0043A8A1 /*
BalancesSectionView.swift in Sources */,
+ 4EEC157329F8242800D46A03 /*
QRGeneratorView.swift in Sources */,
+ 4E5A88F72A3B9E5B00072618 /*
WithdrawAcceptDone.swift in Sources */,
4EB095222989CBCB0043A8A1 /* Transaction.swift
in Sources */,
- 4EB0955D2989CBFE0043A8A1 /*
CurrenciesListView.swift in Sources */,
- 4EB095532989CBFE0043A8A1 /*
PaymentAcceptView.swift in Sources */,
+ 4E9320432A14F6EA00A87B0E /* WalletColors.swift
in Sources */,
+ 4EB0955D2989CBFE0043A8A1 /*
BalancesListView.swift in Sources */,
4EB095212989CBCB0043A8A1 /*
WalletBackendError.swift in Sources */,
- 4EB0955E2989CBFE0043A8A1 /* PendingRow.swift in
Sources */,
- 4EB0955B2989CBFE0043A8A1 /* BalancesModel.swift
in Sources */,
- 4EB095632989CBFE0043A8A1 /*
WithdrawAcceptView.swift in Sources */,
+ 4EB0955E2989CBFE0043A8A1 /*
PendingRowView.swift in Sources */,
4EB0956D2989CBFE0043A8A1 /* LoadingView.swift
in Sources */,
- 4EB095542989CBFE0043A8A1 /*
PaymentURIModel.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 */,
+ 4E578E942A4822D500F21F1C /* P2pPayURIView.swift
in Sources */,
+ 4EB095542989CBFE0043A8A1 /* Model+Payment.swift
in Sources */,
4EB0954F2989CBFE0043A8A1 /* SettingsView.swift
in Sources */,
4EB095552989CBFE0043A8A1 /*
PaymentURIView.swift in Sources */,
4EB095612989CBFE0043A8A1 /*
WithdrawURIView.swift in Sources */,
+ 4EF840A72A0B85F400EE0D47 /* CopyShare.swift in
Sources */,
4EB094ED298979620043A8A1 /*
TalerWallet1App.swift in Sources */,
4EB095652989CBFE0043A8A1 /*
WithdrawTOSView.swift in Sources */,
+ 4EEC157829F9032900D46A03 /* Sheet.swift in
Sources */,
+ 4E6EDD852A3615BE0031D520 /* ManualDetails.swift
in Sources */,
4EB0950B2989CB7C0043A8A1 /*
View+dismissTop.swift in Sources */,
4EB095562989CBFE0043A8A1 /*
TransactionsListView.swift in Sources */,
4EB0951F2989CBCB0043A8A1 /*
WalletBackendRequest.swift in Sources */,
- 4EB095572989CBFE0043A8A1 /*
TransactionRow.swift in Sources */,
+ 4EAD117629F672FA008EDD0B /*
KeyboardResponder.swift in Sources */,
+ 4EB095572989CBFE0043A8A1 /*
TransactionRowView.swift in Sources */,
4EA1ABBE29A3833A008821EA /*
PublicConstants.swift in Sources */,
+ 4EB3136129FEE79B007D68BC /* SendNow.swift in
Sources */,
4EB0956B2989CBFE0043A8A1 /*
TextFieldAlert.swift in Sources */,
+ 4EBA82AD2A3F580500E5F39A /*
QuiteSomeCoins.swift in Sources */,
+ 4EB431672A1E55C700C5690E /*
ManualWithdrawDone.swift in Sources */,
+ 4E9320472A164BC700A87B0E /*
PaymentPurpose.swift in Sources */,
+ 4E753A082A0B6A5F002D9328 /* ShareSheet.swift in
Sources */,
4EB0956C2989CBFE0043A8A1 /* AmountView.swift in
Sources */,
- 4EB095592989CBFE0043A8A1 /*
TransactionsModel.swift in Sources */,
+ 4E3B4BC32A42252300CC88B8 /* P2pAcceptDone.swift
in Sources */,
+ 4E363CBE2A23CB2100D7E98C /*
AnyTransition+backslide.swift in Sources */,
+ 4EB065442A4CD1A80039B91D /*
BalanceRowButtons.swift in Sources */,
+ 4EB095592989CBFE0043A8A1 /*
Model+Transactions.swift in Sources */,
+ 4E578E922A481D8600F21F1C /* playSound.swift in
Sources */,
4EB0955F2989CBFE0043A8A1 /*
WalletEmptyView.swift in Sources */,
- 4EB095192989CBB00043A8A1 /*
WalletInitModel.swift in Sources */,
+ 4E16E12329F3BB99008B9C86 /*
CurrencyFormatter.swift in Sources */,
4EB095092989CB7C0043A8A1 /* TalerDater.swift in
Sources */,
+ 4E3B4BC52A428AF700CC88B8 /*
Model+Balances.swift in Sources */,
+ 4E363CC22A2621C200D7E98C /*
LocalizedAlertError.swift in Sources */,
4EB0950E2989CB9A0043A8A1 /* quickjs.swift in
Sources */,
- 4EB095152989CBB00043A8A1 /*
ExchangeTestModel.swift in Sources */,
+ 4E53A33729F50B7B00830EC2 /* CurrencyField.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 */,
- 4EB095582989CBFE0043A8A1 /*
TransactionDetail.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 */,
4EB095672989CBFE0043A8A1 /*
LaunchAnimationView.swift in Sources */,
4EB095662989CBFE0043A8A1 /* SideBarView.swift
in Sources */,
@@ -614,10 +792,18 @@
4EB095702989CBFE0043A8A1 /*
PendingOpsListView.swift in Sources */,
4EB095162989CBB00043A8A1 /* WalletModel.swift
in Sources */,
4EB0955A2989CBFE0043A8A1 /* URLSheet.swift in
Sources */,
- 4EB095622989CBFE0043A8A1 /*
WithdrawURIModel.swift in Sources */,
+ 4ED2F94B2A278F5100453B40 /* ThreeAmounts.swift
in Sources */,
+ 4EB095622989CBFE0043A8A1 /*
Model+Withdraw.swift in Sources */,
+ 4EC90C782A1B528B0071DC58 /*
ExchangeSectionView.swift in Sources */,
+ 4E7940DE29FC307C00A9AEA1 /* SendPurpose.swift
in Sources */,
+ 4ECB62802A0BA6DF004ABBB7 /* Model+P2P.swift in
Sources */,
4EB0950A2989CB7C0043A8A1 /* TalerStrings.swift
in Sources */,
+ 4EA551252A2C923600FEC9A8 /*
CurrencyInputView.swift in Sources */,
+ 4E363CBC2A237E0900D7E98C /* URL+id+iban.swift
in Sources */,
+ 4E9320452A1645B600A87B0E /*
RequestPayment.swift in Sources */,
4EB095502989CBFE0043A8A1 /* SettingsItem.swift
in Sources */,
- 4EB0955C2989CBFE0043A8A1 /* BalanceRow.swift in
Sources */,
+ 4EB0955C2989CBFE0043A8A1 /*
BalanceRowView.swift in Sources */,
+ 4E753A062A0952F8002D9328 /* DebugViewC.swift in
Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -645,12 +831,12 @@
/* Begin PBXTargetDependency section */
D14AFD3524D232B500C51073 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
- target = D14AFD1C24D232B300C51073 /* TalerWalletT */;
+ target = D14AFD1C24D232B300C51073 /* Taler_Wallet */;
targetProxy = D14AFD3424D232B500C51073 /*
PBXContainerItemProxy */;
};
D14AFD4024D232B500C51073 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
- target = D14AFD1C24D232B300C51073 /* TalerWalletT */;
+ target = D14AFD1C24D232B300C51073 /* Taler_Wallet */;
targetProxy = D14AFD3F24D232B500C51073 /*
PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
@@ -776,16 +962,20 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
- CODE_SIGN_ENTITLEMENTS =
TalerWalletT.entitlements;
- CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 2;
- DEVELOPMENT_TEAM = GUDDQ9428Y;
+ CODE_SIGN_ENTITLEMENTS = "GNU
Taler.entitlements";
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone
Developer";
+ CODE_SIGN_STYLE = Manual;
+ CURRENT_PROJECT_VERSION = 10;
+ DEVELOPMENT_TEAM = "";
+ "DEVELOPMENT_TEAM[sdk=iphoneos*]" = GUDDQ9428Y;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Info.plist;
- INFOPLIST_KEY_CFBundleDisplayName = "Taler
Wallet";
+ INFOPLIST_KEY_CFBundleDisplayName = "GNU Taler";
INFOPLIST_KEY_LSApplicationCategoryType =
"public.app-category.finance";
+ INFOPLIST_KEY_NSCameraUsageDescription = "Scan
QR Codes";
INFOPLIST_KEY_NSHumanReadableCopyright = "©
Taler-Systems.com";
+
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations
= UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad =
"UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
@@ -795,8 +985,10 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.9.3;
- PRODUCT_BUNDLE_IDENTIFIER =
"com.taler-systems.talerwallet15";
- PRODUCT_NAME = "$(TARGET_NAME)";
+ PRODUCT_BUNDLE_IDENTIFIER =
"com.taler-systems.talerwallet-1";
+ PRODUCT_NAME = "GNU Taler";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]"
= Dev230612a;
SUPPORTED_PLATFORMS = "iphoneos
iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
@@ -812,16 +1004,20 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME
= AccentColor;
CLANG_ENABLE_MODULES = YES;
- CODE_SIGN_ENTITLEMENTS =
TalerWalletT.entitlements;
- CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 2;
- DEVELOPMENT_TEAM = GUDDQ9428Y;
+ CODE_SIGN_ENTITLEMENTS = "GNU
Taler.entitlements";
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone
Distribution";
+ CODE_SIGN_STYLE = Manual;
+ CURRENT_PROJECT_VERSION = 10;
+ DEVELOPMENT_TEAM = "";
+ "DEVELOPMENT_TEAM[sdk=iphoneos*]" = GUDDQ9428Y;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Info.plist;
- INFOPLIST_KEY_CFBundleDisplayName = "Taler
Wallet";
+ INFOPLIST_KEY_CFBundleDisplayName = "GNU Taler";
INFOPLIST_KEY_LSApplicationCategoryType =
"public.app-category.finance";
+ INFOPLIST_KEY_NSCameraUsageDescription = "Scan
QR Codes";
INFOPLIST_KEY_NSHumanReadableCopyright = "©
Taler-Systems.com";
+
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations
= UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad =
"UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
@@ -831,8 +1027,10 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.9.3;
- PRODUCT_BUNDLE_IDENTIFIER =
"com.taler-systems.talerwallet15";
- PRODUCT_NAME = "$(TARGET_NAME)";
+ PRODUCT_BUNDLE_IDENTIFIER =
"com.taler-systems.talerwallet-1";
+ PRODUCT_NAME = "GNU Taler";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]"
= iOS_Distribution_230606;
SUPPORTED_PLATFORMS = "iphoneos
iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
@@ -880,7 +1078,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
- MARKETING_VERSION = 1.0;
+ MARKETING_VERSION = 0.9.3;
PRODUCT_BUNDLE_IDENTIFIER =
com.taler.TalerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO;
@@ -902,7 +1100,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
- MARKETING_VERSION = 1.0;
+ MARKETING_VERSION = 0.9.3;
PRODUCT_BUNDLE_IDENTIFIER =
com.taler.TalerUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO;
@@ -925,7 +1123,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
- MARKETING_VERSION = 1.0;
+ MARKETING_VERSION = 0.9.3;
PRODUCT_BUNDLE_IDENTIFIER =
com.taler.TalerUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO;
@@ -947,7 +1145,7 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
- D14AFD4724D232B500C51073 /* Build configuration list for
PBXNativeTarget "TalerWalletT" */ = {
+ D14AFD4724D232B500C51073 /* Build configuration list for
PBXNativeTarget "Taler_Wallet" */ = {
isa = XCConfigurationList;
buildConfigurations = (
D14AFD4824D232B500C51073 /* Debug */,
@@ -985,6 +1183,14 @@
minimumVersion = 0.1.0;
};
};
+ 4EEC157429F8ECBF00D46A03 /* XCRemoteSwiftPackageReference
"CodeScanner" */ = {
+ isa = XCRemoteSwiftPackageReference;
+ repositoryURL =
"https://github.com/twostraws/CodeScanner";
+ requirement = {
+ kind = upToNextMajorVersion;
+ minimumVersion = 2.0.0;
+ };
+ };
ABE97B1B286D82BF00580772 /* XCRemoteSwiftPackageReference
"AnyCodable" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL =
"https://github.com/Flight-School/AnyCodable";
@@ -1001,6 +1207,11 @@
package = 4EB094FB29897D280043A8A1 /*
XCRemoteSwiftPackageReference "SymLog" */;
productName = SymLog;
};
+ 4EEC157529F8ECBF00D46A03 /* CodeScanner */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = 4EEC157429F8ECBF00D46A03 /*
XCRemoteSwiftPackageReference "CodeScanner" */;
+ productName = CodeScanner;
+ };
ABC13AA22859962800D23185 /* taler-swift */ = {
isa = XCSwiftPackageProductDependency;
productName = "taler-swift";
diff --git a/TalerWallet1/Assets.xcassets/Taler-logo.imageset/Contents.json
b/TalerWallet1/Assets.xcassets/taler-logo-2023-red.imageset/Contents.json
similarity index 68%
copy from TalerWallet1/Assets.xcassets/Taler-logo.imageset/Contents.json
copy to TalerWallet1/Assets.xcassets/taler-logo-2023-red.imageset/Contents.json
index eb70695..50ee748 100644
--- a/TalerWallet1/Assets.xcassets/Taler-logo.imageset/Contents.json
+++ b/TalerWallet1/Assets.xcassets/taler-logo-2023-red.imageset/Contents.json
@@ -1,9 +1,9 @@
{
"images" : [
{
- "filename" : "Taler-logo.jpg",
+ "filename" : "taler-logo-2023-red.svg",
"idiom" : "universal"
- }
+ },
],
"info" : {
"author" : "xcode",
diff --git
a/TalerWallet1/Assets.xcassets/taler-logo-2023-red.imageset/taler-logo-2023-red.svg
b/TalerWallet1/Assets.xcassets/taler-logo-2023-red.imageset/taler-logo-2023-red.svg
new file mode 100644
index 0000000..3820d36
--- /dev/null
+++
b/TalerWallet1/Assets.xcassets/taler-logo-2023-red.imageset/taler-logo-2023-red.svg
@@ -0,0 +1,19 @@
+<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
+<g id="aa" style="fill:red;fill-rule:evenodd">
+<!-- 90% -->
+<path d="M57.6 43.4
+c-25.5 4.3-44.9 28-44.9 56.5 0 31.5 23.9 57.2 53.3 57.2
+s53.3-25.6 53.3-57.2
+c0-15.4-5.7-29.3-14.9-39.6
+c1.6-1.9 6.3-4.8 6.4-4.6 10 11.6 16.1 27.2 16.1 44.2 0 36-27.3 65.3-60.9
65.3-33.6 0-60.9-29.3-60.9-65.3
+s27.3-65.3 60.9-65.3
+c1.7 0 5.7.3 5.5.4-4.3 2.3-9.7 5.4-13.9 8.5"/>
+<!-- 40% -->
+<path d="M60.8 149.8
+c-13.4-12-22-29.9-22-50 0-36 27.4-65.2 61.1-65.2 1.5 0 3 .1 4.5.2
+a67.6 67.6 0 0 0-13.4 8.6
+c-25.4 4.5-44.7 28.1-44.7 56.4 0 21.3 11 40 27.3 49.8
+a45.9 45.9 0 0 1-12.7.3z"/>
+</g>
+<use transform="translate(200,200) rotate(180)" href="#aa"/>
+</svg>
\ No newline at end of file
diff --git a/TalerWallet1/Backend/Transaction.swift
b/TalerWallet1/Backend/Transaction.swift
index 873733e..cf323aa 100644
--- a/TalerWallet1/Backend/Transaction.swift
+++ b/TalerWallet1/Backend/Transaction.swift
@@ -1,17 +1,6 @@
/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
*/
import Foundation
import AnyCodable
@@ -26,22 +15,168 @@ enum TransactionDecodingError: Error {
case invalidStringValue
}
+enum TransactionMinorState: String, Codable {
+ // Placeholder until D37 is fully implemented
+ case unknown
+ case deposit
+ case kyc // KycRequired
+ case aml // AmlRequired
+ case mergeKycRequired = "merge-kyc"
+ case track
+ case pay
+ case rebindSession = "rebind-session"
+ case refresh
+ case pickup
+ case autoRefund = "auto-refund"
+ case user
+ case bank
+ case exchange
+ case claimProposal = "claim-proposal"
+ case checkRefund = "check-refund"
+ case createPurse = "create-purse"
+ case deletePurse = "delete-purse"
+ case ready
+ case merge
+ case repurchase
+ case bankRegisterReserve = "bank-register-reserve"
+ case bankConfirmTransfer = "bank-confirm-transfer"
+ case withdrawCoins = "withdraw-coins"
+ case exchangeWaitReserve = "exchange-wait-reserve"
+ case abortingBank = "aborting-bank"
+ case refused
+ case withdraw
+ case merchantOrderProposed = "merchant-order-proposed"
+ case proposed
+ case refundAvailable = "refund-available"
+ case acceptRefund = "accept-refund"
+
+ case submitPayment = "submit-payment"
+}
+
+enum TransactionMajorState: String, Codable {
+ // No state, only used when reporting transitions into the initial state
+ case none
+ case pending
+ case done
+ case aborting
+ case aborted
+ case suspended
+ case dialog
+ case suspendedAborting = "suspended-aborting"
+ case failed
+ case expired
+ // Only used for the notification, never in the transaction history
+ case deleted
+ // Placeholder until D37 is fully implemented
+ case unknown
+}
+
+struct TransactionState: Codable, Hashable {
+ var major: TransactionMajorState
+ var minor: TransactionMinorState?
+}
+
+struct TransactionTransition: Codable { // Notification
+ enum TransitionType: String, Codable {
+ case transition = "transaction-state-transition"
+ }
+ var type: TransitionType
+ var oldTxState: TransactionState
+ var newTxState: TransactionState
+ var transactionId: String
+}
+
+enum TxAction: String, Codable {
+ case abort // pending,dialog,suspended -> aborting
+// case revive // aborting -> pending ?? maybe post 1.0
+ case fail // aborting -> failed
+ case delete // dialog,done,expired,aborted,failed -> ()
+ case suspend // pending -> suspended; aborting -> ab_suspended
+ case resume // suspended -> pending; ab_suspended -> aborting
+}
+
+enum TransactionType: String, Codable {
+ case dummy
+ case withdrawal
+ case deposit
+ case payment
+ case refund
+ case refresh
+ case reward = "tip" // TODO: 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
+ case scanPullDebit = "peer-pull-debit" // scan QR, pay requested
+
+ var isWithdrawal : Bool { self == .withdrawal }
+ var isDeposit : Bool { self == .deposit }
+ var isPayment : Bool { self == .payment }
+ var isRefund : Bool { self == .refund }
+ var isRefresh : Bool { self == .refresh }
+ var isReward : Bool { self == .reward }
+ // var isTipPayment : Bool { self == .tip }
+ var isSendCoins : Bool { self == .peerPushDebit }
+ var isRcvCoins : Bool { self == .scanPushCredit }
+ var isSendInvoice: Bool { self == .peerPullCredit }
+ var isPayInvoice : Bool { self == .scanPullDebit }
+
+ var isP2pOutgoing: Bool { isSendCoins || isPayInvoice}
+ var isP2pIncoming: Bool { isSendInvoice || isRcvCoins}
+ var isIncoming : Bool { isP2pIncoming || isWithdrawal || isRefund ||
isReward }
+}
+
struct TransactionCommon: Decodable {
- var type: String
+ var type: TransactionType
+ var txState: TransactionState
var amountEffective: Amount
var amountRaw: Amount
var transactionId: String
var timestamp: Timestamp
- var extendedStatus: String // TODO: enum with some fixed values?
- var pending: Bool
- var frozen: Bool
+ var txActions: [TxAction]
+
+ func localizedType(_ type: TransactionType) -> String {
+ switch type {
+ case .dummy: return String("")
+ case .withdrawal: return String(localized: "Withdrawal",
+ comment: "TransactionType")
+ case .deposit: return String(localized: "Deposit",
+ comment: "TransactionType")
+ case .payment: return String(localized: "Payment",
+ comment: "TransactionType")
+ case .refund: return String(localized: "Refund",
+ comment: "TransactionType")
+ case .refresh: return String(localized: "Refresh",
+ comment: "TransactionType")
+ case .reward: return String(localized: "Reward",
+ comment: "TransactionType")
+ case .peerPushDebit: return String(localized: "P2P Send",
+ comment: "TransactionType,
send coins to another wallet")
+ case .scanPushCredit: return String(localized: "P2P Receive",
+ comment: "TransactionType,
scan to receive coins sent from another wallet")
+ case .peerPullCredit: return String(localized: "P2P Invoice",
+ comment: "TransactionType,
send invoice to another wallet")
+ case .scanPullDebit: return String(localized: "P2P Payment",
+ comment: "TransactionType,
scan invoice to pay to another wallet")
+ }
+ }
func fee() -> Amount {
do {
return try Amount.diff(amountRaw, amountEffective)
- } catch {
- return Amount(currency: amountRaw.currencyStr, integer: 0,
fraction: 0)
- }
+ } catch {}
+ do {
+ return try Amount.diff(amountEffective, amountRaw)
+ } catch {}
+ return Amount(currency: amountRaw.currencyStr, integer: 0, fraction: 0)
+ }
+
+ func incoming() -> Bool {
+ return type == .withdrawal
+ || type == .refund
+ || type == .reward
+ || type == .peerPullCredit
+ || type == .scanPushCredit
}
}
@@ -49,12 +184,11 @@ struct WithdrawalDetails: Decodable {
enum WithdrawalType: String, Decodable {
case manual = "manual-transfer"
case bankIntegrated = "taler-bank-integration-api"
- case peerPullCredit = "peer-pull-credit"
- case peerPushCredit = "peer-push-credit"
}
var type: WithdrawalType
/// The public key of the reserve.
var reservePub: String
+ var reserveIsReady: Bool
/// Details for manual withdrawals:
/// The payto URIs that the exchange supports.
@@ -79,11 +213,11 @@ struct WithdrawalTransaction {
struct PaymentTransactionDetails: Decodable {
var proposalId: String
- var status: String // "paid"
var totalRefundRaw: Amount
var totalRefundEffective: Amount
var refundPending: Amount?
-// var refunds: []
+ var refundQueryActive: Bool?
+ var refunds: [String]? // TODO: array type?
var info: OrderShortInfo
}
@@ -97,7 +231,7 @@ struct RefundTransactionDetails: Decodable {
var refundPending: Amount?
/// The amount that couldn't be applied because refund permissions expired.
var amountInvalid: Amount?
- var info: OrderShortInfo
+ var info: OrderShortInfo? // TODO: is this still here?
}
struct RefundTransaction {
@@ -105,19 +239,44 @@ struct RefundTransaction {
var details: RefundTransactionDetails
}
-struct TipTransactionDetails: Decodable {
- /// The exchange that the tip will be withdrawn from
+struct RewardTransactionDetails: Decodable {
+ /// The exchange that the reward will be withdrawn from
var exchangeBaseUrl: String
}
-struct TipTransaction {
+struct RewardTransaction {
var common: TransactionCommon
- var details: TipTransactionDetails
+ var details: RewardTransactionDetails
}
+//struct TipTransactionDetails: Decodable {
+// /// The exchange that the tip will be withdrawn from
+// var exchangeBaseUrl: String
+//}
+
+//struct TipTransaction {
+// var common: TransactionCommon
+// var details: TipTransactionDetails
+//}
+
+enum RefreshReason: String, Decodable {
+ case manual
+ case payMerchant = "pay-merchant"
+ case payDeposit = "pay-deposit"
+ case payPeerPush = "pay-peer-push"
+ case payPeerPull = "pay-peer-pull"
+ case refund
+ case abortPay = "abort-pay"
+ case abortDeposit = "abort-deposit"
+ case recoup
+ case backupRestored = "backup-restored"
+ case scheduled
+}
struct RefreshTransactionDetails: Decodable {
- /// The exchange that the coins are refreshed with.
- var exchangeBaseUrl: String
+ var refreshReason: RefreshReason
+ var originatingTransactionId: String?
+ var refreshInputAmount: Amount
+ var refreshOutputAmount: Amount
}
struct RefreshTransaction {
@@ -125,111 +284,182 @@ struct RefreshTransaction {
var details: RefreshTransactionDetails
}
+struct P2pShortInfo: Codable {
+ var summary: String
+ var expiration: Timestamp
+}
+
+struct P2PTransactionDetails: Codable {
+ var exchangeBaseUrl: String
+ var talerUri: String? // only if we initiated the transaction
+ var info: P2pShortInfo
+}
+
+struct P2PTransaction {
+ var common: TransactionCommon
+ var details: P2PTransactionDetails
+}
+
+struct DummyTransaction {
+ var common: TransactionCommon
+}
+
enum Transaction: Decodable, Hashable, Identifiable {
+ case dummy (DummyTransaction)
case withdrawal (WithdrawalTransaction)
case payment (PaymentTransaction)
case refund (RefundTransaction)
- case tip (TipTransaction)
+ case reward (RewardTransaction)
+// case tip (TipTransaction)
case refresh (RefreshTransaction)
+ case peer2peer (P2PTransaction)
init(from decoder: Decoder) throws {
- let common = try TransactionCommon.init(from: decoder)
-
- switch (common.type) {
- case WITHDRAWAL:
- let details = try WithdrawalTransactionDetails.init(from:
decoder)
- self = .withdrawal(WithdrawalTransaction(common: common,
details: details))
- case PAYMENT:
- let details = try PaymentTransactionDetails.init(from: decoder)
- self = .payment(PaymentTransaction(common: common, details:
details))
- case REFUND:
- let details = try RefundTransactionDetails.init(from: decoder)
- self = .refund(RefundTransaction(common: common, details:
details))
- case TIP:
- let details = try TipTransactionDetails.init(from: decoder)
- self = .tip(TipTransaction(common: common, details: details))
- case REFRESH:
- let details = try RefreshTransactionDetails.init(from: decoder)
- self = .refresh(RefreshTransaction(common: common, details:
details))
- default:
- let context = DecodingError.Context(
- codingPath: decoder.codingPath,
- debugDescription: "Invalid transaction type")
- throw DecodingError.typeMismatch(Transaction.self, context)
+ do {
+ let common = try TransactionCommon.init(from: decoder)
+ switch (common.type) {
+ case .withdrawal:
+ let details = try WithdrawalTransactionDetails.init(from:
decoder)
+ self = .withdrawal(WithdrawalTransaction(common: common,
details: details))
+ case .payment:
+ let details = try PaymentTransactionDetails.init(from:
decoder)
+ self = .payment(PaymentTransaction(common: common,
details: details))
+ case .refund:
+ let details = try RefundTransactionDetails.init(from:
decoder)
+ self = .refund(RefundTransaction(common: common, details:
details))
+ case .reward:
+ let details = try RewardTransactionDetails.init(from:
decoder)
+ self = .reward(RewardTransaction(common: common, details:
details))
+// case .tip:
+// let details = try TipTransactionDetails.init(from:
decoder)
+// self = .tip(TipTransaction(common: common, details:
details))
+ case .refresh:
+ let details = try RefreshTransactionDetails.init(from:
decoder)
+ self = .refresh(RefreshTransaction(common: common,
details: details))
+ case .peerPushDebit, .peerPullCredit, .scanPullDebit,
.scanPushCredit:
+ let details = try P2PTransactionDetails.init(from: decoder)
+ self = .peer2peer(P2PTransaction(common: common, details:
details))
+ default:
+ let context = DecodingError.Context(
+ codingPath: decoder.codingPath,
+ debugDescription: "Invalid transaction type")
+ throw DecodingError.typeMismatch(Transaction.self, context)
+ }
+ return
+ } catch DecodingError.dataCorrupted(let context) {
+ print(context)
+ throw TransactionDecodingError.invalidStringValue
+ } catch DecodingError.keyNotFound(let key, let context) {
+ print("Key '\(key)' not found:", context.debugDescription)
+ print("codingPath:", context.codingPath)
+ throw TransactionDecodingError.invalidStringValue
+ } catch DecodingError.valueNotFound(let value, let context) {
+ print("Value '\(value)' not found:", context.debugDescription)
+ print("codingPath:", context.codingPath)
+ throw TransactionDecodingError.invalidStringValue
+ } catch DecodingError.typeMismatch(let type, let context) {
+ print("Type '\(type)' mismatch:", context.debugDescription)
+ print("codingPath:", context.codingPath)
+ throw TransactionDecodingError.invalidStringValue
+ } catch { // TODO: native logging
+ print("error: ", error)
+ throw error
}
}
- var id: String { common().transactionId }
+ var id: String { common.transactionId }
+
+ var localizedType: String {
+ common.localizedType(common.type)
+ }
static func == (lhs: Transaction, rhs: Transaction) -> Bool {
return lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
- id.hash(into: &hasher)
+ hasher.combine(id)
+ hasher.combine(common.txState) // let SwiftUI redraw if txState
changes
}
- func common() -> TransactionCommon {
+ var isWithdrawal : Bool { common.type == .withdrawal }
+ var isDeposit : Bool { common.type == .deposit }
+ var isPayment : Bool { common.type == .payment }
+ var isRefund : Bool { common.type == .refund }
+ var isRefresh : Bool { common.type == .refresh }
+ var isReward : Bool { common.type == .reward }
+// var isTipPayment : Bool { common.type == .tip }
+ var isSendCoins : Bool { common.type == .peerPushDebit }
+ var isRcvCoins : Bool { common.type == .scanPushCredit }
+ var isSendInvoice: Bool { common.type == .peerPullCredit }
+ var isPayInvoice : Bool { common.type == .scanPullDebit }
+
+ var isP2pOutgoing: Bool { isSendCoins || isPayInvoice}
+ var isP2pIncoming: Bool { isSendInvoice || isRcvCoins}
+
+ 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 { !(isDone || isPending) }
+
+ var isAbortable : Bool { common.txActions.contains(.abort) }
+ var isFailable : Bool { common.txActions.contains(.fail) }
+ var isDeleteable : Bool { common.txActions.contains(.delete) }
+ var isResumable : Bool { common.txActions.contains(.resume) }
+ var isSuspendable: Bool { common.txActions.contains(.suspend) }
+
+ var common: TransactionCommon {
switch self {
+ case .dummy(let dummyTransaction):
+ return dummyTransaction.common
case .withdrawal(let withdrawalTransaction):
return withdrawalTransaction.common
case .payment(let paymentTransaction):
return paymentTransaction.common
case .refund(let refundTransaction):
return refundTransaction.common
- case .tip(let tipTransaction):
- return tipTransaction.common
+ case .reward(let rewardTransaction):
+ return rewardTransaction.common
+// case .tip(let tipTransaction):
+// return tipTransaction.common
case .refresh(let refreshTransaction):
return refreshTransaction.common
+ case .peer2peer(let p2pTransaction):
+ return p2pTransaction.common
}
}
func detailsToShow() -> Dictionary<String, String> {
var result: [String:String] = [:]
switch self {
+ case .dummy(let dummyTransaction):
+ break
case .withdrawal(let withdrawalTransaction):
result[EXCHANGEBASEURL] =
withdrawalTransaction.details.exchangeBaseUrl
case .payment(let paymentTransaction):
- result["status"] = paymentTransaction.details.status
+ result["summary"] = paymentTransaction.details.info.summary
case .refund(let refundTransaction):
- result["summary"] = refundTransaction.details.info.summary
- case .tip(let tipTransaction):
- result[EXCHANGEBASEURL] =
tipTransaction.details.exchangeBaseUrl
+ if let info = refundTransaction.details.info {
+ result["summary"] = info.summary
+ }
+ case .reward(let rewardTransaction):
+ result[EXCHANGEBASEURL] =
rewardTransaction.details.exchangeBaseUrl
+// case .tip(let tipTransaction):
+// result[EXCHANGEBASEURL] =
tipTransaction.details.exchangeBaseUrl
case .refresh(let refreshTransaction):
- result[EXCHANGEBASEURL] =
refreshTransaction.details.exchangeBaseUrl
+ result["reason"] =
refreshTransaction.details.refreshReason.rawValue
+ case .peer2peer(let p2pTransaction):
+ result[EXCHANGEBASEURL] =
p2pTransaction.details.exchangeBaseUrl
+ result["summary"] = p2pTransaction.details.info.summary
+ result[TALERURI] = p2pTransaction.details.talerUri ?? ""
}
- return result
- }
-}
-
-#if DEBUG
-extension Transaction { // for PreViews
- init(incoming: Bool, id: String, time: Timestamp) {
- let effective = incoming ? "Taler:4.8" : "Taler:5.2"
- let common = TransactionCommon(type: incoming ? WITHDRAWAL : PAYMENT,
- amountEffective: try! Amount(fromString:
effective),
- amountRaw: try! Amount(fromString:
"Taler:5"),
- transactionId: id, timestamp: time,
- extendedStatus: "done", pending: false, frozen:
false)
- if incoming {
- let withdrawalDetails = WithdrawalDetails(type:
WithdrawalDetails.WithdrawalType.bankIntegrated,
- reservePub: "Public Key
of the Exchange",
- confirmed: true)
- let wDetails = WithdrawalTransactionDetails(exchangeBaseUrl:
"Exchange.Demo.Taler.net",
- withdrawalDetails:
withdrawalDetails)
- self = .withdrawal(WithdrawalTransaction(common: common, details:
wDetails))
- } else {
- let merchant = Merchant(name: "some random shop")
- let info = OrderShortInfo(orderId: "some order ID",
- merchant: merchant, summary: "some
product summary", products: [])
- let pDetails = PaymentTransactionDetails(proposalId: "some
proposal ID",
- status: "paid",
- totalRefundRaw: try!
Amount(fromString: "Taler:3.2"),
- totalRefundEffective:
try! Amount(fromString: "Taler:3"),
- info: info)
- self = .payment(PaymentTransaction(common: common, details:
pDetails))
- }
+ return result
}
}
-#endif
diff --git a/TalerWallet1/Backend/WalletBackendError.swift
b/TalerWallet1/Backend/WalletBackendError.swift
index cb0bfeb..802d027 100644
--- a/TalerWallet1/Backend/WalletBackendError.swift
+++ b/TalerWallet1/Backend/WalletBackendError.swift
@@ -1,17 +1,6 @@
/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
*/
import Foundation
@@ -25,7 +14,7 @@ enum WalletBackendError: Error {
}
/// Information supplied by the backend describing an error.
-struct WalletBackendResponseError: Decodable {
+struct WalletBackendResponseError: Codable {
/// Numeric error code defined defined in the GANA gnu-taler-error-codes
registry.
var talerErrorCode: Int
diff --git a/TalerWallet1/Backend/WalletBackendRequest.swift
b/TalerWallet1/Backend/WalletBackendRequest.swift
index ad48879..9bf8d80 100644
--- a/TalerWallet1/Backend/WalletBackendRequest.swift
+++ b/TalerWallet1/Backend/WalletBackendRequest.swift
@@ -1,17 +1,6 @@
/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
*/
import Foundation
import AnyCodable
@@ -34,8 +23,37 @@ protocol WalletBackendFormattedRequest {
func args() -> Args
}
// MARK: -
-
-
+/// The scope of a currency: either global or a dedicated exchange
+struct ScopeInfo: Codable, Hashable {
+ enum ScopeInfoType: String, Codable {
+ case global
+ case exchange
+ }
+ var type: ScopeInfoType
+ var exchangeBaseUrl: String? // only for "exchange"
+ var currency: String
+
+ public static func == (lhs: ScopeInfo, rhs: ScopeInfo) -> Bool {
+ if let lhsBaseURL = lhs.exchangeBaseUrl {
+ if let rhsBaseURL = rhs.exchangeBaseUrl {
+ if lhsBaseURL != rhsBaseURL { return false } // different
exchanges
+ // else fall
thru and check type & currency
+ } else { return false } // left but
not right
+ } else if rhs.exchangeBaseUrl != nil {
+ return false // right but
not left
+ }
+ return lhs.type == rhs.type &&
+ lhs.currency == rhs.currency
+ }
+ public func hash(into hasher: inout Hasher) {
+ hasher.combine(type)
+ if let baseURL = exchangeBaseUrl {
+ hasher.combine(baseURL)
+ }
+ hasher.combine(currency)
+ }
+}
+// MARK: -
/// A billing or mailing location.
struct Location: Codable {
var country: String?
@@ -64,14 +82,15 @@ struct Tax: Codable {
}
/// A product being purchased from a merchant.
+/// https://docs.taler.net/core/api-merchant.html#the-contract-terms
struct Product: Codable {
var product_id: String?
var description: String
- // description_i18n?
- var quantity: Int
- var unit: String
+// var description_i18n: ?
+ var quantity: Int?
+ var unit: String?
var price: Amount?
- var image: String // URL to a product image
+ var image: String? // URL to a product image
var taxes: [Tax]?
var delivery_date: Timestamp?
}
@@ -81,33 +100,14 @@ struct OrderShortInfo: Codable {
var orderId: String
var merchant: Merchant
var summary: String
- // summary_i18n?
+// var summary_i18n: ?
var products: [Product]
var fulfillmentUrl: String?
var fulfillmentMessage: String?
- // fulfillmentMessage_i18n?
+// var fulfillmentMessage_i18n: ?
+ var contractTermsHash: String?
}
-
-
-/// A request to delete a wallet transaction by ID.
-struct WalletBackendDeleteTransactionRequest: WalletBackendFormattedRequest {
- var transactionId: String
-
- struct Args: Encodable {
- var transactionId: String
- }
-
- struct Response: Decodable {}
-
- func operation() -> String {
- return "deleteTransaction"
- }
-
- func args() -> Args {
- return Args(transactionId: transactionId)
- }
-}
-
+// MARK: -
/// A request to process a refund.
struct WalletBackendApplyRefundRequest: WalletBackendFormattedRequest {
var talerRefundUri: String
@@ -154,55 +154,6 @@ struct WalletBackendForceUpdateRequest:
WalletBackendFormattedRequest {
}
}
-
-
-
-/// A request to accept a bank-integrated withdrawl.
-struct WalletBackendAcceptBankIntegratedWithdrawalRequest:
WalletBackendFormattedRequest {
- var talerWithdrawUri: String
- var exchangeBaseUrl: String
-
- struct Args: Encodable {
- var talerWithdrawUri: String
- var exchangeBaseUrl: String
- }
-
- struct Response: Decodable {
- var bankConfirmationUrl: String?
- }
-
- func operation() -> String {
- return "acceptWithdrawal"
- }
-
- func args() -> Args {
- return Args(talerWithdrawUri: talerWithdrawUri, exchangeBaseUrl:
exchangeBaseUrl)
- }
-}
-
-/// A request to accept a manual withdrawl.
-struct WalletBackendAcceptManualWithdrawalRequest:
WalletBackendFormattedRequest {
- var exchangeBaseUrl: String
- var amount: Amount
-
- struct Args: Encodable {
- var exchangeBaseUrl: String
- var amount: Amount
- }
-
- struct Response: Decodable {
- var exchangePaytoUris: [String]
- }
-
- func operation() -> String {
- return "acceptManualWithdrawal"
- }
-
- func args() -> Args {
- return Args(exchangeBaseUrl: exchangeBaseUrl, amount: amount)
- }
-}
-
/// A request to deposit funds.
struct WalletBackendCreateDepositGroupRequest: WalletBackendFormattedRequest {
var depositePayToUri: String
@@ -264,51 +215,40 @@ struct WalletBackendConfirmPayRequest:
WalletBackendFormattedRequest {
}
}
-/// A request to prepare a tip.
-struct WalletBackendPrepareTipRequest: WalletBackendFormattedRequest {
- var talerTipUri: String
-
- struct Args: Encodable {
- var talerTipUri: String
- }
-
- struct Response: Decodable {
- var walletTipId: String
- var accepted: Bool
- var tipAmountRaw: Amount
- var tipAmountEffective: Amount
- var exchangeBaseUrl: String
- var expirationTimestamp: Timestamp
- }
-
- func operation() -> String {
- return "prepareTip"
- }
-
- func args() -> Args {
- return Args(talerTipUri: talerTipUri)
- }
+// MARK: -
+/// The result from PrepareReward
+struct PrepareRewardResponse: Decodable {
+ var walletRewardId: String
+ var accepted: Bool
+ var rewardAmountRaw: Amount
+ var rewardAmountEffective: Amount
+ var exchangeBaseUrl: String
+ var expirationTimestamp: Timestamp
}
+/// A request to prepare a reward.
+struct PrepareRewardRequest: WalletBackendFormattedRequest {
+ typealias Response = PrepareRewardResponse
+ func operation() -> String { return "prepareReward" }
+ func args() -> Args { return Args(talerRewardUri: talerRewardUri) }
-/// A request to accept a tip.
-struct WalletBackendAcceptTipRequest: WalletBackendFormattedRequest {
- var walletTipId: String
-
+ var talerRewardUri: String
struct Args: Encodable {
- var walletTipId: String
+ var talerRewardUri: String
}
-
+}
+// MARK: -
+/// A request to accept a reward.
+struct AcceptRewardRequest: WalletBackendFormattedRequest {
struct Response: Decodable {}
-
- func operation() -> String {
- return "acceptTip"
- }
-
- func args() -> Args {
- return Args(walletTipId: walletTipId)
+ func operation() -> String { return "acceptReward" }
+ func args() -> Args { return Args(walletRewardId: walletRewardId) }
+
+ var walletRewardId: String
+ struct Args: Encodable {
+ var walletRewardId: String
}
}
-
+// MARK: -
/// A request to abort a failed payment.
struct WalletBackendAbortFailedPaymentRequest: WalletBackendFormattedRequest {
var proposalId: String
@@ -327,8 +267,7 @@ struct WalletBackendAbortFailedPaymentRequest:
WalletBackendFormattedRequest {
return Args(proposalId: proposalId)
}
}
-
-
+// MARK: -
struct IntegrationTestArgs: Codable {
var exchangeBaseUrl: String
var bankBaseUrl: String
diff --git a/TalerWallet1/Backend/WalletCore.swift
b/TalerWallet1/Backend/WalletCore.swift
index be7450d..6e0b045 100644
--- a/TalerWallet1/Backend/WalletCore.swift
+++ b/TalerWallet1/Backend/WalletCore.swift
@@ -1,22 +1,12 @@
/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
*/
import SwiftUI // FOUNDATION has no AppStorage
import AnyCodable
import FTalerWalletcore
import SymLog
+import os
/// Delegate for the wallet backend.
protocol WalletBackendDelegate {
@@ -24,24 +14,30 @@ protocol WalletBackendDelegate {
func walletBackendReceivedUnknownMessage(_ walletCore: WalletCore,
message: String)
}
+// MARK: -
/// An interface to the wallet backend.
class WalletCore: QuickjsMessageHandler {
+ public static let shared = try! WalletCore() // will (and should)
crash on failure
private let symLog = SymLogC()
- private var quickjs: Quickjs
+ private var queue: DispatchQueue
+ private var semaphore: DispatchSemaphore
+
+ private let quickjs: Quickjs
private var requestsMade: UInt // counter for array of completion
closures
- private var completions: [UInt : (UInt, Data?,
WalletBackendResponseError?) -> Void] = [:]
+ 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
let id: UInt
let args: AnyEncodable
}
-
+
private struct FullResponse: Decodable {
let type: String
let operation: String
@@ -58,102 +54,200 @@ class WalletCore: QuickjsMessageHandler {
var lastError: FullError?
+ struct ResponseOrNotification: Decodable {
+ let type: String
+ let operation: String?
+ let id: UInt?
+ let result: AnyCodable?
+ let error: AnyCodable? // should be WalletBackendResponseError?
+ let payload: AnyCodable?
+ }
+
+ struct Payload: Decodable {
+ let type: String
+ let id: String?
+ let reservePub: String?
+ }
+
deinit {
- symLog.log()
+ logger.log("shutdown Quickjs")
// TODO: send shutdown message to talerWalletInstance
// quickjs.waitStopped()
}
init() throws {
+ logger.info("init Quickjs")
requestsMade = 0
+ queue = DispatchQueue(label: "net.taler.myQueue", attributes:
.concurrent)
+ semaphore = DispatchSemaphore(value: 1)
quickjs = Quickjs()
quickjs.messageHandler = self
+ logger.log("Quickjs running")
}
}
// MARK: - completionHandler functions
extension WalletCore {
-
- private func handleResponse(dict responseDict: [String : Any], data
messageData: Data, isError: Bool = false) throws {
- guard let id = responseDict["id"] as? UInt else { throw
WalletBackendError.deserializationError }
- guard let completion = completions[id] else { throw
WalletBackendError.deserializationError }
- completions[id] = nil
- if isError {
+ private func handleError(_ decoded: ResponseOrNotification) throws {
+ guard let requestId = decoded.id else {
+ logger.error("didn't find requestId in error response")
+ // TODO: show error alert
+ throw WalletBackendError.deserializationError
+ }
+ guard let (timeSent, completion) = completions[requestId] else {
+ logger.error("requestId \(requestId, privacy: .public) not in
list")
+ // TODO: show error alert
+ throw WalletBackendError.deserializationError
+ }
+ completions[requestId] = nil
+ if let walletError = decoded.error { // wallet-core sent an
error message
do {
- let decoded = try JSONDecoder().decode(FullError.self, from:
messageData)
+ let jsonData = try JSONEncoder().encode(walletError)
+ } catch { // JSON encoding of response.result failed /
should never happen
symLog.log(decoded)
- completion(id, nil, decoded.error)
- } catch {
- symLog.log(responseDict) // TODO: error
- completion(id, nil, WalletCore.parseFailureError())
- }
- } else {
- do { // pass response.result
- let decoded = try JSONDecoder().decode(FullResponse.self,
from: messageData)
- symLog.log(decoded)
- let jsonData = try JSONEncoder().encode(decoded.result)
- completion(id, jsonData, nil)
- } catch {
- symLog.log(responseDict) // TODO: error
- completion(id, nil, WalletCore.parseResponseError())
+ logger.error("cannot encode wallet-core Error")
+ // TODO: show error alert
+ completion(requestId, timeSent, nil,
WalletCore.parseFailureError())
}
+ // TODO: decode jsonData to WalletBackendResponseError - or
HTTPError
+ logger.error("wallet-core sent back an error for request
\(requestId, privacy: .public)")
+// completion(requestId, timeSent, nil, walletError)
+ completion(requestId, timeSent, nil,
WalletCore.parseFailureError())
+ } else { // JSON decoding of
error message failed
+ completion(requestId, timeSent, nil,
WalletCore.parseFailureError())
+ }
+ }
+ private func handleResponse(_ decoded: ResponseOrNotification) throws {
+ guard let requestId = decoded.id else {
+ logger.error("didn't find requestId in response")
+ symLog.log(decoded) // TODO: .error
+ throw WalletBackendError.deserializationError
+ }
+ guard let (timeSent, completion) = completions[requestId] else {
+ logger.error("requestId \(requestId, privacy: .public) not in
list")
+ throw WalletBackendError.deserializationError
+ }
+ completions[requestId] = nil
+ guard let result = decoded.result else {
+ logger.error("requestId \(requestId, privacy: .public) got no
result")
+ throw WalletBackendError.deserializationError
+ }
+ do {
+ let jsonData = try JSONEncoder().encode(result)
+ symLog.log(result)
+// logger.info(result) TODO: log result
+ completion(requestId, timeSent, jsonData, nil)
+ } catch { // JSON encoding of response.result failed / should
never happen
+ symLog.log(result) // TODO: .error
+ completion(requestId, timeSent, nil,
WalletCore.parseResponseError())
}
}
- private func handleNotification(dict responseDict: [String : Any]) throws {
+ @MainActor
+ private func postNotificationM(_ aName: NSNotification.Name,
+ object anObject: Any? = nil,
+ userInfo: [AnyHashable: Any]? = nil) async {
+ NotificationCenter.default.post(name: aName, object: anObject,
userInfo: userInfo)
+ }
+ private func postNotification(_ aName: NSNotification.Name,
+ object anObject: Any? = nil,
+ userInfo: [AnyHashable: Any]? = nil) {
+ Task {
+ if let userInfo { symLog.log(userInfo) } else { symLog.log(aName) }
+ await postNotificationM(aName, object: anObject, userInfo:
userInfo)
+ logger.log("Notification sent: \(aName.rawValue)")
+ }
+ }
+
+ private func handlePendingProcessed(_ payload: Payload) throws {
+ guard let id = payload.id else { throw
WalletBackendError.deserializationError }
+ let pendingOp = Notification.Name.PendingOperationProcessed.rawValue
+ if id.hasPrefix("exchange-update:") {
+ // Bla Bla Bla
+ } else if id.hasPrefix("refresh:") {
+ // Bla Bla Bla
+ } else if id.hasPrefix("purchase:") {
+ // TODO: handle purchase
+// symLog.log("\(pendingOp): \(id)")
+ } 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)")
+ } else {
+ // TODO: handle other pending-operation-processed
+ logger.log("❗️ \(pendingOp, privacy: .public): \(id, privacy:
.public)") // this is a new pendingOp I haven't seen before
+ }
+ }
+ private func handleStateTransition(_ jsonData: Data) throws {
do {
- guard let payload = (responseDict["payload"] as? [String : Any])
else { throw WalletBackendError.deserializationError }
- guard let type = (payload["type"] as? String) else { throw
WalletBackendError.deserializationError }
- switch type {
- case "pending-operation-processed":
- guard let id = (payload["id"] as? String) else { throw
WalletBackendError.deserializationError }
- if id.hasPrefix("exchange-update:") {
- // TODO: handle exchange-update
- } else {
- symLog.log(id)
- // TODO: handle other pending-operation-processed
- }
- case "coin-withdrawn", "withdraw-group-finished",
"pay-operation-success":
- symLog.log(payload)
- Task {
- do {
- // automatically fetch balances after receiving
these notifications
- try await
Controller.shared.balancesModel.fetchBalances()
- } catch {
- // TODO: show error
- symLog.log(error.localizedDescription)
+ let decoded = try JSONDecoder().decode(TransactionTransition.self,
from: jsonData)
+ if decoded.newTxState != decoded.oldTxState {
+ if decoded.newTxState.major == .done {
+ let components =
decoded.transactionId.components(separatedBy: [":"])
+ if components.count >= 3 { // txn:$txtype:$uid
+ if let type = TransactionType(rawValue: components[1])
{
+ Controller.shared.playSound(type.isIncoming ? 2 :
1)
}
}
- break
- case "proposal-accepted":
- symLog.log(payload)
- break
- case "proposal-downloaded":
- symLog.log(payload)
- break
- case "waiting-for-retry":
- // Bla Bla Bla
- break
- case "exchange-added":
- symLog.log(payload)
- break
- case "refresh-started":
- symLog.log(payload)
- break
- case "refresh-melted":
- symLog.log(payload)
- break
- case "refresh-revealed":
- symLog.log(payload)
- break
- case "reserve-registered-with-bank":
- symLog.log(payload)
+ }
+ postNotification(.TransactionStateTransition,
+ userInfo: [TRANSACTIONTRANSITION: decoded])
+ }
+ } catch { // rethrows
+ symLog.log(jsonData) // TODO: .error
+ throw WalletBackendError.deserializationError
+ }
+ }
+
+ private func handleNotification(_ anyCodable: AnyCodable?) throws {
+ guard let anyPayload = anyCodable else { throw
WalletBackendError.deserializationError }
+ do {
+ let jsonData = try JSONEncoder().encode(anyPayload)
+ let payload = try JSONDecoder().decode(Payload.self, from:
jsonData)
+
+ switch payload.type {
+ case Notification.Name.TransactionStateTransition.rawValue:
+ try handleStateTransition(jsonData)
+ case Notification.Name.PendingOperationProcessed.rawValue:
+ try handlePendingProcessed(payload)
+ case Notification.Name.BalanceChange.rawValue:
+ postNotification(.BalanceChange)
+ case Notification.Name.ExchangeAdded.rawValue:
+ postNotification(.ExchangeAdded)
+ case Notification.Name.ReserveNotYetFound.rawValue:
+ if let reservePub = payload.reservePub {
+ let userInfo = ["reservePub" : reservePub]
+// postNotification(.ReserveNotYetFound, userInfo:
userInfo) // TODO: remind User to confirm withdrawal
+ } // else { throw WalletBackendError.deserializationError
} shouldn't happen, but if it does just ignore it
+
+ case Notification.Name.ProposalAccepted.rawValue:
// "proposal-accepted":
+ symLog.log(anyPayload)
+ postNotification(.ProposalAccepted, userInfo: nil)
+ case Notification.Name.ProposalDownloaded.rawValue:
// "proposal-downloaded":
+ symLog.log(anyPayload)
+ postNotification(.ProposalDownloaded, userInfo: nil)
+
+ // TODO: remove these once wallet-core doesn't send them
anymore
+// case "reserve-registered-with-bank":
+// symLog.log(anyPayload)
+// case "withdraw-group-finished",
+// "pay-operation-success",
+// "withdrawal-group-bank-confirmed", // replaced
by transaction-state-transition
+// "withdrawal-group-reserve-ready",
+// "waiting-for-retry", // Bla Bla
Bla
+ case "refresh-started", "refresh-melted",
+ "refresh-revealed", "refresh-unwarranted":
break
default:
- symLog.log(payload)
+print("\n❗️ WalletCore.swift:226 Notification: ", anyPayload, "\n") //
this is a new notification I haven't seen before
break
}
} catch let error {
- symLog.log("Error \(error) parsing notification: \(responseDict)")
// TODO: .error
+ symLog.log("Error \(error) parsing notification: \(anyPayload)")
// TODO: .error
// TODO: if DevMode then should log into file for user
}
}
@@ -162,7 +256,7 @@ extension WalletCore {
func handleMessage(message: String) {
do {
var asyncDelay = 0
- if let delay = developDelay {
+ if let delay: Bool = developDelay { // Settings: 2 seconds delay
if delay {
asyncDelay = 2
}
@@ -173,42 +267,62 @@ extension WalletCore {
sleep(UInt32(asyncDelay))
symLog.log("waking up again after \(asyncDelay) seconds, will
deliver message")
}
- guard let messageData = message.data(using: .utf8) else { throw
WalletBackendError.deserializationError }
- let jsonDict = try JSONSerialization.jsonObject(with: messageData,
options: .allowFragments) as? [String : Any]
- guard let responseDict = jsonDict else { throw
WalletBackendError.deserializationError }
- guard let responseType = (responseDict["type"] as? String) else {
throw WalletBackendError.deserializationError }
- switch responseType {
+ guard let messageData = message.data(using: .utf8) else {
+ throw WalletBackendError.deserializationError
+ }
+ let decoded = try
JSONDecoder().decode(ResponseOrNotification.self, from: messageData)
+ switch decoded.type {
case "error":
- try handleResponse(dict: responseDict, data: messageData,
isError: true)
+ symLog.log(decoded) // TODO: .error
+ try handleError(decoded)
case "response":
- try handleResponse(dict: responseDict, data: messageData)
+ try handleResponse(decoded)
case "notification":
- try handleNotification(dict: responseDict)
+ try handleNotification(decoded.payload)
case "tunnelHttp": // TODO: Handle tunnelHttp
- break
+ symLog.log("Can't handle tunnelHttp: \(decoded)") //
TODO: .error
+ throw WalletBackendError.deserializationError
default:
- symLog.log("Unknown response type: \(responseDict)") //
TODO: .error
+ symLog.log("Unknown response type: \(decoded)") //
TODO: .error
throw WalletBackendError.deserializationError
}
- } catch {
+ } catch DecodingError.dataCorrupted(let context) {
+ print(context)
+ } catch DecodingError.keyNotFound(let key, let context) {
+ print("Key '\(key)' not found:", context.debugDescription)
+ print("codingPath:", context.codingPath)
+ } catch DecodingError.valueNotFound(let value, let context) {
+ print("Value '\(value)' not found:", context.debugDescription)
+ print("codingPath:", context.codingPath)
+ } catch DecodingError.typeMismatch(let type, let context) {
+ print("Type '\(type)' mismatch:", context.debugDescription)
+ print("codingPath:", context.codingPath)
+ } catch { // TODO: ?
delegate?.walletBackendReceivedUnknownMessage(self, message:
message)
}
}
- private func sendRequest(request: WalletBackendRequest, completionHandler:
@escaping (UInt, Data?, WalletBackendResponseError?) -> Void) {
+ private func sendRequest(request: WalletBackendRequest, completionHandler:
@escaping (UInt, Date, Data?, WalletBackendResponseError?) -> Void) {
// Encode the request and send it to the backend.
- let id = requestsMade
- do {
- let full = FullRequest(operation: request.operation, id: id, args:
request.args)
-// symLog.log(full)
- let encoded = try JSONEncoder().encode(full)
- guard let jsonString = String(data: encoded, encoding: .utf8) else
{ throw WalletBackendError.serializationError }
- completions[id] = completionHandler
- requestsMade += 1
- symLog.log(jsonString)
- quickjs.sendMessage(message: jsonString)
- } catch {
- completionHandler(id, nil, WalletCore.serializeRequestError());
+ queue.async {
+ self.semaphore.wait() // guard access to requestsMade
+ let requestId = self.requestsMade
+ let now = Date.now
+ do {
+ let full = FullRequest(operation: request.operation, id:
requestId, args: request.args)
+// symLog.log(full)
+ let encoded = try JSONEncoder().encode(full)
+ guard let jsonString = String(data: encoded, encoding: .utf8)
else { throw WalletBackendError.serializationError }
+ self.completions[requestId] = (now, completionHandler)
+ self.requestsMade += 1
+ self.semaphore.signal() // free requestsMade
+ self.symLog.log(jsonString)
+ self.quickjs.sendMessage(message: jsonString)
+ } catch { // call completion
+ self.semaphore.signal()
+ self.symLog.log(error)
+ completionHandler(requestId, now, nil,
WalletCore.serializeRequestError());
+ }
}
}
}
@@ -219,27 +333,44 @@ extension WalletCore {
let reqData = WalletBackendRequest(operation: request.operation(),
args: AnyEncodable(request.args()))
return try await withCheckedThrowingContinuation { continuation in
- sendRequest(request: reqData) { id, result, error in
+ sendRequest(request: reqData) { requestId, timeSent, result, error
in
+ let timeUsed = Date.now - timeSent
+ let millisecs = timeUsed.milliseconds
+ self.logger.log("Request \(requestId) took \(millisecs) ms")
+ var err: Error? = nil
if let json = result {
do {
let decoded = try
JSONDecoder().decode(T.Response.self, from: json)
- continuation.resume(returning: (decoded, id))
- } catch {
+ continuation.resume(returning: (decoded, requestId))
+ return
+ } catch DecodingError.dataCorrupted(let context) {
+ print(context)
+ } catch DecodingError.keyNotFound(let key, let context) {
+ print("Key '\(key)' not found:",
context.debugDescription)
+ print("codingPath:", context.codingPath)
+ } catch DecodingError.valueNotFound(let value, let
context) {
+ print("Value '\(value)' not found:",
context.debugDescription)
+ print("codingPath:", context.codingPath)
+ } catch DecodingError.typeMismatch(let type, let context) {
+ print("Type '\(type)' mismatch:",
context.debugDescription)
+ print("codingPath:", context.codingPath)
+ } catch { // rethrows
if let jsonString = String(data: json, encoding:
.utf8) {
self.symLog.log(jsonString) // TODO: .error
} else {
self.symLog.log(json) // TODO: .error
}
- continuation.resume(throwing: error)
+ err = error // this will be thrown in
continuation.resume(throwing:), otherwise keep nil
}
} else {
if let error = error {
- self.lastError = FullError(type: "error", operation:
request.operation(), id: id, error: error)
+ self.lastError = FullError(type: "error", operation:
request.operation(), id: requestId, error: error)
} else {
self.lastError = nil
}
- continuation.resume(throwing:
WalletBackendError.walletCoreError)
+ err = WalletBackendError.walletCoreError
}
+ continuation.resume(throwing: err ??
TransactionDecodingError.invalidStringValue)
}
}
}
diff --git a/TalerWallet1/Controllers/Controller.swift
b/TalerWallet1/Controllers/Controller.swift
index 8e10ec6..c95c5a9 100644
--- a/TalerWallet1/Controllers/Controller.swift
+++ b/TalerWallet1/Controllers/Controller.swift
@@ -1,19 +1,9 @@
/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
*/
import Foundation
+import SwiftUI
import SymLog
enum BackendState {
@@ -28,54 +18,40 @@ enum UrlCommand {
case unknown
case withdraw
case pay
+ case payPull
+ case payPush
}
+// MARK: -
class Controller: ObservableObject {
- public static var shared = Controller()
+ public static let shared = Controller()
private let symLog = SymLogC()
- @Published var backendState: BackendState = .none
-
- var walletCore: WalletCore
- var exchangeModel: ExchangeModel
- var balancesModel: BalancesModel
- var transactionsModel: TransactionsModel
- var pendingModel: PendingModel
- var withdrawURIModel: WithdrawURIModel
- var paymentURIModel: PaymentURIModel
-// @Published var withdrawTestModel: WithdrawTestModel
+ @Published var backendState: BackendState = .none // only used for
launch animation
+ @AppStorage("playSounds") var playSounds: Bool = false
var messageForSheet: String? = nil
init() {
- symLog.log("init wallet-core")
- walletCore = try! WalletCore() // will (and should) crash on
failure
- symLog.log("wallet-core done")
- exchangeModel = ExchangeModel(walletCore: walletCore)
- balancesModel = BalancesModel(walletCore: walletCore)
- transactionsModel = TransactionsModel(walletCore: walletCore)
- pendingModel = PendingModel(walletCore: walletCore)
- withdrawURIModel = WithdrawURIModel(walletCore: walletCore)
- paymentURIModel = PaymentURIModel(walletCore: walletCore)
-// withdrawTestModel = WithdrawTestModel(walletCore: walletCore)
- symLog.log("models inited")
backendState = .instantiated
}
- @MainActor func initWalletCore() async throws {
+ @MainActor
+ func initWalletCore(_ model: WalletModel)
+ async throws {
if backendState == .instantiated {
backendState = .initing
do {
- let walletInitModel = WalletInitModel(walletCore: walletCore)
- let versionInfo = try await walletInitModel.initWallet()
- walletCore.versionInfo = versionInfo
- backendState = .ready // dismiss the launch
animation
- } catch {
- backendState = .error
+ let versionInfo = try await model.initWalletCoreT()
+ WalletCore.shared.versionInfo = versionInfo
+ backendState = .ready // dismiss the
launch animation
+ } catch { // rethrows
+ symLog.log(error.localizedDescription) // TODO: .error
+ backendState = .error // ❗️Yikes app
cannot continue
throw error
}
} else {
- symLog.log("Yikes\(logSymbol(-1)) wallet-core already
initialized") // TODO: .warning
+ symLog.log("Yikes❗️ wallet-core already initialized") // TODO:
.fault
}
}
}
@@ -112,7 +88,7 @@ extension Controller {
func talerScheme(_ url:URL,_ uncrypted: Bool = false) -> UrlCommand {
guard let command = url.host else {return UrlCommand.unknown}
if uncrypted {
- print("uncrypted")
+ symLog.log("uncrypted: taler://\(command)")
// TODO: uncrypted
}
switch command {
@@ -120,6 +96,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
new file mode 100644
index 0000000..0d587db
--- /dev/null
+++ b/TalerWallet1/Controllers/DebugViewC.swift
@@ -0,0 +1,189 @@
+/* MIT License
+ * Copyright (c) 2023 Marc Stibane
+ *
+ * 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
+import SymLog
+import os.log
+
+// Numbering Scheme for Views
+// MARK: - Main View
+public let VIEW_EMPTY = 10 // 10
WalletEmptyView
+public let VIEW_BALANCES = VIEW_EMPTY + 1 // 11
BalancesListView
+public let VIEW_SETTINGS = VIEW_BALANCES + 1 // 12
SettingsView
+public let VIEW_PENDING = VIEW_SETTINGS + 1 // 13
PendingOpsListView
+public let VIEW_EXCHANGES = VIEW_PENDING + 1 // 14
ExchangeListView
+
+// MARK: Transactions
+public let VIEW_TRANSACTIONLIST = VIEW_EMPTY + 10 // 20
TransactionsListView
+public let VIEW_TRANSACTIONDETAIL = VIEW_TRANSACTIONLIST + 1 // 21
TransactionDetail
+
+
+
+// MARK: - Manual Withdrawal (from ExchangeList)
+// receive coins from bank ==> shows IBAN + Purpose for manual wire transfer
+public let VIEW_WITHDRAWAL = VIEW_TRANSACTIONLIST + 10 // 30
WithdrawAmount
+public let VIEW_WITHDRAW_TOS = VIEW_WITHDRAWAL + 1 // 31
WithdrawTOSView
+public let VIEW_WITHDRAW_ACCEPT = VIEW_WITHDRAW_TOS + 1 // 32
+
+// MARK: Manual Deposit (from ExchangeList)
+// send coins back to bank account ==> orders exchange to make the wire
transfer
+public let VIEW_DEPOSIT = VIEW_WITHDRAWAL + 10 // 40
Deposit Coins
+//public let VIEW_DEPOSIT_TOS // 41 -
user already accepted the ToS at withdrawal, invoice or receive
+public let VIEW_DEPOSIT_ACCEPT = VIEW_DEPOSIT + 2 // 42
+
+// MARK: P2P Invoice (from Balances)
+// pull credit from another wallet ==> shows QR code to be scanned / link to
be sent by mail or messenger
+public let VIEW_INVOICE_P2P = VIEW_DEPOSIT + 10 // 50
Invoice Amount
+public let VIEW_INVOICE_TOS = VIEW_INVOICE_P2P + 1 // 51
Invoice ToS
+public let VIEW_INVOICE_PURPOSE = VIEW_INVOICE_TOS + 1 // 52
Invoice Purpose
+
+// MARK: P2P Send Coins (from Balances)
+// push debit to another wallet ==> shows QR code to be scanned / link to be
sent by mail or messenger
+public let VIEW_SEND_P2P = VIEW_INVOICE_P2P + 10 // 60 Send
Coins
+//public let VIEW_SEND_TOS // 61 -
user already accepted the ToS at withdrawal, invoice or receive
+public let VIEW_SEND_PURPOSE = VIEW_SEND_P2P + 2 // 62
+
+
+
+// MARK: - Bank-Integrated Withdrawal
+// openURL (Link or scan QR) ==> obtains coins from bank
+public let SHEET_WITHDRAWAL = VIEW_WITHDRAWAL + 100 // 130
WithdrawURIView
+public let SHEET_WITHDRAW_TOS = SHEET_WITHDRAWAL + 1 // 131
WithdrawTOSView
+public let SHEET_WITHDRAW_ACCEPT = SHEET_WITHDRAW_TOS + 1 // 132
WithdrawAcceptView
+public let SHEET_WITHDRAW_CONFIRM = SHEET_WITHDRAW_ACCEPT + 1 // 133
waiting for bank confirmation
+
+// MARK: Merchant Payment
+// openURL (Link, NFC or scan QR) ==> pays merchant
+public let SHEET_PAYMENT = SHEET_WITHDRAWAL + 10 // 140 Pay
Merchant
+
+// MARK: Reward - Receive Coins (from merchant)
+// openURL (Link, NFC or scan QR) ==> receive coins from merchant
+public let SHEET_RCV_REWARD = SHEET_PAYMENT + 10 // 150
Receive Reward
+
+// MARK: P2P Pay Invoice
+// p2p pull debit - openURL (Link or scan QR)
+public let SHEET_PAY_P2P = SHEET_RCV_REWARD + 10 // 160 Pay
P2P Invoice
+
+// 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 =
+
+extension UIDevice {
+ var hasNotch: Bool {
+ let bottom = UIApplication.shared.keyWindow?.safeAreaInsets.bottom ?? 0
+ return bottom > 0
+ }
+}
+// MARK: -
+struct DebugViewV: View {
+ private let symLog = SymLogV(0)
+ @EnvironmentObject private var debugViewC: DebugViewC
+
+ var body: some View {
+#if DEBUG
+// let _ = Self._printChanges()
+// let _ = symLog.vlog() // just to get the # to compare it with
.onAppear & onDisappear
+#endif
+ let viewIDString = debugViewC.viewID > 0 ? String(debugViewC.viewID)
+ : ""
+ HStack {
+ Spacer()
+ Spacer()
+ if UIDevice.current.hasNotch {
+ Spacer()
+ Spacer()
+ }
+ Text(viewIDString)
+ .font(.caption2)
+ .foregroundColor(.red)
+ Spacer()
+ }
+ .edgesIgnoringSafeArea(.top)
+ }
+}
+// MARK: -
+class DebugViewC: ObservableObject {
+ private let symLog = SymLogC(0) // 0 to switch off
viewID change logging
+ public static let shared = DebugViewC()
+#if DEBUG
+ @AppStorage("developerMode") var developerMode: Bool = true
+#else
+ @AppStorage("developerMode") var developerMode: Bool = false
+#endif
+ let logger = Logger (subsystem: "net.taler.gnu", category: "DebugView")
+
+ @Published var viewID: Int = 0
+ @Published var sheetID: Int = 0
+
+ @MainActor func setViewID(_ newID: Int) -> Void {
+ if developerMode {
+ if viewID == 0 {
+ symLog.log("switching ON, \(newID)")
+ logger.info("switching ON, \(newID, privacy: .public)")
+ viewID = newID // publish ON
+ } else if viewID != newID {
+ symLog.log("switching from \(viewID) to \(newID)")
+ logger.info("switching from \(self.viewID, privacy: .public)
to \(newID, privacy: .public)")
+ viewID = newID // publish new
viewID
+ } else {
+ symLog.log("\(newID) stays")
+ logger.info("\(newID, privacy: .public) stays")
+ // don't set viewID to the same value, it would just trigger
an unneccessary redraw
+ }
+ } else if viewID > 0 {
+ symLog.log("switching OFF, will not use \(newID)")
+ logger.info("switching OFF, will not use \(newID, privacy:
.public)")
+ viewID = 0 // publish OFF
+ } else {
+ symLog.log("off, will not use \(newID)")
+ logger.info("off, will not use \(newID, privacy: .public)")
+ // don't set viewID from 0 to 0 again, it would just trigger an
unneccessary redraw
+ }
+ }
+
+ @MainActor func setSheetID(_ newID: Int) -> Void {
+ if developerMode {
+ if sheetID != newID {
+ symLog.log("switching from \(sheetID) to \(newID)")
+ logger.info("switching from \(self.sheetID, privacy: .public)
to \(newID, privacy: .public) for sheet")
+ sheetID = newID // publish new
sheetID
+ } else {
+ symLog.log("\(newID) stays")
+ logger.info("\(newID, privacy: .public) stays for sheet")
+ // don't set sheetID to the same value, it would just trigger
an unneccessary redraw
+ }
+ } else if sheetID > 0 {
+ // might happen after switching DevMode off, if sheetID still has
the old value of the last sheet
+ symLog.log("switching OFF, will not use \(newID)")
+ logger.info("switching OFF, will not use \(newID, privacy:
.public) for sheet")
+ sheetID = 0 // publish OFF
+ } else {
+ symLog.log("off, will not use \(newID)")
+ logger.info("off, will not use \(newID, privacy: .public) for
sheet")
+ // don't set sheetID from 0 to 0 again, it would just trigger an
unneccessary redraw
+ }
+ }
+
+}
diff --git a/TalerWallet1/Controllers/PublicConstants.swift
b/TalerWallet1/Controllers/PublicConstants.swift
new file mode 100644
index 0000000..35ff9f1
--- /dev/null
+++ b/TalerWallet1/Controllers/PublicConstants.swift
@@ -0,0 +1,48 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import Foundation
+
+public let LAUNCHDURATION: Double = 1.60
+public let SLIDEDURATION: Double = 0.45
+
+public let HTTPS = "https://"
+//public let DEMOBANK = HTTPS + "bAnK.dEmO.tAlEr.nEt" // should be
weird to read, but still work
+public let DEMOBANK = HTTPS + "bank.demo.taler.net"
+public let DEMOSHOP = HTTPS + "shop.demo.taler.net"
+public let DEMOBACKEND = HTTPS + "backend.demo.taler.net"
+//public let DEMOEXCHANGE = HTTPS + "eXcHaNgE.dEmO.tAlEr.nEt"
+public let DEMOEXCHANGE = HTTPS + "exchange.demo.taler.net"
+public let DEMO_AGE_EXCHANGE = HTTPS + "exchange-age.taler.ar"
+public let DEMO_EXP_EXCHANGE = HTTPS + "exchange-expensive.taler.ar"
+public let DEMOCURRENCY = "KUDOS"
+//public let LONGCURRENCY = "gold-pressed Latinum" // 20
characters, with dash and space
+public let LONGCURRENCY = "GOLDLATINUM" // 11
characters, no dash, no space
+
+// MARK: - Keys used in JSON
+
+public let EXCHANGEBASEURL = "exchangeBaseUrl"
+public let TALERURI = "talerUri"
+
+public let TRANSACTIONTRANSITION = "transactionTransition"
+
+/// Notifications sent by wallet-core
+extension Notification.Name {
+ static let BalanceChange = Notification.Name("balance-change")
+ static let TransactionStateTransition =
Notification.Name(TransactionTransition.TransitionType.transition.rawValue)
+ static let ExchangeAdded = Notification.Name("exchange-added")
+ static let PendingOperationProcessed =
Notification.Name("pending-operation-processed")
+ static let ReserveNotYetFound = Notification.Name("reserve-not-yet-found")
+// static let WithdrawalGroupBankConfirmed =
Notification.Name("withdrawal-group-bank-confirmed")
+// static let WithdrawalGroupReserveReady =
Notification.Name("withdrawal-group-reserve-ready")
+// static let WithdrawGroupFinished =
Notification.Name("withdraw-group-finished")
+// static let PayOperationSuccess =
Notification.Name("pay-operation-success")
+ static let ProposalAccepted = Notification.Name("proposal-accepted")
+ static let ProposalDownloaded = Notification.Name("proposal-downloaded")
+}
+
+/// Notifications for internal synchronization
+extension Notification.Name {
+ static let BalanceReloaded = Notification.Name("balanceReloaded")
+}
diff --git a/TalerWallet1/Controllers/TalerWallet1App.swift
b/TalerWallet1/Controllers/TalerWallet1App.swift
index 1eac830..bff3e7a 100644
--- a/TalerWallet1/Controllers/TalerWallet1App.swift
+++ b/TalerWallet1/Controllers/TalerWallet1App.swift
@@ -1,53 +1,75 @@
/*
- * This file is part of GNU Taler
- * (C) 2021 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+/**
+ * Main app entry point
*
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ * @author Marc Stibane
+ * @author Jonathan Buchanan
*/
import BackgroundTasks
import SwiftUI
import SymLog
+import os.log
@main
struct TalerWallet1App: App {
- private let symLog = SymLogV(0)
+ private let symLog = SymLogV()
@Environment(\.scenePhase) private var phase
+ @StateObject private var viewState = ViewState.shared //
popToRootView()
+ @State private var isActive = true
+ @State private var soundPlayed = false
+ private let walletCore = WalletCore.shared
// our main controller
- @StateObject private var controller = Controller.shared
+ private let controller = Controller.shared
+ private let model = WalletModel.shared
+ private let debugViewC = DebugViewC.shared
+ let logger = Logger (subsystem: "net.taler.gnu", category: "Main App")
func scheduleAppRefresh() {
- let request = BGAppRefreshTaskRequest(identifier: "net.taler.refresh")
+ let request = BGAppRefreshTaskRequest(identifier:
"net.taler.gnu.refresh")
request.earliestBeginDate = .now.addingTimeInterval(24 * 3600)
try? BGTaskScheduler.shared.submit(request)
}
var body: some Scene {
WindowGroup {
- symLog { ContentView()
- .environmentObject(controller)
- /// external events are taler:// or payto:// URLs
passed to this app
- /// we handle them in .onOpenURL in ContentView.swift
- .handlesExternalEvents(preferring: ["*"], allowing: ["*"])
- .task {
- symLog.log("task -> initWalletCore")
- try! await controller.initWalletCore() // will
(and should) crash on failure
- symLog.log("task done")
- }
- }
+ MainView(soundPlayed: $soundPlayed)
+ .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 {
+ try! await controller.initWalletCore(model) // will
(and should) crash on failure
+ }
+ .onReceive(NotificationCenter.default.publisher(for:
UIApplication.didBecomeActiveNotification, object: nil)) { _ in
+ logger.log("❗️App Did Become Active")
+ }
+ .onReceive(NotificationCenter.default.publisher(for:
UIApplication.willResignActiveNotification, object: nil)) { _ in
+ logger.log("❗️App Will Resign")
+ isActive = false
+ }
+ .onReceive(NotificationCenter.default.publisher(for:
UIApplication.willEnterForegroundNotification, object: nil)) { _ in
+ logger.log("❗️App Will Enter Foreground")
+ isActive = true
+ }
+ .onReceive(NotificationCenter.default.publisher(for:
UIApplication.willTerminateNotification, object: nil)) { _ in
+ logger.log("❗️App Will Terminate")
+ }
+
}
.onChange(of: phase) { newPhase in
switch newPhase {
- case .background: scheduleAppRefresh()
+ case .active:
+ logger.log("❗️.onChange() ==> Active")
+ case .background:
+ logger.log("❗️.onChange() ==> Background)")
+// scheduleAppRefresh()
default: break
}
}
@@ -76,3 +98,16 @@ struct TalerWallet1App: App {
}
}
+
+final class ViewState : ObservableObject {
+ static let shared = ViewState()
+ @Published var rootViewId = UUID()
+ let logger = Logger (subsystem: "net.taler.gnu", category: "ViewState")
+
+ public func popToRootView() -> Void {
+ logger.info("popToRootView")
+ rootViewId = UUID() // setting a new ID will cause tableView
popToRootView behaviour
+ }
+
+ private init() { }
+}
diff --git a/TalerWallet1/Helper/AgePicker.swift
b/TalerWallet1/Helper/AgePicker.swift
new file mode 100644
index 0000000..6f02d82
--- /dev/null
+++ b/TalerWallet1/Helper/AgePicker.swift
@@ -0,0 +1,49 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import SwiftUI
+
+struct AgePicker: View {
+ @Binding var ageMenuList: [Int]
+ @Binding var selectedAge: Int
+
+ func setAges(ages: [Int]?) {
+ if let ages {
+ var zero: [Int] = []
+ if ages.count > 0 { // need at least 1 value from exchange
which is not 0
+ if ages[0] != 0 { // ensure that the first age
is "0"
+ zero.insert(0, at: 0) // if not, insert "0" at
position 0
+ }
+ zero += ages
+ if selectedAge >= zero.count { // check for out of bounds
+ selectedAge = 0
+ }
+ } else {
+ selectedAge = 0 // first ensure that selected
is not out of bounds
+ }
+ ageMenuList = zero // set State (will update view)
+ }
+ }
+
+ var body: some View {
+ if ageMenuList.count > 1 {
+ VStack {
+ HStack {
+ Text("If this wallet belongs to a child or teenager, the
generated coins should be age-restricted:")
+ .multilineTextAlignment(.leading)
+ .font(.footnote)
+ Spacer()
+ }.padding(.top)
+ Picker("Select age", selection: $selectedAge) {
+ ForEach($ageMenuList, id: \.self) { item in
+ let index = item.wrappedValue
+ Text((index == 0) ? "unrestricted"
+ : "\(index) years").tag(index)
+ }
+ }
+ }
+ }
+ }
+}
+
diff --git a/TalerWallet1/Helper/AnyTransition+backslide.swift
b/TalerWallet1/Helper/AnyTransition+backslide.swift
new file mode 100644
index 0000000..3cd1d81
--- /dev/null
+++ b/TalerWallet1/Helper/AnyTransition+backslide.swift
@@ -0,0 +1,30 @@
+/* MIT License
+ * Copyright (c) 2021 noranraskin
+ *
+ * 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 AnyTransition {
+ static var backslide: AnyTransition {
+ AnyTransition.asymmetric(
+ insertion: .move(edge: .trailing),
+ removal: .move(edge: .leading))
+ }
+}
diff --git a/TalerWallet1/Helper/CurrencyFormatter.swift
b/TalerWallet1/Helper/CurrencyFormatter.swift
new file mode 100644
index 0000000..42f391b
--- /dev/null
+++ b/TalerWallet1/Helper/CurrencyFormatter.swift
@@ -0,0 +1,27 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import Foundation
+import taler_swift
+
+public class CurrencyFormatter: NumberFormatter {
+ public static var shared = CurrencyFormatter()
+
+ private override convenience init() {
+ self.init(fractionDigits: 2)
+ }
+
+ public init(fractionDigits: Int) {
+ super.init()
+ self.numberStyle = .decimal // currency could be changed by user
+ self.minimumFractionDigits = fractionDigits
+ self.maximumFractionDigits = fractionDigits
+ self.usesGroupingSeparator = true
+ self.locale = Locale.current
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+}
diff --git a/TalerWallet1/Helper/EqualIconWidthDomain.swift
b/TalerWallet1/Helper/EqualIconWidthDomain.swift
new file mode 100644
index 0000000..e8c1e74
--- /dev/null
+++ b/TalerWallet1/Helper/EqualIconWidthDomain.swift
@@ -0,0 +1,141 @@
+/* MIT License
+ * Copyright (c) 2021 rob mayoff
+ *
+ * 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
+
+fileprivate struct IconWidthKey: PreferenceKey {
+ static var defaultValue: CGFloat? { nil }
+
+ static func reduce(value: inout CGFloat?, nextValue: () -> CGFloat?) {
+ switch (value, nextValue()) {
+ case (nil, let next): value = next
+ case (_, nil): break
+ case (.some(let current), .some(let next)): value = max(current,
next)
+ }
+ }
+}
+
+extension IconWidthKey: EnvironmentKey { }
+
+extension EnvironmentValues {
+ fileprivate var iconWidth: CGFloat? {
+ get { self[IconWidthKey.self] }
+ set { self[IconWidthKey.self] = newValue }
+ }
+}
+
+fileprivate struct IconWidthModifier: ViewModifier {
+ @Environment(\.iconWidth) var width
+
+ func body(content: Content) -> some View {
+ content
+ .background(GeometryReader { proxy in
+ Color.clear
+ .preference(key: IconWidthKey.self, value:
proxy.size.width)
+ })
+ .frame(width: width)
+ }
+}
+
+struct EqualIconWidthLabelStyle: LabelStyle {
+ func makeBody(configuration: Configuration) -> some View {
+ HStack {
+ configuration.icon.modifier(IconWidthModifier())
+ configuration.title //(alignment: .leading)
+ .multilineTextAlignment(.leading)
+ }
+ }
+}
+
+struct EqualIconWidthDomain<Content: View>: View {
+ let content: Content
+ @State var iconWidth: CGFloat? = nil
+
+ init(@ViewBuilder _ content: () -> Content) {
+ self.content = content()
+ }
+
+ var body: some View {
+ content
+ .environment(\.iconWidth, iconWidth)
+ .onPreferenceChange(IconWidthKey.self) { self.iconWidth = $0 }
+ .labelStyle(EqualIconWidthLabelStyle())
+ }
+}
+// MARK: -
+#if DEBUG
+struct Demo1View: View {
+ var body: some View {
+ VStack(alignment: .leading) {
+ VStack(alignment: .leading) {
+ Label("People", systemImage: "person.3")
+ Label("Star", systemImage: "star")
+ Label("This is a plane", systemImage: "airplane")
+ }
+ .padding()
+ EqualIconWidthDomain {
+ VStack(alignment: .leading) {
+ Label("People", systemImage: "person.3")
+ Label("Star", systemImage: "star")
+ Label("This is a plane", systemImage: "airplane")
+ }
+ }
+ }
+ }
+}
+
+struct Demo1_Previews: PreviewProvider {
+ static var previews: some View {
+ Demo1View()
+ }
+}
+
+
+struct FancyView: View {
+ var body: some View {
+ EqualIconWidthDomain {
+ VStack {
+ Text("Le Menu")
+ .font(.caption)
+ Divider()
+ HStack {
+ VStack(alignment: .leading) {
+ Label(
+ title: { Text("Strawberry") },
+ icon: { Text("🍓") })
+ Label("Money", systemImage: "banknote")
+ }
+ VStack(alignment: .leading) {
+ Label("People", systemImage: "person.3")
+ Label("Star", systemImage: "star")
+ }
+ }
+ }
+ }
+ }
+}
+
+struct Demo2_Previews: PreviewProvider {
+ static var previews: some View {
+ FancyView()
+ }
+}
+#endif
diff --git a/TalerWallet1/Helper/KeyboardResponder.swift
b/TalerWallet1/Helper/KeyboardResponder.swift
new file mode 100644
index 0000000..c5d9cde
--- /dev/null
+++ b/TalerWallet1/Helper/KeyboardResponder.swift
@@ -0,0 +1,45 @@
+// MIT License
+// Copyright © Nicolai Harbo
+//
+// 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 Combine
+import UIKit
+
+public final class KeyboardResponder: ObservableObject {
+
+ @Published public var keyboardHeight: CGFloat = 0
+ var showCancellable: AnyCancellable?
+ var hideCancellable: AnyCancellable?
+
+ public init() {
+ showCancellable = NotificationCenter.default.publisher(for:
UIResponder.keyboardWillShowNotification)
+ .map { notification in
+
(notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as?
NSValue)?.cgRectValue.height ?? 0.0
+ }
+ .receive(on: DispatchQueue.main)
+ .sink(receiveValue: { height in
+// print("keyboard height: \(height)")
+ self.keyboardHeight = height
+ })
+
+ hideCancellable = NotificationCenter.default.publisher(for:
UIResponder.keyboardWillHideNotification)
+ .receive(on: DispatchQueue.main)
+ .sink(receiveValue: { _ in
+ self.keyboardHeight = 0
+ })
+ }
+}
diff --git a/TalerWallet1/Helper/LocalizedAlertError.swift
b/TalerWallet1/Helper/LocalizedAlertError.swift
new file mode 100644
index 0000000..00070e5
--- /dev/null
+++ b/TalerWallet1/Helper/LocalizedAlertError.swift
@@ -0,0 +1,54 @@
+// MIT License
+// Copyright © Antoine van der Lee
+//
+// 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
+
+
+struct LocalizedAlertError: LocalizedError {
+ let underlyingError: LocalizedError
+ var errorDescription: String? {
+ underlyingError.errorDescription
+ }
+ var recoverySuggestion: String? {
+ underlyingError.recoverySuggestion
+ }
+
+ init?(error: Error?) {
+ guard let localizedError = error as? LocalizedError else { return nil }
+ underlyingError = localizedError
+ }
+}
+
+extension View {
+
+ @ViewBuilder
+ func errorAlert(error: Binding<Error?>,
+ buttonTitle: LocalizedStringKey = "xloc.generic.ok",
+ action: (() -> Void)? = nil) -> some View
+ {
+ let localizedAlertError = LocalizedAlertError(error:
error.wrappedValue)
+ alert(isPresented: .constant(localizedAlertError != nil), error:
localizedAlertError) { _ in
+ Button(buttonTitle) {
+ action?()
+ error.wrappedValue = nil
+ }
+ } message: { error in
+ Text(error.failureReason ?? error.recoverySuggestion ?? "")
+ }
+ }
+}
diff --git a/TalerWallet1/Helper/PublicConstants.swift
b/TalerWallet1/Helper/PublicConstants.swift
deleted file mode 100644
index 01680a0..0000000
--- a/TalerWallet1/Helper/PublicConstants.swift
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-import Foundation
-
-public let EXCHANGEBASEURL = "exchangeBaseUrl"
-
-public let WITHDRAWAL = "withdrawal"
-public let PAYMENT = "payment"
-public let REFUND = "refund"
-public let TIP = "tip"
-public let REFRESH = "refresh"
diff --git a/TalerWallet1/Helper/TalerDater.swift
b/TalerWallet1/Helper/TalerDater.swift
index 5a7abdb..de694a7 100644
--- a/TalerWallet1/Helper/TalerDater.swift
+++ b/TalerWallet1/Helper/TalerDater.swift
@@ -1,23 +1,12 @@
/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
*/
import Foundation
import taler_swift
public class TalerDater: DateFormatter {
- public static var shared = TalerDater()
+ public static let shared = TalerDater()
static func relativeDate(from: TimeInterval) -> String? {
if from > 0 { // transactions should always be in the past
@@ -42,7 +31,7 @@ public class TalerDater: DateFormatter {
if day < 14 { return "More than a week ago" }
// will fall thru...
return nil
- } else { // Yikes! transaction date is in the future
+ } else { // Yikes❗️ transaction date is in the future
return nil
}
}
@@ -78,7 +67,7 @@ public class TalerDater: DateFormatter {
}
}
return shared.string(from: date)
- } catch {
+ } catch { // Never
return "Never"
}
}
@@ -100,3 +89,19 @@ public class TalerDater: DateFormatter {
fatalError("init(coder:) has not been implemented")
}
}
+
+extension Date {
+ static func - (lhs: Date, rhs: Date) -> TimeInterval {
+ return lhs.timeIntervalSinceReferenceDate -
rhs.timeIntervalSinceReferenceDate
+ }
+}
+extension TimeInterval {
+
+ var seconds: Int {
+ return Int(self.rounded())
+ }
+
+ var milliseconds: Int {
+ return Int(self * 1000)
+ }
+}
diff --git a/TalerWallet1/Helper/TalerStrings.swift
b/TalerWallet1/Helper/TalerStrings.swift
index b642798..d1443b4 100644
--- a/TalerWallet1/Helper/TalerStrings.swift
+++ b/TalerWallet1/Helper/TalerStrings.swift
@@ -1,17 +1,6 @@
/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
*/
import Foundation
diff --git a/TalerWallet1/Helper/URL+id+iban.swift
b/TalerWallet1/Helper/URL+id+iban.swift
new file mode 100644
index 0000000..ca4689c
--- /dev/null
+++ b/TalerWallet1/Helper/URL+id+iban.swift
@@ -0,0 +1,37 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import SwiftUI
+
+extension URL: Identifiable {
+ public var id: URL {self}
+}
+
+extension URL {
+ init(_ string: StaticString) {
+ self.init(string: "\(string)")!
+ }
+
+ var iban: String? {
+ /// https://datatracker.ietf.org/doc/rfc8905/
+ /// payto://iban/DE75512108001245126199?amount=EUR:200.0&message=hello
+ if scheme == "payto" && host == "iban" {
+ return lastPathComponent
+ }
+ return nil
+ }
+
+ /// SwifterSwift: Dictionary of the URL's query parameters.
+ var queryParameters: [String: String]? {
+ guard let components = URLComponents(url: self,
resolvingAgainstBaseURL: false),
+ let queryItems = components.queryItems else { return nil }
+
+ var items: [String: String] = [:]
+
+ for queryItem in queryItems {
+ items[queryItem.name] = queryItem.value
+ }
+ return items
+ }
+}
diff --git a/TalerWallet1/Helper/View+Notification.swift
b/TalerWallet1/Helper/View+Notification.swift
new file mode 100644
index 0000000..00760bf
--- /dev/null
+++ 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
index bf6721e..40f0084 100644
--- a/TalerWallet1/Helper/View+dismissTop.swift
+++ b/TalerWallet1/Helper/View+dismissTop.swift
@@ -1,18 +1,21 @@
-/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
+// MIT License
+// Copyright © Nicolai Harbo
+//
+// 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
/// This is just a workaround for a SwiftUI bug
diff --git a/TalerWallet1/Helper/WalletColors.swift
b/TalerWallet1/Helper/WalletColors.swift
new file mode 100644
index 0000000..1f037cf
--- /dev/null
+++ b/TalerWallet1/Helper/WalletColors.swift
@@ -0,0 +1,56 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import SwiftUI
+
+public struct WalletColors {
+
+ let tint = Color(.tintColor)
+ let gray1 = Color(.systemGray)
+ let gray2 = Color(.systemGray2)
+ let gray3 = Color(.systemGray3)
+ let gray4 = Color(.systemGray4)
+ let gray5 = Color(.systemGray5)
+ let gray6 = Color(.systemGray6)
+
+ func buttonForeColor(pressed: Bool, disabled: Bool, prominent: Bool =
false) -> Color {
+ disabled ? gray2
+ : !prominent ? tint
+ : pressed ? gray6 : gray5
+ }
+
+ func buttonBackColor(pressed: Bool, disabled: Bool, prominent: Bool =
false) -> Color {
+ disabled ? gray5
+ : prominent ? tint
+ : pressed ? gray5 : gray4
+ }
+
+ var backgroundColor: Color {
+ gray6
+ }
+
+ var sideBackground: Color {
+ gray5
+ }
+
+ var fieldForeground: Color { // text color
+ Color.primary
+ }
+ var fieldBackground: Color {
+ Color(.systemBackground)
+ }
+
+ var uncompletedColor: Color {
+ gray1
+ }
+ func pendingColor(_ incoming: Bool) -> Color {
+ incoming ? Color("PendingIncoming")
+ : Color("PendingOutgoing")
+ }
+ func transactionColor(_ incoming: Bool) -> Color {
+ incoming ? Color("Incoming")
+ : Color("Outgoing")
+ }
+
+}
diff --git a/TalerWallet1/Helper/playSound.swift
b/TalerWallet1/Helper/playSound.swift
new file mode 100644
index 0000000..ed39bef
--- /dev/null
+++ b/TalerWallet1/Helper/playSound.swift
@@ -0,0 +1,25 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import Foundation
+import AVFoundation
+
+extension Controller {
+
+ func playSound(_ number: Int) {
+ var soundID: SystemSoundID = 0
+ if number < 999 {
+ let sound = (number == 0) ? "payment_failure" :
+ (number == 1) ? "payment_success" : "PaymentReceived"
+ let fileURL = URL(fileURLWithPath:
"/System/Library/Audio/UISounds/"
+ + sound + ".caf")
+ AudioServicesCreateSystemSoundID(fileURL as CFURL, &soundID)
+ } else {
+ soundID = UInt32(number)
+ }
+ if playSounds {
+ AudioServicesPlaySystemSound(soundID);
+ }
+ }
+}
diff --git a/TalerWallet1/Model/ExchangeTestModel.swift
b/TalerWallet1/Model/ExchangeTestModel.swift
deleted file mode 100644
index 98702d5..0000000
--- a/TalerWallet1/Model/ExchangeTestModel.swift
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-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 = "https://exchange.demo.taler.net/"
-fileprivate let DEMO_BANKBASEURL = "https://bank.demo.taler.net/"
-fileprivate let DEMO_BANKAPIBASEURL =
"https://bank.demo.taler.net/demobanks/default/access-api/"
-fileprivate let DEMO_MERCHANTBASEURL = "https://backend.demo.taler.net/"
-fileprivate let DEMO_MERCHANTAUTHTOKEN = "secret-token:sandbox"
-
-// MARK: -
-class ExchangeTestModel: WalletModel {
-}
-// MARK: -
-extension ExchangeTestModel {
- @MainActor func loadTestKudos() async throws {
- do {
- let amount = Amount(currency: "KUDOS", 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)")
- } catch {
- throw error
- }
- }
-
- @MainActor func runIntegrationTest() async throws {
- do {
- let amountW = Amount(currency: "KUDOS", integer: 3, fraction: 0)
- let amountS = Amount(currency: "KUDOS", integer: 1, fraction: 0)
- let request = WalletBackendRunIntegration(amountToWithdraw:
amountW,
- amountToSpend:
amountS,
- bankBaseUrl:
DEMO_BANKAPIBASEURL,
- exchangeBaseUrl:
DEMO_EXCHANGEBASEURL,
- merchantBaseUrl:
DEMO_MERCHANTBASEURL,
- merchantAuthToken:
DEMO_MERCHANTAUTHTOKEN)
- let response = try await sendRequest(request, ASYNCDELAY)
- symLog?.log("received: \(response)")
- } catch {
- throw error
- }
- }
-}
-
-/// 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
- }
-}
-
-/// A request to add a test balance to the wallet.
-fileprivate struct WalletBackendRunIntegration: WalletBackendFormattedRequest {
- typealias Response = String
- func operation() -> String { return "runIntegrationTest" }
- func args() -> Args {
- return Args(amountToWithdraw: amountToWithdraw,
- amountToSpend: amountToSpend,
- bankBaseUrl: bankBaseUrl,
- exchangeBaseUrl: exchangeBaseUrl,
- merchantBaseUrl: merchantBaseUrl,
- merchantAuthToken: merchantAuthToken
- )
- }
-
- var amountToWithdraw: Amount
- var amountToSpend: Amount
- var bankBaseUrl: String
- var exchangeBaseUrl: String
- var merchantBaseUrl: String
- var merchantAuthToken: String
-
- struct Args: Encodable {
- var amountToWithdraw: Amount
- var amountToSpend: Amount
- var bankBaseUrl: String
- var exchangeBaseUrl: String
- var merchantBaseUrl: String
- var merchantAuthToken: String
- }
-}
diff --git a/TalerWallet1/Model/Model+Balances.swift
b/TalerWallet1/Model/Model+Balances.swift
new file mode 100644
index 0000000..f1a651c
--- /dev/null
+++ b/TalerWallet1/Model/Model+Balances.swift
@@ -0,0 +1,57 @@
+/*
+ * 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()
+ 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/Views/Exchange/ExchangeModel.swift
b/TalerWallet1/Model/Model+Exchange.swift
similarity index 53%
rename from TalerWallet1/Views/Exchange/ExchangeModel.swift
rename to TalerWallet1/Model/Model+Exchange.swift
index c16e720..8446893 100644
--- a/TalerWallet1/Views/Exchange/ExchangeModel.swift
+++ b/TalerWallet1/Model/Model+Exchange.swift
@@ -1,54 +1,15 @@
/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ * 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 {
- @Published var exchanges: [Exchange]?
-}
// 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: -
-struct Exchange: Codable, Hashable {
+/// 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 &&
@@ -57,13 +18,16 @@ struct Exchange: Codable, Hashable {
var exchangeBaseUrl: String
var currency: String?
- var tosStatus: String
var paytoUris: [String]
+ var tosStatus: String
var exchangeStatus: String
- var permanent: Bool
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 {
@@ -73,43 +37,64 @@ struct Exchange: Codable, Hashable {
return nil
}
}
+
struct ExchangeError: Codable, Hashable {
var error: HTTPError
}
struct HTTPError: Codable, Hashable {
var code: Int
- var requestUrl: String
+ var requestUrl: String?
var hint: String
- var requestMethod: 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 {
+ struct Response: Decodable {} // no result - getting no error back means
success
+ func operation() -> String { return "addExchange" }
+ func args() -> Args { return Args(exchangeBaseUrl: exchangeBaseUrl) }
+
+ var exchangeBaseUrl: String
+
+ struct Args: Encodable {
+ var exchangeBaseUrl: String
+ }
+}
// MARK: -
-extension ExchangeModel {
+extension WalletModel {
/// ask wallet-core for its list of known exchanges
- @MainActor func updateList() async throws {
+ @MainActor func listExchangesM()
+ async -> [Exchange] { // M for MainActor
do {
let request = ListExchanges()
let response = try await sendRequest(request, ASYNCDELAY)
- exchanges = response.exchanges // trigger view update
in ExchangeListView
- } catch { // TODO: Error
- symLog?.log(error.localizedDescription)
- throw error
+ return response.exchanges
+ } catch {
+ return [] // empty, but not nil
}
}
- /// add a new exchange with URL to wallet's list of known exchanges
- func add(url: String) async throws {
- do {
- symLog?.log("adding exchange: \(url)") // TODO: notice
- let request = AddExchange(exchangeBaseUrl: url)
- _ = try await sendRequest(request)
- symLog?.log("added exchange: \(url)")
- try await updateList()
- } catch { // TODO: Error
- symLog?.log(error.localizedDescription)
- throw error
- }
+ /// add a new exchange with URL to the wallet's list of known exchanges
+ func addExchange(url: String)
+ async throws {
+ let request = AddExchange(exchangeBaseUrl: url)
+ logger.info("adding exchange: \(url, privacy: .public)")
+ _ = try await sendRequest(request)
}
}
diff --git a/TalerWallet1/Model/Model+P2P.swift
b/TalerWallet1/Model/Model+P2P.swift
new file mode 100644
index 0000000..a671673
--- /dev/null
+++ b/TalerWallet1/Model/Model+P2P.swift
@@ -0,0 +1,259 @@
+/*
+ * 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: common structures
+struct PeerContractTerms: Codable {
+ let amount: Amount
+ let summary: String
+ let purse_expiration: Timestamp
+}
+// MARK: - PeerPushDebit
+/// Check if initiating a peer push payment is possible, check fees
+struct AmountResponse: Codable {
+ let effectiveAmount: Amount
+ let rawAmount: Amount
+}
+fileprivate struct GetMaxPeerPushAmount: WalletBackendFormattedRequest {
+ typealias Response = AmountResponse
+ func operation() -> String { return "GetMaxPeerPushAmount" }
+ func args() -> Args { return Args(currency: currency) }
+
+ var currency: String
+ struct Args: Encodable {
+ var currency: String
+ }
+}
+extension WalletModel {
+ @MainActor
+ func getMaxPeerPushAmountM(_ currency: String) // M for MainActor
+ async throws -> AmountResponse {
+ let request = GetMaxPeerPushAmount(currency: currency)
+ let response = try await sendRequest(request, ASYNCDELAY)
+ return response
+ }
+} // getMaxPeerPushAmountM
+// - - - - - -
+struct CheckPeerPushDebitResponse: Codable {
+ let exchangeBaseUrl: String?
+ let amountRaw: Amount
+ let amountEffective: Amount
+ let maxExpirationDate: Timestamp? // TODO: limit expiration (30
days or 7 days)
+}
+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
+ }
+}
+extension WalletModel {
+ @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
+ }
+} // checkPeerPushDebitM
+// - - - - - -
+/// Initiate an outgoing peer push payment, send coins
+struct InitiatePeerPushDebitResponse: Codable {
+ let contractPriv: String
+ let mergePriv: String
+ let pursePub: String
+ let exchangeBaseUrl: String
+ let talerUri: String
+ let transactionId: String
+}
+fileprivate struct InitiatePeerPushDebit: WalletBackendFormattedRequest {
+ typealias Response = InitiatePeerPushDebitResponse
+ 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
+ }
+}
+extension WalletModel {
+ @MainActor
+ func initiatePeerPushDebitM(_ baseURL: String?, terms: PeerContractTerms)
// M for MainActor
+ async throws -> InitiatePeerPushDebitResponse {
+ let request = InitiatePeerPushDebit(exchangeBaseUrl: baseURL,
+ partialContractTerms: terms)
+ let response = try await sendRequest(request, ASYNCDELAY)
+ return response
+ }
+} // initiatePeerPushDebitM
+// MARK: - PeerPullCredit
+/// Check fees before sending an invoice to another wallet
+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
+}
+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
+ }
+}
+extension WalletModel {
+ @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
+ }
+} // checkPeerPullCreditM
+// - - - - - -
+/// Initiate an outgoing peer pull payment, send an invoice
+struct InitiatePeerPullCreditResponse: Codable {
+ let talerUri: String
+ let transactionId: String
+}
+fileprivate struct InitiatePeerPullCredit: WalletBackendFormattedRequest {
+ typealias Response = InitiatePeerPullCreditResponse
+ 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
+ }
+}
+extension WalletModel {
+ @MainActor
+ func initiatePeerPullCreditM(_ baseURL: String?, terms: PeerContractTerms)
// M for MainActor
+ async throws -> InitiatePeerPullCreditResponse {
+ let request = InitiatePeerPullCredit(exchangeBaseUrl: baseURL,
+ partialContractTerms: terms)
+ let response = try await sendRequest(request, ASYNCDELAY)
+ return response
+ }
+} // initiatePeerPullCreditM
+// MARK: - PeerPushCredit
+/// Prepare an incoming peer push payment, receive coins
+struct PreparePeerPushCreditResponse: Codable {
+ let contractTerms: PeerContractTerms
+ let amountRaw: Amount
+ let amountEffective: Amount
+ // the dialog transaction is already created in the local DB - must either
accept or delete
+ let transactionId: String
+}
+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
+ }
+}
+extension WalletModel {
+ @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
+ }
+} // preparePeerPushCreditM
+// - - - - - -
+/// Accept an incoming peer push payment
+fileprivate struct AcceptPeerPushCredit: WalletBackendFormattedRequest {
+ struct Response: Decodable {} // no result - getting no error back means
success
+ func operation() -> String { return "confirmPeerPushCredit" } // should be
"acceptPeerPushCredit"
+ func args() -> Args { return Args(transactionId: transactionId) }
+
+ var transactionId: String
+ struct Args: Encodable {
+ var transactionId: String
+ }
+}
+extension WalletModel {
+ @MainActor
+ func acceptPeerPushCreditM(_ transactionId: String) // M for
MainActor
+ async throws -> Decodable {
+ let request = AcceptPeerPushCredit(transactionId: transactionId)
+ let response = try await sendRequest(request, ASYNCDELAY)
+ return response
+ }
+} // acceptPeerPushCreditM
+// MARK: - PeerPullDebit
+/// Prepare an incoming peer push invoice, pay coins
+struct PreparePeerPullDebitResponse: Codable {
+ let contractTerms: PeerContractTerms
+ let amountRaw: Amount
+ let amountEffective: Amount
+ // the dialog transaction is already created in the local DB - must either
accept or delete
+ let transactionId: String
+}
+fileprivate struct PreparePeerPullDebit: WalletBackendFormattedRequest {
+ typealias Response = PreparePeerPullDebitResponse
+ func operation() -> String { return "preparePeerPullDebit" }
+ func args() -> Args { return Args(talerUri: talerUri) }
+
+ var talerUri: String
+ struct Args: Encodable {
+ var talerUri: String
+ }
+}
+extension WalletModel {
+ @MainActor
+ func preparePeerPullDebitM(_ talerUri: String) // M for MainActor
+ async throws -> PreparePeerPullDebitResponse {
+ let request = PreparePeerPullDebit(talerUri: talerUri)
+ let response = try await sendRequest(request, ASYNCDELAY)
+ return response
+ }
+} // preparePeerPullDebitM
+// - - - - - -
+/// Confirm incoming peer push invoice and pay
+fileprivate struct ConfirmPeerPullDebit: WalletBackendFormattedRequest {
+ struct Response: Decodable {} // no result - getting no error back means
success
+ func operation() -> String { return "confirmPeerPullDebit" }
+ func args() -> Args { return Args(transactionId: transactionId) }
+
+ var transactionId: String
+ struct Args: Encodable {
+ var transactionId: String
+ }
+}
+extension WalletModel {
+ @MainActor
+ func confirmPeerPullDebitM(_ transactionId: String) // M for
MainActor
+ async throws -> Decodable {
+ let request = ConfirmPeerPullDebit(transactionId: transactionId)
+ let response = try await sendRequest(request, ASYNCDELAY)
+ return response
+ }
+} // confirmPeerPullDebitM
diff --git a/TalerWallet1/Views/Payment/PaymentURIModel.swift
b/TalerWallet1/Model/Model+Payment.swift
similarity index 64%
rename from TalerWallet1/Views/Payment/PaymentURIModel.swift
rename to TalerWallet1/Model/Model+Payment.swift
index 8fd4142..940b126 100644
--- a/TalerWallet1/Views/Payment/PaymentURIModel.swift
+++ b/TalerWallet1/Model/Model+Payment.swift
@@ -1,17 +1,6 @@
/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
*/
import Foundation
import taler_swift
@@ -19,19 +8,6 @@ import AnyCodable
//import SymLog
fileprivate let ASYNCDELAY: UInt = 0 //set e.g to 6 or 9 seconds for
debugging
-enum PaymentState {
- case error
- case waitingForUriDetails
- case receivedUriDetails
- case waitingForPaymentAck
- case receivedPaymentAck
-}
-
-class PaymentURIModel: WalletModel {
- @Published var paymentState: PaymentState?
-}
-
-
// MARK: - ContractTerms
struct ContractTerms: Codable {
let amount: Amount
@@ -77,7 +53,6 @@ struct ContractTerms: Codable {
case wireFeeAmortization = "wire_fee_amortization"
}
}
-
// MARK: - Auditor
struct Auditor: Codable {
let name: String
@@ -90,7 +65,6 @@ struct Auditor: Codable {
case url
}
}
-
// MARK: - Exchange
struct ExchangeForPay: Codable {
let url: String
@@ -101,7 +75,6 @@ struct ExchangeForPay: Codable {
case masterPub = "master_pub"
}
}
-
// MARK: - Extra
struct Extra: Codable {
let articleName: String
@@ -110,11 +83,10 @@ struct Extra: Codable {
case articleName = "article_name"
}
}
-
// MARK: -
/// The result from PreparePayForUri
struct PaymentDetailsForUri: Codable {
- let status: String
+// let status: String
let amountRaw: Amount
let amountEffective: Amount
let noncePriv: String
@@ -134,7 +106,7 @@ fileprivate struct PreparePayForUri:
WalletBackendFormattedRequest {
}
}
// MARK: -
-/// The result from getPaymentDetailsForAmount
+/// The result from confirmPayForUri
struct ConfirmPayResult: Decodable {
var type: String
var contractTerms: ContractTerms
@@ -152,32 +124,20 @@ fileprivate struct confirmPayForUri:
WalletBackendFormattedRequest {
}
}
// MARK: -
-extension PaymentURIModel {
+extension WalletModel {
/// load payment details. Networking involved
@MainActor
- func preparePayForUri(_ talerPayUri: String) async throws ->
PaymentDetailsForUri {
- do {
- paymentState = .waitingForUriDetails
- let request = PreparePayForUri(talerPayUri: talerPayUri)
- let response = try await sendRequest(request, ASYNCDELAY)
- paymentState = .receivedUriDetails
- return response
- } catch {
- paymentState = .error
- throw error
- }
+ 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 confirmPay(_ proposalId: String) async throws -> ConfirmPayResult {
- do {
- paymentState = .waitingForPaymentAck
- let request = confirmPayForUri(proposalId: proposalId)
- let response = try await sendRequest(request, ASYNCDELAY)
- paymentState = .receivedPaymentAck
- return response
- } catch {
- paymentState = .error
- throw error
- }
+ 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
new file mode 100644
index 0000000..eb9f447
--- /dev/null
+++ 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
new file mode 100644
index 0000000..6d13b15
--- /dev/null
+++ b/TalerWallet1/Model/Model+Settings.swift
@@ -0,0 +1,100 @@
+/*
+ * 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: -
+/// A request to add a test balance to the wallet.
+fileprivate struct WalletBackendWithdrawTestBalance:
WalletBackendFormattedRequest {
+ struct Response: Decodable {} // no result - getting no error back means
success
+ 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
+ }
+}
+extension WalletModel {
+ @MainActor func loadTestKudosM()
+ async throws { // M for MainActor
+ let amount = Amount(currency: DEMOCURRENCY, integer: 11, fraction: 0)
+ let request = WalletBackendWithdrawTestBalance(amount: amount,
+ bankBaseUrl:
DEMO_BANKBASEURL,
+ exchangeBaseUrl:
DEMO_EXCHANGEBASEURL,
+ bankAccessApiBaseUrl:
DEMO_BANKAPIBASEURL)
+ let response = try await sendRequest(request, ASYNCDELAY)
+ }
+} // loadTestKudosM()
+// MARK: -
+/// A request to add a test balance to the wallet.
+fileprivate struct WalletBackendRunIntegration: WalletBackendFormattedRequest {
+ struct Response: Decodable {} // no result - getting no error back means
success
+ 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
+ }
+}
+extension WalletModel {
+ @MainActor func runIntegrationTestM(newVersion: Bool)
+ async throws { // M for MainActor
+ let amountW = Amount(currency: DEMOCURRENCY, integer: 3, fraction: 0)
+ let amountS = Amount(currency: DEMOCURRENCY, integer: 1, fraction: 0)
+ let request = WalletBackendRunIntegration(newVersion: newVersion,
+ amountToWithdraw: amountW,
+ amountToSpend: amountS,
+ bankBaseUrl:
DEMO_BANKAPIBASEURL,
+ bankAccessApiBaseUrl:
DEMO_BANKAPIBASEURL,
+ exchangeBaseUrl:
DEMO_EXCHANGEBASEURL,
+ merchantBaseUrl:
DEMO_MERCHANTBASEURL,
+ merchantAuthToken:
DEMO_MERCHANTAUTHTOKEN)
+ let _ = try await sendRequest(request, ASYNCDELAY)
+ }
+} // runIntegrationTestM()
diff --git a/TalerWallet1/Model/Model+Transactions.swift
b/TalerWallet1/Model/Model+Transactions.swift
new file mode 100644
index 0000000..0795255
--- /dev/null
+++ b/TalerWallet1/Model/Model+Transactions.swift
@@ -0,0 +1,158 @@
+/*
+ * 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 {
+ struct Response: Decodable {} // no result - getting no error back means
success
+ func operation() -> String { return "abortTransaction" }
+ func args() -> Args { return Args(transactionId: transactionId) }
+
+ var transactionId: String
+ struct Args: Encodable {
+ var transactionId: String
+ }
+}
+/// A request to delete a wallet transaction by ID.
+struct DeleteTransaction: WalletBackendFormattedRequest {
+ struct Response: Decodable {} // no result - getting no error back means
success
+ func operation() -> String { return "deleteTransaction" }
+ func args() -> Args { return Args(transactionId: transactionId) }
+
+ var transactionId: String
+ struct Args: Encodable {
+ var transactionId: String
+ }
+}
+/// A request to delete a wallet transaction by ID.
+struct FailTransaction: WalletBackendFormattedRequest {
+ struct Response: Decodable {} // no result - getting no error back means
success
+ func operation() -> String { return "failTransaction" }
+ func args() -> Args { return Args(transactionId: transactionId) }
+
+ var transactionId: String
+ struct Args: Encodable {
+ var transactionId: String
+ }
+}
+/// A request to suspend a wallet transaction by ID.
+struct SuspendTransaction: WalletBackendFormattedRequest {
+ struct Response: Decodable {} // no result - getting no error back means
success
+ func operation() -> String { return "suspendTransaction" }
+ func args() -> Args { return Args(transactionId: transactionId) }
+
+ var transactionId: String
+ struct Args: Encodable {
+ var transactionId: String
+ }
+}
+/// A request to suspend a wallet transaction by ID.
+struct ResumeTransaction: WalletBackendFormattedRequest {
+ struct Response: Decodable {} // no result - getting no error back means
success
+ func operation() -> String { return "resumeTransaction" }
+ 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 transactionsT(currency: String? = nil, searchString: String? = nil)
+ async -> [Transaction] { // might
be called from a background thread itself
+ do {
+ let request = GetTransactions(currency: currency, search:
searchString)
+ let response = try await sendRequest(request, ASYNCDELAY)
+ return response.transactions
+ } catch {
+ return []
+ }
+ }
+ /// fetch transactions from Wallet-Core. No networking involved
+ @MainActor func transactionsMA(currency: String? = nil, searchString:
String? = nil)
+ async -> [Transaction] { // M for MainActor
+ return await transactionsT(currency: currency, searchString:
searchString)
+ }
+
+ /// abort the specified transaction from Wallet-Core. No networking
involved
+ func abortTransaction(transactionId: String) async throws {
+ let request = AbortTransaction(transactionId: transactionId)
+ logger.info("abortTransaction: \(transactionId, privacy:
.private(mask: .hash))")
+ let _ = try await sendRequest(request, ASYNCDELAY)
+ }
+
+ /// delete the specified transaction from Wallet-Core. No networking
involved
+ func deleteTransaction(transactionId: String) async throws {
+ let request = DeleteTransaction(transactionId: transactionId)
+ logger.info("deleteTransaction: \(transactionId, privacy:
.private(mask: .hash))")
+ let _ = try await sendRequest(request, ASYNCDELAY)
+ }
+
+ func failTransaction(transactionId: String) async throws {
+ let request = FailTransaction(transactionId: transactionId)
+ logger.info("failTransaction: \(transactionId, privacy: .private(mask:
.hash))")
+ let _ = try await sendRequest(request, ASYNCDELAY)
+ }
+
+ func suspendTransaction(transactionId: String) async throws {
+ let request = SuspendTransaction(transactionId: transactionId)
+ logger.info("suspendTransaction: \(transactionId, privacy:
.private(mask: .hash))")
+ let _ = try await sendRequest(request, ASYNCDELAY)
+ }
+
+ func resumeTransaction(transactionId: String) async throws {
+ let request = ResumeTransaction(transactionId: transactionId)
+ logger.info("resumeTransaction: \(transactionId, privacy:
.private(mask: .hash))")
+ let _ = try await sendRequest(request, ASYNCDELAY)
+ }
+}
diff --git a/TalerWallet1/Model/Model+Withdraw.swift
b/TalerWallet1/Model/Model+Withdraw.swift
new file mode 100644
index 0000000..0a1b5e2
--- /dev/null
+++ b/TalerWallet1/Model/Model+Withdraw.swift
@@ -0,0 +1,187 @@
+/*
+ * 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 WithdrawalAmountDetails: 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 = WithdrawalAmountDetails
+ 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
+ }
+}
+/// A request to mark an exchange's terms of service as accepted.
+fileprivate struct SetExchangeTOSAccepted: WalletBackendFormattedRequest {
+ struct Response: Decodable {} // no result - getting no error back means
success
+ 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 -> WithdrawalAmountDetails {
+ 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/WalletInitModel.swift
b/TalerWallet1/Model/WalletInitModel.swift
deleted file mode 100644
index 7be0dff..0000000
--- a/TalerWallet1/Model/WalletInitModel.swift
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-import Foundation
-import SymLog
-
-let DATABASE = "talerwalletdb-v30"
-
-class WalletInitModel: WalletModel {
-
-}
-// 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 { // versioninfo
- var versionInfo: VersionInfo
- enum CodingKeys: String, CodingKey {
- case 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 initWallet() async throws -> VersionInfo? {
- do {
- 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
- } catch {
- symLog?.log("error: \(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")
- 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
index d340278..1b56aa2 100644
--- a/TalerWallet1/Model/WalletModel.swift
+++ b/TalerWallet1/Model/WalletModel.swift
@@ -1,55 +1,126 @@
/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
*/
import Foundation
import SymLog
+import os.log
+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: ObservableObject {
+ public static let shared = WalletModel()
static func className() -> String {"\(self)"}
- var symLog: SymLogC?
- var walletCore: WalletCore
-
- @Published var loading: Bool = false // update view
-
- init(walletCore: WalletCore) {
- self.symLog = SymLogC(funcName: Self.className())
- self.walletCore = walletCore
- }
+ let logger = Logger (subsystem: "net.taler.gnu", category: "WalletModel")
- @MainActor func sendRequest<T: WalletBackendFormattedRequest> (_ request:
T, _ delay: UInt = 0)
- async throws -> T.Response {
- loading = true // enter progressView
+ func sendRequest<T: WalletBackendFormattedRequest> (_ request: T, _ delay:
UInt = 0)
+ async throws -> T.Response { // T for any Thread
+#if !DEBUG
+ logger.log("sending: \(request.operation(), privacy: .public)")
+#endif
+ let sendTime = Date.now
do {
- symLog?.log("sending: \(request)")
- let (response, id) = try await
walletCore.sendFormattedRequest(request: request)
+ let (response, id) = try await
WalletCore.shared.sendFormattedRequest(request: request)
+#if !DEBUG
+ let timeUsed = Date.now - sendTime
+ logger.log("received: \(request.operation(), privacy: .public)
(\(id, privacy: .public)) after \(timeUsed.milliseconds, privacy: .public) ms")
+#endif
let asyncDelay: UInt = delay > 0 ? delay : UInt(ASYNCDELAY)
if asyncDelay > 0 { // test LoadingView, sleep some seconds
- symLog?.log("received: (\(id)), going to sleep for
\(asyncDelay) seconds...")
try? await Task.sleep(nanoseconds: 1_000_000_000 *
UInt64(asyncDelay))
- symLog?.log("waking up again after \(asyncDelay) seconds, will
deliver \(response)")
- } else {
- symLog?.log("received: \(response)")
}
- loading = false // exit progressView
return response
- } catch {
+ } catch { // rethrows
+ let timeUsed = Date.now - sendTime
+ logger.error("\(request.operation(), privacy: .public) failed
after \(timeUsed.milliseconds, privacy: .public) ms\n\(error)")
throw error
}
}
+ func getTransactionByIdT(_ transactionId: String)
+ async throws -> Transaction { // T for any Thread
+ // might be called from a background thread itself
+ let request = GetTransactionById(transactionId: transactionId)
+ return try await sendRequest(request, ASYNCDELAY)
+ }
+ /// get the specified transaction from Wallet-Core. No networking involved
+ @MainActor func getTransactionByIdM(_ transactionId: String)
+ async throws -> Transaction { // M for MainActor
+ return try await getTransactionByIdT(transactionId) // call
GetTransactionById on main thread
+ }
+}
+// MARK: -
+/// A request to get a wallet transaction by ID.
+fileprivate struct GetTransactionById: WalletBackendFormattedRequest {
+ typealias Response = Transaction
+ func operation() -> String { return "getTransactionById" }
+ func args() -> Args { return Args(transactionId: transactionId) }
+
+ var transactionId: String
+
+ struct Args: Encodable {
+ 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 {
+ // T for any Thread
+ let docPath = try docPath()
+ let request = InitRequest(persistentStoragePath: docPath)
+ 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")
+ let docPath = storageDir.path
+ logger.debug("\(docPath)")
+ return docPath
+ } else { // should never happen
+ logger.error("documentURLs empty")
+ throw WalletBackendError.initializationError
+ }
+ }
}
diff --git a/TalerWallet1/Preview Content/transactions.json
b/TalerWallet1/Preview Content/transactions.json
new file mode 100644
index 0000000..b2db39c
--- /dev/null
+++ b/TalerWallet1/Preview Content/transactions.json
@@ -0,0 +1,300 @@
+{
+ "transactions":[
+ { "type": "withdrawal",
+ "amountEffective": "KUDOS:2.9",
+ "amountRaw": "KUDOS:3",
+ "transactionId":
"txn:withdrawal:QQWXZ908YYWFPH9QV2GB72Z29C83FFK612WJARXBX6YSNRGPP660",
+ "timestamp": ["t_s": 1683531967]
+ "txState": {
+ "major": "done"
+ },
+ "pending": false,
+
+ "exchangeBaseUrl": "https://exchange.demo.taler.net/",
+ "withdrawalDetails": {
+ "type": "taler-bank-integration-api",
+ "reservePub":
"R4G8E58Q97M6HQP69V3J3Q6XX1DXQ06FARWNSNRAWYX446WB2R8G"
+ "bankConfirmationUrl": "https://bank.demo.taler.net/",
+ "confirmed": true,
+ "reserveIsReady": true,
+ },
+ },
+ { "type": "payment",
+ "amountEffective": "KUDOS:1.1",
+ "amountRaw": "KUDOS:1",
+ "transactionId":
"txn:payment:43JV9JTB270X1EBH5T59HV9JF2CQW8ZY7M23PS8444BGY4F2Z110",
+ "timestamp": ["t_s": 1683531978],,
+ "txState": {
+ "major": "done"
+ },
+
+ "proposalId":
"43JV9JTB270X1EBH5T59HV9JF2CQW8ZY7M23PS8444BGY4F2Z110",
+ "status": "paid",
+ "totalRefundRaw": "KUDOS:0",
+ "totalRefundEffective": "KUDOS:0"
+ "refundQueryActive": false
+ "refunds": [],
+ "info": {
+ "orderId": "2023.128-03A25VEED68QC",
+ "merchant": {
+ "name": "GNU Taler",
+ "jurisdiction": [:],
+ "address": [:]
+ },
+ "summary": "hello world",
+ "products": []
+ "fulfillmentUrl": "taler://fulfillment-success/thx",
+ "contractTermsHash":
"X0QJGEPSDKARFPBNTSF5HMXJYDGQ4WRX789PAB2G3EER9J7B5Z8WHHJ0W8KX3Z175A2FGVBKPZ85H8X0Y6H4MNHARJCA9ACW43QS10R",
+ },
+ },
+ { "type": "withdrawal",
+ "amountEffective": "KUDOS:17.8",
+ "amountRaw": "KUDOS:18",
+ "transactionId":
"txn:withdrawal:N9CR5EH8BSPB2NVEDW1HAXX94C9DCKSAWF097M4S9AHJ5KW5R01G",
+ "timestamp": ["t_s": 1683531980],
+ "txState": {
+ "major": "done"
+ },
+ "pending": false,
+
+ "exchangeBaseUrl": "https://exchange.demo.taler.net/",
+ "withdrawalDetails": {
+ "type": "taler-bank-integration-api",
+ "reservePub":
"6P2Y5N7H1NY8938H60S9WJ1HFS11F90P0RR1H51WCV7MHYYFNS80",
+ "reserveIsReady": true,
+ "confirmed": true,
+ "bankConfirmationUrl": "https://bank.demo.taler.net/",
+ },
+ },
+ { "type": "payment",
+ "amountEffective": "KUDOS:7.2",
+ "amountRaw": "KUDOS:7",
+ "transactionId":
"txn:payment:3F9DWB6DNAT8WDPRHQ160AZ1G8WNJH23069Q39HDK8Z3EAR804M0",
+ "timestamp": ["t_s": 1683531996],
+ "txState": {
+ "major": "done"
+ },
+
+ "proposalId":
"3F9DWB6DNAT8WDPRHQ160AZ1G8WNJH23069Q39HDK8Z3EAR804M0",
+ "status": "paid",
+ "totalRefundRaw": "KUDOS:0",
+ "totalRefundEffective": "KUDOS:0",
+ "refundPending": "KUDOS:6",
+ "refundQueryActive": false,
+ "refunds": [],
+ "info": {
+ "orderId": "2023.128-0388C075CX1R0",
+ "merchant": {
+ "name": "GNU Taler",
+ "jurisdiction": [:],
+ "address": [:]
+ },
+ "summary": "order that will be refunded",
+ "products": [],
+ "fulfillmentUrl": "taler://fulfillment-success/thx",
+ "contractTermsHash":
"SPD5XJTFE8N73FXQT0JRVZ2ABSGKWVHFWV1GAZGDAWZT7CK0WSHCJSWV1FMCFEGTHT786ZPFVFHJWWB3V0ADCNPBT0YFS78Z2P3KYH0",
+ },
+ },
+ { "type": "refund",
+ "amountEffective": "KUDOS:5.8",
+ "amountRaw": "KUDOS:6",
+ "transactionId":
"txn:refund:PFW6JNX6QVKMF5HHER8KHPS9ADJTPXBQ1JK6C7W55F4X6CK59Q20",
+ "timestamp": {"t_s": 1683531997},
+ "txState": {
+ "major": "done"
+ },
+
+ "refundedTransactionId":
"txn:payment:3F9DWB6DNAT8WDPRHQ160AZ1G8WNJH23069Q39HDK8Z3EAR804M0",
+ },
+ { "type": "payment",
+ "amountEffective": "KUDOS:3.2",
+ "amountRaw": "KUDOS:3",
+ "transactionId":
"txn:payment:8AG5GVQ7E2FGREEH8V5ADNW1D6NPVD4YCF26763A9JYEQ76AVBX0",
+ "timestamp": ["t_s": 1683531998],
+ "txState": {
+ "major": "done"
+ },
+
+ "proposalId":
"8AG5GVQ7E2FGREEH8V5ADNW1D6NPVD4YCF26763A9JYEQ76AVBX0"
+ "status": "paid",
+ "totalRefundRaw": "KUDOS:0",
+ "totalRefundEffective": "KUDOS:0",
+ "refundQueryActive": false,
+ "refunds": [],
+ "info": {
+ "orderId": "2023.128-00G672RZHTRWA",
+ "merchant": [
+ "name": "GNU Taler",
+ "address": [:],
+ "jurisdiction": [:]
+ ],
+ "summary": "payment after refund",
+ "products": []
+ "fulfillmentUrl": "taler://fulfillment-success/thx",
+ "contractTermsHash":
"GZZ75P8G5H7PEV93E6M7TNNT1RNCSP0MJT7K7K4VXJAZGK5FAFC2YJYDGX3CT077VPQFZJR3QV5M9ZNH2T86ZWBA8BDDREM12RZYWN0",
+ },
+ }
+ ]
+}
+
+
+"transactions":[
+ {
+ "type":"withdrawal",
+ "txState":{
+ "major":"done"
+ },
+ "amountEffective":"KUDOS:2.9",
+ "amountRaw":"KUDOS:3",
+ "withdrawalDetails":{
+ "type":"taler-bank-integration-api",
+ "confirmed":true,
+
"reservePub":"R4G8E58Q97M6HQP69V3J3Q6XX1DXQ06FARWNSNRAWYX446WB2R8G",
+ "bankConfirmationUrl":"https://bank.demo.taler.net/",
+ "reserveIsReady":true
+ },
+ "exchangeBaseUrl":"https://exchange.demo.taler.net/",
+ "pending":false,
+ "timestamp":{ "t_s":1683531967 },
+
"transactionId":"txn:withdrawal:QQWXZ908YYWFPH9QV2GB72Z29C83FFK612WJARXBX6YSNRGPP660",
+ },
+ {
+ "type":"payment",
+ "txState":{
+ "major":"done"
+ },
+ "amountRaw":"KUDOS:1",
+ "amountEffective":"KUDOS:1.1",
+ "totalRefundRaw":"KUDOS:0",
+ "totalRefundEffective":"KUDOS:0",
+ "status":"paid",
+ "extendedStatus":"done",
+ "pending":false,
+ "refunds":[],
+ "timestamp":{
+ "t_s":1683531978
+ },
+
"transactionId":"txn:payment:43JV9JTB270X1EBH5T59HV9JF2CQW8ZY7M23PS8444BGY4F2Z110",
+ "proposalId":"43JV9JTB270X1EBH5T59HV9JF2CQW8ZY7M23PS8444BGY4F2Z110",
+ "info":{
+ "merchant":{
+ "name":"GNU Taler",
+ "address":{
+
+ },
+ "jurisdiction":{
+ }
+ },
+ "orderId":"2023.128-03A25VEED68QC",
+ "products":[],
+ "summary":"hello world",
+
"contractTermsHash":"X0QJGEPSDKARFPBNTSF5HMXJYDGQ4WRX789PAB2G3EER9J7B5Z8WHHJ0W8KX3Z175A2FGVBKPZ85H8X0Y6H4MNHARJCA9ACW43QS10R",
+ "fulfillmentUrl":"taler://fulfillment-success/thx"
+ },
+ "refundQueryActive":false,
+ },
+ {
+ "type":"withdrawal",
+ "txState":{
+ "major":"done"
+ },
+ "amountEffective":"KUDOS:17.8",
+ "amountRaw":"KUDOS:18",
+ "withdrawalDetails":{
+ "type":"taler-bank-integration-api",
+ "confirmed":true,
+
"reservePub":"6P2Y5N7H1NY8938H60S9WJ1HFS11F90P0RR1H51WCV7MHYYFNS80",
+ "bankConfirmationUrl":"https://bank.demo.taler.net/",
+ "reserveIsReady":true
+ },
+ "exchangeBaseUrl":"https://exchange.demo.taler.net/",
+ "extendedStatus":"done",
+ "pending":false,
+ "timestamp":{ "t_s":1683531980 },
+
"transactionId":"txn:withdrawal:N9CR5EH8BSPB2NVEDW1HAXX94C9DCKSAWF097M4S9AHJ5KW5R01G",
+ },
+ {
+ "type":"payment",
+ "txState":{
+ "major":"done"
+ },
+ "amountRaw":"KUDOS:7",
+ "amountEffective":"KUDOS:7.2",
+ "totalRefundRaw":"KUDOS:0",
+ "totalRefundEffective":"KUDOS:0",
+ "refundPending":"KUDOS:6",
+ "status":"paid",
+ "extendedStatus":"done",
+ "pending":false,
+ "refunds":[],
+ "timestamp":{ "t_s":1683531996 },
+
"transactionId":"txn:payment:3F9DWB6DNAT8WDPRHQ160AZ1G8WNJH23069Q39HDK8Z3EAR804M0",
+ "proposalId":"3F9DWB6DNAT8WDPRHQ160AZ1G8WNJH23069Q39HDK8Z3EAR804M0",
+ "info":{
+ "merchant":{
+ "name":"GNU Taler",
+ "address":{
+
+ },
+ "jurisdiction":{
+ }
+ },
+ "orderId":"2023.128-0388C075CX1R0",
+ "products":[],
+ "summary":"order that will be refunded",
+
"contractTermsHash":"SPD5XJTFE8N73FXQT0JRVZ2ABSGKWVHFWV1GAZGDAWZT7CK0WSHCJSWV1FMCFEGTHT786ZPFVFHJWWB3V0ADCNPBT0YFS78Z2P3KYH0",
+ "fulfillmentUrl":"taler://fulfillment-success/thx"
+ },
+ "refundQueryActive":false,
+ },
+ {
+ "type":"refund",
+ "amountEffective":"KUDOS:5.8",
+ "amountRaw":"KUDOS:6",
+
"refundedTransactionId":"txn:payment:3F9DWB6DNAT8WDPRHQ160AZ1G8WNJH23069Q39HDK8Z3EAR804M0",
+ "timestamp":{
+ "t_s":1683531997
+ },
+
"transactionId":"txn:refund:PFW6JNX6QVKMF5HHER8KHPS9ADJTPXBQ1JK6C7W55F4X6CK59Q20",
+ "txState":{
+ "major":"done"
+ },
+ "extendedStatus":"done",
+ "pending":false
+ },
+ {
+ "type":"payment",
+ "txState":{
+ "major":"done"
+ },
+ "amountRaw":"KUDOS:3",
+ "amountEffective":"KUDOS:3.2",
+ "totalRefundRaw":"KUDOS:0",
+ "totalRefundEffective":"KUDOS:0",
+ "status":"paid",
+ "extendedStatus":"done",
+ "pending":false,
+ "refunds":[],
+ "timestamp":{
+ "t_s":1683531998
+ },
+
"transactionId":"txn:payment:8AG5GVQ7E2FGREEH8V5ADNW1D6NPVD4YCF26763A9JYEQ76AVBX0",
+ "proposalId":"8AG5GVQ7E2FGREEH8V5ADNW1D6NPVD4YCF26763A9JYEQ76AVBX0",
+ "info":{
+ "merchant":{
+ "name":"GNU Taler",
+ "address":{
+
+ },
+ "jurisdiction":{
+ }
+ },
+ "orderId":"2023.128-00G672RZHTRWA",
+ "products":[],
+ "summary":"payment after refund",
+
"contractTermsHash":"GZZ75P8G5H7PEV93E6M7TNNT1RNCSP0MJT7K7K4VXJAZGK5FAFC2YJYDGX3CT077VPQFZJR3QV5M9ZNH2T86ZWBA8BDDREM12RZYWN0",
+ "fulfillmentUrl":"taler://fulfillment-success/thx"
+ },
+ "refundQueryActive":false,
+ }
+]
diff --git a/TalerWallet1/Quickjs/quickjs.swift
b/TalerWallet1/Quickjs/quickjs.swift
index 040bfcb..b301190 100644
--- a/TalerWallet1/Quickjs/quickjs.swift
+++ b/TalerWallet1/Quickjs/quickjs.swift
@@ -1,17 +1,6 @@
/*
- * This file is part of GNU Taler
- * (C) 2021 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
*/
import Foundation
diff --git a/TalerWallet1/Settings.bundle/Root.plist
b/TalerWallet1/Settings.bundle/Root.plist
new file mode 100644
index 0000000..89aaf6b
--- /dev/null
+++ b/TalerWallet1/Settings.bundle/Root.plist
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>StringsTable</key>
+ <string>Root</string>
+ <key>PreferenceSpecifiers</key>
+ <array>
+ <dict>
+ <key>Type</key>
+ <string>PSToggleSwitchSpecifier</string>
+ <key>Title</key>
+ <string>Diagnostic Mode</string>
+ <key>Key</key>
+ <string>diagnostic_mode_enabled</string>
+ <key>DefaultValue</key>
+ <false/>
+ </dict>
+ </array>
+</dict>
+</plist>
diff --git a/TalerWallet1/Settings.bundle/en.lproj/Root.strings
b/TalerWallet1/Settings.bundle/en.lproj/Root.strings
new file mode 100644
index 0000000..8cd87b9
Binary files /dev/null and b/TalerWallet1/Settings.bundle/en.lproj/Root.strings
differ
diff --git a/TalerWallet1/Views/Balances/BalanceRow.swift
b/TalerWallet1/Views/Balances/BalanceRow.swift
deleted file mode 100644
index 9c8aeee..0000000
--- a/TalerWallet1/Views/Balances/BalanceRow.swift
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-import SwiftUI
-import taler_swift
-
-struct BalanceRow: View {
- let amount: Amount
- let sendAction: () -> Void
- let recvAction: () -> Void
- var body: some View {
- HStack {
- Button("Send\nFunds", action: sendAction)
- .lineLimit(nil)
- .buttonStyle(.bordered)
- .padding(.trailing)
- Button("Receive\nFunds", action: recvAction)
- .buttonStyle(.bordered)
- Spacer()
- VStack(alignment: .trailing) {
- Text("Balance")
- .font(.footnote)
- Text("\(amount.valueStr)")
- .font(.title)
- }
- }
- }
-}
-
-struct Balance_Previews: PreviewProvider {
- static var previews: some View {
- BalanceRow(amount: try! Amount(fromString: "Taler:0.1"), sendAction:
{}, recvAction: {})
- }
-}
diff --git a/TalerWallet1/Views/Balances/BalanceRowButtons.swift
b/TalerWallet1/Views/Balances/BalanceRowButtons.swift
new file mode 100644
index 0000000..1eb0a61
--- /dev/null
+++ b/TalerWallet1/Views/Balances/BalanceRowButtons.swift
@@ -0,0 +1,51 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import SwiftUI
+import taler_swift
+
+struct BalanceRowButtons: View {
+ let amount: Amount
+ let narrow: Bool
+ let lineLimit: Int
+ let sendAction: () -> Void
+ let recvAction: () -> Void
+ @Environment(\.sizeCategory) var sizeCategory
+
+ var body: some View {
+ let currency = amount.currencyStr
+ Group {
+ Button("Request\nPayment", action: recvAction)
+ .lineLimit(lineLimit)
+ .disabled(false)
+ .buttonStyle(TalerButtonStyle(type: .bordered, narrow: narrow,
aligned: .center))
+ Button("Send\n\(currency)", action: sendAction)
+ .lineLimit(lineLimit)
+ .disabled(amount.isZero)
+ .buttonStyle(TalerButtonStyle(type: .bordered, narrow: narrow,
aligned: .center))
+
+ }
+ }
+}
+
+struct BalanceRowButtons_Previews: PreviewProvider {
+ static var previews: some View {
+ List {
+ VStack {
+ let amount = try! Amount(fromString: LONGCURRENCY + ":1234.56")
+ BalanceRowButtons(amount: Amount(currency: "TestKUDOS", value:
1234),
+ narrow: false, lineLimit: 0,
+ sendAction: {}, recvAction: {})
+ BalanceButton(amount: amount, rowAction: {})
+ }
+ HStack {
+ let amount = try! Amount(fromString: "KUDOS" + ":1234.56")
+ BalanceRowButtons(amount: amount,
+ narrow: true, lineLimit: 2,
+ sendAction: {}, recvAction: {})
+ BalanceButton(amount: amount, rowAction: {})
+ }
+ }
+ }
+}
diff --git a/TalerWallet1/Views/Balances/BalanceRowView.swift
b/TalerWallet1/Views/Balances/BalanceRowView.swift
new file mode 100644
index 0000000..6f51790
--- /dev/null
+++ b/TalerWallet1/Views/Balances/BalanceRowView.swift
@@ -0,0 +1,98 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import SwiftUI
+import taler_swift
+
+/// This view shows the currency row in a currency section
+/// [Send Coins] [Receive Coins] Balance
+
+struct BalanceButton: View {
+ let amount: Amount
+ let rowAction: () -> Void
+
+ var body: some View {
+ Button(action: rowAction) {
+ VStack(alignment: .trailing, spacing: 0) {
+ HStack(alignment: .firstTextBaseline, spacing: 0) {
+ Text("B", comment: "the first letter of Balance - or leave
empty")
+ .font(.title2)
+ Text("alance", comment: "the remaining letters of Balance
- or all if you left B empty")
+ .font(.footnote).bold()
+ }
+ Text("\(amount.valueStr)") // TODO: CurrencyFormatter?
+ .font(.title)
+ }
+ } .disabled(false)
+ .accessibilityElement(children:
/*@START_MENU_TOKEN@*/.ignore/*@END_MENU_TOKEN@*/)
+ .accessibilityLabel("Balance \(amount.readableDescription)") //
TODO: CurrencyFormatter!
+ .buttonStyle(TalerButtonStyle(type: .plain, aligned: .trailing))
+// .background(Color.yellow)
+ }
+}
+
+struct BalanceRowView: View {
+ let amount: Amount
+ let sendAction: () -> Void
+ let recvAction: () -> Void
+ let rowAction: () -> Void
+ @Environment(\.sizeCategory) var sizeCategory
+
+ func needVStack(_ amount: Amount) -> Bool {
+ // Sizes: 320 (SE), 375 (X, Xs, 12, 13 mini), 390 (12,13,14), 414
(Plus, Max), 428 (Pro Max)
+ guard 350 < UIScreen.main.bounds.width else {return true} // always
for iPhone SE 1st Gen
+ var count = amount.currencyStr.count
+// print(sizeCategory)
+ switch sizeCategory {
+ case ContentSizeCategory.extraSmall:
+ count += 0
+ case ContentSizeCategory.small:
+ count += 1
+ case ContentSizeCategory.medium:
+ count += 2
+ case ContentSizeCategory.large:
+ count += 3
+ case ContentSizeCategory.extraLarge:
+ count += 4
+ default:
+ count += 5
+ }
+ return count > 9
+ }
+
+ var body: some View {
+ Group {
+ if needVStack(amount) {
+ VStack (alignment: .trailing) {
+ BalanceButton(amount: amount, rowAction: rowAction)
+ HStack {
+ BalanceRowButtons(amount: amount, narrow: false,
lineLimit: 5,
+ sendAction: sendAction, recvAction:
recvAction)
+ }
+ }
+ } else {
+ HStack {
+ BalanceRowButtons(amount: amount, narrow: true, lineLimit:
5,
+ sendAction: sendAction, recvAction:
recvAction)
+ BalanceButton(amount: amount, rowAction: rowAction)
+ }
+// .fixedSize(horizontal: true, vertical: true) // should
make all buttons equal height - but doesn't
+ }
+ }
+ .accessibilityElement(children: .combine)
+ }
+}
+// MARK: -
+#if DEBUG
+struct BalanceRowView_Previews: PreviewProvider {
+ static var previews: some View {
+ List {
+ BalanceRowView(amount: try! Amount(fromString: "TestKUDOS" +
":1234.56"),
+ sendAction: {}, recvAction: {}, rowAction: {})
+ BalanceRowView(amount: try! Amount(fromString: "KUDOS" +
":1234.56"),
+ sendAction: {}, recvAction: {}, rowAction: {})
+ }
+ }
+}
+#endif
diff --git a/TalerWallet1/Views/Balances/BalancesListView.swift
b/TalerWallet1/Views/Balances/BalancesListView.swift
new file mode 100644
index 0000000..2628bd3
--- /dev/null
+++ b/TalerWallet1/Views/Balances/BalancesListView.swift
@@ -0,0 +1,171 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import SwiftUI
+import taler_swift
+import SymLog
+import AVFoundation
+
+/// This view shows the list of balances / currencies, each in its own section
+
+struct BalancesListView: View {
+ private let symLog = SymLogV(0)
+ let navTitle: String
+ let hamburgerAction: () -> Void
+
+ @EnvironmentObject private var model: WalletModel
+ @State private var balances: [Balance] = []
+ @State private var centsToTransfer: UInt64 = 0
+ @State private var summary: String = ""
+ @State private var showQRScanner: Bool = false
+ @State private var showCameraAlert: Bool = false
+
+ private var openSettingsButton: some View {
+ Button("Open Settings") {
+ showCameraAlert = false
+ UIApplication.shared.open(URL(string:
UIApplication.openSettingsURLString)!)
+ }
+ }
+ var ClosingAnnouncement = AttributedString(localized: "Closing Camera")
+ private var dismissAlertButton: some View {
+ Button("Cancel", role: .cancel) {
+ if #available(iOS 17.0, *) {
+//
AccessibilityNotification.Announcement(ClosingAnnouncement).post()
+ }
+ showCameraAlert = false
+ }
+ }
+
+ var defaultPriorityAnnouncement = AttributedString(localized: "Opening
Camera")
+ var lowPriorityAnnouncement: AttributedString {
+ var lowPriorityString = AttributedString ("Camera Loading")
+ if #available(iOS 17.0, *) {
+// lowPriorityString.accessibilitySpeechAnnouncementPriority = .low
+ }
+ return lowPriorityString
+ }
+ var highPriorityAnnouncement: AttributedString {
+ var highPriorityString = AttributedString("Camera Active")
+ if #available(iOS 17.0, *) {
+// highPriorityString.accessibilitySpeechAnnouncementPriority =
.high
+ }
+ return highPriorityString
+ }
+ private func checkCameraAvailable() -> Void {
+ /// Open Camera Code
+ if #available(iOS 17.0, *) {
+//
AccessibilityNotification.Announcement(defaultPriorityAnnouncement).post()
+ }
+ AVCaptureDevice.requestAccess(for: .video, completionHandler: {
(granted: Bool) -> Void in
+ if granted {
+ showQRScanner = true
+ } else {
+ // Camera Loaded Code
+ if #available(iOS 17.0, *) {
+//
AccessibilityNotification.Announcement(highPriorityAnnouncement).post()
+ }
+ showCameraAlert = true
+ }
+ })
+ }
+
+ private func reloadAction() async -> Int {
+ let reloaded = await model.balancesM()
+ let count = reloaded.count
+ balances = reloaded
+ return count
+ }
+
+ var body: some View {
+#if DEBUG
+ let _ = Self._printChanges()
+ let _ = symLog.vlog() // just to get the # to compare it with
.onAppear & onDisappear
+#endif
+ Content(symLog: symLog, balances: $balances,
+ centsToTransfer: $centsToTransfer, summary: $summary,
+ reloadAction: reloadAction)
+ .navigationTitle(navTitle)
+ .navigationBarTitleDisplayMode(.automatic)
+ .navigationBarItems(leading: HamburgerButton(action:
hamburgerAction),
+ trailing: QRButton(action:
checkCameraAvailable))
+ .overlay {
+ if balances.isEmpty {
+ WalletEmptyView()
+ .refreshable { // already async
+ symLog.log("refreshing")
+ let count = await reloadAction()
+ if count > 0 {
+// postNotificationM(.BalanceReloaded)
+ NotificationCenter.default.post(name:
.BalanceReloaded, object: nil)
+ }
+ }
+
+ }
+ }
+ .alert("Scanning QR-codes requires access to the camera",
+ isPresented: $showCameraAlert,
+ actions: { openSettingsButton
+ dismissAlertButton },
+ message: { Text("Please allow camera access in
settings.") })
+ .onAppear() {
+ DebugViewC.shared.setViewID(VIEW_BALANCES)
+ }
+ .sheet(isPresented: $showQRScanner) {
+ let sheet = AnyView(QRSheet())
+ Sheet(sheetView: sheet)
+ } // sheet
+ .task {
+ symLog.log(".task getBalances")
+ _ = await reloadAction()
+ } // task
+ }
+}
+// MARK: -
+extension BalancesListView {
+ struct Content: View {
+ let symLog: SymLogV?
+ @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic
+ @Binding var balances: [Balance]
+ @Binding var centsToTransfer: UInt64
+ @Binding var summary: String
+ var reloadAction: () async -> Int
+
+ @State private var shouldLoad = false
+ var body: some View {
+#if DEBUG
+ let _ = Self._printChanges()
+ let _ = symLog?.vlog() // just to get the # to compare it
with .onAppear & onDisappear
+#endif
+ Group { // necessary for .backslide transition (bug in SwiftUI)
+ List(balances, id: \.self) { balance in
+ BalancesSectionView(balance: balance,
+ centsToTransfer: $centsToTransfer,
+ summary: $summary)
+ }
+ .refreshable { // already async
+ symLog?.log("refreshing")
+ let count = await reloadAction()
+ if count > 0 {
+// postNotificationM(.BalanceReloaded)
+ NotificationCenter.default.post(name:
.BalanceReloaded, object: nil)
+ }
+ }
+ .listStyle(myListStyle.style).anyView
+ }
+ .onAppear {
+ if shouldLoad {
+ shouldLoad = false
+ symLog?.log(".onAppear: shouldLoad ==> reloading balances")
+ Task { await reloadAction() }
+ }
+ }
+ // automatically reload balances after receiving BalanceChange
notification ...
+ .onNotification(.BalanceChange) { notification in
+ // doesn't need to be received on main thread because we just
reload in the background anyway
+ symLog?.log(".onNotification(.BalanceChange) ==> shouldLoad =
true")
+ shouldLoad = true
+ }
+ } // body
+ } // Content
+}
diff --git a/TalerWallet1/Views/Balances/BalancesModel.swift
b/TalerWallet1/Views/Balances/BalancesModel.swift
deleted file mode 100644
index 80197d9..0000000
--- a/TalerWallet1/Views/Balances/BalancesModel.swift
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-import Foundation
-import taler_swift
-import SymLog
-fileprivate let ASYNCDELAY: UInt = 0 //set e.g to 6 or 9 seconds for
debugging
-
-class BalancesModel: WalletModel {
- @Published var balances: [Balance]? // update view
-}
-// 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
-
- 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 fetchBalances() async throws {
- do {
- let request = GetBalances()
- let response = try await sendRequest(request, ASYNCDELAY)
- balances = response.balances // trigger view update
in CurrenciesListView
- } catch {
- throw error
- }
- }
-}
diff --git a/TalerWallet1/Views/Balances/BalancesSectionView.swift
b/TalerWallet1/Views/Balances/BalancesSectionView.swift
new file mode 100644
index 0000000..e16015b
--- /dev/null
+++ b/TalerWallet1/Views/Balances/BalancesSectionView.swift
@@ -0,0 +1,206 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import SwiftUI
+import taler_swift
+import SymLog
+
+/// This view shows a currency section
+/// Currency Name
+/// [Send Coins] [Receive Coins] Balance
+/// tapping on Balance leads to completed Transactions (.done)
+/// optional: Pending Incoming
+/// optional: Pending Outgoing
+/// optional: Suspended / Aborting / Aborted / Expired
+
+struct BalancesSectionView: View {
+ private let symLog = SymLogV()
+ var balance:Balance
+ @Binding var centsToTransfer: UInt64
+ @Binding var summary: String
+
+ @EnvironmentObject private var model: WalletModel
+
+ @State private var isShowingDetailView = false
+ @State private var buttonSelected: Int? = nil
+
+ @State private var transactions: [Transaction] = []
+ @State private var completedTransactions: [Transaction] = []
+ @State private var pendingTransactions: [Transaction] = []
+ @State private var uncompletedTransactions: [Transaction] = []
+
+ func reloadOneAction(_ transactionId: String) async throws -> Transaction {
+ 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)
+ }
+
+ @State private var sectionID = UUID()
+ @State private var shownSectionID = UUID() // guaranteed to be different
the first time
+
+ var body: some View {
+#if DEBUG
+ let _ = Self._printChanges()
+ let _ = symLog.vlog() // just to get the # to compare it with
.onAppear & onDisappear
+#endif
+ let currency = balance.available.currencyStr
+ let reloadCompleted = {
+ transactions = await model.transactionsT(currency: currency)
+ completedTransactions =
WalletModel.completedTransactions(transactions)
+// sectionID = UUID()
+ }
+ let reloadPending = {
+ transactions = await model.transactionsT(currency: currency)
+ pendingTransactions = WalletModel.pendingTransactions(transactions)
+// sectionID = UUID()
+ }
+ let reloadUncompleted = {
+ transactions = await model.transactionsT(currency: currency)
+ uncompletedTransactions =
WalletModel.uncompletedTransactions(transactions)
+// sectionID = UUID()
+ }
+
+ Section {
+ if "KUDOS" == currency && !balance.available.isZero {
+ Text("You can spend these KUDOS in the [Demo
Shop](https://shop.demo.taler.net), or send coins to another wallet.")
+ .multilineTextAlignment(.leading)
+ }
+ HStack(spacing: 0) {
+ NavigationLink(destination: LazyView {
+ SendAmount(amountAvailable: balance.available,
+ centsToTransfer: $centsToTransfer,
+ summary: $summary)
+ }, tag: 1, selection: $buttonSelected
+ ) { EmptyView() }.frame(width: 0).opacity(0).hidden()
// SendAmount
+
+ NavigationLink(destination: LazyView {
+ RequestPayment(scopeInfo: balance.scopeInfo,
+ centsToTransfer: $centsToTransfer,
+ summary: $summary)
+ }, tag: 2, selection: $buttonSelected
+ ) { EmptyView() }.frame(width: 0).opacity(0).hidden()
// RequestPayment
+
+ NavigationLink(destination: LazyView {
+ TransactionsListView(navTitle: String(localized:
"Transactions"), currency: currency,
+ transactions: completedTransactions,
+ reloadAllAction: reloadCompleted,
+ reloadOneAction: reloadOneAction)
+ }, tag: 3, selection: $buttonSelected
+ ) { EmptyView() }.frame(width: 0).opacity(0).hidden()
// TransactionsListView
+
+ BalanceRowView(amount: balance.available, sendAction: {
+ buttonSelected = 1 // will trigger SendAmount
NavigationLink
+ }, recvAction: {
+ buttonSelected = 2 // will trigger RequestPayment
NavigationLink
+ }, rowAction: {
+ buttonSelected = 3 // will trigger
TransactionList NavigationLink
+ })
+ }
+ let hasPending = pendingTransactions.count > 0
+ if hasPending {
+ let (pendingIncoming, pendingOutgoing) =
computePending(currency: currency)
+
+ NavigationLink {
+//let _ = print("button: Pending Transactions: \(currency)")
+ LazyView {
+ TransactionsListView(navTitle: String(localized:
"Pending"), currency: currency,
+ transactions: pendingTransactions,
+ reloadAllAction: reloadPending,
+ reloadOneAction: reloadOneAction)
+ }
+ } label: {
+ VStack(spacing: 6) {
+ 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 rows == 0 {
+ Text("Some pending transactions")
+ }
+ }
+ }
+ }
+ let hasUncompleted = uncompletedTransactions.count > 0
+ if hasUncompleted {
+ NavigationLink {
+//let _ = print("button: Uncompleted Transactions: \(currency)")
+ LazyView {
+ TransactionsListView(navTitle: String(localized:
"Uncompleted"), currency: currency,
+ transactions:
uncompletedTransactions,
+ reloadAllAction: reloadUncompleted,
+ reloadOneAction: reloadOneAction)
+ }
+ } label: {
+ UncompletedRowView(uncompletedTransactions:
$uncompletedTransactions)
+ }
+
+ }
+ } header: {
+ Text(currency)
+ .font(.title)
+ }.id(sectionID)
+ .task {
+// if shownSectionID != sectionID {
+ symLog.log("task for BalancesSectionView \(sectionID) - reload
Transactions")
+ let response = await model.transactionsT(currency: currency)
+ transactions = response
+ pendingTransactions = WalletModel.pendingTransactions(response)
+ uncompletedTransactions =
WalletModel.uncompletedTransactions(response)
+ shownSectionID = sectionID
+// } else {
+// symLog.log("task for BalancesSectionView \(sectionID) ❗️
skip reloading Transactions")
+// }
+ }
+ } // body
+}
+// MARK: -
+#if DEBUG
+fileprivate struct BindingViewContainer : View {
+ @State var centsToTransfer: UInt64 = 333
+ @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"),
+ scopeInfo: scopeInfo,
+ requiresUserInput: false,
+ hasPendingTransactions: true)
+ List {
+ BalancesSectionView(balance: balance,
+ centsToTransfer: $centsToTransfer,
+ summary: $summary)
+ }
+ }
+}
+
+struct BalancesSectionView_Previews: PreviewProvider {
+ static var previews: some View {
+ BindingViewContainer()
+ }
+}
+#endif
diff --git a/TalerWallet1/Views/Balances/CurrenciesListView.swift
b/TalerWallet1/Views/Balances/CurrenciesListView.swift
deleted file mode 100644
index 192d531..0000000
--- a/TalerWallet1/Views/Balances/CurrenciesListView.swift
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-import SwiftUI
-import SymLog
-
-struct CurrenciesListView: View {
- private let symLog = SymLogV()
- let navTitle = "GNU Taler Wallet"
-
- @ObservedObject var viewModel: BalancesModel
- var hamburgerAction: () -> Void
-
- var body: some View {
- let reloadAction = viewModel.fetchBalances
- VStack {
- if viewModel.balances == nil {
- symLog { LoadingView(backButtonHidden: true) }
- } else {
- symLog { NavigationView {
- Content(symLog: symLog, viewModel: viewModel,
reloadAction: reloadAction)
- .navigationBarItems(leading: HamburgerButton(action:
hamburgerAction))
- .navigationTitle(navTitle)
- } }
- }
- }.task {
- symLog.log(".task")
- do {
- try await reloadAction()
- } catch {
- // TODO: show error
- symLog.log(error.localizedDescription)
- }
- }
- }
-}
-// MARK: -
-extension CurrenciesListView {
- struct Content: View {
- let symLog: SymLogV?
- @ObservedObject var viewModel: BalancesModel
- @EnvironmentObject var controller : Controller
- var reloadAction: () async throws -> ()
-
- var body: some View {
- if viewModel.balances!.isEmpty { // TODO: all spent?
- WalletEmptyView()
- .navigationBarTitleDisplayMode(.large)
- } else {
- List (viewModel.balances!, id: \.self) { balance in
- NavigationLink {
- TransactionsListView(viewModel:
controller.transactionsModel)
- } label: {
- // TODO: sendAction, recvAction
- CurrencyView(balance: balance, sendAction: {},
recvAction: {})
- }
- }
- .navigationBarTitleDisplayMode(.large) // .inline
- .refreshable {
- do {
- symLog?.log("refreshing")
- try await reloadAction()
- } catch {
- // TODO: error
- symLog?.log(error.localizedDescription)
- }
- }
- }
- }
- }
-}
diff --git a/TalerWallet1/Views/Balances/CurrencyView.swift
b/TalerWallet1/Views/Balances/CurrencyView.swift
deleted file mode 100644
index 0978de0..0000000
--- a/TalerWallet1/Views/Balances/CurrencyView.swift
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-import SwiftUI
-import taler_swift
-
-/// This view shows a currency
-/// Header: Currency Name (e.g. Kudos)
-/// [Send Funds] [Receive Funds] Balance
-/// Pending Incoming
-/// Pending Outgoing
-
-struct CurrencyView: View {
- var balance:Balance
- let sendAction: () -> Void
- let recvAction: () -> Void
- var body: some View {
- VStack {
- Text(balance.available.currencyStr)
- .font(.title)
- BalanceRow(amount: balance.available, sendAction: sendAction,
recvAction: recvAction)
-
- let inAmount = balance.pendingIncoming
- if !inAmount.isZero {
- PendingRow(amount: inAmount, incoming: true, counterparty:
"exchange.demo.taler.net")
- }
- let outAmount = balance.pendingOutgoing
- if !outAmount.isZero {
- PendingRow(amount: outAmount, incoming: false, counterparty:
"merchant")
- }
- }
-// .padding()
- }
-}
-
-struct CurrencyView_Previews: PreviewProvider {
- static var balance = Balance(available: try! Amount(fromString:
"Taler:0.1"),
- pendingIncoming: try! Amount(fromString:
"Taler:4.8"),
- pendingOutgoing: try! Amount(fromString:
"Taler:3.25"),
- hasPendingTransactions: true,
- requiresUserInput: false)
-
- static var previews: some View {
- CurrencyView(balance: balance, sendAction: {}, recvAction: {})
- }
-}
diff --git a/TalerWallet1/Views/Balances/PendingRow.swift
b/TalerWallet1/Views/Balances/PendingRow.swift
deleted file mode 100644
index d6321d2..0000000
--- a/TalerWallet1/Views/Balances/PendingRow.swift
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-import SwiftUI
-import taler_swift
-
-struct PendingRow: View {
- let amount: Amount
- let incoming: Bool
- let counterparty: String
- var body: some View {
- HStack {
- Image(systemName: incoming ? "text.badge.plus" :
"text.badge.minus")
- .padding(.trailing)
- .font(.largeTitle)
- .foregroundColor(incoming ? Color("PendingIncoming") :
Color("PendingOutgoing"))
-
- VStack(alignment: .leading) {
- Text("\(counterparty)")
- .font(.headline)
- .fontWeight(.medium)
- Text("Waiting for confirmation")
- .font(.callout)
- .padding(.vertical, -2.0)
- Text("some time ago") // TODO: show time-interval
- .font(.callout)
- }
- Spacer()
- VStack(alignment: .trailing) {
- let sign = incoming ? "+" : "-"
- Text(sign + "\(amount.valueStr)")
- .font(.title)
- .foregroundColor(Color.gray)
- Text("PENDING")
- .font(.callout)
- }
- }
- .padding(.top)
- }
-}
-
-struct PendingRow_Previews: PreviewProvider {
- static var previews: some View {
- Group {
- PendingRow(amount: try! Amount(fromString: "Taler:4.8"), incoming:
true, counterparty: "exchange.demo.taler.net")
- PendingRow(amount: try! Amount(fromString: "Taler:3.25"),
incoming: false, counterparty: "merchant")
- }
- }
-}
diff --git a/TalerWallet1/Views/Balances/PendingRowView.swift
b/TalerWallet1/Views/Balances/PendingRowView.swift
new file mode 100644
index 0000000..95345f0
--- /dev/null
+++ b/TalerWallet1/Views/Balances/PendingRowView.swift
@@ -0,0 +1,48 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import SwiftUI
+import taler_swift
+
+/// This view shows a pending transaction row in a currency section
+struct PendingRowView: View {
+ let amount: Amount
+ let incoming: Bool
+
+ var body: some View {
+ HStack {
+ Image(systemName: incoming ? "text.badge.plus"
+ : "text.badge.minus")
+ .font(.largeTitle)
+// .foregroundColor(WalletColors().pendingColor) // pending
is always gray
+ .foregroundColor(WalletColors().pendingColor(incoming))
+ .accessibility(hidden: true)
+
+ Spacer()
+ Text("pending\n" + (incoming ? "incoming" : "outgoing"))
+ Spacer()
+ VStack(alignment: .trailing) {
+ let sign = incoming ? "+" : "-"
+ Text(sign + "\(amount.valueStr)")
+ .font(.title)
+ .foregroundColor(WalletColors().pendingColor(incoming))
+// Text("PENDING")
+// .font(.callout)
+// .foregroundColor(WalletColors().pendingColor(incoming))
+ }
+ }
+ .accessibilityElement(children: .combine)
+ }
+}
+// MARK: -
+#if DEBUG
+struct PendingRowView_Previews: PreviewProvider {
+ static var previews: some View {
+ List {
+ PendingRowView(amount: try! Amount(fromString: LONGCURRENCY +
":4.8"), incoming: true)
+ PendingRowView(amount: try! Amount(fromString: LONGCURRENCY +
":3.25"), incoming: false)
+ }
+ }
+}
+#endif
diff --git a/TalerWallet1/Views/Balances/UncompletedRowView.swift
b/TalerWallet1/Views/Balances/UncompletedRowView.swift
new file mode 100644
index 0000000..b4637a2
--- /dev/null
+++ b/TalerWallet1/Views/Balances/UncompletedRowView.swift
@@ -0,0 +1,34 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import SwiftUI
+import taler_swift
+
+/// This view shows an uncompleted transaction row in a currency section
+struct UncompletedRowView: View {
+ @Binding var uncompletedTransactions: [Transaction]
+
+ var body: some View {
+ let count = uncompletedTransactions.count
+ HStack {
+ Spacer()
+ Text("\(count) uncompleted transactions")
+ .font(.title2)
+ .foregroundColor(WalletColors().uncompletedColor)
+ Spacer()
+ }
+ .accessibilityElement(children: .combine)
+ }
+}
+// MARK: -
+#if DEBUG
+//struct UncompletedRowView_Previews: PreviewProvider {
+// static var previews: some View {
+// let uncompletedTransactions: [Transaction] = []
+// List {
+// UncompletedRowView(uncompletedTransactions:
uncompletedTransactions)
+// }
+// }
+//}
+#endif
diff --git a/TalerWallet1/Views/Balances/WalletEmptyView.swift
b/TalerWallet1/Views/Balances/WalletEmptyView.swift
deleted file mode 100644
index 72d5192..0000000
--- a/TalerWallet1/Views/Balances/WalletEmptyView.swift
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-import SwiftUI
-
-struct WalletEmptyView: View {
-
- var body: some View {
- Form {
- Section {
- Text("There is no digital cash in your wallet.")
- .padding()
- }
- Section {
- Text("You can get test money from the demo bank:")
- .padding()
- }
- Section {
- Text("https://bank.demo.taler.net")
- .padding()
- }
- }
-// .multilineTextAlignment(.center)
- .font(.title2)
- }
-}
-
-struct EmptyView_Previews: PreviewProvider {
- static var previews: some View {
- WalletEmptyView()
- }
-}
diff --git a/TalerWallet1/Views/Exchange/ExchangeListView.swift
b/TalerWallet1/Views/Exchange/ExchangeListView.swift
index fbd52e4..83ba3b3 100644
--- a/TalerWallet1/Views/Exchange/ExchangeListView.swift
+++ b/TalerWallet1/Views/Exchange/ExchangeListView.swift
@@ -1,103 +1,130 @@
/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
*/
import SwiftUI
+import taler_swift
import SymLog
+/// This view shows the list of exchanges
struct ExchangeListView: View {
- private let symLog = SymLogV()
- let navTitle = "Exchanges"
+ private let symLog = SymLogV(0)
+ let navTitle: String
+ var hamburgerAction: () -> Void
- @ObservedObject var viewModel: ExchangeModel
+ @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 {
+ exchanges = await model.listExchangesM()
+ }
+
+ func addExchange(_ exUrl: String) -> Void {
+ Task {
+ 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)")
+ }
+ }
+ }
+
+ @State var showAlert: Bool = false
+ @State var newExchange: String = "https://exchange-age.taler.ar/"
var body: some View {
- let reloadAction = viewModel.updateList
- VStack {
- if viewModel.exchanges == nil {
- symLog { LoadingView(backButtonHidden: false) }
- } else {
- Content(symLog: symLog, viewModel: viewModel, reloadAction:
reloadAction)
- .navigationTitle(navTitle)
+#if DEBUG
+ let _ = Self._printChanges()
+ let _ = symLog.vlog() // just to get the # to compare it with
.onAppear & onDisappear
+#endif
+ let plusAction: () -> Void = {
+// withAnimation { showAlert = true }
+ showAlert = true
+ }
+
+ //Text("Exchanges...")
+ Content(symLog: symLog,
+ exchanges: $exchanges,
+ centsToTransfer: $centsToTransfer,
+ reloadAction: reloadAction)
+ .navigationTitle(navTitle)
+ .navigationBarTitleDisplayMode(.automatic)
+ .navigationBarItems(leading: HamburgerButton(action: hamburgerAction),
+ trailing: PlusButton(action: plusAction))
+ .overlay {
+ if exchanges.isEmpty {
+ Text("No Exchanges yet...")
}
- }.task {
+ }
+ .task {
symLog.log(".task")
- do {
- try await reloadAction()
- } catch {
- // TODO: show error
- symLog.log(error.localizedDescription)
- }
+ await reloadAction()
}
+ .textFieldAlert(isPresented: $showAlert, title: "Add Exchange",
+ doneText: "Add", text: $newExchange, action:
addExchange)
}
}
// MARK: -
+//struct ExchangeAmount: Identifiable {
+// let exchange: Exchange
+// let amountAvailable: Amount
+//
+// var id: String { // needed for Identifiable
+// exchange.exchangeBaseUrl
+// }
+//}
+// MARK: -
extension ExchangeListView {
struct Content: View {
- let symLog: SymLogV
- @ObservedObject var viewModel: ExchangeModel
- var reloadAction: () async throws -> ()
- @State var showAlert: Bool = false
- @State var newExchange: String = "https://exchange-age.taler.ar/"
+ let symLog: SymLogV?
+ @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic
+ @Binding var exchanges: [Exchange]
+ @Binding var centsToTransfer: UInt64
+ var reloadAction: () async -> Void
+
+ func currenciesDict(_ exchanges: [Exchange]) -> [String : [Exchange]] {
+ var currencies: [String : [Exchange]] = [:]
- func addExchange(_ exUrl: String) -> Void {
- Task {
- do {
- symLog.log("adding: \(exUrl)")
- try await viewModel.add(url: exUrl)
- symLog.log("added: \(exUrl)")
- } catch {
- symLog.log("error: \(error)")
- // TODO: error handling - couldn't add exchangeURL
+ for exchange in exchanges {
+ let currency = exchange.currency ?? "Unknown"
+ if currencies[currency] != nil {
+ currencies[currency]!.append(exchange)
+ } else {
+ currencies[currency] = [exchange]
}
}
+ return currencies
}
+// @State private var exchangeAmount: ExchangeAmount? = nil
+
var body: some View {
- let plusAction: () -> Void = {
-// withAnimation { showPopup = true }
- showAlert = true
- }
- VStack {
- if viewModel.exchanges!.isEmpty {
- Text("No Exchanges yet...")
- } else {
- List(viewModel.exchanges!, id: \.self) { exchange in
- VStack {
- Text(exchange.exchangeBaseUrl)
- .frame(maxWidth: .infinity)
- .padding()
- Text("Currency: " + (exchange.currency ?? "?"))
- .frame(maxWidth: .infinity)
- .padding()
- }
- }
- .navigationBarTitleDisplayMode(.large) // .inline
- .refreshable {
- do {
- symLog.log("refreshing")
- try await reloadAction()
- } catch {
- // TODO: error
- symLog.log(error.localizedDescription)
- }
- }
+ let dict = currenciesDict(exchanges)
+ let sortedDict = dict.sorted{ $0.key < $1.key}
+ Group { // necessary for .backslide transition (bug in SwiftUI)
+ List(sortedDict, id: \.key) { key, value in
+ ExchangeSectionView(currency: key, exchanges: value,
centsToTransfer: $centsToTransfer)
}
+ .refreshable {
+ symLog?.log("refreshing")
+ await reloadAction()
+ }
+ .listStyle(myListStyle.style).anyView
+ }
+ .onAppear() {
+ DebugViewC.shared.setViewID(VIEW_EXCHANGES)
+ }
+ .onNotification(.ExchangeAdded) { notification in
+ // doesn't need to be received on main thread because we just
reload in the background anyway
+ symLog?.log(".onNotification(.ExchangeAdded) ==> reloading
exchanges")
+ Task { await reloadAction() }
}
- .navigationBarItems(trailing: PlusButton(action: plusAction))
- .textFieldAlert(isPresented: $showAlert, title: "Add Exchange",
- doneText: "Add", text: $newExchange, action:
addExchange)
} // body
}
}
diff --git a/TalerWallet1/Views/Exchange/ExchangeSectionView.swift
b/TalerWallet1/Views/Exchange/ExchangeSectionView.swift
new file mode 100644
index 0000000..4b6b635
--- /dev/null
+++ b/TalerWallet1/Views/Exchange/ExchangeSectionView.swift
@@ -0,0 +1,102 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import SwiftUI
+import taler_swift
+
+struct ExchangeRowView: View {
+ let exchange: Exchange
+ let currency: String
+ @Binding var centsToTransfer: UInt64
+
+ @State private var buttonSelected: Int? = nil
+ var body: some View {
+ let baseURL = exchange.exchangeBaseUrl
+
+ HStack(spacing: 0) { // can't use the built in Label because it
adds the accessory arrow
+ Text(baseURL.trimURL())
+
+ NavigationLink(destination: LazyView {
+ EmptyView() // TODO: Deposit
+ }, tag: 1, selection: $buttonSelected
+ ) { EmptyView() }.frame(width: 0).opacity(0)
+ NavigationLink(destination: LazyView {
+ ManualWithdraw(exchange: exchange,
+ centsToTransfer: $centsToTransfer)
+ }, tag: 2, selection: $buttonSelected
+ ) { EmptyView() }.frame(width: 0).opacity(0)
+ }.listRowSeparator(.hidden)
+
+ HStack { // buttons just set "buttonSelected" so the
NavigationLink will trigger
+ Button("Deposit\n\(currency)") { buttonSelected = 1 }
+ .multilineTextAlignment(.center)
+ .buttonStyle(TalerButtonStyle(type: .bordered))
+ .disabled(true) // TODO: after implementing Deposit check
available
+
+ Button("Withdraw\n\(currency)") { buttonSelected = 2 }
+ .multilineTextAlignment(.center)
+ .buttonStyle(TalerButtonStyle(type: .bordered))
+ }.listRowSeparator(.visible)
+// .listRowSeparatorTint(.red)
+ .fixedSize(horizontal: false, vertical: true)
+ }
+}
+/// This view shows the currency name in an exchange section
+/// currency
+/// [Deposit Coins] [Withdraw Coins]
+struct ExchangeSectionView: View {
+ let currency: String
+ let exchanges: [Exchange]
+
+ @Binding var centsToTransfer: UInt64
+
+ var body: some View {
+#if DEBUG
+ let _ = Self._printChanges()
+// let _ = symLog.vlog() // just to get the # to compare it with
.onAppear & onDisappear
+#endif
+ Section {
+ ForEach(exchanges) { exchange in
+ ExchangeRowView(exchange: exchange, currency: currency,
centsToTransfer: $centsToTransfer)
+ }
+ .accessibilityElement(children: .combine)
+ } header: {
+ Text(currency)
+ .font(.title)
+ }
+ }
+}
+// MARK: -
+#if DEBUG
+struct ExchangeRow_Container : View {
+ @State private var centsToTransfer: UInt64 = 100
+
+ var body: some View {
+ let exchange1 = Exchange(exchangeBaseUrl: DEMO_AGE_EXCHANGE,
+ currency: LONGCURRENCY,
+ paytoUris: [],
+ tosStatus: "tosStatus",
+ exchangeStatus: "exchangeStatus",
+ ageRestrictionOptions: [12,16],
+ permanent: true)
+ let exchange2 = Exchange(exchangeBaseUrl: DEMO_EXP_EXCHANGE,
+ currency: LONGCURRENCY,
+ paytoUris: [],
+ tosStatus: "tosStatus",
+ exchangeStatus: "exchangeStatus",
+ ageRestrictionOptions: [],
+ permanent: false)
+ List {
+ ExchangeSectionView(currency: LONGCURRENCY, exchanges: [exchange1,
exchange2],
+ centsToTransfer: $centsToTransfer)
+ }
+ }
+}
+
+struct ExchangeRow_Previews: PreviewProvider {
+ static var previews: some View {
+ ExchangeRow_Container()
+ }
+}
+#endif
diff --git a/TalerWallet1/Views/Exchange/ManualWithdraw.swift
b/TalerWallet1/Views/Exchange/ManualWithdraw.swift
new file mode 100644
index 0000000..50b7c38
--- /dev/null
+++ b/TalerWallet1/Views/Exchange/ManualWithdraw.swift
@@ -0,0 +1,124 @@
+/*
+ * 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 by the user tapping "Withdraw Coins" in the exchange list
+struct ManualWithdraw: View {
+ private let symLog = SymLogV()
+
+ let exchange: Exchange
+ @Binding var centsToTransfer: UInt64
+
+ @EnvironmentObject private var model: WalletModel
+
+ @State var withdrawalAmountDetails: WithdrawalAmountDetails? = nil
+
+// @State var ageMenuList: [Int] = []
+// @State var selectedAge = 0
+
+ var body: some View {
+#if DEBUG
+ let _ = Self._printChanges()
+ let _ = symLog.vlog() // just to get the # to compare it with
.onAppear & onDisappear
+#endif
+ let currency = exchange.currency!
+ let navTitle = String(localized: "Withdraw \(currency)")
+ let currencyField = CurrencyField(value: $centsToTransfer, currency:
currency) // becomeFirstResponder
+// let agePicker = AgePicker(ageMenuList: $ageMenuList, selectedAge:
$selectedAge)
+
+ ScrollViewReader { scrollView in
+ VStack {
+ Text("from \(exchange.exchangeBaseUrl.trimURL())")
+ .font(.title3)
+ CurrencyInputView(currencyField: currencyField,
+ title: String(localized: "Amount to withdraw:"))
+
+ let someCoins = SomeCoins(details: withdrawalAmountDetails)
+ QuiteSomeCoins(someCoins: someCoins, shouldShowFee: true,
+ currency: currency, amountEffective:
withdrawalAmountDetails?.amountEffective)
+
+ if !someCoins.invalid {
+ if !someCoins.tooMany {
+// agePicker
+
+ if let tosAccepted = withdrawalAmountDetails?.tosAccepted {
+ if tosAccepted {
+// let restrictAge: Int? = (selectedAge == 0) ? nil
+// :
selectedAge
+//let _ = print(selectedAge, restrictAge)
+ NavigationLink(destination: LazyView {
+ ManualWithdrawDone(exchange: exchange,
+ centsToTransfer: centsToTransfer)
+// restrictAge: restrictAge)
+ }) {
+ Text("Confirm Withdrawal") //
VIEW_WITHDRAW_ACCEPT
+ }.buttonStyle(TalerButtonStyle(type: .prominent))
+ } else {
+ NavigationLink(destination: LazyView {
+ WithdrawTOSView(exchangeBaseUrl:
exchange.exchangeBaseUrl,
+ viewID:
VIEW_WITHDRAW_TOS,
+ acceptAction: nil)
// pop back to here
+ }) {
+ Text("Check Terms of Service") //
VIEW_WITHDRAW_TOS
+ }.buttonStyle(TalerButtonStyle(type: .prominent))
+ }
+ }
+ } // tooMany
+ } // invalid
+ Spacer()
+ }
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(.horizontal)
+ .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all))
+ .navigationTitle(navTitle)
+ .onAppear {
+ symLog.log("onAppear")
+ DebugViewC.shared.setViewID(VIEW_WITHDRAWAL)
+ }
+ .task(id: centsToTransfer) {
+ let amount = Amount.amountFromCents(currency, centsToTransfer)
+ do {
+ withdrawalAmountDetails = try await
model.loadWithdrawalDetailsForAmountM(exchange.exchangeBaseUrl, amount: amount)
+// agePicker.setAges(ages:
withdrawalAmountDetails?.ageRestrictionOptions)
+ } catch { // TODO: error
+ symLog.log(error.localizedDescription)
+ withdrawalAmountDetails = nil
+ }
+ }
+ }
+}
+// MARK: -
+#if DEBUG
+struct ManualWithdraw_Container : View {
+ @State private var centsToTransfer: UInt64 = 510
+ @State private var details = WithdrawalAmountDetails(tosAccepted: false,
+ amountRaw: try!
Amount(fromString: LONGCURRENCY + ":5.1"),
+ amountEffective: try!
Amount(fromString: LONGCURRENCY + ":5.0"),
+ paytoUris: [],
+ ageRestrictionOptions: [],
+ numCoins: 6)
+ var body: some View {
+ let exchange = Exchange(exchangeBaseUrl: DEMOEXCHANGE,
+ currency: LONGCURRENCY,
+ paytoUris: [],
+ tosStatus: "tosStatus",
+ exchangeStatus: "exchangeStatus",
+ ageRestrictionOptions: [],
+ permanent: false)
+ ManualWithdraw(exchange: exchange,
+ centsToTransfer: $centsToTransfer,
+ withdrawalAmountDetails: details)
+ }
+}
+
+struct ManualWithdraw_Previews: PreviewProvider {
+ static var previews: some View {
+ ManualWithdraw_Container()
+ }
+}
+#endif
diff --git a/TalerWallet1/Views/Exchange/ManualWithdrawDone.swift
b/TalerWallet1/Views/Exchange/ManualWithdrawDone.swift
new file mode 100644
index 0000000..0048820
--- /dev/null
+++ b/TalerWallet1/Views/Exchange/ManualWithdrawDone.swift
@@ -0,0 +1,81 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import SwiftUI
+import taler_swift
+import SymLog
+
+struct ManualWithdrawDone: View {
+ private let symLog = SymLogV()
+ let navTitle = String(localized: "Wire Transfer")
+
+ let exchange: Exchange
+ let centsToTransfer: UInt64
+// let restrictAge: Int?
+
+ @EnvironmentObject private var model: WalletModel
+
+ @State var acceptManualWithdrawalResult: AcceptManualWithdrawalResult?
+ @State var transactionId: String?
+
+ 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 transactionId {
+ TransactionDetailView(transactionId: transactionId,
+ reloadAction: reloadOneAction,
+ doneAction:
ViewState.shared.popToRootView)
+ .navigationBarBackButtonHidden(true) // exit only
by Done-Button
+ .navigationTitle(navTitle)
+ } else {
+ WithdrawProgressView(message:
exchange.exchangeBaseUrl.trimURL())
+ .navigationTitle("Loading " + navTitle)
+ }
+ }.onAppear() {
+ symLog.log("onAppear")
+ DebugViewC.shared.setViewID(VIEW_WITHDRAW_ACCEPT)
+ }.task {
+ do {
+ let amount = Amount.amountFromCents(exchange.currency!,
centsToTransfer)
+ let result = try await
model.sendAcceptManualWithdrawalM(exchange.exchangeBaseUrl,
+
amount: amount, restrictAge: 0)
+ transactionId = result!.transactionId
+ } catch { // TODO: error
+ symLog.log(error.localizedDescription)
+ }
+ }
+ }
+}
+
+// MARK: -
+#if DEBUG
+struct ManualWithdrawDone_Container : View {
+ @State private var centsToTransfer: UInt64 = 510
+
+ var body: some View {
+ let exchange = Exchange(exchangeBaseUrl: DEMOEXCHANGE,
+ currency: LONGCURRENCY,
+ paytoUris: [],
+ tosStatus: "tosStatus",
+ exchangeStatus: "exchangeStatus",
+ ageRestrictionOptions: [],
+ permanent: false)
+ ManualWithdrawDone(exchange: exchange,
+ centsToTransfer: centsToTransfer)
+ }
+}
+
+struct ManualWithdrawDone_Previews: PreviewProvider {
+ static var previews: some View {
+ ManualWithdrawDone_Container()
+ }
+}
+#endif
diff --git a/TalerWallet1/Views/Exchange/QuiteSomeCoins.swift
b/TalerWallet1/Views/Exchange/QuiteSomeCoins.swift
new file mode 100644
index 0000000..7efe434
--- /dev/null
+++ b/TalerWallet1/Views/Exchange/QuiteSomeCoins.swift
@@ -0,0 +1,102 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import SwiftUI
+import taler_swift
+import SymLog
+
+struct SomeCoins {
+ let numCoins: Int // 0 == invalid, -1 == unknown
+ var unknown: Bool { numCoins < 0 }
+ var invalid: Bool { numCoins == 0 }
+ var manyCoins: Bool { numCoins > 99 }
+ var quiteSome: Bool { numCoins > 199 }
+ var tooMany: Bool { numCoins > 999 }
+
+ let fee: String
+ var hasFee: Bool { fee.count > 0 }
+}
+
+extension SomeCoins {
+ init(details: WithdrawalAmountDetails?) {
+ do {
+ if let details {
+ // Incoming: fee = raw - effective
+ let fee = try details.amountRaw - details.amountEffective
+ self.init(numCoins: details.numCoins ?? -1, // either the
number of coins, or unknown
+ fee: fee.isZero ? "" : fee.readableDescription)
+ return
+ }
+ } catch {}
+ self.init(numCoins: 0, fee:"") // invalid
+ }
+
+ init(details: CheckPeerPullCreditResponse?) {
+ do {
+ if let details {
+ // Incoming: fee = raw - effective
+ let fee = try details.amountRaw - details.amountEffective
+ self.init(numCoins: details.numCoins ?? -1, // either the
number of coins, or unknown
+ fee: fee.isZero ? "" : fee.readableDescription)
+ return
+ }
+ } catch {}
+ self.init(numCoins: 0, fee:"") // invalid
+ }
+}
+// MARK: -
+struct QuiteSomeCoins: View {
+ private let symLog = SymLogV()
+ let someCoins: SomeCoins
+ let shouldShowFee: Bool
+ let currency: String
+ let amountEffective: Amount?
+
+ var body: some View {
+ if shouldShowFee {
+ let shownFee = someCoins.hasFee ? String(localized: "-
\(someCoins.fee)")
+ : String(localized: "No")
+ Text(someCoins.invalid ? "invalid amount"
+ : someCoins.tooMany ? "too many coins for a single withdrawal"
+ : "\(shownFee) withdrawal fee")
+ .foregroundColor((someCoins.invalid || someCoins.tooMany ||
someCoins.hasFee) ? .red : .primary)
+ .padding(4)
+ }
+ if !someCoins.invalid {
+ HStack {
+ Text(someCoins.unknown ? "Some" : "\(someCoins.numCoins)")
+ .foregroundColor(someCoins.quiteSome ? .red : .primary)
+ Text(someCoins.tooMany ? "coins" : "coins to obtain:")
+ .foregroundColor(someCoins.tooMany ? .red : .primary)
+
+ Spacer()
+ if !someCoins.tooMany {
+ let effective = amountEffective ?? Amount(currency:
currency, value: 0)
+ Text(effective.readableDescription)
+ }
+ } // xx coins to obtain: YYY currency
+// .font(.title3)
+ .padding(.top)
+
+ if !someCoins.tooMany {
+ if someCoins.manyCoins {
+ Text(someCoins.quiteSome ? "Warning: It will take quite
some time\nto generate this many coins!"
+ : "Warning: It will take some
time\nto generate this many coins.")
+ .multilineTextAlignment(.leading)
+ .padding(.top, 6)
+ .foregroundColor(someCoins.quiteSome ? .red : .primary)
+ } // warnings
+ }
+ }
+ }
+}
+// MARK: -
+struct QuiteSomeCoins_Previews: PreviewProvider {
+ static var previews: some View {
+ QuiteSomeCoins(someCoins: SomeCoins(numCoins: 4, fee: "20 " +
LONGCURRENCY),
+ shouldShowFee: true,
+ currency: LONGCURRENCY,
+ amountEffective: nil)
+ }
+}
diff --git a/TalerWallet1/Views/HelperViews/AmountView.swift
b/TalerWallet1/Views/HelperViews/AmountView.swift
index 4249876..bf2b32f 100644
--- a/TalerWallet1/Views/HelperViews/AmountView.swift
+++ b/TalerWallet1/Views/HelperViews/AmountView.swift
@@ -1,17 +1,6 @@
/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
*/
import SwiftUI
@@ -19,13 +8,14 @@ struct AmountView: View {
let title: String
let value: String
let color: Color
+ let large: Bool // set to false for QR or IBAN
var body: some View {
VStack {
Text(title)
.font(.title3)
Text(value)
- .font(.largeTitle)
- .fontWeight(.medium)
+ .font(large ? .largeTitle : .title)
+ .fontWeight(large ? .medium : .regular)
.foregroundColor(color)
}
.frame(maxWidth: .infinity, alignment: .center)
@@ -35,9 +25,11 @@ struct AmountView: View {
struct AmountView_Previews: PreviewProvider {
static var previews: some View {
- Form {
- AmountView(title: "Fee", value: "- 0,2 Taler", color:
Color("Outgoing"))
- AmountView(title: "Coins", value: "4,8 Taler", color:
Color("Incoming"))
+ List {
+ AmountView(title: "Fee", value: "- 0,2 Taler",
+ color: Color("Outgoing"), large: true)
+ AmountView(title: "Coins", value: "4,8 Taler",
+ color: Color("Incoming"), large: false)
}
}
}
diff --git a/TalerWallet1/Views/HelperViews/Buttons.swift
b/TalerWallet1/Views/HelperViews/Buttons.swift
index fd298cb..294e57b 100644
--- a/TalerWallet1/Views/HelperViews/Buttons.swift
+++ b/TalerWallet1/Views/HelperViews/Buttons.swift
@@ -1,108 +1,261 @@
/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
*/
import SwiftUI
+import Foundation
+
+
+
+extension ShapeStyle where Self == Color {
+ static var random: Color {
+ Color(
+ red: .random(in: 0...1),
+ green: .random(in: 0...1),
+ blue: .random(in: 0...1)
+ )
+ }
+}
struct HamburgerButton : View {
+ var font: Font?
let action: () -> Void
var body: some View {
Button(action: action) {
Image(systemName: "line.3.horizontal")
}
- .font(.title)
+ .font(font ?? .title)
+ }
+}
+
+struct QRButton : View {
+ var font: Font?
+ let action: () -> Void
+
+ var body: some View {
+ Button(action: action) {
+ Image(systemName: "qrcode.viewfinder")
+ }
+ .font(font ?? .title)
}
}
struct PlusButton : View {
+ var font: Font?
let action: () -> Void
var body: some View {
Button(action: action) {
Image(systemName: "plus")
}
- .font(.title)
+ .font(font ?? .title)
}
}
struct ArrowUpButton : View {
+ var font: Font?
let action: () -> Void
var body: some View {
Button(action: action) {
Image(systemName: "arrow.up.to.line")
}
- .font(.title3)
+ .font(font ?? .title3)
}
}
struct ArrowDownButton : View {
+ var font: Font?
let action: () -> Void
var body: some View {
Button(action: action) {
Image(systemName: "arrow.down.to.line")
}
- .font(.title3)
+ .font(font ?? .title3)
}
}
struct ReloadButton : View {
let disabled: Bool
+ var font: Font?
let action: () -> Void
var body: some View {
Button(action: action) {
Image(systemName: "arrow.clockwise")
}
- .font(.title)
+ .font(font ?? .title)
.disabled(disabled)
}
}
-struct AwesomeButton: View {
- let title: String
- let action: () -> Void
- var body: some View {
- Button(action: action) {
- Text(title)
- .frame(minWidth: 0, maxWidth: 300)
- .padding()
- .foregroundColor(.white)
- .background(LinearGradient(gradient: Gradient(colors:
[Color.red, Color.blue]), startPoint: .leading, endPoint: .trailing))
- .cornerRadius(40)
- .font(.title)
+struct TalerButtonStyle: ButtonStyle {
+ @Environment(\.isEnabled) private var isEnabled: Bool
+ func disabled() -> Bool { !isEnabled }
+
+ enum TalerButtonStyleType {
+ case plain
+ case bordered
+ case prominent
+ }
+ var type: TalerButtonStyleType = .plain
+ var dimmed: Bool = false
+ var narrow: Bool = false
+ var aligned: TextAlignment = .center
+
+ public func makeBody(configuration: ButtonStyle.Configuration) -> some
View {
+ MyBigButton(type: type,
+ foreColor: foreColor(type: type, pressed:
configuration.isPressed),
+ backColor: backColor(type: type, pressed:
configuration.isPressed),
+ dimmed: dimmed,
+ configuration: configuration,
+ disabled: disabled(),
+ narrow: narrow,
+ aligned: aligned)
+ }
+
+ func foreColor(type: TalerButtonStyleType, pressed: Bool) -> Color {
+// return if type == .plain {
+// WalletColors().fieldForeground
+// } else {
+// WalletColors().buttonForeColor(pressed: pressed,
+// disabled: disabled(),
+// prominent: type == .prominent)
+// }
+ return type == .plain ? WalletColors().fieldForeground :
+ WalletColors().buttonForeColor(pressed: pressed,
+ disabled: disabled(),
+ prominent: type == .prominent)
+ }
+ func backColor(type: TalerButtonStyleType, pressed: Bool) -> Color {
+// return if type == .plain {
+// Color.clear
+// } else {
+// WalletColors().buttonBackColor(pressed: pressed,
+// disabled: disabled(),
+// prominent: type == .prominent)
+// }
+ return type == .plain ? Color.clear :
+ WalletColors().buttonBackColor(pressed: pressed,
+ disabled: disabled(),
+ prominent: type == .prominent)
+ }
+ struct BackgroundView: View {
+ let color: Color
+ let dimmed: Bool
+ var body: some View {
+ RoundedRectangle(
+ cornerRadius: 15,
+ style: .continuous
+ )
+ .fill(color)
+ .opacity(dimmed ? 0.6 : 1.0)
+ }
+ }
+
+ struct MyBigButton: View {
+ var type: TalerButtonStyleType
+ let foreColor: Color
+ let backColor: Color
+ let dimmed: Bool
+ let configuration: ButtonStyle.Configuration
+ let disabled: Bool
+ let narrow: Bool
+ let aligned: TextAlignment
+
+ var body: some View {
+ let aligned2: Alignment = (aligned == .center) ? Alignment.center
+ : (aligned == .leading) ? Alignment.leading
+ : Alignment.trailing
+ configuration.label
+ .multilineTextAlignment(aligned)
+ .font(.title3)
+// .font(narrow ? .title3 : .title2)
+ .frame(minWidth: 0, maxWidth: narrow ? nil : .infinity,
alignment: aligned2)
+ .padding(.vertical, 10)
+ .padding(.horizontal, 6)
+ .foregroundColor(foreColor)
+ .background(BackgroundView(color: backColor, dimmed: dimmed))
+ .contentShape(Rectangle()) // make sure the button can be
pressed even if backgroundColor == clear
+ .scaleEffect(configuration.isPressed ? 0.95 : 1)
+ .animation(.spring(response: 0.1), value:
configuration.isPressed)
+ .disabled(disabled)
}
}
}
+
struct Buttons_Previews: PreviewProvider {
static var previews: some View {
VStack {
- HamburgerButton() {}
- .padding()
- PlusButton() {}
- .padding()
+ HamburgerButton() {
+ Controller.shared.playSound(1000)
+ }.padding()
+ QRButton() {
+ Controller.shared.playSound(1001)
+ }.padding()
+ PlusButton() {
+ Controller.shared.playSound(1002) // == 7
+ }.padding()
+ HamburgerButton() {
+ Controller.shared.playSound(1003)
+ }.padding()
+ QRButton() {
+ Controller.shared.playSound(1004)
+ }.padding()
+ PlusButton() {
+ Controller.shared.playSound(1005)
+ }.padding()
HStack {
- ReloadButton(disabled: false) {}
- .padding()
- ReloadButton(disabled: true) {}
- .padding()
+ ReloadButton(disabled: false) {
+ Controller.shared.playSound(1006)
+ }.padding()
+ ReloadButton(disabled: true) {
+ }.padding()
}
- AwesomeButton(title: "AwesomeButton") {}
+ Button(String(localized: "Accept"), action: {
+ Controller.shared.playSound(1008)
+ })
+ .buttonStyle(TalerButtonStyle(type: .prominent))
+ .padding(.horizontal)
+ }
+ }
+}
+
+#if DEBUG
+fileprivate struct ContentView: View {
+ @State var isOn = false
+ //The better route is to have a separate variable to control the animations
+ // This prevents unpleasant side-effects.
+ @State private var animate = false
+
+ var body: some View {
+ VStack {
+ Text("I don't change.")
.padding()
+ Button("Press me, I do change") {
+ isOn.toggle()
+ animate = false
+ // Because .opacity is animated, we need to switch it
+ // back so the button shows.
+ DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
+ animate = true
+ }
+ }
+ // In this case I chose to animate .opacity
+ .opacity(animate ? 1 : 0)
+ .animation(.easeIn, value: animate)
+ .frame(width: 300, height: 400)
+ // If you want the button to animate when the view appears, you
need to change the value
+ .onAppear { animate = true }
}
}
}
+fileprivate struct ContentView_Previews: PreviewProvider {
+ static var previews: some View {
+ ContentView()
+ }
+}
+#endif
diff --git a/TalerWallet1/Views/HelperViews/CopyShare.swift
b/TalerWallet1/Views/HelperViews/CopyShare.swift
new file mode 100644
index 0000000..b3de098
--- /dev/null
+++ b/TalerWallet1/Views/HelperViews/CopyShare.swift
@@ -0,0 +1,80 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import UniformTypeIdentifiers
+import SwiftUI
+import SymLog
+
+struct CopyButton: View {
+ private let symLog = SymLogV(0)
+ @Environment(\.isEnabled) private var isEnabled: Bool
+ let textToCopy: String
+ let vertical: Bool
+
+ func copyAction() -> Void {
+ symLog.log(textToCopy)
+ UIPasteboard.general.setValue(textToCopy,
+ forPasteboardType:
UTType.plainText.identifier)
+ }
+
+ var body: some View {
+ Button(action: copyAction) {
+ if vertical {
+ VStack {
+ Image(systemName: "doc.on.doc")
+ Text("Copy", comment: "5 letters max, else abbreviate")
+ }
+ } else {
+ HStack {
+ Image(systemName: "doc.on.doc")
+ Text("Copy", comment: "may be a bit longer")
+ }
+ }
+ }
+ .disabled(!isEnabled)
+ }
+}
+// MARK: -
+struct ShareButton: View {
+ private let symLog = SymLogV(0)
+ @Environment(\.isEnabled) private var isEnabled: Bool
+
+ let textToShare: String
+
+ func shareAction() -> Void {
+ symLog.log(textToShare)
+ ShareSheet.shareSheet(url: textToShare)
+ }
+
+ var body: some View {
+ Button(action: shareAction) {
+ HStack {
+ Image(systemName: "square.and.arrow.up")
+ Text("Share")
+ }
+ }
+ .disabled(!isEnabled)
+ }
+}
+// MARK: -
+struct CopyShare: View {
+ @Environment(\.isEnabled) private var isEnabled: Bool
+
+ let textToCopy: String
+
+ var body: some View {
+ HStack {
+ CopyButton(textToCopy: textToCopy, vertical: false)
+ .buttonStyle(TalerButtonStyle(type: .bordered))
+ ShareButton(textToShare: textToCopy)
+ .buttonStyle(TalerButtonStyle(type: .bordered))
+ } // two buttons
+ }
+}
+// MARK: -
+struct CopyShare_Previews: PreviewProvider {
+ static var previews: some View {
+ CopyShare(textToCopy: "Hallö")
+ }
+}
diff --git a/TalerWallet1/Views/HelperViews/CurrencyField.swift
b/TalerWallet1/Views/HelperViews/CurrencyField.swift
new file mode 100644
index 0000000..8f2e525
--- /dev/null
+++ b/TalerWallet1/Views/HelperViews/CurrencyField.swift
@@ -0,0 +1,221 @@
+/* MIT License
+ * Copyright (c) 2022 Javier Trinchero
+ *
+ * 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
+import UIKit
+
+public struct CurrencyField: View {
+ @Binding var value: UInt64
+ var currency: String
+ var formatter: NumberFormatter
+ private var currencyInputField: CurrencyInputField! = nil
+
+ public func becomeFirstResponder() -> Void {
+ currencyInputField.becomeFirstResponder()
+ }
+
+ public func resignFirstResponder() -> Void {
+ currencyInputField.resignFirstResponder()
+ }
+
+ private var label: String {
+ let mag = pow(10, formatter.maximumFractionDigits)
+ return formatter.string(for: Decimal(value) / mag) ?? ""
+ }
+
+ public init(value: Binding<UInt64>, currency: String, formatter:
NumberFormatter) {
+ self._value = value
+ self.currency = currency
+ self.formatter = formatter
+ self.currencyInputField = CurrencyInputField(value: $value, formatter:
formatter)
+ }
+
+ public init(value: Binding<UInt64>, currency: String) {
+ let formatter = NumberFormatter()
+ formatter.locale = .current
+ formatter.numberStyle = .currency
+ formatter.currencySymbol = currency
+ formatter.minimumFractionDigits = 2
+ formatter.maximumFractionDigits = 2
+
+ self.init(value: value, currency: currency, formatter: formatter)
+ }
+
+ public var body: some View {
+ ZStack {
+ // Text view to display the formatted currency
+ // Set as priority so CurrencyInputField size doesn't affect parent
+ Text(label)
+ .layoutPriority(1)
+
+ // Input text field to handle UI
+ currencyInputField
+ }
+ }
+}
+
+// Sub-class UITextField to remove selection and caret
+class NoCaretTextField: UITextField {
+ override func canPerformAction(_ action: Selector, withSender sender:
Any?) -> Bool {
+ false
+ }
+
+ override func selectionRects(for range: UITextRange) ->
[UITextSelectionRect] {
+ []
+ }
+
+ override func caretRect(for position: UITextPosition) -> CGRect {
+ .null
+ }
+}
+
+struct CurrencyInputField: UIViewRepresentable {
+ @Binding var value: UInt64
+ var formatter: NumberFormatter
+ private let textField = NoCaretTextField(frame: .zero)
+
+ func makeCoordinator() -> Coordinator {
+ Coordinator(self)
+ }
+
+ public func becomeFirstResponder() -> Void {
+ textField.becomeFirstResponder()
+ }
+
+ public func resignFirstResponder() -> Void {
+ textField.resignFirstResponder()
+ }
+
+ func makeUIView(context: Context) -> NoCaretTextField {
+ // Assign delegate
+ textField.delegate = context.coordinator
+
+ // Set keyboard type
+ textField.keyboardType = .numberPad
+
+ // Make visual components invisible
+ textField.tintColor = .clear
+ textField.textColor = .clear
+ textField.backgroundColor = .clear
+
+ // Add editingChanged event handler
+ textField.addTarget(
+ context.coordinator,
+ action: #selector(Coordinator.editingChanged(textField:)),
+ for: .editingChanged
+ )
+
+ // Set initial textfield text
+ context.coordinator.updateText(value, textField: textField)
+
+ return textField
+ }
+
+ func updateUIView(_ uiView: NoCaretTextField, context: Context) {}
+
+ class Coordinator: NSObject, UITextFieldDelegate {
+ // Reference to currency input field
+ private var input: CurrencyInputField
+
+ // Last valid text input string to be displayed
+ private var lastValidInput: String? = ""
+
+ init(_ currencyTextField: CurrencyInputField) {
+ self.input = currencyTextField
+ }
+
+ func setValue(_ value: UInt64, textField: UITextField) {
+ // Update input value
+ input.value = value
+
+ // Update textfield text
+ updateText(value, textField: textField)
+ }
+
+ func updateText(_ value: UInt64, textField: UITextField) {
+ // Update field text and last valid input text
+ textField.text = String(value)
+ lastValidInput = String(value)
+ }
+
+ func textField(_ textField: UITextField, shouldChangeCharactersIn
range: NSRange, replacementString string: String) -> Bool {
+ // If replacement string is empty, we can assume the backspace key
was hit
+ if string.isEmpty {
+ // Resign first responder when delete is hit when value is 0
+ if input.value == 0 {
+ textField.resignFirstResponder()
+ }
+
+ // Remove trailing digit
+ setValue(UInt64(input.value / 10), textField: textField)
+ }
+ return true
+ }
+
+ @objc func editingChanged(textField: NoCaretTextField) {
+ // Get a mutable copy of last text
+ guard var oldText = lastValidInput else {
+ return
+ }
+
+ // Iterate through each char of the new string and compare LTR
with old string
+ let char = (textField.text ?? "").first { next in
+ // If old text is empty or its next character doesn't match new
+ if oldText.isEmpty || next != oldText.removeFirst() {
+ // Found the mismatching character
+ return true
+ }
+ return false
+ }
+
+ // Find new character and try to get an Int value from it
+ guard let char = char, let digit = Int(String(char)) else {
+ // New character could not be converted to Int
+ // Revert to last valid text
+ textField.text = lastValidInput
+ return
+ }
+
+ // Multiply by 10 to shift numbers one position to the left,
revert if an overflow occurs
+ let (multValue, multOverflow) =
input.value.multipliedReportingOverflow(by: 10)
+ if multOverflow {
+ textField.text = lastValidInput
+ return
+ }
+
+ // Add the new trailing digit, revert if an overflow occurs
+ let (addValue, addOverflow) =
multValue.addingReportingOverflow(UInt64(digit))
+ if addOverflow {
+ textField.text = lastValidInput
+ return
+ }
+
+ // If new value has more digits than allowed by formatter, revert
+ if input.formatter.maximumFractionDigits +
input.formatter.maximumIntegerDigits < String(addValue).count {
+ textField.text = lastValidInput
+ return
+ }
+
+ // Update new value
+ setValue(addValue, textField: textField)
+ }
+ }
+}
diff --git a/TalerWallet1/Views/HelperViews/CurrencyInputView.swift
b/TalerWallet1/Views/HelperViews/CurrencyInputView.swift
new file mode 100644
index 0000000..da84250
--- /dev/null
+++ b/TalerWallet1/Views/HelperViews/CurrencyInputView.swift
@@ -0,0 +1,60 @@
+//
+// CurrencyInputView.swift
+// TalerWalletT
+//
+// Created by Marc Stibane on 2023-06-04.
+// Copyright © 2023 Taler. All rights reserved.
+//
+
+import SwiftUI
+
+struct CurrencyInputView: View {
+ let currencyField: CurrencyField
+ let title: String
+
+ @State var hasBeenShown = false
+ var body: some View {
+ VStack (alignment: .leading) {
+ Text(title)
+ .padding(.top)
+ .font(.title3)
+ currencyField
+ .frame(maxWidth: .infinity, alignment: .trailing)
+ .foregroundColor(WalletColors().fieldForeground) // text
color
+ .background(WalletColors().fieldBackground)
+ .font(.title)
+ .border(.primary)
+ }.onAppear { // make CurrencyField show the keyboard after 0.4
seconds
+ if hasBeenShown {
+ print("❗️Yikes: CurrencyInputView hasBeenShown")
+ } else {
+ print("❗️Yikes: First CurrencyInputView❗️")
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
+ hasBeenShown = true
+ currencyField.becomeFirstResponder()
+ }
+ }
+ }.onDisappear {
+ currencyField.resignFirstResponder()
+ }
+ }
+}
+#if DEBUG
+fileprivate struct BindingViewContainer : View {
+ @State var centsToTransfer: UInt64 = 0
+
+ var body: some View {
+ let currencyField = CurrencyField(value: $centsToTransfer, currency:
LONGCURRENCY)
+ CurrencyInputView(currencyField: currencyField,
+ title: "Amount to withdraw:")
+ }
+}
+
+struct CurrencyInputView_Previews: PreviewProvider {
+ static var previews: some View {
+ List {
+ BindingViewContainer()
+ }
+ }
+}
+#endif
diff --git a/TalerWallet1/Views/Main/LaunchAnimationView.swift
b/TalerWallet1/Views/HelperViews/LaunchAnimationView.swift
similarity index 67%
rename from TalerWallet1/Views/Main/LaunchAnimationView.swift
rename to TalerWallet1/Views/HelperViews/LaunchAnimationView.swift
index cc20fa4..03a9eac 100644
--- a/TalerWallet1/Views/Main/LaunchAnimationView.swift
+++ b/TalerWallet1/Views/HelperViews/LaunchAnimationView.swift
@@ -1,23 +1,24 @@
-
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
import SwiftUI
-import SymLog
struct LaunchAnimationView: View {
- private let symLog = SymLogV(0)
@State private var rotationDirection = false
private let animationTimer = Timer
- .publish(every: 1.4, on: .current, in: .common)
+ .publish(every: 1.6, on: .current, in: .common)
.autoconnect()
var body: some View {
ZStack {
- Color.teal.ignoresSafeArea()
- Image(systemName: "hurricane")
+ Color(.systemGray6).ignoresSafeArea()
+ Image("taler-logo-2023-red")
.resizable()
.scaledToFit()
- .frame(width: 200, height: 200)
- .rotationEffect(rotationDirection ? Angle(degrees: 0) :
Angle(degrees: 1080))
+ .frame(width: 250, height: 250)
+ .rotationEffect(rotationDirection ? Angle(degrees: 0) :
Angle(degrees: 900))
}
.onReceive(animationTimer) { timerValue in
withAnimation(.easeInOut(duration: 1.9)) {
@@ -26,6 +27,7 @@ struct LaunchAnimationView: View {
}
}
}
+// MARK: -
struct LaunchAnimationView_Previews: PreviewProvider {
static var previews: some View {
LaunchAnimationView()
diff --git a/TalerWallet1/Views/HelperViews/ListStyle.swift
b/TalerWallet1/Views/HelperViews/ListStyle.swift
new file mode 100644
index 0000000..c3f625a
--- /dev/null
+++ b/TalerWallet1/Views/HelperViews/ListStyle.swift
@@ -0,0 +1,97 @@
+/* MIT License
+ * Copyright (c) 2022 young rtSwift
+ *
+ * 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
+
+public extension View {
+ var anyView: AnyView {
+ AnyView(self)
+ }
+}
+// MARK: -
+// Our ListStyle each case corresponds to a SwiftUI.ListStyle
+// Here we make it CaseIterable for SwiftUI.ForEach
+// and the UI Display name
+public enum MyListStyle: String, CaseIterable, Hashable {
+ case automatic
+ case grouped
+ case inset
+ case insetGrouped
+ case plain
+ case sidebar
+
+ // map to SwiftUI ListStyle
+ var style: any SwiftUI.ListStyle {
+ switch self {
+ case .automatic: return .automatic
+ case .grouped: return .grouped
+ case .inset: return .inset
+ case .insetGrouped: return .insetGrouped
+ case .plain: return .plain
+ case .sidebar: return .sidebar
+ }
+ }
+
+ var displayName: String {
+ String(self.rawValue)
+ }
+}
+// MARK: -
+#if DEBUG
+struct AnyViewDemo: View {
+ @State private var selectedStyle = MyListStyle.automatic
+
+ let sections = ["Breakfast" : ["pancakes", "bacon", "orange juice"],
+ "Lunch" : ["sandwich", "chips",
"lemonade"],
+ "Dinner" : ["spaghetti", "bread",
"water"]]
+
+
+ var body: some View {
+ VStack {
+ Picker("List Style", selection: $selectedStyle) {
+ ForEach(MyListStyle.allCases, id: \.self) {
+ Text($0.displayName.capitalized).tag($0)
+ }
+ }
+
+ let keys = Array(sections.keys)
+ List(keys.indices, id: \.self) { index in
+ let key = keys[index]
+ if let section = sections[key] {
+ Section(key) {
+ ForEach(section, id: \.self) { item in
+ Text(item)
+ }
+ }
+ }
+ }
+ .listStyle(selectedStyle.style)
+ .anyView
+ }
+ }
+}
+
+struct AnyViewDemo_Previews: PreviewProvider {
+ static var previews: some View {
+ AnyViewDemo()
+ }
+}
+#endif
diff --git a/TalerWallet1/Views/HelperViews/LoadingView.swift
b/TalerWallet1/Views/HelperViews/LoadingView.swift
index 10c245e..0c1c452 100644
--- a/TalerWallet1/Views/HelperViews/LoadingView.swift
+++ b/TalerWallet1/Views/HelperViews/LoadingView.swift
@@ -1,45 +1,28 @@
/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
*/
import SwiftUI
import SymLog
struct LoadingView: View {
private let symLog = SymLogV(0)
+ let navTitle = String(localized: "Loading...")
let backButtonHidden: Bool
var body: some View {
- symLog { NavigationView {
- VStack {
- Spacer()
- ProgressView()
- Spacer()
- Spacer()
- Spacer()
- }
- .navigationBarBackButtonHidden(backButtonHidden)
- .navigationTitle("Loading...")
- }
+ LaunchAnimationView()
+ .navigationBarBackButtonHidden(backButtonHidden)
+ .navigationTitle(navTitle)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment:
.center)
- .background(Color(.systemGray6))
- }
+//
.background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all))
}
}
-
+// MARK: -
struct LoadingView_Previews: PreviewProvider {
static var previews: some View {
- LoadingView(backButtonHidden: true)
+ NavigationView {
+ LoadingView(backButtonHidden: true)
+ }
}
}
diff --git a/TalerWallet1/Views/HelperViews/QRCodeDetailView.swift
b/TalerWallet1/Views/HelperViews/QRCodeDetailView.swift
new file mode 100644
index 0000000..e155c48
--- /dev/null
+++ b/TalerWallet1/Views/HelperViews/QRCodeDetailView.swift
@@ -0,0 +1,57 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import SwiftUI
+import taler_swift
+import AVFoundation
+
+
+struct QRCodeDetailView: View {
+ let talerURI: String
+ let incoming: Bool
+
+ var body: some View {
+ if talerURI.count > 10 {
+ VStack {
+ 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)
+
+ QRGeneratorView(text: talerURI)
+// Text(talerURI)
+
+ Text("Alternatively, copy and send this URI:")
+ .fixedSize(horizontal: false, vertical: true)
+ .font(.title3)
+ .padding(.vertical)
+
+ Text(talerURI)
+ .padding(.bottom)
+
+ CopyShare(textToCopy: talerURI)
+ .disabled(false)
+ }
+ }
+ }
+}
+// MARK: -
+#if DEBUG
+fileprivate struct ContentView: View {
+ @State var talerURI: String =
"taler://pay-push/exchange.demo.taler.net/95ZG4D1AGFGZQ7CNQ1V49D3FT18HXKA6HQT4X3XME9YSJQVFQ520"
+
+ var body: some View {
+ List {
+ QRCodeDetailView(talerURI: talerURI, incoming: false)
+ }
+ }
+}
+struct QRCodeDetailView_Previews: PreviewProvider {
+
+ static var previews: some View {
+ ContentView()
+ }
+}
+#endif
diff --git a/TalerWallet1/Views/HelperViews/QRGeneratorView.swift
b/TalerWallet1/Views/HelperViews/QRGeneratorView.swift
new file mode 100644
index 0000000..9f51b37
--- /dev/null
+++ b/TalerWallet1/Views/HelperViews/QRGeneratorView.swift
@@ -0,0 +1,62 @@
+/* MIT License
+ * Copyright (c) 2020 Jeeva Tamilselvan
+ *
+ * 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
+
+struct QRGeneratorView: View {
+ var text: String
+
+ var body: some View {
+ if let data = getQRCodeData(text: text) {
+ if let uiImage = UIImage(data: data) {
+ Image(uiImage: uiImage)
+ .interpolation(.none)
+ .resizable()
+ .scaledToFit()
+ .frame(width: 200, height: 200)
+ } else {
+ EmptyView()
+ }
+ } else {
+ EmptyView()
+ }
+ }
+
+ func getQRCodeData(text: String) -> Data? {
+ guard let filter = CIFilter(name: "CIQRCodeGenerator") else { return
nil }
+ let data = text.data(using: .ascii, allowLossyConversion: false)
+ filter.setValue(data, forKey: "inputMessage")
+ guard let ciimage = filter.outputImage else { return nil }
+ let transform = CGAffineTransform(scaleX: 10, y: 10)
+ let scaledCIImage = ciimage.transformed(by: transform)
+ let uiimage = UIImage(ciImage: scaledCIImage)
+ return uiimage.pngData()!
+ }
+}
+
+struct QRGeneratorView_Previews: PreviewProvider {
+ static var previews: some View {
+ VStack {
+ QRGeneratorView(text: "Hello World!")
+ QRGeneratorView(text:
"taler://pay-pull/exchange.demo.taler.net/7J7SNHYMCCAZ1ARY9YCB5Z9FTY0YZP8F2KDRXV94KZCQ6WAVMTX0")
+ }
+ }
+}
diff --git a/TalerWallet1/Views/HelperViews/SelectDays.swift
b/TalerWallet1/Views/HelperViews/SelectDays.swift
new file mode 100644
index 0000000..9c982e5
--- /dev/null
+++ b/TalerWallet1/Views/HelperViews/SelectDays.swift
@@ -0,0 +1,61 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import SwiftUI
+import taler_swift
+import SymLog
+
+struct SelectDays: View {
+ private let symLog = SymLogV(0)
+ @Environment(\.isEnabled) private var isEnabled: Bool
+#if DEBUG
+ @AppStorage("developerMode") var developerMode: Bool = true
+#else
+ @AppStorage("developerMode") var developerMode: Bool = false
+#endif
+
+ @Binding var selected: UInt
+ let maxExpiration: UInt
+
+ func oneDayAction() -> Void {
+ selected = 1
+ symLog.log(selected)
+ }
+
+ func sevenDayAction() -> Void {
+ selected = 7
+ symLog.log(selected)
+ }
+
+ func thirtyDayAction() -> Void {
+ selected = 30
+ symLog.log(selected)
+ }
+
+ var body: some View {
+ HStack {
+ Button(action: oneDayAction) {
+ Text(developerMode ? "3 Min." : "1 Day")
+ }.buttonStyle(TalerButtonStyle(type: (selected == 1) ? .prominent
: .bordered, dimmed: true))
+ .disabled(!isEnabled)
+
+ Button(action: sevenDayAction) {
+ Text(developerMode ? "1 Hour" : "7 Days")
+ }.buttonStyle(TalerButtonStyle(type: (selected == 7) ? .prominent
: .bordered, dimmed: true))
+ .disabled(!isEnabled || maxExpiration < 7)
+
+ Button(action: thirtyDayAction) {
+ Text(developerMode ? "1 Day" : "30 Days")
+ }.buttonStyle(TalerButtonStyle(type: (selected == 30) ? .prominent
: .bordered, dimmed: true))
+ .disabled(!isEnabled || maxExpiration < 30)
+ } // 3 buttons
+ }
+}
+
+struct SelectDays_Previews: PreviewProvider {
+ static var previews: some View {
+ @State var expireDays: UInt = 1
+ SelectDays(selected: $expireDays, maxExpiration: 20)
+ }
+}
diff --git a/TalerWallet1/Views/HelperViews/TextFieldAlert.swift
b/TalerWallet1/Views/HelperViews/TextFieldAlert.swift
index de2eaf6..e307ff1 100644
--- a/TalerWallet1/Views/HelperViews/TextFieldAlert.swift
+++ b/TalerWallet1/Views/HelperViews/TextFieldAlert.swift
@@ -1,17 +1,6 @@
/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
*/
import SwiftUI
diff --git a/TalerWallet1/Views/HelperViews/TransactionButton.swift
b/TalerWallet1/Views/HelperViews/TransactionButton.swift
new file mode 100644
index 0000000..ed4f3fa
--- /dev/null
+++ b/TalerWallet1/Views/HelperViews/TransactionButton.swift
@@ -0,0 +1,89 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import SwiftUI
+import taler_swift
+import AVFoundation
+
+struct TransactionButton: View {
+ let transactionId : String
+ let command : TxAction
+ let action: (_ transactionId: String) async throws -> Void
+
+ @State var disabled: Bool = false
+ @State var executed: Bool = false
+ var body: some View {
+ let isDestructive = (command == .delete) || (command == .fail)
+ let role: ButtonRole? = (command == .abort) ? .cancel
+ : isDestructive ? .destructive
+ : nil
+ Button(role: role, action: {
+ Task {
+ disabled = true // don't try this more than once
+ do {
+ try await action(transactionId)
+// symLog.log("\(executed) \(transactionId)")
+ executed = true
+ } catch { // TODO: error
+// symLog.log(error.localizedDescription)
+ }
+ }
+ }, label: {
+ HStack {
+ if executed {
+ switch command {
+ case .delete:
+ Text("Deleted from list")
+ case .abort:
+ Text("Abort pending...")
+ case .fail:
+ Text("Failing...")
+ case .suspend:
+ Text("Suspending...")
+ case .resume:
+ Text("Resuming...")
+ }
+ } else {
+ let spaces = " "
+ switch command {
+ case .delete:
+ Text("Delete from list" + spaces)
+ Image(systemName: "trash") //
+ case .abort:
+ Text("Abort" + spaces)
+ Image(systemName: "x.circle") //
+ case .fail:
+ Text("Fail" + spaces)
+ Image(systemName: "fanblades.slash") //
+ case .suspend:
+ Text("Suspend" + spaces)
+ Image(systemName: "clock.badge.xmark") //
+ case .resume:
+ Text("Resume" + spaces)
+ Image(systemName: "clock.arrow.circlepath") //
+ }
+ }
+ }
+ .font(.title)
+ .frame(maxWidth: .infinity)
+ })
+ .buttonStyle(.bordered)
+ .controlSize(.large)
+ .padding(.horizontal)
+ .disabled(disabled)
+ }
+
+}
+
+
+#if DEBUG
+struct TransactionButton_Previews: PreviewProvider {
+
+ static var previews: some View {
+ List {
+ TransactionButton(transactionId: "String", command: .abort,
action: {transactionId in })
+ }
+ }
+}
+#endif
diff --git a/TalerWallet1/Views/Main/ContentView.swift
b/TalerWallet1/Views/Main/ContentView.swift
deleted file mode 100644
index 1609351..0000000
--- a/TalerWallet1/Views/Main/ContentView.swift
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-import SwiftUI
-import SymLog
-
-extension URL: Identifiable {
- public var id: URL {self}
-}
-
-struct ContentView: View {
- private let symLog = SymLogV()
- @EnvironmentObject private var controller: Controller
- @State private var sheetPresented = false
- @State private var urlToOpen: URL? = nil
-
- var body: some View {
- if controller.backendState == .ready {
- symLog {
- Content(symLog: symLog, controller: controller)
- .onAppear { // called e.g. after coming to foreground
- symLog.log(".onAppear")
- }
- .onOpenURL { url in
- // TODO: check if this is called when launching the app from the camera
scanning a QR code
- symLog.log(".onOpenURL: \(url)")
- urlToOpen = url
- }
- .sheet(item: $urlToOpen, onDismiss: {}) { url in
- URLSheet(urlToOpen: url)
- }
- } // symLog
- } else if controller.backendState == .error {
- ErrorView() // TODO: show Error View
- } else {
- LaunchAnimationView()
- }
- }
-}
-// MARK: - Content
-extension ContentView {
- struct Content: View {
- let symLog: SymLogV?
- var controller: Controller
-
- @State var sidebarVisible: Bool = false
- @State var currentView: Int = 0
- var views: [SidebarItem] {[
- SidebarItem(name: "Balances",
- sysImage: "creditcard.fill", // TODO: Wallet
Icon
- view: AnyView(CurrenciesListView(viewModel:
controller.balancesModel)
- { sidebarVisible = true }
- )),
- SidebarItem(name: "Settings",
- sysImage: "gearshape.fill",
- view: AnyView(SettingsView()
- { sidebarVisible = true }
- )),
- SidebarItem(name: "Pending Operations",
- sysImage: "arrow.triangle.2.circlepath",
- view: AnyView(PendingOpsListView(viewModel:
controller.pendingModel)
- { sidebarVisible = true }
- ))
- ]}
- var body: some View {
- ZStack(alignment: .leading) {
- views[currentView].view
- .frame(maxWidth: .infinity, maxHeight: .infinity,
alignment: .center)
- SideBarView(views: views, currentView: $currentView,
sidebarVisible: $sidebarVisible)
- }
- .background(Color(.systemGray6))
- }
- }
-}
-// MARK: -
-//struct ContentView_Previews: PreviewProvider {
-// static var previews: some View {
-// ContentView.Content(symLog: nil, controller: controller)
-// }
-//}
diff --git a/TalerWallet1/Views/Main/ErrorView.swift
b/TalerWallet1/Views/Main/ErrorView.swift
index 25fc2f0..d322200 100644
--- a/TalerWallet1/Views/Main/ErrorView.swift
+++ b/TalerWallet1/Views/Main/ErrorView.swift
@@ -1,31 +1,23 @@
/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
*/
import SwiftUI
import SymLog
struct ErrorView: View {
- private let symLog = SymLogV()
+ private let symLog = SymLogV(0)
+
+ let errortext: String?
+
var body: some View {
- Text("Couldn't load Wallet-Core!")
+ Text(errortext ?? "Couldn't load Wallet-Core!")
}
}
struct ErrorView_Previews: PreviewProvider {
static var previews: some View {
- ErrorView()
+ ErrorView(errortext: "")
}
}
diff --git a/TalerWallet1/Views/Main/MainView.swift
b/TalerWallet1/Views/Main/MainView.swift
new file mode 100644
index 0000000..810e052
--- /dev/null
+++ b/TalerWallet1/Views/Main/MainView.swift
@@ -0,0 +1,124 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import SwiftUI
+import SymLog
+
+// Use this to delay instantiation when using `NavigationLink`, etc...
+struct LazyView<Content: View>: View {
+ var content: () -> Content
+ var body: some View {
+ self.content()
+ }
+}
+
+struct MainView: View {
+ private let symLog = SymLogV(0)
+ @EnvironmentObject private var viewState: ViewState //
popToRootView()
+ @EnvironmentObject private var controller: Controller
+ @State private var sheetPresented = false
+ @State private var urlToOpen: URL? = nil
+ @Binding var soundPlayed: Bool
+
+ func sheetDismissed() -> Void {
+ symLog.log("sheet dismiss")
+ }
+ var body: some View {
+#if DEBUG
+ let _ = Self._printChanges()
+ let _ = symLog.vlog() // just to get the # to compare it with
.onAppear & onDisappear
+#endif
+ Group {
+ if controller.backendState == .ready {
+ Content(symLog: symLog)
+ // any change to rootViewId triggers popToRootView behaviour
+ .id(viewState.rootViewId)
+ .onAppear() {
+ controller.playSound(1008) // TODO: Startup chime
+ soundPlayed = true
+ }
+ } else if controller.backendState == .error {
+ ErrorView(errortext: nil) // TODO: show Error View
+ } else {
+ LaunchAnimationView()
+// .task {
+// let deviceW = UIScreen.main.bounds.width
+// let deviceH = UIScreen.main.bounds.height
+// print("UIScreen: \(deviceW), \(deviceH)")
+// }
+ }
+ }
+ .animation(.easeOut(duration: LAUNCHDURATION), value:
controller.backendState)
+ .overlay(alignment: .top) {
+ // Show the viewID on top of the app's NavigationView
+ DebugViewV()
+ .id("ViewID")
+ }
+ .onOpenURL { url in
+ symLog.log(".onOpenURL: \(url)")
+ // will be called on a taler:// scheme either
+ // by user tapping such link in a browser (bank website)
+ // or when launching the app from iOS Camera.app scanning a QR code
+ urlToOpen = url // raise sheet
+ }
+ .sheet(item: $urlToOpen, onDismiss: sheetDismissed) { url in
+ let sheet = AnyView(URLSheet(urlToOpen: url))
+ Sheet(sheetView: sheet)
+ }
+ } // body
+}
+// MARK: - Content
+extension MainView {
+ struct Content: View {
+ let symLog: SymLogV?
+
+ @State var sidebarVisible: Bool = false
+ func hamburgerAction() {
+ sidebarVisible = !sidebarVisible
+ }
+
+ let balances = String(localized: "Balances")
+ let exchanges = String(localized: "Exchanges")
+ let settings = String(localized: "Settings")
+ var views: [SidebarItem] {[
+ SidebarItem(name: balances,
+ sysImage: "creditcard.fill", // TODO: Wallet Icon
+ view: AnyView(BalancesListView(navTitle: balances,
+ hamburgerAction:
hamburgerAction)
+ )),
+ SidebarItem(name: exchanges,
+ sysImage: "building.columns",
+ view: AnyView(ExchangeListView(navTitle: exchanges,
+ hamburgerAction:
hamburgerAction)
+ )),
+ SidebarItem(name: settings, // TODO: "About"?
+ sysImage: "gearshape.fill",
+ view: AnyView(SettingsView(navTitle: settings,
+ hamburgerAction: hamburgerAction)
+ ))
+ ]}
+ @State var currentView: Int = 0
+
+ var body: some View {
+#if DEBUG
+ let _ = Self._printChanges()
+ let _ = symLog?.vlog() // just to get the # to compare it
with .onAppear & onDisappear
+#endif
+ ZStack(alignment: .leading) {
+ NavigationView { // the one and only for all non-sheet views
+ VStack(alignment: .leading) { // only needed for
backslide transition
+ views[currentView].view
+ .id(views[currentView].name)
+ .frame(maxWidth: .infinity, maxHeight: .infinity,
alignment: .center)
+ .transition(.backslide)
+ }
+ }.navigationViewStyle(.stack)
+ // The side view is on top of the current view
+ SideBarView(views: views,
+ currentView: $currentView,
+ sidebarVisible: $sidebarVisible)
+ }
+ }
+ } // Content
+}
diff --git a/TalerWallet1/Views/Main/SideBarView.swift
b/TalerWallet1/Views/Main/SideBarView.swift
index bec2b2c..af47861 100644
--- a/TalerWallet1/Views/Main/SideBarView.swift
+++ b/TalerWallet1/Views/Main/SideBarView.swift
@@ -1,22 +1,11 @@
/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
*/
import SwiftUI
import SymLog
-fileprivate let sidebarWidth = 250.0
+fileprivate let sidebarWidth = 220.0
struct SidebarItem {
var name: String
@@ -26,20 +15,45 @@ struct SidebarItem {
struct SideBarView: View {
private let symLog = SymLogV(0)
- var views: [SidebarItem]
+ let views: [SidebarItem]
@Binding var currentView: Int
@Binding var sidebarVisible: Bool
+ @State private var rotationEnabled = false
+ @State private var rotationDirection = false
+
+ private let animationTimer = Timer
+ .publish(every: 1.6, on: .current, in: .common)
+ .autoconnect()
var body: some View {
- symLog {
- HStack {
- VStack {
- Spacer()
+ HStack { // sideView left, clear dismiss target right
+ EqualIconWidthDomain {
+ VStack(spacing: 10) {
+ let gnuTaler = String("GNU Taler") // this should NOT be
translated
+ Link(gnuTaler, destination:
URL(string:"https://taler.net")!)
+ .font(.largeTitle) //.bold() iOS 16
+ .padding(.top, 30)
+ Image("taler-logo-2023-red")
+ .resizable()
+ .scaledToFit()
+ .frame(width: 100, height: 100)
+ .rotationEffect(rotationDirection ? Angle(degrees: 0)
: Angle(degrees: 900))
+ .accessibilityHidden(true) // decorative logo
+ .onTapGesture {
+ rotationEnabled.toggle()
+ }
+ .onReceive(animationTimer) { timerValue in
+ if rotationEnabled {
+ withAnimation(.easeInOut(duration: 1.9)) {
+ rotationDirection.toggle()
+ }
+ }
+ }
ForEach(0..<views.count, id: \.self) { i in
Button {
symLog.log("sidebar item \"\(views[i].name)\"
selected")
sidebarVisible = false // slide sidebar to
the left
- currentView = i // switch to the view
the user selected
+ withAnimation(.easeInOut) {currentView = i}
// switch to the view the user selected
} label: {
if let sysImage = views[i].sysImage {
Label(views[i].name, systemImage: sysImage)
@@ -51,39 +65,35 @@ struct SideBarView: View {
}
.buttonStyle(.bordered)
.font(.title)
-// .padding(.vertical)
-
-// Divider()
+ .disabled(i == currentView)
+ .accessibilityHidden(i == currentView) // don't
suggest the current item
}
Spacer()
- Spacer()
}
- .background(Color(.systemGray5))
+ .background(WalletColors().sideBackground)
.frame(width: sidebarWidth, alignment: .center)
// TODO: use leading instead of sidebarWidth for right-to-left
.offset(x: sidebarVisible ? 0 : -sidebarWidth)
- .animation(.easeInOut, value: sidebarVisible)
- .ignoresSafeArea()
// .onAppear can NOT be used here, because we don't show or
dismiss this view,
// but only slide it left or right - so it is always there.
- // .onAppear {} would be called once even before
LaunchScreen is dismissed and then never again
-
- // this is just a target for a tap gesture outside the sidebar
to dismiss it
- Color.clear
- .frame(maxWidth: sidebarVisible ? .infinity : 0,
maxHeight: .infinity, alignment: .leading)
- // TODO: right-to-left ?
- .offset(x: sidebarVisible ? sidebarWidth : 0)
- .contentShape(Rectangle())
- .onTapGesture {
- sidebarVisible = false
- }
}
+ // this is just a target for a tap gesture outside the sidebar to
dismiss it
+ Color.clear
+ .frame(maxWidth: sidebarVisible ? .infinity : 0, maxHeight:
.infinity, alignment: .leading)
+ // TODO: right-to-left ?
+ .offset(x: sidebarVisible ? sidebarWidth : 0)
+ .contentShape(Rectangle())
+ .onTapGesture {
+ sidebarVisible = false
+ }
}
+ .animation(.linear //(duration: SLIDEDURATION)
+ , value: sidebarVisible)
}
}
-
+// MARK: -
#if DEBUG
-struct BindingViewContainer : View {
+fileprivate struct BindingViewContainer : View {
@State var currentView: Int = 0
@State var sidebarVisible: Bool = true
var views: [SidebarItem]
@@ -91,6 +101,7 @@ struct BindingViewContainer : View {
var body: some View {
ZStack(alignment: .leading) {
views[currentView].view
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment:
.center)
SideBarView(views: views, currentView: $currentView,
sidebarVisible: $sidebarVisible)
}
}
@@ -99,8 +110,10 @@ struct BindingViewContainer : View {
struct SideBarView_Previews: PreviewProvider {
static var views: [SidebarItem] {[
SidebarItem(name: "Balances",
+ sysImage: "creditcard.fill", // TODO: Wallet Icon
view: AnyView(WalletEmptyView())),
SidebarItem(name: "Settings",
+ sysImage: "gearshape.fill",
view: AnyView(WalletEmptyView()))
]}
static var previews: some View {
diff --git a/TalerWallet1/Views/Main/WalletEmptyView.swift
b/TalerWallet1/Views/Main/WalletEmptyView.swift
new file mode 100644
index 0000000..1ec8cd7
--- /dev/null
+++ b/TalerWallet1/Views/Main/WalletEmptyView.swift
@@ -0,0 +1,44 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import SwiftUI
+import SymLog
+
+/// This view shows hints if a wallet is empty
+/// It is the very first thing the user sees after installing the app
+
+struct WalletEmptyView: View {
+ private let symLog = SymLogV(0)
+ @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic
+
+ var body: some View {
+ List {
+ Section {
+ Text("There is no digital cash in your wallet.")
+ }
+ Section {
+ Text("You can get some test money from the demo bank:")
+ }
+ Section {
+ Link(DEMOBANK, destination: URL(string: DEMOBANK)!)
+ }
+ Section {
+ Text("Just register a test account, then withdraw some coins.")
+ }
+ }
+// .padding(.vertical)
+ .font(.title2)
+ .listStyle(myListStyle.style).anyView
+ .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all))
+ .onAppear() {
+ DebugViewC.shared.setViewID(VIEW_EMPTY) // 10
+ }
+ }
+}
+
+struct WalletEmptyView_Previews: PreviewProvider {
+ static var previews: some View {
+ WalletEmptyView()
+ }
+}
diff --git a/TalerWallet1/Views/Payment/DeleteMe.swift
b/TalerWallet1/Views/Payment/DeleteMe.swift
new file mode 100644
index 0000000..1ff5352
--- /dev/null
+++ b/TalerWallet1/Views/Payment/DeleteMe.swift
@@ -0,0 +1,79 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import SwiftUI
+import taler_swift
+import AVFoundation
+import SymLog
+
+struct PaymentAcceptView: View {
+ private let symLog = SymLogV()
+
+ let detailsForUri: PaymentDetailsForUri
+ let acceptAction: () -> Void
+
+ let navTitle = String(localized: "Accept Payment")
+
+ @State private var disabled = false
+ var body: some View {
+ Group {
+ let raw = detailsForUri.amountRaw
+ let effective = detailsForUri.amountEffective
+ let currency = raw.currencyStr
+ let fee = try! Amount.diff(raw, effective) // TODO: different
currencies
+ ThreeAmountsView(topTitle: String(localized: "Amount to pay:"),
+ topAmount: raw, fee: fee,
+ bottomTitle: String(localized: "\(currency) to be
spent:"),
+ bottomAmount: effective,
+ large: true, pending: false, incoming: false,
+ baseURL:
detailsForUri.contractTerms.exchanges.first?.url)
+ // TODO: payment: popup with all possible exchanges, check fees
+ .safeAreaInset(edge: .bottom) {
+ Button(String(localized: "Accept"), action: acceptAction)
+ .buttonStyle(TalerButtonStyle(type: .prominent))
+ .padding(.horizontal)
+ }
+ }
+ .navigationTitle(navTitle)
+ }
+}
+// MARK: -
+struct PaymentAccept_Previews: PreviewProvider {
+ static var previews: some View {
+ let merchant = Merchant(name: "Merchant")
+ let extra = Extra(articleName: "articleName")
+ let product = Product(description: "description")
+ let terms = ContractTerms(amount: try! Amount(fromString: LONGCURRENCY
+ ":2.2"),
+ maxFee: try! Amount(fromString: LONGCURRENCY
+ ":0.2"),
+ maxWireFee: try! Amount(fromString:
LONGCURRENCY + ":0.2"),
+ merchant: merchant,
+ extra: extra,
+ summary: "summary",
+ timestamp: Timestamp.now(),
+ payDeadline: Timestamp.tomorrow(),
+ refundDeadline: Timestamp.tomorrow(),
+ wireTransferDeadline: Timestamp.tomorrow(),
+ merchantBaseURL: "merchantBaseURL",
+ fulfillmentURL: "fulfillmentURL",
+ publicReorderURL: "publicReorderURL",
+ auditors: [],
+ exchanges: [],
+ orderID: "orderID",
+ nonce: "nonce",
+ merchantPub: "merchantPub",
+ products: [product],
+ hWire: "hWire",
+ wireMethod: "wireMethod",
+ wireFeeAmortization: 0)
+ let details = PaymentDetailsForUri(
+ amountRaw: try! Amount(fromString: LONGCURRENCY + ":2.2"),
+ amountEffective: try! Amount(fromString: LONGCURRENCY + ":2.4"),
+ noncePriv: "noncePriv",
+ proposalId: "proposalId",
+ contractTerms: terms,
+ contractTermsHash: "termsHash"
+ )
+ PaymentAcceptView(detailsForUri: details, acceptAction: {})
+ }
+}
diff --git a/TalerWallet1/Views/Payment/PaymentAcceptView.swift
b/TalerWallet1/Views/Payment/PaymentAcceptView.swift
deleted file mode 100644
index 26eb916..0000000
--- a/TalerWallet1/Views/Payment/PaymentAcceptView.swift
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-import SwiftUI
-import taler_swift
-import SymLog
-
-struct PaymentAcceptView: View {
- private let symLog = SymLogV()
- @ObservedObject var viewModel: PaymentURIModel
-
- var detailsForAmount: PaymentDetailsForUri
-
-// @Environment(\.dismiss) var dismiss // call dismiss() to get rid of
the sheet
- let navTitle = "Accept Payment"
- var cancelButton: some View {
- Button("Cancel6") { dismissTop() }
- }
-
- @State var confirmPayResult: ConfirmPayResult?
-
- var body: some View {
- symLog { Group {
- let raw = detailsForAmount.amountRaw
- let effective = detailsForAmount.amountEffective
- let fee = try! Amount.diff(raw, effective) // TODO: different
currencies
- Form {
- AmountView(title: "Amount to pay:",
- value: raw.readableDescription, color:
Color(UIColor.label))
- .padding(.bottom)
- AmountView(title: "Exchange fee:",
- value: fee.readableDescription, color:
Color("Outgoing"))
- .padding(.bottom)
- AmountView(title: "Coins to be spent:",
- value: effective.readableDescription, color:
Color("Outgoing"))
- }
- AwesomeButton(title: "Accept") {
- Task {
- do {
- confirmPayResult = try await
viewModel.confirmPay(detailsForAmount.proposalId)
- symLog.log(confirmPayResult as Any)
- if confirmPayResult?.type == "done" {
- // TODO: Show Hints that Payment was successfull
- // success
- } else {
- // TODO: show error
- }
- } catch {
- symLog.log(error.localizedDescription)
// TODO: error
- }
- dismissTop()
- }
- }
- }
- .navigationBarItems(leading: cancelButton)
- .navigationTitle(navTitle)
- }
- }
-}
diff --git a/TalerWallet1/Views/Payment/PaymentURIView.swift
b/TalerWallet1/Views/Payment/PaymentURIView.swift
index 38ad04c..1bb19c2 100644
--- a/TalerWallet1/Views/Payment/PaymentURIView.swift
+++ b/TalerWallet1/Views/Payment/PaymentURIView.swift
@@ -1,65 +1,122 @@
/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ * 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,
+// both from the shop's website. We show the payment details
struct PaymentURIView: View {
private let symLog = SymLogV()
- var url: URL
- @ObservedObject var viewModel: PaymentURIModel
+ let navTitle = String(localized: "Confirm Payment", comment:"pay merchant")
-// @Environment(\.dismiss) var dismiss // call dismiss() to get rid of
the sheet
- let navTitle = "Payment"
- var cancelButton: some View {
- Button("Cancel5") { dismissTop() }
+ @EnvironmentObject private var controller: Controller
+
+ // the scanned URL
+ let url: URL
+
+ @EnvironmentObject private var model: WalletModel
+
+ func acceptAction(detailsForUri: PaymentDetailsForUri) {
+ Task {
+ do {
+ let confirmPayResult = try await
model.confirmPayM(detailsForUri.proposalId)
+ symLog.log(confirmPayResult as Any)
+ if confirmPayResult.type != "done" {
+ controller.playSound(0)
+ // TODO: show error
+ }
+ } catch {
+ controller.playSound(0)
+ // TODO: error
+ symLog.log(error.localizedDescription)
+ }
+ dismissTop()
+ }
}
- @State var detailsForUri: PaymentDetailsForUri?
+ @State var detailsForUri: PaymentDetailsForUri? = nil
var body: some View {
- let badURL = "Error in URL: \(url)"
- VStack {
- if viewModel.paymentState == nil {
- LoadingView(backButtonHidden: false)
- } else { switch viewModel.paymentState {
- case .waitingForUriDetails:
- let _ = symLog.vlog("waitingForUriDetails")
- WithdrawProgressView(message: url.host ?? badURL) {
dismissTop() }
- .navigationTitle("Contacting Exchange")
- case .receivedUriDetails:
- let _ = symLog.vlog("waitingForUser")
- PaymentAcceptView(viewModel: viewModel, detailsForAmount:
detailsForUri!)
- default:
- symLog {
- Text("Payment")
- .navigationBarItems(leading: cancelButton)
- .navigationTitle(navTitle)
- }
- } }
- }.task {
- do { // TODO: cancelled
- symLog.log(".task")
- detailsForUri = try await
viewModel.preparePayForUri(url.absoluteString)
-// print(detailsForUri?.status)
-// print(detailsForUri?.amountRaw.description)
-// print(detailsForUri?.amountEffective.description)
-// print(detailsForUri?.proposalId)
- } catch {
- symLog.log(error.localizedDescription) // TODO:
error
+ if let detailsForUri {
+ ScrollViewReader { scrollView in
+ VStack {
+ let baseURL = detailsForUri.contractTerms.exchanges.first?.url
+ let raw = detailsForUri.amountRaw
+ let effective = detailsForUri.amountEffective
+ let currency = raw.currencyStr
+ let fee = try! Amount.diff(raw, effective) // TODO:
different currencies
+ ThreeAmountsView(topTitle: String(localized: "Amount to pay:"),
+ topAmount: raw, fee: fee,
+ bottomTitle: String(localized: "\(currency) to
be spent:"),
+ bottomAmount: effective,
+ large: true, pending: false, incoming:
false,
+ baseURL: baseURL)
+ // TODO: payment: popup with all possible exchanges, check fees
+ .safeAreaInset(edge: .bottom) {
+ Button(navTitle, action: { acceptAction(detailsForUri:
detailsForUri) })
+ .buttonStyle(TalerButtonStyle(type: .prominent))
+ .padding(.horizontal)
+ }
}
+ .navigationTitle(navTitle)
+ }
+ } else {
+ let badURL = "Error in URL: \(url)"
+ WithdrawProgressView(message: url.host ?? badURL)
+ .navigationTitle("Contacting Exchange")
+ .task {
+ do {
+ symLog.log(".task")
+ let details = try await
model.preparePayForUriM(url.absoluteString)
+ detailsForUri = details
+ } catch { // TODO: error
+ symLog.log(error.localizedDescription)
+ }
+ }
}
}
}
+// MARK: -
+struct PaymentURIView_Previews: PreviewProvider {
+ static var previews: some View {
+ let merchant = Merchant(name: "Merchant")
+ let extra = Extra(articleName: "articleName")
+ let product = Product(description: "description")
+ let terms = ContractTerms(amount: try! Amount(fromString: LONGCURRENCY
+ ":2.2"),
+ maxFee: try! Amount(fromString: LONGCURRENCY
+ ":0.2"),
+ maxWireFee: try! Amount(fromString:
LONGCURRENCY + ":0.2"),
+ merchant: merchant,
+ extra: extra,
+ summary: "summary",
+ timestamp: Timestamp.now(),
+ payDeadline: Timestamp.tomorrow(),
+ refundDeadline: Timestamp.tomorrow(),
+ wireTransferDeadline: Timestamp.tomorrow(),
+ merchantBaseURL: "merchantBaseURL",
+ fulfillmentURL: "fulfillmentURL",
+ publicReorderURL: "publicReorderURL",
+ auditors: [],
+ exchanges: [],
+ orderID: "orderID",
+ nonce: "nonce",
+ merchantPub: "merchantPub",
+ products: [product],
+ hWire: "hWire",
+ wireMethod: "wireMethod",
+ wireFeeAmortization: 0)
+ let details = PaymentDetailsForUri(
+ amountRaw: try! Amount(fromString: LONGCURRENCY + ":2.2"),
+ amountEffective: try! Amount(fromString: LONGCURRENCY + ":2.4"),
+ noncePriv: "noncePriv",
+ proposalId: "proposalId",
+ contractTerms: terms,
+ contractTermsHash: "termsHash"
+ )
+ let url = URL(string: "taler://pay/some_amount")!
+
+ PaymentURIView(url: url, detailsForUri: details)
+ }
+}
diff --git a/TalerWallet1/Views/Peer2peer/PaymentPurpose.swift
b/TalerWallet1/Views/Peer2peer/PaymentPurpose.swift
new file mode 100644
index 0000000..0a69e48
--- /dev/null
+++ 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")
+ .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all))
+ .onAppear {
+ DebugViewC.shared.setViewID(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/RequestPayment.swift
b/TalerWallet1/Views/Peer2peer/RequestPayment.swift
new file mode 100644
index 0000000..726bfaf
--- /dev/null
+++ b/TalerWallet1/Views/Peer2peer/RequestPayment.swift
@@ -0,0 +1,93 @@
+/*
+ * 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 by the user tapping "Request Payment" in the balances list
+struct RequestPayment: View {
+ private let symLog = SymLogV()
+
+ var scopeInfo: ScopeInfo
+ @Binding var centsToTransfer: UInt64
+ @Binding var summary: String
+
+ @EnvironmentObject private var model: WalletModel
+
+ @State private var peerPullCheck: CheckPeerPullCreditResponse? = nil
+ @State private var expireDays: UInt = 0
+
+ var body: some View {
+#if DEBUG
+ let _ = Self._printChanges()
+ let _ = symLog.vlog() // just to get the # to compare it with
.onAppear & onDisappear
+#endif
+ let currency = scopeInfo.currency
+ let navTitle = String(localized: "Request \(currency)")
+ let currencyField = CurrencyField(value: $centsToTransfer, currency:
currency)
+
+ ScrollViewReader { scrollView in
+ VStack {
+ CurrencyInputView(currencyField: currencyField,
+ title: String(localized: "Amount to receive:"))
+
+ let someCoins = SomeCoins(details: peerPullCheck)
+ QuiteSomeCoins(someCoins: someCoins, shouldShowFee: true,
+ currency: currency, amountEffective:
peerPullCheck?.amountEffective)
+
+ HStack {
+ let disabled = centsToTransfer == 0
+
+ NavigationLink(destination: LazyView {
+ PaymentPurpose(scopeInfo: scopeInfo,
+ centsToTransfer: centsToTransfer,
+ 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)
+ .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all))
+ .navigationTitle(navTitle)
+ .onAppear { // make CurrencyField show the keyboard
+ DebugViewC.shared.setViewID(VIEW_INVOICE_P2P)
+ symLog.log("❗️Yikes \(navTitle) onAppear")
+ }
+ .onDisappear {
+ symLog.log("❗️Yikes \(navTitle) onDisappear")
+ }
+ .task(id: centsToTransfer) {
+ let amount = Amount.amountFromCents(currency, centsToTransfer)
+ do {
+ 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
+ }
+ }
+ }
+}
+// MARK: -
+#if DEBUG
+//struct ReceiveAmount_Previews: PreviewProvider {
+// static var scopeInfo = ScopeInfo(type: ScopeInfo.ScopeInfoType.exchange,
exchangeBaseUrl: DEMOEXCHANGE, currency: LONGCURRENCY)
+// static var previews: some View {
+// let amount = Amount(currency: LONGCURRENCY, integer: 10, fraction: 0)
+// RequestPayment(exchangeBaseUrl: DEMOEXCHANGE, currency: LONGCURRENCY)
+// }
+//}
+#endif
diff --git a/TalerWallet1/Views/Peer2peer/SendAmount.swift
b/TalerWallet1/Views/Peer2peer/SendAmount.swift
new file mode 100644
index 0000000..53d736e
--- /dev/null
+++ b/TalerWallet1/Views/Peer2peer/SendAmount.swift
@@ -0,0 +1,118 @@
+/*
+ * 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 by the user tapping "Send Coins" in the balances list
+struct SendAmount: View {
+ private let symLog = SymLogV()
+
+ let amountAvailable: Amount // TODO: GetMaxPeerPushAmount
+ @Binding var centsToTransfer: UInt64
+ @Binding var summary: String
+
+ @EnvironmentObject private var model: WalletModel
+
+ @State var peerPushCheck: CheckPeerPushDebitResponse? = nil
+ @State private var expireDays: UInt = 0
+
+ private func fee(ppCheck: CheckPeerPushDebitResponse?) -> String {
+ do {
+ if let ppCheck {
+ // Outgoing: fee = effective - raw
+ let fee = try ppCheck.amountEffective - ppCheck.amountRaw
+ return fee.readableDescription
+ }
+ } catch {}
+ return ""
+ }
+
+ var body: some View {
+#if DEBUG
+ let _ = Self._printChanges()
+ let _ = symLog.vlog() // just to get the # to compare it with
.onAppear & onDisappear
+#endif
+ let currency = amountAvailable.currencyStr
+ let navTitle = String(localized: "Send \(currency)")
+ let currencyField = CurrencyField(value: $centsToTransfer, currency:
currency)
+
+ let fee = fee(ppCheck: peerPushCheck)
+// ScrollViewReader { scrollView in
+ VStack {
+ let available = amountAvailable.readableDescription
+ Text("Available: \(available)")
+ .font(.title3)
+ CurrencyInputView(currencyField: currencyField,
+ title: String(localized: "Amount to send:"))
+
+ Text("+ \(fee) payment fee")
+ .foregroundColor(.red)
+ .padding(4)
+
+ HStack {
+ let disabled = centsToTransfer == 0 // TODO: check
amountAvailable
+
+ NavigationLink(destination: LazyView {
+ 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)
+ .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all))
+ .navigationTitle(navTitle)
+ .onAppear { // make CurrencyField show the keyboard
+ DebugViewC.shared.setViewID(VIEW_SEND_P2P)
+ symLog.log("❗️Yikes SendAmount onAppear")
+ }
+ .onDisappear {
+ symLog.log("❗️Yikes SendAmount onDisappear")
+ }
+ .task(id: centsToTransfer) {
+ let amount = Amount.amountFromCents(currency, centsToTransfer)
+ do {
+ 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
+ }
+ }
+ }
+}
+// MARK: -
+#if DEBUG
+struct SendAmount_Container : View {
+ @State private var centsToTransfer: UInt64 = 510
+ @State private var summary: String = ""
+
+ var body: some View {
+ let amount = Amount(currency: LONGCURRENCY, integer: 10, fraction: 0)
+ SendAmount(amountAvailable: amount,
+ centsToTransfer: $centsToTransfer,
+ summary: $summary)
+ }
+}
+
+struct SendAmount_Previews: PreviewProvider {
+ static var previews: some View {
+ SendAmount_Container()
+ }
+}
+#endif
diff --git a/TalerWallet1/Views/Peer2peer/SendNow.swift
b/TalerWallet1/Views/Peer2peer/SendNow.swift
new file mode 100644
index 0000000..bc07dbc
--- /dev/null
+++ b/TalerWallet1/Views/Peer2peer/SendNow.swift
@@ -0,0 +1,93 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import SwiftUI
+import taler_swift
+import SymLog
+
+struct SendNow: View {
+ private let symLog = SymLogV()
+#if DEBUG
+ @AppStorage("developerMode") var developerMode: Bool = true
+#else
+ @AppStorage("developerMode") var developerMode: Bool = false
+#endif
+
+ let amountToSend: Amount?
+ let amountToReceive: Amount?
+ let summary: String
+ let expireDays: UInt
+
+ @EnvironmentObject private var model: WalletModel
+
+ @State var talerURI: String = ""
+
+ var body: some View {
+ VStack {
+ if talerURI.isEmpty {
+ LoadingView(backButtonHidden: true)
+ } else {
+ QRCodeDetailView(talerURI: talerURI,
+ incoming: amountToSend == nil)
+ .padding()
+ Text("The QR code can also be copied and shared from
Transactions later")
+ .fixedSize(horizontal: false, vertical: true)
+ .font(.subheadline)
+ .padding(.vertical, 20)
+
+ Spacer()
+ Button("Done") {
+ withAnimation(){ ViewState.shared.popToRootView() }
+ }
+ .buttonStyle(TalerButtonStyle(type: .prominent))
+ .padding()
+
+ }
+ }
+// .frame(maxWidth: .infinity, maxHeight: .infinity, alignment:
.leading)
+// .padding(.horizontal)
+ .interactiveDismissDisabled() // can only use "Done" button to
dismiss
+ .navigationBarHidden(true) // no back button, no title
+ .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all))
+ .task {
+ symLog.log(".task")
+ do {
+ // generate talerURI
+ var timestamp = developerMode ?
Timestamp.inSomeMinutes(expireDays > 20 ? (24*60)
+ :
expireDays > 5 ? 60 : 3)
+ :
Timestamp.inSomeDays(expireDays)
+ if let amountToSend {
+ let terms = PeerContractTerms(amount: amountToSend,
+ summary: summary,
+ purse_expiration: timestamp)
+ // TODO: user might choose baseURL
+ 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: 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
new file mode 100644
index 0000000..660c64f
--- /dev/null
+++ b/TalerWallet1/Views/Peer2peer/SendPurpose.swift
@@ -0,0 +1,119 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import SwiftUI
+import taler_swift
+import SymLog
+
+struct SendPurpose: View {
+ private let symLog = SymLogV()
+ @FocusState private var isFocused: Bool
+
+ let amountAvailable: Amount
+ let centsToTransfer: UInt64
+ let fee: String
+ @Binding var summary: 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) ?? ""
+ }
+
+ var body: some View {
+ let amount = Amount.amountFromCents(amountAvailable.currencyStr,
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)
+
+ // TODO: compute max Expiration day from peerPushCheck to
disable 30 (and even 7)
+ SelectDays(selected: $expireDays, maxExpiration: 35)
+ .disabled(false)
+ .padding(.bottom)
+
+ let disabled = (expireDays == 0) || (summary.count < 1) //
TODO: check amountAvailable
+
+ NavigationLink(destination: LazyView {
+ SendNow(amountToSend: amount,
+ amountToReceive: nil,
+ summary: summary, expireDays: expireDays)
+ }) {
+ Text("Send \(label) \(amountAvailable.currencyStr) now")
+ .font(buttonFont)
+ }
+ .buttonStyle(TalerButtonStyle(type: .prominent))
+ .disabled(disabled)
+
+ Spacer()
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(.horizontal)
+ }
+ .navigationTitle("To another Wallet")
+ .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all))
+ .onAppear {
+ DebugViewC.shared.setViewID(VIEW_SEND_PURPOSE)
+ print("❗️ SendPurpose onAppear")
+ }
+ .onDisappear {
+ print("❗️ SendPurpose onDisappear")
+// deactivateAction()
+ }
+ .task {
+ symLog.log(".task")
+ do {
+
+ } catch { // TODO: error
+ symLog.log(error.localizedDescription)
+ }
+ }
+ }
+
+}
+// MARK: -
+#if DEBUG
+struct SendPurpose_Previews: PreviewProvider {
+ static var previews: some View {
+ @State var summary: String = ""
+ @State var expireDays: UInt = 0
+ let amount = Amount(currency: LONGCURRENCY, integer: 10, fraction: 0)
+ SendPurpose(amountAvailable: amount,
+ centsToTransfer: 543,
+ fee: "0,43",
+ summary: $summary,
+ expireDays: $expireDays)
+// { print("deactivateAction") }
+ }
+}
+#endif
diff --git a/TalerWallet1/Views/Pending/PendingModel.swift
b/TalerWallet1/Views/Pending/PendingModel.swift
deleted file mode 100644
index 6b0e38e..0000000
--- a/TalerWallet1/Views/Pending/PendingModel.swift
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-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 {
- @Published var pendingOperations: [PendingOperation]?
-}
-// 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)
- }
-
-}
-//let pending1 = ["type": "exchange-update",
-// EXCHANGEBASEURL: "https://exchange.demo.taler.net/",
-// "id": "exchange-update:https://exchange.demo.taler.net/",
-// "timestampDue": ["t_ms": 1669931055000],
-// "isDue": false,
-// "isLongpolling": false,
-// "givesLifeness": false] as [String : Any]
-//
-//let pending2 = ["type": "exchange-check-refresh",
-// EXCHANGEBASEURL: "https://exchange.demo.taler.net/",
-// "id": "exchange-update:https://exchange.demo.taler.net/",
-// "timestampDue": ["t_ms": 1670013862000],
-// "isDue": false,
-// "isLongpolling": false,
-// "givesLifeness": false] as [String : Any]
-// MARK: -
-extension PendingModel {
- @MainActor func update() async throws {
- do {
- let request = GetPendingOperations()
- let response = try await sendRequest(request, ASYNCDELAY)
- pendingOperations = response.pendingOperations
- }
- }
-}
diff --git a/TalerWallet1/Views/Pending/PendingOpsListView.swift
b/TalerWallet1/Views/Pending/PendingOpsListView.swift
deleted file mode 100644
index a4d9e61..0000000
--- a/TalerWallet1/Views/Pending/PendingOpsListView.swift
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-import SwiftUI
-import SymLog
-
-struct PendingOpsListView: View {
- private let symLog = SymLogV()
- let navTitle = "Pending"
-
- @ObservedObject var viewModel: PendingModel
- var hamburgerAction: () -> Void
-
- var body: some View {
- let reloadAction = viewModel.update
- VStack {
- if viewModel.pendingOperations == nil {
- symLog { LoadingView(backButtonHidden: true) }
- } else {
- symLog { NavigationView {
- Content(symLog: symLog, viewModel: viewModel,
reloadAction: reloadAction)
- .navigationBarItems(leading: HamburgerButton(action:
hamburgerAction))
- } }
- .navigationTitle(navTitle)
- }
- }.task {
- symLog.log(".task")
- do {
- try await reloadAction()
- } catch {
- // TODO: show error
- symLog.log(error.localizedDescription)
- }
- }
- }
-}
-// MARK: -
-extension PendingOpsListView {
- struct Content: View {
- let symLog: SymLogV?
- @ObservedObject var viewModel: PendingModel
-// @EnvironmentObject var controller : Controller
- var reloadAction: () async throws -> ()
-
- var body: some View {
- Group {
- List(viewModel.pendingOperations!, id: \.self) { pendingOp in
- PendingOpView(pendingOp: pendingOp)
- }
- .navigationBarTitleDisplayMode(.large) // .inline
- .refreshable {
- do {
- symLog?.log("refreshing")
- try await reloadAction()
- } catch {
- // TODO: error
- symLog?.log(error.localizedDescription)
- }
- }
- }
- }
- }
-}
diff --git a/TalerWallet1/Views/Pending/PendingOpView.swift
b/TalerWallet1/Views/Settings/Pending/PendingOpView.swift
similarity index 62%
rename from TalerWallet1/Views/Pending/PendingOpView.swift
rename to TalerWallet1/Views/Settings/Pending/PendingOpView.swift
index 12c2924..16a9aab 100644
--- a/TalerWallet1/Views/Pending/PendingOpView.swift
+++ b/TalerWallet1/Views/Settings/Pending/PendingOpView.swift
@@ -1,17 +1,6 @@
/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
*/
import SwiftUI
import taler_swift
@@ -24,7 +13,9 @@ struct PendingOpView: View {
var body: some View {
Section {
- Text(pendingOp.exchangeBaseUrl)
+ if let baseURL = pendingOp.exchangeBaseUrl {
+ Text(baseURL)
+ }
Text(pendingOp.id)
.font(.caption)
Toggle("isLongPolling", isOn: $polling)
@@ -47,18 +38,20 @@ struct PendingOpView: View {
}
}
}
-
+// MARK: -
+#if DEBUG
struct PendingOpView_Previews: PreviewProvider {
static var pending1 = PendingOperation(type: "exchange-check-refresh",
- exchangeBaseUrl:
"https://exchange.demo.taler.net/",
- id:
"exchange-update:https://exchange.demo.taler.net/",
+ exchangeBaseUrl: DEMOEXCHANGE,
+ id: "exchange-update:" +
DEMOEXCHANGE,
isLongpolling: false,
givesLifeness: true,
isDue: false,
- timestampDue: Timestamp(from:1669931055000))
+ timestampDue: Timestamp(from:1700000000000))
static var previews: some View {
- Form {
+ List {
PendingOpView(pendingOp: pending1)
}
}
}
+#endif
diff --git a/TalerWallet1/Views/Settings/Pending/PendingOpsListView.swift
b/TalerWallet1/Views/Settings/Pending/PendingOpsListView.swift
new file mode 100644
index 0000000..66691b6
--- /dev/null
+++ b/TalerWallet1/Views/Settings/Pending/PendingOpsListView.swift
@@ -0,0 +1,58 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import SwiftUI
+import SymLog
+
+struct PendingOpsListView: View {
+ private let symLog = SymLogV(0)
+ let navTitle = String(localized: "Pending")
+
+ @EnvironmentObject private var model: WalletModel
+
+ @State var pendingOperations: [PendingOperation] = []
+
+ var body: some View {
+#if DEBUG
+ let _ = Self._printChanges()
+ let _ = symLog.vlog() // just to get the # to compare it with
.onAppear & onDisappear
+#endif
+ let reloadAction = model.getPendingOperationsM
+ Content(symLog: symLog, pendingOperations: $pendingOperations,
reloadAction: reloadAction)
+ .navigationTitle(navTitle)
+ .task {
+ symLog.log(".task")
+ pendingOperations = await reloadAction()
+ }
+ }
+}
+// MARK: -
+extension PendingOpsListView {
+ struct Content: View {
+ 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()
+ let _ = symLog?.vlog() // just to get the # to compare it
with .onAppear & onDisappear
+#endif
+ ScrollViewReader { scrollView in
+ List(pendingOperations, id: \.self) { pendingOp in
+ PendingOpView(pendingOp: pendingOp)
+ }
+ .listStyle(myListStyle.style).anyView
+ .navigationBarTitleDisplayMode(.large)
+ .onAppear() {
+ DebugViewC.shared.setViewID(VIEW_PENDING)
+ }
+ .refreshable {
+ symLog?.log("refreshing")
+ pendingOperations = await reloadAction()
+ }
+ }
+ }
+ }
+}
diff --git a/TalerWallet1/Views/Settings/SettingsItem.swift
b/TalerWallet1/Views/Settings/SettingsItem.swift
index 7735c97..b8443fe 100644
--- a/TalerWallet1/Views/Settings/SettingsItem.swift
+++ b/TalerWallet1/Views/Settings/SettingsItem.swift
@@ -1,19 +1,7 @@
/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
*/
-
import SwiftUI
struct SettingsItem<Content: View>: View {
@@ -44,16 +32,21 @@ struct SettingsItem<Content: View>: View {
}.padding([.bottom], 4)
}
}
-
+// MARK: -
struct SettingsToggle: View {
var name: String
@Binding var value: Bool
var description: String?
+ var action: () -> Void = {}
var body: some View {
VStack {
- Toggle(name, isOn: $value.animation(.spring()))
+ Toggle(name, isOn: $value.animation())
.font(.title2)
+ .onChange(of: value) { value in
+ action()
+ }
+
if let desc = description {
Text(desc)
.frame(maxWidth: .infinity, alignment: .leading)
@@ -62,9 +55,8 @@ struct SettingsToggle: View {
}.padding([.bottom], 4)
}
}
-
-
-
+// MARK: -
+#if DEBUG
struct SettingsItemPreview : View {
@State var developerMode: Bool = false
@@ -78,9 +70,7 @@ struct SettingsItemPreview : View {
struct SettingsItem_Previews: PreviewProvider {
static var previews: some View {
List {
- NavigationLink { } label: {
- SettingsItem (name: "Exchanges", description: "Manage list of
exchanges known to this wallet") {}
- }
+ SettingsItem (name: "Exchanges", description: "Manage list of
exchanges known to this wallet") {}
SettingsItemPreview()
SettingsItem(name: "Save Logfile", description: "Help debugging
wallet-core") {
Button("Save") {
@@ -91,3 +81,4 @@ struct SettingsItem_Previews: PreviewProvider {
}
}
}
+#endif
diff --git a/TalerWallet1/Views/Settings/SettingsView.swift
b/TalerWallet1/Views/Settings/SettingsView.swift
index b84b242..51cedff 100644
--- a/TalerWallet1/Views/Settings/SettingsView.swift
+++ b/TalerWallet1/Views/Settings/SettingsView.swift
@@ -1,17 +1,6 @@
/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
*/
import SwiftUI
import taler_swift
@@ -21,115 +10,178 @@ import SymLog
* Backup
* Last backup: 5 hr. ago
*
- *
* Debug log
* View/send internal log
*
- *
* Reset Wallet (dangerous!)
* Throws away your money
*/
struct SettingsView: View {
private let symLog = SymLogV(0)
-
- @EnvironmentObject var controller: Controller
+ let navTitle: String
+
+#if DEBUG
+ @AppStorage("developerMode") var developerMode: Bool = true
+#else
@AppStorage("developerMode") var developerMode: Bool = false
+#endif
+ @AppStorage("playSounds") var playSounds: Bool = false
@AppStorage("developDelay") var developDelay: Bool = false
+ @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic
- var showSidebar: () -> Void
- init(showSidebar: @escaping () -> Void) {
- self.showSidebar = showSidebar
- }
+ var hamburgerAction: () -> Void
+
+ @EnvironmentObject private var model: WalletModel
@State private var checkDisabled = false
@State private var withDrawDisabled = false
+#if DEBUG
+ @State private var diagnosticModeEnabled = true
+#else
+ @State private var diagnosticModeEnabled =
UserDefaults.standard.bool(forKey: "diagnostic_mode_enabled")
+#endif
+ @State private var showDevelopItems = false
var body: some View {
- symLog { NavigationView {
+#if DEBUG
+ let _ = Self._printChanges()
+ let _ = symLog.vlog() // just to get the # to compare it with
.onAppear & onDisappear
+#endif
+ let walletCore = WalletCore.shared
+ Group {
List {
- NavigationLink {
- ExchangeListView(viewModel: controller.exchangeModel)
- } label: {
- SettingsItem(name: "Exchanges", description: "Manage list
of exchanges known to this wallet") {}
+ SettingsToggle(name: String(localized: "Play Payment Sounds"),
value: $playSounds,
+ description: String(localized: "After a transaction
finished"))
+ HStack {
+ Text("Liststyle:")
+ .font(.title2)
+ Spacer()
+ Picker(selection: $myListStyle) {
+ ForEach(MyListStyle.allCases, id: \.self) {
+ Text($0.displayName.capitalized).tag($0)
+ .font(.title2)
+ }
+ } label: {}
+ .pickerStyle(.menu)
+// .frame(alignment: .trailing)
+// .background(WalletColors().buttonBackColor(pressed:
false, disabled: false)) TODO: RoundRect
}
- SettingsToggle(name: "Developer Mode", value: $developerMode,
- description: "More information intended for
debugging")
- if developerMode { // show or hide the following items
- let walletCore = controller.walletCore
- SettingsToggle(name: "Set 2 seconds delay", value:
$developDelay,
- description: "After each wallet-core
action")
- .onChange(of: developDelay, perform: { developDelay in
- walletCore.developDelay = developDelay
- })
+ if diagnosticModeEnabled {
+ SettingsToggle(name: String(localized: "Developer Mode"),
value: $developerMode,
+ description: String(localized: "More
information intended for debugging")) {
+ DebugViewC.shared.setViewID(VIEW_SETTINGS)
+ withAnimation { showDevelopItems = developerMode }
+ }
+ if showDevelopItems { // show or hide the following items
+ NavigationLink { // whole row like in a
tableView
+ LazyView { PendingOpsListView() }
+ } label: {
+ SettingsItem(name: String(localized: "Pending
Operations"), description: String(localized: "Exchange not yet ready...")) {}
+ }
+ SettingsToggle(name: String(localized: "Set 2 seconds
delay"), value: $developDelay,
+ description: String(localized: "After
each wallet-core action"))
+ .onChange(of: developDelay, perform: { developDelay in
+ walletCore.developDelay = developDelay
+ })
- SettingsItem(name: "Withdraw KUDOS", description: "Get
money for testing") {
- Button("Withdraw") {
- withDrawDisabled = true // don't run twice
- Task {
- let testModel: ExchangeTestModel =
ExchangeTestModel(walletCore: walletCore)
- symLog.log("Withdrawing")
- do {
- try await testModel.loadTestKudos()
- } catch {
- // TODO: show error
- symLog.log(error.localizedDescription)
+ SettingsItem(name: String(localized: "Withdraw
\(DEMOCURRENCY)"), description: String(localized: "Get money for testing")) {
+ Button("Withdraw") {
+ withDrawDisabled = true // don't run twice
+ Task {
+ symLog.log("Withdraw TestKUDOS")
+ do {
+ try await model.loadTestKudosM()
+ } catch { // TODO: show error
+ symLog.log(error.localizedDescription)
+ }
}
}
+ .buttonStyle(.bordered)
+ .disabled(withDrawDisabled)
}
- .buttonStyle(.bordered)
- .disabled(withDrawDisabled)
- }
- SettingsItem(name: "Run Integration Test", description:
"Check if wallet-core works") {
- Button("Check") {
- checkDisabled = true // don't run twice
- Task {
- let testModel: ExchangeTestModel =
ExchangeTestModel(walletCore: walletCore)
- symLog.log("running integration test")
- do {
- try await testModel.runIntegrationTest()
- } catch {
- // TODO: show error
- symLog.log(error.localizedDescription)
+ SettingsItem(name: String(localized: "Run Integration
Test"),
+ description: String(localized: "Perform
basic test transactions")) {
+ Button("Test 1") {
+ checkDisabled = true // don't run twice
+ Task {
+ symLog.log("running integration test")
+ do {
+ try await
model.runIntegrationTestM(newVersion: false)
+ } catch { // TODO: show error
+ symLog.log(error.localizedDescription)
+ }
}
}
+ .buttonStyle(.bordered)
+ .disabled(checkDisabled)
}
- .buttonStyle(.bordered)
- .disabled(checkDisabled)
- }
- SettingsItem(name: "Save Logfile", description: "Help
debugging wallet-core") {
- Button("Save") {
- symLog.log("Saving Log")
- // FIXME: Save Logfile
- }
- .buttonStyle(.bordered)
- .disabled(true)
- }
- VStack {
- SettingsItem(name: "App Version") {
- Text("\(Bundle.main.releaseVersionNumberPretty)")
- }
- SettingsItem(name: "Wallet Core Version") {
- Text("\(walletCore.versionInfo!.version)")
- }
- SettingsItem(name: "Wallet Core DevMode") {
- Text("\(walletCore.versionInfo!.devMode ? "YES" :
"NO")")
- }
- SettingsItem(name: "Supported Exchange Versions") {
- Text("\(walletCore.versionInfo!.exchange)")
- }
- SettingsItem(name: "Supported Merchant Versions") {
- Text("\(walletCore.versionInfo!.merchant)")
+ SettingsItem(name: String(localized: "Run Integration
Test V2"),
+ description: String(localized: "Perform
more test transactions")) {
+ Button("Test 2") {
+ checkDisabled = true // don't run twice
+ Task {
+ symLog.log("running integration test V2")
+ do {
+ try await
model.runIntegrationTestM(newVersion: true)
+ } catch { // TODO: show error
+ symLog.log(error.localizedDescription)
+ }
+ }
+ }
+ .buttonStyle(.bordered)
+ .disabled(checkDisabled)
}
- SettingsItem(name: "Used Bank") {
- Text("\(walletCore.versionInfo!.bank)")
+ SettingsItem(name: String(localized: "Save Logfile"),
+ description: String(localized: "Help
debugging wallet-core")) {
+ Button("Save") {
+ symLog.log("Saving Log")
+ // FIXME: Save Logfile
+ }
+ .buttonStyle(.bordered)
+ .disabled(true)
}
}
}
+
+ VStack {
+ SettingsItem(name: String(localized: "App Version")) {
+ Text("\(Bundle.main.releaseVersionNumberPretty)")
+ }
+ SettingsItem(name: String(localized: "Wallet Core
Version")) {
+ Text("\(walletCore.versionInfo!.version)")
+ }
+ SettingsItem(name: String(localized: "Wallet Core
DevMode")) {
+ Text("\(walletCore.versionInfo!.devMode ? "YES" :
"NO")")
+ }
+ SettingsItem(name: String(localized: "Supported Exchange
Versions")) {
+ Text("\(walletCore.versionInfo!.exchange)")
+ }
+ SettingsItem(name: String(localized: "Supported Merchant
Versions")) {
+ Text("\(walletCore.versionInfo!.merchant)")
+ }
+ SettingsItem(name: String(localized: "Used Bank")) {
+ Text("\(walletCore.versionInfo!.bank)")
+ }
+ }
}
- .navigationTitle("Settings")
- .navigationBarItems(leading: HamburgerButton(action:
showSidebar))
- } } // symLog
+ .listStyle(myListStyle.style).anyView
+ }
+ .navigationTitle(navTitle)
+ .navigationBarItems(leading: HamburgerButton(action: hamburgerAction))
+ .onAppear() {
+ showDevelopItems = developerMode
+ DebugViewC.shared.setViewID(VIEW_SETTINGS)
+ }
+#if !DEBUG
+ .onReceive(
+ NotificationCenter.default
+ .publisher(for: UserDefaults.didChangeNotification)
+ .receive(on: RunLoop.main)
+ ) { _ in // user changed Diagnostic Mode in iOS Settings.app
+ withAnimation { diagnosticModeEnabled =
UserDefaults.standard.bool(forKey: "diagnostic_mode_enabled") }
+ }
+#endif
}
}
extension Bundle {
@@ -143,11 +195,11 @@ extension Bundle {
return "v\(releaseVersionNumber ?? "1.0.0")"
}
}
-
+// MARK: -
+#if DEBUG
struct SettingsView_Previews: PreviewProvider {
static var previews: some View {
- SettingsView {
-
- }
+ SettingsView(navTitle: "Settings") { }
}
}
+#endif
diff --git a/TalerWallet1/Views/Sheets/P2P_Sheets/P2pAcceptDone.swift
b/TalerWallet1/Views/Sheets/P2P_Sheets/P2pAcceptDone.swift
new file mode 100644
index 0000000..6eb706d
--- /dev/null
+++ b/TalerWallet1/Views/Sheets/P2P_Sheets/P2pAcceptDone.swift
@@ -0,0 +1,63 @@
+/*
+ * 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 transactionId: String
+ let incoming: Bool
+
+ @EnvironmentObject private var model: WalletModel
+ @EnvironmentObject private var controller: Controller
+
+ @State private var finished: Bool = false
+
+ 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
+ let navTitle = incoming ? String(localized: "Received P2P")
+ : String(localized: "Paid P2P")
+ ScrollViewReader { scrollView in
+ VStack {
+ TransactionDetailView(transactionId: transactionId,
+ reloadAction: reloadOneAction,
+ doneAction: { dismissTop() })
+ .navigationBarBackButtonHidden(true)
+ .interactiveDismissDisabled() // can only use "Done"
button to dismiss
+ .navigationTitle(navTitle)
+ }.onAppear() {
+ symLog.log("onAppear")
+ DebugViewC.shared.setSheetID(SHEET_RCV_P2P_ACCEPT)
+ }.task {
+ do {
+ if incoming {
+ _ = try await model.acceptPeerPushCreditM(transactionId)
+ } else {
+ _ = try await model.confirmPeerPullDebitM(transactionId)
+ }
+ finished = true
+ } catch { // TODO: error
+ symLog.log(error.localizedDescription)
+ controller.playSound(0)
+ }
+ }
+ }
+ }
+}
+// MARK: -
+struct P2pAcceptDone_Previews: PreviewProvider {
+ static var previews: some View {
+ P2pAcceptDone(transactionId: "some ID", incoming: true)
+ }
+}
diff --git a/TalerWallet1/Views/Sheets/P2P_Sheets/P2pPayURIView.swift
b/TalerWallet1/Views/Sheets/P2P_Sheets/P2pPayURIView.swift
new file mode 100644
index 0000000..d3cb92e
--- /dev/null
+++ b/TalerWallet1/Views/Sheets/P2P_Sheets/P2pPayURIView.swift
@@ -0,0 +1,71 @@
+/*
+ * 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 SendInvoice. We show the P2P details.
+struct P2pPayURIView: View {
+ private let symLog = SymLogV()
+ let navTitle = String(localized: "Pay P2P Invoice")
+
+ // the scanned URL
+ let url: URL
+
+ @EnvironmentObject private var model: WalletModel
+
+ @State private var peerPullDebitResponse: PreparePeerPullDebitResponse?
+
+ var body: some View {
+ VStack {
+ if let peerPullDebitResponse {
+ List {
+ let raw = peerPullDebitResponse.amountRaw
+ let effective = peerPullDebitResponse.amountEffective
+ let currency = raw.currencyStr
+ let fee = try! Amount.diff(raw, effective)
+ ThreeAmountsView(topTitle: String(localized: "Amount to
pay:"),
+ topAmount: raw, fee: fee,
+ bottomTitle: String(localized:
"\(currency) to be spent:"),
+ bottomAmount: effective,
+ large: false, pending: false, incoming:
false,
+ baseURL: nil)
+ }
+ .navigationTitle(navTitle)
+
+ NavigationLink(destination: LazyView {
+ P2pAcceptDone(transactionId:
peerPullDebitResponse.transactionId,
+ incoming: false)
+ }) {
+ Text("Confirm Payment", comment:"pay P2P invoice")
// SHEET_PAY_P2P
+ }.buttonStyle(TalerButtonStyle(type: .prominent))
+ .padding()
+ } else {
+ WithdrawProgressView(message: url.host ?? "Yikes - no valid
URL")
+ .navigationTitle("Contacting Exchange")
+ }
+ }
+ .onAppear() {
+ symLog.log("onAppear")
+ DebugViewC.shared.setSheetID(SHEET_PAY_P2P)
+ }
+ .task {
+ do { // TODO: cancelled
+ symLog.log(".task")
+ let ppDebitResponse = try await
model.preparePeerPullDebitM(url.absoluteString)
+ peerPullDebitResponse = ppDebitResponse
+ } catch { // TODO: error
+ symLog.log(error.localizedDescription)
+ peerPullDebitResponse = nil
+ }
+ }
+
+ }
+}
+// MARK: -
+//#Preview {
+// P2pPayURIView(url: <#T##URL#>, model: <#T##WalletModel#>)
+//}
diff --git a/TalerWallet1/Views/Sheets/P2P_Sheets/P2pReceiveURIView.swift
b/TalerWallet1/Views/Sheets/P2P_Sheets/P2pReceiveURIView.swift
new file mode 100644
index 0000000..f40a5bf
--- /dev/null
+++ b/TalerWallet1/Views/Sheets/P2P_Sheets/P2pReceiveURIView.swift
@@ -0,0 +1,78 @@
+/*
+ * 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 P2P details - but first the ToS
must be accepted.
+struct P2pReceiveURIView: View {
+ private let symLog = SymLogV()
+ let navTitle = String(localized: "Accept P2P Receive")
+
+ // the scanned URL
+ let url: URL
+
+ @EnvironmentObject private var model: WalletModel
+
+ @State private var peerPushCreditResponse: PreparePeerPushCreditResponse?
+
+ var body: some View {
+ VStack {
+ if let peerPushCreditResponse {
+ List {
+ let raw = peerPushCreditResponse.amountRaw
+ let effective = peerPushCreditResponse.amountEffective
+ let currency = raw.currencyStr
+ let fee = try! Amount.diff(raw, effective)
+ 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(transactionId:
peerPushCreditResponse.transactionId,
+ incoming: true)
+ }) {
+ Text("Accept 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/QRSheet.swift
b/TalerWallet1/Views/Sheets/QRSheet.swift
new file mode 100644
index 0000000..6385ec4
--- /dev/null
+++ b/TalerWallet1/Views/Sheets/QRSheet.swift
@@ -0,0 +1,52 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import SwiftUI
+import CodeScanner
+import SymLog
+import AVFoundation
+
+struct QRSheet: View {
+ private let symLog = SymLogV()
+ @State private var scannedCode: String?
+
+ var body: some View {
+ Group {
+ if scannedCode != nil {
+ let _ = print(scannedCode as Any) // TODO: logging
+
+ if let scannedURL = URL(string: scannedCode!) {
+ let scheme = scannedURL.scheme
+ if scheme == "taler" {
+ URLSheet(urlToOpen: scannedURL)
+ } else {
+ let _ = print(scannedURL) // TODO: logging
+ ErrorView(errortext: scannedURL.absoluteString)
+ }
+ } else {
+ ErrorView(errortext: scannedCode)
+ }
+ } else {
+ CodeScannerView(codeTypes: [AVMetadataObject.ObjectType.qr],
showViewfinder: true) { response in
+ switch response {
+ case .success(let result):
+ symLog.log("Found code: \(result.string)")
+ scannedCode = result.string
+ case .failure(let error):
+ ErrorView(errortext: error.localizedDescription)
+ }
+ }
+ // TODO: errorAlert
+ }
+ }
+ }
+
+}
+// MARK: -
+//struct PaySheet_Previews: PreviewProvider {
+// static var previews: some View {
+ // needs BackendManager
+// URLSheet(urlToOpen: URL(string: "ftp://this.URL.is.invalid")!)
+// }
+//}
diff --git a/TalerWallet1/Views/Sheets/ShareSheet.swift
b/TalerWallet1/Views/Sheets/ShareSheet.swift
new file mode 100644
index 0000000..e56148f
--- /dev/null
+++ b/TalerWallet1/Views/Sheets/ShareSheet.swift
@@ -0,0 +1,40 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import Foundation
+import UIKit
+import SymLog
+
+// You can control the appearance of the link by providing view content.
+// For example, you can use a Label to display a link with a custom icon:
+// ShareLink(item: URL(string:
"https://developer.apple.com/xcode/swiftui/")!) {
+// Label("Share", image: "MyCustomShareIcon")
+// }
+// If you only wish to customize the link’s title, you can use one of the
convenience
+// initializers that takes a string and creates a Label for you:
+// ShareLink("Share URL", item: URL(string:
"https://developer.apple.com/xcode/swiftui/")!)
+// The link can share any content that is Transferable.
+// Many framework types, like URL, already conform to this protocol.
+
+
+public class ShareSheet: ObservableObject {
+ private let symLog = SymLogC()
+
+ static func shareSheet(url: String) {
+ let url = URL(string: url)
+ let activityView = UIActivityViewController(activityItems: [url!],
applicationActivities: nil)
+
+ let allScenes = UIApplication.shared.connectedScenes
+ let scene = allScenes.first { $0.activationState == .foregroundActive }
+
+ if let windowScene = scene as? UIWindowScene {
+ windowScene.keyWindow?.rootViewController?.present(activityView,
animated: true, completion: nil)
+ }
+ }
+
+ init() {
+ symLog.log("init")
+
+ }
+}
diff --git a/TalerWallet1/Views/Sheets/Sheet.swift
b/TalerWallet1/Views/Sheets/Sheet.swift
new file mode 100644
index 0000000..43b1895
--- /dev/null
+++ b/TalerWallet1/Views/Sheets/Sheet.swift
@@ -0,0 +1,42 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import SwiftUI
+import SymLog
+import os.log
+
+struct Sheet: View {
+ private let symLog = SymLogV()
+ @Environment(\.dismiss) var dismiss // call dismiss() to get rid of
the sheet
+ @EnvironmentObject private var debugViewC: DebugViewC
+
+ var sheetView: AnyView
+
+ let logger = Logger (subsystem: "net.taler.gnu", category: "Sheet")
+
+ var cancelButton: some View {
+ Button("Cancel") {
+ logger.log("Cancel")
+ dismissTop()
+ }
+ }
+
+ var body: some View {
+ let idString = debugViewC.sheetID > 0 ? String(debugViewC.sheetID)
+ : "" // show nothing if 0
+ NavigationView {
+ sheetView
+ .navigationBarItems(leading: cancelButton)
+
.background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all))
+ }
+ .overlay(alignment: .top) {
+ // Show the viewID on top of the sheet's NavigationView
+ Text(idString)
+ .font(.caption2)
+ .foregroundColor(.purple)
+ .edgesIgnoringSafeArea(.top)
+ .id("sheetID")
+ }
+ }
+}
diff --git a/TalerWallet1/Views/Sheets/URLSheet.swift
b/TalerWallet1/Views/Sheets/URLSheet.swift
new file mode 100644
index 0000000..6902d55
--- /dev/null
+++ b/TalerWallet1/Views/Sheets/URLSheet.swift
@@ -0,0 +1,52 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import SwiftUI
+import SymLog
+
+struct URLSheet: View {
+ private let symLog = SymLogV()
+ let navTitle = String(localized: "Examining URL")
+ var urlToOpen: URL
+ @EnvironmentObject private var controller: Controller
+
+ @State private var urlCommand: UrlCommand? = nil
+
+ var body: some View {
+ Group {
+ if let urlCommand {
+ switch urlCommand {
+ case .withdraw:
+ WithdrawURIView(url: urlToOpen)
+ case .pay:
+ PaymentURIView(url: urlToOpen)
+ case .payPull:
+ P2pPayURIView(url: urlToOpen)
+ case .payPush:
+ P2pReceiveURIView(url: urlToOpen)
+ case .unknown:
+ Text("unknown command")
+ }
+ } else {
+ VStack { // Error view
+ Spacer()
+ Text(controller.messageForSheet ??
urlToOpen.absoluteString)
+ .font(.title)
+ Spacer()
+ Spacer()
+ }
+ .navigationTitle(navTitle)
+ }
+ }.task {
+ urlCommand = controller.openURL(urlToOpen)
+ }
+ }
+}
+// MARK: -
+//struct PaySheet_Previews: PreviewProvider {
+// static var previews: some View {
+ // needs BackendManager
+// URLSheet(urlToOpen: URL(string: "ftp://this.URL.is.invalid")!)
+// }
+//}
diff --git a/TalerWallet1/Views/Transactions/ManualDetails.swift
b/TalerWallet1/Views/Transactions/ManualDetails.swift
new file mode 100644
index 0000000..0254126
--- /dev/null
+++ b/TalerWallet1/Views/Transactions/ManualDetails.swift
@@ -0,0 +1,74 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import SwiftUI
+import taler_swift
+
+struct ManualDetails: View {
+ var common : TransactionCommon
+ var details : WithdrawalDetails
+ var body: some View {
+ if let paytoUris = details.exchangePaytoUris {
+ let payto = paytoUris[0]
+ let payURL = URL(string: payto)
+ let iban = payURL?.iban ?? "unknown IBAN"
+ let amount = common.amountRaw.readableDescription
+ Text("Make a wire transfer of \(amount) to:")
+ .listRowSeparator(.hidden)
+ HStack {
+ Text(iban)
+ Spacer()
+ CopyButton(textToCopy: iban, vertical: true)
+ .accessibilityLabel("Copy IBAN")
+ .disabled(false)
+ } .padding(.leading)
+ .padding(.vertical, -8)
+ .listRowSeparator(.hidden)
+ Text("and use the transaction subject:")
+ .listRowSeparator(.hidden)
+ HStack {
+ Text(details.reservePub)
+ .accessibilityLabel("Taler cryptocode")
+ Spacer()
+ CopyButton(textToCopy: details.reservePub, vertical: true)
+ .accessibilityLabel("Copy subject")
+ .disabled(false)
+ } .padding(.leading)
+ .padding(.vertical, -8)
+ .listRowSeparator(.hidden)
+ HStack {
+ Spacer()
+ ShareButton(textToShare: payto)
+ .accessibilityLabel("Share PayTo ULR")
+ .disabled(false)
+ Spacer()
+ } .listRowSeparator(.hidden)
+ Text(verbatim: "PayTo URL") // the only reason for this
leading-aligned text is to get a nice full lenght listRowSeparator
+ .font(.footnote)
+ .foregroundColor(Color.yellow) // clear
+ .padding(.vertical, -8)
+ .listRowSeparator(.automatic)
+ .accessibilityHidden(true)
+ }
+ }
+}
+// MARK: -
+#if DEBUG
+struct ManualDetails_Previews: PreviewProvider {
+ static var previews: some View {
+ let common = TransactionCommon(type: .withdrawal,
+ txState: TransactionState(major: .done),
+ amountEffective: try! Amount(fromString:
LONGCURRENCY + ":1.1"),
+ amountRaw: try! Amount(fromString:
LONGCURRENCY + ":2.2"),
+ transactionId: "someTxID",
+ timestamp: Timestamp(from:
1_666_666_000_000),
+ txActions: [])
+ let details = WithdrawalDetails(type: .manual, reservePub:
"ReSeRvEpUbLiC_KeY_FoR_WiThDrAwAl", reserveIsReady: false,
+
exchangePaytoUris:["payto://iban/SANDBOXX/DE159593?receiver-name=Exchange+Company"])
+ List {
+ ManualDetails(common: common, details: details)
+ }
+ }
+}
+#endif
diff --git a/TalerWallet1/Views/Transactions/ThreeAmounts.swift
b/TalerWallet1/Views/Transactions/ThreeAmounts.swift
new file mode 100644
index 0000000..5e60ce4
--- /dev/null
+++ b/TalerWallet1/Views/Transactions/ThreeAmounts.swift
@@ -0,0 +1,112 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import SwiftUI
+import taler_swift
+
+struct ThreeAmountsSheet: View {
+ var common: TransactionCommon
+ var topTitle: String
+ var bottomTitle: String?
+ let baseURL: String?
+ let large: Bool // set to false for QR or IBAN
+
+ var body: some View {
+ let raw = common.amountRaw
+ let effective = common.amountEffective
+ let fee = common.fee()
+ let incoming = common.incoming()
+ let pending = (common.txState.major == TransactionMajorState.pending)
+
+ let defaultBottomTitle = incoming ? (pending ? "Pending coins to
obtain:" : "Obtained coins:")
+ : "Paid coins:"
+ ThreeAmountsView(topTitle: topTitle, topAmount: raw, fee: fee,
+ bottomTitle: bottomTitle ?? defaultBottomTitle,
bottomAmount: effective,
+ large: large, pending: pending, incoming: incoming,
+ baseURL: baseURL,
+ status: common.txState.major.rawValue)
+ }
+}
+// MARK: -
+struct ThreeAmountsView: View {
+ var topTitle: String
+ var topAmount: Amount
+ var fee: Amount
+ var bottomTitle: String
+ var bottomAmount: Amount
+ let large: Bool
+ let pending: Bool
+ let incoming: Bool
+ let baseURL: String?
+ var status: String?
+
+ var body: some View {
+ let labelColor = Color(UIColor.label)
+ let foreColor = pending ? WalletColors().pendingColor(incoming)
+ : WalletColors().transactionColor(incoming)
+ let feeColor = WalletColors().transactionColor(false)
+ let feeSign = incoming ? "-" : "+"
+
+ VStack {
+ AmountView(title: topTitle,
+ value: topAmount.readableDescription,
+ color: labelColor,
+ large: large)
+ .padding(.bottom, 4)
+ AmountView(title: "Exchange fee:",
+ value: feeSign + fee.readableDescription,
+ color: fee.isZero ? labelColor : feeColor,
+ large: false)
+ .padding(.bottom, 4)
+ AmountView(title: bottomTitle,
+ value: bottomAmount.readableDescription,
+ color: foreColor,
+ large: large)
+ if let baseURL {
+ VStack {
+ Text(incoming ? "from Exchange:" : "to Exchange:")
+ .font(.title3)
+ Text(baseURL.trimURL())
+ .font(large ? .title : .title2)
+ .fontWeight(large ? .medium : .regular)
+ .foregroundColor(labelColor)
+ }
+ .padding(.top, 4)
+ .frame(maxWidth: .infinity, alignment: .center)
+ .listRowSeparator(.hidden)
+
+ }
+ if let status {
+ HStack {
+ Spacer()
+ Text("Status: \(status)")
+ .font(.title2)
+ }.padding()
+ }
+ }
+ }
+}
+// MARK: -
+struct ThreeAmounts_Previews: PreviewProvider {
+ static var previews: some View {
+ let common = TransactionCommon(type: .withdrawal,
+ txState: TransactionState(major: .done),
+ amountEffective: try! Amount(fromString:
LONGCURRENCY + ":0.1"),
+ amountRaw: try! Amount(fromString:
LONGCURRENCY + ":0.2"),
+ transactionId: "someTxID",
+ timestamp: Timestamp(from:
1_666_666_000_000),
+ txActions: [])
+ Group {
+ List {
+ ThreeAmountsSheet(common: common, topTitle: "Withdrawal",
baseURL: DEMOEXCHANGE, large: 1==1)
+ .safeAreaInset(edge: .bottom) {
+ Button(String(localized: "Accept"), action: {})
+ .buttonStyle(TalerButtonStyle(type: .prominent))
+ .padding(.horizontal)
+ .disabled(true)
+ }
+ }
+ }
+ }
+}
diff --git a/TalerWallet1/Views/Transactions/TransactionDetail.swift
b/TalerWallet1/Views/Transactions/TransactionDetail.swift
deleted file mode 100644
index 6e74f0e..0000000
--- a/TalerWallet1/Views/Transactions/TransactionDetail.swift
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-import SwiftUI
-import taler_swift
-
-struct TransactionDetail: View {
- var transaction : Transaction
-
- var body: some View {
- let common = transaction.common()
- let dateString = TalerDater.dateString(from: common.timestamp)
-
- VStack() {
- Spacer()
- Text("\(common.type)") // TODO: translation
- .font(.title)
- .fontWeight(.medium)
- .padding(.bottom)
- Spacer()
- Text("\(dateString)")
- .font(.title2)
- .padding(.vertical)
- switch transaction {
- case .withdrawal(let withdrawalTransaction):
- details(transaction: transaction)
- threeAmounts(common: common, topTitle: "Chosen amount to
withdraw:", bottomTitle: "Obtained coins:", incoming: true)
- case .payment(let paymentTransaction):
- let details = paymentTransaction.details
- let info = details.info
- Text("Status: \(details.status)")
- .font(.title2)
- .padding(.bottom)
- Text(info.summary)
- .font(.title)
- .lineLimit(4)
- .padding(.bottom)
- threeAmounts(common: common, topTitle: "Sum to be paid:",
bottomTitle: "Paid coins:", incoming: false)
- case .refund(let refundTransaction):
- threeAmounts(common: common, topTitle: "Refunded amount:",
bottomTitle: "Obtained coins:", incoming: true)
- case .tip(let tipTransaction):
- threeAmounts(common: common, topTitle: "Tip to be paid:",
bottomTitle: "Paid coins:", incoming: false)
- case .refresh(let refreshTransaction):
- threeAmounts(common: common, topTitle: "Refreshed
amount:", bottomTitle: "Paid coins:", incoming: false)
- }
- Spacer()
- Button(role: .destructive, action: {
- // TODO: delete from wallet-core
- print("Should delete \(common.transactionId)")
- }, label: {
- HStack {
- Text("Delete from list" + " ")
- Image(systemName: "trash")
- }
- .font(.title)
- .frame(maxWidth: .infinity)
- })
- .buttonStyle(.bordered)
- .controlSize(.large)
- }
- }
-}
-
-extension TransactionDetail {
- struct threeAmounts: View {
- var common: TransactionCommon
- var topTitle: String
- var bottomTitle: String
- var incoming: Bool
-
- var body: some View {
- let raw = common.amountRaw
- let effective = common.amountEffective
- let fee = common.fee()
- let labelColor = Color(UIColor.label)
- let outColor = Color("Outgoing")
- let inColor = Color("Incoming")
-
- AmountView(title: topTitle,
- value: raw.readableDescription,
- color: labelColor)
- .padding(.bottom)
- AmountView(title: "Exchange fee:",
- value: fee.readableDescription,
- color: fee.isZero ? labelColor : outColor)
- .padding(.bottom)
- AmountView(title: bottomTitle,
- value: effective.readableDescription,
- color: incoming ? inColor : outColor)
- .padding(.bottom)
- }
- }
- struct details: View {
- var transaction : Transaction
- var body: some View {
- let details = transaction.detailsToShow()
- let keys = details.keys
- if keys.contains(EXCHANGEBASEURL) {
- if let baseURL = details[EXCHANGEBASEURL] {
- Text("from \(baseURL.trimURL())")
- .font(.title2)
- .padding(.bottom)
- }
- }
- }
- }
-}
-
-#if DEBUG
-struct TransactionDetail_Previews: PreviewProvider {
- static var withdrawal = Transaction(incoming: true,
- id: "some withdrawal ID",
- time: Timestamp(from:
1_666_000_000_000))
- static var payment = Transaction(incoming: false,
- id: "some payment ID",
- time: Timestamp(from: 1_666_666_000_000))
- static var previews: some View {
- Group {
- TransactionDetail(transaction: withdrawal)
- TransactionDetail(transaction: payment)
- }
- }
-}
-#endif
diff --git a/TalerWallet1/Views/Transactions/TransactionDetailView.swift
b/TalerWallet1/Views/Transactions/TransactionDetailView.swift
new file mode 100644
index 0000000..b16877e
--- /dev/null
+++ b/TalerWallet1/Views/Transactions/TransactionDetailView.swift
@@ -0,0 +1,285 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import SwiftUI
+import taler_swift
+import SymLog
+
+extension Transaction { // for Dummys
+ init(dummyCurrency: String) {
+ let amount = try! Amount(fromString: "\(dummyCurrency):0")
+ let now = Timestamp.now()
+ let common = TransactionCommon(type: .dummy,
+ txState: TransactionState(major:
.pending),
+ amountEffective: amount,
+ amountRaw: amount,
+ transactionId: "",
+ timestamp: now,
+ txActions: [])
+ self = .dummy(DummyTransaction(common: common))
+ }
+}
+// MARK: -
+struct TransactionDetailView: View {
+ private let symLog = SymLogV()
+ @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic
+#if DEBUG
+ @AppStorage("developerMode") var developerMode: Bool = true
+#else
+ @AppStorage("developerMode") var developerMode: Bool = false
+#endif
+
+ var transactionId: String
+ @State var transaction: Transaction = Transaction(dummyCurrency:
DEMOCURRENCY)
+ @State var viewId = UUID()
+
+ var reloadAction: ((_ transactionId: String) async throws -> Transaction)
+ var abortAction: ((_ transactionId: String) async throws -> Void)?
+ var deleteAction: ((_ transactionId: String) async throws -> Void)?
+ var failAction: ((_ transactionId: String) async throws -> Void)?
+ var doneAction: (() -> Void)?
+ var suspendAction: ((_ transactionId: String) async throws -> Void)?
+ var resumeAction: ((_ transactionId: String) async throws -> Void)?
+
+ var body: some View {
+#if DEBUG
+ let _ = Self._printChanges()
+ let _ = symLog.vlog() // just to get the # to compare it with
.onAppear & onDisappear
+#endif
+ let common = transaction.common
+ let pending = transaction.isPending
+ let dateString = TalerDater.dateString(from: common.timestamp)
+ let localizedType = transaction.localizedType
+ let navTitle = pending ? String(localized: "Pending \(localizedType)")
+ : localizedType
+ Group {
+ List {
+ if developerMode {
+ if transaction.isSuspendable { if let suspendAction {
+ TransactionButton(transactionId: common.transactionId,
+ command: .suspend, action:
suspendAction)
+ } }
+ if transaction.isResumable { if let resumeAction {
+ TransactionButton(transactionId: common.transactionId,
+ command: .resume, action:
resumeAction)
+ } }
+ } // Suspend + Resume buttons
+ Text("\(dateString)")
+ .font(.title2)
+// .listRowSeparator(.hidden)
+ SwitchCase(transaction: $transaction)
+
+ if transaction.isAbortable { if let abortAction {
+ TransactionButton(transactionId: common.transactionId,
+ command: .abort, action: abortAction)
+ } } // Abort button
+ if transaction.isFailable { if let failAction {
+ TransactionButton(transactionId: common.transactionId,
+ command: .fail, action: failAction)
+ } } // Fail button
+ if transaction.isDeleteable { if let deleteAction {
+ TransactionButton(transactionId: common.transactionId,
+ command: .delete, action: deleteAction)
+ } } // Delete button
+// if let doneAction {
+// DoneButton(doneAction: doneAction)
+// } // Done button
+ }.id(viewId) // change viewId to enforce a draw update
+ .listStyle(myListStyle.style).anyView
+ .safeAreaInset(edge: .bottom) {
+ if let doneAction {
+ Button("Done", action: doneAction)
+ .buttonStyle(TalerButtonStyle(type: .prominent))
+ .padding(.horizontal)
+ }
+ }
+ }.onNotification(.TransactionStateTransition) { notification in
+ if let transition = notification.userInfo?[TRANSACTIONTRANSITION]
as? TransactionTransition {
+ if transition.transactionId == common.transactionId {
+ let newMajor = transition.newTxState.major
+ let newMinor = transition.newTxState.minor
+ if let doneAction {
+ if newMajor == .done {
+ symLog.log("newTxState.major == done => dismiss
sheet")
+ doneAction() // if this view is in a sheet
this action will dissmiss it
+ } else if newMajor == .pending {
+ if let newMinor {
+ if newMinor == .withdrawCoins { //
coin-withdrawal has started
+ symLog.log("newTxState.minor ==
withdrawCoins => dismiss sheet")
+ doneAction() // if this view is in
a sheet this action will dissmiss it
+ } else {
+ symLog.log("ignoring newTxState:
\(newMajor):\(newMinor)")
+ }
+ }
+ } else {
+ symLog.log("ignoring newTxState.major:
\(newMajor)")
+ }
+ } else { Task {
+ do {
+ symLog.log("newState: \(newMajor), reloading
transaction")
+ withAnimation() { transaction =
Transaction(dummyCurrency: DEMOCURRENCY); viewId = UUID() }
+ let reloadedTransaction = try await
reloadAction(common.transactionId)
+ symLog.log("reloaded transaction:
\(reloadedTransaction.common.txState.major)")
+ withAnimation() { transaction =
reloadedTransaction; viewId = UUID() } // redraw
+ } catch {
+ symLog.log(error.localizedDescription)
+ }}}
+ }
+ } else {
+ // Yikes - should never happen
+ symLog.log(notification.userInfo as Any)
+ }
+ }
+ .navigationTitle(navTitle)
+ .task {
+ do {
+ symLog.log("task - load transaction")
+ let reloadedTransaction = try await reloadAction(transactionId)
+ withAnimation() { transaction = reloadedTransaction; viewId =
UUID() } // redraw
+ } catch {
+ symLog.log(error)
+ withAnimation() { transaction = Transaction(dummyCurrency:
DEMOCURRENCY); viewId = UUID() }
+ }
+ }
+ .onAppear {
+ symLog.log("onAppear")
+ DebugViewC.shared.setViewID(VIEW_TRANSACTIONDETAIL)
+ }
+ .onDisappear {
+ symLog.log("onDisappear")
+ }
+ }
+//}
+//
+//extension TransactionDetail {
+ struct SwitchCase: View {
+ @Binding var transaction: Transaction
+
+ var body: some View {
+ let common = transaction.common
+ let pending = transaction.isPending
+ Group {
+ switch transaction {
+ case .dummy(let dummyTransaction):
+ Text("")
+ case .withdrawal(let withdrawalTransaction):
+ let details = withdrawalTransaction.details
+ if pending {
+ let withdrawalDetails = details.withdrawalDetails
+ switch withdrawalDetails.type {
+ case .manual: // "Make a wire
transfer of \(amount) to"
+ ManualDetails(common: common, details:
withdrawalDetails)
+
+ case .bankIntegrated: // "Confirm with
bank"
+ VStack {
+ if let confirmationUrl =
withdrawalDetails.bankConfirmationUrl {
+ if let destination = URL(string:
confirmationUrl) {
+ // Show Hint that User should
Confirm on bank website
+ Text("Waiting for bank
confirmation")
+
.multilineTextAlignment(.leading)
+ .listRowSeparator(.hidden)
+ Link("Confirm with bank",
destination: destination)
+
.buttonStyle(TalerButtonStyle(type: .prominent, narrow: false, aligned:
.center))
+ .padding(.horizontal)
+
+ }
+ }
+ }
+ }
+ } // ManualDetails or Confirm with bank
+ ThreeAmountsSheet(common: common, topTitle:
String(localized: "Chosen amount to withdraw:"),
+ baseURL:
withdrawalTransaction.details.exchangeBaseUrl, large: true)
+ case .payment(let paymentTransaction):
+ let details = paymentTransaction.details
+ let info = details.info
+ Text(info.summary)
+ .font(.title)
+ .lineLimit(4)
+ .padding(.bottom)
+ ThreeAmountsSheet(common: common, topTitle:
String(localized: "Sum to be paid:"),
+ baseURL: nil, large: true) //
TODO: baseURL
+ case .refund(let refundTransaction):
+ let details = refundTransaction.details
// TODO: more details
+ ThreeAmountsSheet(common: common, topTitle:
String(localized: "Refunded amount:"),
+ baseURL: nil, large: true) //
TODO: baseURL
+ case .reward(let rewardTransaction):
+ let details = rewardTransaction.details
// TODO: more details
+ ThreeAmountsSheet(common: common, topTitle:
String(localized: "Received Reward:"),
+ baseURL: details.exchangeBaseUrl,
large: true)
+// case .tip(let tipTransaction):
+// let details = tipTransaction.details //
TODO: details
+// ThreeAmountsSheet(common: common, topTitle:
String(localized: "Received Tip:"),
+// baseURL: nil, large: true)
+ case .refresh(let refreshTransaction):
+ let details = refreshTransaction.details
// TODO: details
+ ThreeAmountsSheet(common: common, topTitle:
String(localized: "Refreshed amount:"),
+ baseURL: nil, large: true) //
TODO: baseURL
+ case .peer2peer(let p2pTransaction):
+ let details = p2pTransaction.details
// TODO: details
+ // TODO: isSendCoins should show QR only while not
expired
+ if pending || transaction.isSendCoins {
+ QRCodeDetails(transaction: transaction)
+ }
+ ThreeAmountsSheet(common: common, topTitle:
String(localized: "Peer to Peer:"),
+ baseURL: details.exchangeBaseUrl,
large: false)
+ } // switch
+ } // Group
+ }
+ }
+
+ struct QRCodeDetails: View {
+ var transaction : Transaction
+ var body: some View {
+ let details = transaction.detailsToShow()
+ let keys = details.keys
+ if keys.contains(TALERURI) {
+ if let talerURI = details[TALERURI] {
+ if talerURI.count > 10 {
+ QRCodeDetailView(talerURI: talerURI,
+ incoming: transaction.isP2pIncoming)
+ }
+ }
+ } else if keys.contains(EXCHANGEBASEURL) {
+ if let baseURL = details[EXCHANGEBASEURL] {
+ Text("from \(baseURL.trimURL())")
+ .font(.title2)
+ .padding(.bottom)
+ }
+ }
+ }
+ }
+ struct DoneButton: View {
+ var doneAction: () -> Void
+
+ var body: some View {
+ Button("Done") {
+ doneAction()
+ }
+ .buttonStyle(TalerButtonStyle(type: .prominent))
+ .padding(.horizontal)
+ }
+ }
+}
+// MARK: -
+#if DEBUG
+//struct TransactionDetail_Previews: PreviewProvider {
+// static func deleteTransactionDummy(transactionId: String) async throws {}
+// static func doneActionDummy() {}
+// static var withdrawal = Transaction(incoming: true,
+// pending: true,
+// id: "some withdrawal ID",
+// time: Timestamp(from:
1_666_000_000_000))
+// static var payment = Transaction(incoming: false,
+// pending: false,
+// id: "some payment ID",
+// time: Timestamp(from:
1_666_666_000_000))
+// static func reloadActionDummy(transactionId: String) async ->
Transaction { return withdrawal }
+// static var previews: some View {
+// Group {
+// TransactionDetailView(transaction: withdrawal, reloadAction:
reloadActionDummy, doneAction: doneActionDummy)
+// TransactionDetailView(transaction: payment, reloadAction:
reloadActionDummy, deleteAction: deleteTransactionDummy)
+// }
+// }
+//}
+#endif
diff --git a/TalerWallet1/Views/Transactions/TransactionRow.swift
b/TalerWallet1/Views/Transactions/TransactionRow.swift
deleted file mode 100644
index 899aaf4..0000000
--- a/TalerWallet1/Views/Transactions/TransactionRow.swift
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-import SwiftUI
-import taler_swift
-
-struct TransactionRowCenter: View {
- var centerTop: String
- var centerBottom: String
-
- var body: some View {
- VStack(alignment: .leading) {
- Text("\(centerTop)")
- .font(.headline)
- .fontWeight(.medium)
- .padding(.bottom, -2.0)
- Text("\(centerBottom)")
- .font(.callout)
- }
- }
-}
-
-struct TransactionRow: View {
- var transaction : Transaction
-
- var body: some View {
- let common = transaction.common()
- let details = transaction.detailsToShow()
- let keys = details.keys
- let amount = common.amountEffective
- let withdraw: Bool = common.type == WITHDRAWAL
- let payment: Bool = common.type == PAYMENT
- let refund: Bool = common.type == REFUND
- let incoming = withdraw || refund
-// let counterparty = transaction.exchangeBaseUrl
- let dateString = TalerDater.dateString(from: common.timestamp,
relative: true)
-
- HStack {
- Image(systemName: incoming ? "text.badge.plus" :
"text.badge.minus")
- .foregroundColor(incoming ? Color("Incoming") :
Color("Outgoing"))
- .padding(.trailing)
- .font(.largeTitle)
-
- if keys.contains(EXCHANGEBASEURL) {
- if let baseURL = details[EXCHANGEBASEURL] {
- TransactionRowCenter(centerTop: baseURL.trimURL(),
centerBottom: dateString)
- }
- } else if payment {
- TransactionRowCenter(centerTop: "Payment", centerBottom:
dateString)
- } else if refund {
- TransactionRowCenter(centerTop: "Refund", centerBottom:
dateString)
- }
- Spacer()
- VStack(alignment: .trailing) {
- let sign = incoming ? "+" : "-"
- Text(sign + "\(amount.valueStr)")
- .font(.title)
- .foregroundColor(incoming ? Color("Incoming") :
Color("Outgoing"))
- }
- }
- .padding(.top)
- }
-}
-
-#if DEBUG
-struct TransactionRow_Previews: PreviewProvider {
- static var withdrawal = Transaction(incoming: true,
- id: "some withdrawal ID",
- time: Timestamp(from:
1_666_000_000_000))
- static var payment = Transaction(incoming: false,
- id: "some payment ID",
- time: Timestamp(from:
1_666_666_000_000))
- static var previews: some View {
- VStack {
- TransactionRow(transaction: withdrawal)
- TransactionRow(transaction: payment)
- }
- }
-}
-#endif
diff --git a/TalerWallet1/Views/Transactions/TransactionRowView.swift
b/TalerWallet1/Views/Transactions/TransactionRowView.swift
new file mode 100644
index 0000000..b6e9286
--- /dev/null
+++ b/TalerWallet1/Views/Transactions/TransactionRowView.swift
@@ -0,0 +1,121 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import SwiftUI
+import taler_swift
+
+struct TransactionRowCenter: View {
+ var centerTop: String
+ var centerBottom: String
+
+ var body: some View {
+ VStack(alignment: .leading) {
+ Text("\(centerTop)")
+ .font(.headline)
+ .fontWeight(.medium)
+ .padding(.bottom, -2.0)
+ Text("\(centerBottom)")
+ .font(.callout)
+ }
+ }
+}
+
+struct TransactionRowView: View {
+ var transaction : Transaction
+
+ var body: some View {
+ let common = transaction.common
+ let amount = common.amountEffective
+ let pending = transaction.isPending
+ let done = transaction.isDone
+ let details = transaction.detailsToShow()
+ let keys = details.keys
+
+ let dateString = TalerDater.dateString(from: common.timestamp,
relative: true)
+ let incoming = common.incoming()
+ let foreColor = pending ? WalletColors().pendingColor(incoming)
+ : done ? WalletColors().transactionColor(incoming)
+ : WalletColors().uncompletedColor
+
+ HStack(spacing: 6) {
+ Image(systemName: incoming ? "text.badge.plus" :
"text.badge.minus")
+ .foregroundColor(foreColor)
+ .font(.largeTitle)
+ .accessibility(hidden: true)
+
+ TransactionRowCenter(centerTop: transaction.localizedType,
+ centerBottom: dateString)
+ Spacer()
+ VStack(alignment: .trailing) {
+ let sign = incoming ? "+" : "-"
+ Text(sign + "\(amount.valueStr)")
+ .font(.title)
+ .foregroundColor(foreColor)
+ }
+ }
+ .accessibilityElement(children: .combine)
+ .padding(.top)
+ }
+}
+// MARK: -
+#if DEBUG
+struct TransactionRow_Previews: PreviewProvider {
+ static var withdrawal = Transaction(incoming: true,
+ pending: false,
+ id: "some withdrawal ID",
+ time: Timestamp(from:
1_666_000_000_000))
+ static var payment = Transaction(incoming: false,
+ pending: false,
+ id: "some payment ID",
+ time: Timestamp(from:
1_666_666_000_000))
+ static var previews: some View {
+ List {
+ TransactionRowView(transaction: withdrawal)
+ TransactionRowView(transaction: payment)
+ }
+ }
+}
+// MARK: -
+extension Transaction { // for PreViews
+ init(incoming: Bool, pending: Bool, id: String, time: Timestamp) {
+ let currency = LONGCURRENCY
+ let raw = currency + ":5"
+ let effective = currency + (incoming ? ":4.8"
+ : ":5.2")
+ let refRaw = currency + ":3"
+ let refEff = currency + ":2.8"
+ let common = TransactionCommon(type: incoming ? .withdrawal : .payment,
+ txState: TransactionState(major: pending ?
TransactionMajorState.pending
+ :
TransactionMajorState.done),
+ amountEffective: try! Amount(fromString:
effective),
+ amountRaw: try! Amount(fromString: raw),
+ transactionId: id,
+ timestamp: time,
+ txActions: [.abort])
+ if incoming {
+ // if pending then manual else bank-integrated
+ let payto =
"payto://iban/SANDBOXX/DE159593?receiver-name=Exchange+Company&amount=KUDOS%3A9.99&message=Taler+Withdrawal+J41FQPJGAP1BED1SFSXHC989EN8HRDYAHK688MQ228H6SKBMV0AG"
+ let withdrawalDetails = WithdrawalDetails(type: pending ?
WithdrawalDetails.WithdrawalType.manual
+ :
WithdrawalDetails.WithdrawalType.bankIntegrated,
+ reservePub:
"PuBlIc_KeY_oF_tHe_ReSeRvE",
+ reserveIsReady: false,
+ exchangePaytoUris: pending ? [payto]
: nil)
+ let wDetails = WithdrawalTransactionDetails(exchangeBaseUrl:
DEMOEXCHANGE,
+ withdrawalDetails:
withdrawalDetails)
+ self = .withdrawal(WithdrawalTransaction(common: common, details:
wDetails))
+ } else {
+ let merchant = Merchant(name: "some random shop")
+ let info = OrderShortInfo(orderId: "some order ID",
+ merchant: merchant,
+ summary: "some product summary",
+ products: [])
+ let pDetails = PaymentTransactionDetails(proposalId: "some
proposal ID",
+ totalRefundRaw: try!
Amount(fromString: refRaw),
+ totalRefundEffective: try!
Amount(fromString: refEff),
+ info: info)
+ self = .payment(PaymentTransaction(common: common, details:
pDetails))
+ }
+ }
+}
+#endif
diff --git a/TalerWallet1/Views/Transactions/TransactionsEmptyView.swift
b/TalerWallet1/Views/Transactions/TransactionsEmptyView.swift
new file mode 100644
index 0000000..eb7479f
--- /dev/null
+++ b/TalerWallet1/Views/Transactions/TransactionsEmptyView.swift
@@ -0,0 +1,37 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import SwiftUI
+import SymLog
+
+/// This view shows hints if a wallet is empty
+/// It is the very first thing the user sees after installing the app
+
+struct TransactionsEmptyView: View {
+ private let symLog = SymLogV()
+ @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic
+
+ let currency: String
+
+ var body: some View {
+ List {
+ Section {
+ Text("There are no transactions for \(currency).")
+ }
+ }
+ .padding(.vertical)
+ .font(.title2)
+ .listStyle(myListStyle.style).anyView
+ .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all))
+ .onAppear() {
+ DebugViewC.shared.setViewID(VIEW_EMPTY) // 10
+ }
+ }
+}
+
+struct TransactionsEmptyView_Previews: PreviewProvider {
+ static var previews: some View {
+ TransactionsEmptyView(currency: LONGCURRENCY)
+ }
+}
diff --git a/TalerWallet1/Views/Transactions/TransactionsListView.swift
b/TalerWallet1/Views/Transactions/TransactionsListView.swift
index 284a6c3..2ae882e 100644
--- a/TalerWallet1/Views/Transactions/TransactionsListView.swift
+++ b/TalerWallet1/Views/Transactions/TransactionsListView.swift
@@ -1,105 +1,125 @@
/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
*/
import SwiftUI
import SymLog
struct TransactionsListView: View {
- private let symLog = SymLogV()
- let navTitle = "Transactions"
+ private let symLog = SymLogV(0)
+ @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic
+ let navTitle: String
+
+ let currency: String
+ let transactions: [Transaction]
+ let reloadAllAction: () async -> ()
+ let reloadOneAction: ((_ transactionId: String) async throws ->
Transaction)
- @ObservedObject var viewModel: TransactionsModel
-
var body: some View {
- let reloadAction = viewModel.fetchTransactions
- VStack {
- if viewModel.transactions == nil {
- symLog { LoadingView(backButtonHidden: false) }
- } else {
- let count = viewModel.transactions!.count
- let title: String = "\(count) \(navTitle)"
- symLog { Content(symLog: symLog, viewModel: viewModel,
reloadAction: reloadAction)
- .navigationTitle(title)
- }
+#if DEBUG
+ let _ = Self._printChanges()
+ let _ = symLog.vlog() // just to get the # to compare it with
.onAppear & onDisappear
+#endif
+ let count = transactions.count
+ // TODO: Unlock the power of grammatical agreement
+// let title = AttributedString(localized: "^[\(count) Ticket](inflect:
true)")
+ let title: String = "\(count) \(navTitle)"
+ Content(symLog: symLog,
+ currency: currency,
+ transactions: transactions,
+ myListStyle: $myListStyle,
+ reloadAllAction: reloadAllAction,
+ reloadOneAction: reloadOneAction)
+ .navigationTitle(title)
+ .navigationBarTitleDisplayMode(.large) // .inline
+ .onAppear {
+ DebugViewC.shared.setViewID(VIEW_TRANSACTIONLIST)
}
- }.task {
- symLog.log(".task")
- do {
- try await reloadAction()
- } catch {
- // TODO: show error
- symLog.log(error.localizedDescription)
+ .task {
+ symLog.log(".task ")
+ await reloadAllAction()
}
- }
}
}
// MARK: -
extension TransactionsListView {
struct Content: View {
let symLog: SymLogV?
- @ObservedObject var viewModel: TransactionsModel
+ let currency: String
+ let transactions: [Transaction]
+ @Binding var myListStyle: MyListStyle
+ let reloadAllAction: () async -> ()
+ let reloadOneAction: ((_ transactionId: String) async throws ->
Transaction)
- var reloadAction: () async throws -> ()
+ @EnvironmentObject private var model: WalletModel
@State private var upAction: () -> Void = {}
@State private var downAction: () -> Void = {}
- var body: some View {
- let transactions = viewModel.transactions!
+// func removeItems(at offsets: IndexSet) {
+// let transactions = model.transactions
+// var idsToDelete: [String] = []
+// for n in offsets { // save IDs of transactions
+// let common = transactions[n].common
+// idsToDelete.append(common.transactionId)
+// }
+// // then remove items from the list model (and the view)
+// model.transactions.remove(atOffsets: offsets)
+// // finally tell wallet-core to delete the saved IDs
+// Task { // delete this transaction from wallet-core
+// for transactionId in idsToDelete {
+// do {
+// try await deleteAction(transactionId)
+// symLog?.log("deleted \(transactionId)")
+// } catch { // TODO: error
+// symLog?.log(error.localizedDescription)
+// }
+// }
+// }
+// }
+ @State var viewId = UUID()
+ var body: some View {
+#if DEBUG
+ let _ = Self._printChanges()
+ let _ = symLog?.vlog() // just to get the # to compare it
with .onAppear & onDisappear
+#endif
+ let abortAction = model.abortTransaction
+ let deleteAction = model.deleteTransaction
+ let failAction = model.failTransaction
+ let suspendAction = model.suspendTransaction
+ let resumeAction = model.resumeTransaction
ScrollViewReader { scrollView in
List {
- ForEach (transactions.indices) { index in
- let transaction = transactions[index]
- let common = transaction.common()
- NavigationLink {
- TransactionDetail(transaction: transaction)
- } label: {
- TransactionRow(transaction: transaction)
+ ForEach(Array(zip(transactions.indices, transactions)),
id: \.1) { index, transaction in
+ NavigationLink { LazyView { // whole row like
in a tableView
+ // pending may not be deleted, but only aborted
+ TransactionDetailView(transactionId:
transaction.id,
+ reloadAction: reloadOneAction,
+ abortAction: abortAction,
+ deleteAction: deleteAction,
+ failAction: failAction,
+ suspendAction: suspendAction,
+ resumeAction: resumeAction)
+ }} label: {
+ TransactionRowView(transaction: transaction)
}
- .id(index)
- .swipeActions(edge: .leading, allowsFullSwipe: true) {
- Button {
- symLog?.log("bookmarked
\(common.transactionId)")
- // TODO: Bookmark
- } label: {
- Label("Bookmark", systemImage: "bookmark")
- }.tint(.indigo)
- }
- .swipeActions(edge: .trailing, allowsFullSwipe: true) {
- Button(role: .destructive) {
- symLog?.log("deleted \(common.transactionId)")
- // TODO: delete from Model. SwiftUI deletes
this row from view already :-)
- } label: {
- Label("Delete", systemImage: "trash")
- }
- }
- }
- .onAppear {
- upAction = { withAnimation { scrollView.scrollTo(0) }}
- downAction = { withAnimation {
scrollView.scrollTo(transactions.count - 1) }}
- downAction()
}
+// .onDelete(perform: removeItems) // delete this row
from the list
}
.refreshable {
- do {
- symLog?.log("refreshing")
- try await reloadAction()
- } catch {
- // TODO: error
- symLog?.log(error.localizedDescription)
+ symLog?.log("refreshing")
+ await reloadAllAction()
+ }.id(viewId)
+ .listStyle(myListStyle.style).anyView
+ .onAppear {
+ upAction = { withAnimation { scrollView.scrollTo(0) }}
+ downAction = { withAnimation {
scrollView.scrollTo(transactions.count - 1) }}
+ downAction()
+ }
+ .overlay {
+ if transactions.isEmpty {
+ TransactionsEmptyView(currency: currency)
}
}
}
@@ -107,7 +127,6 @@ extension TransactionsListView {
ArrowUpButton(action: upAction)
ArrowDownButton(action: downAction)
})
- .navigationBarTitleDisplayMode(.large) // .inline
}
}
}
diff --git a/TalerWallet1/Views/Transactions/TransactionsModel.swift
b/TalerWallet1/Views/Transactions/TransactionsModel.swift
deleted file mode 100644
index 253a557..0000000
--- a/TalerWallet1/Views/Transactions/TransactionsModel.swift
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-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 {
- @Published var transactions: [Transaction]? // update view
-}
-
-// MARK: -
-/// A request to get the transactions in the wallet's history.
-fileprivate struct GetTransactions: WalletBackendFormattedRequest {
- func operation() -> String { return "getTransactions" }
- 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]
- }
-}
-// MARK: -
-extension TransactionsModel {
- /// ask wallet-core for its list of transactions filtered by searchString
- func fetchTransactions() async throws { // might be called
from a background thread itself
- try await fetchTransactions(currency: nil, searchString: nil)
- }
- /// fetch Balances from Wallet-Core. No networking involved
- @MainActor func fetchTransactions(currency: String? = nil, searchString:
String? = nil)
- async throws {
- do {
- let request = GetTransactions(currency: nil, search: nil)
- let response = try await sendRequest(request, ASYNCDELAY)
- transactions = response.transactions // trigger view update
in TransactionsListView
- } catch {
- throw error
- }
- }
-}
diff --git a/TalerWallet1/Views/URLSheet.swift
b/TalerWallet1/Views/URLSheet.swift
deleted file mode 100644
index 1aaab6b..0000000
--- a/TalerWallet1/Views/URLSheet.swift
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-import SwiftUI
-import SymLog
-
-struct URLSheet: View {
- private let symLog = SymLogV()
- var urlToOpen: URL
- @Environment(\.dismiss) var dismiss // call dismiss() to get rid of
the sheet
- @EnvironmentObject private var controller: Controller
-
- @State private var urlCommand: UrlCommand? = nil
-
- var cancelButton: some View {
- Button("Cancel0") {
- print(dismiss)
- dismissTop()
- }
- }
-
- var body: some View {
- symLog {
- NavigationView {
- if urlCommand == UrlCommand.withdraw {
- WithdrawURIView(url: urlToOpen, viewModel:
controller.withdrawURIModel)
- } else if urlCommand == UrlCommand.pay {
- PaymentURIView(url: urlToOpen, viewModel:
controller.paymentURIModel)
- } else {
- VStack { // show Error view with cancelButton
- Spacer()
- Text(controller.messageForSheet ??
urlToOpen.absoluteString)
- .font(.title)
- Spacer()
- Spacer()
- }
- .navigationBarItems(leading: cancelButton)
- .navigationTitle("Invalid URL")
- }
- }.task {
- urlCommand = controller.openURL(urlToOpen)
- }
- }
- }
-}
-// MARK: -
-//struct PaySheet_Previews: PreviewProvider {
-// static var previews: some View {
- // needs BackendManager
-// URLSheet(urlToOpen: URL(string: "ftp://this.URL.is.invalid")!)
-// }
-//}
diff --git a/TalerWallet1/Views/Withdraw/WithdrawAcceptView.swift
b/TalerWallet1/Views/Withdraw/WithdrawAcceptView.swift
deleted file mode 100644
index 4f9578c..0000000
--- a/TalerWallet1/Views/Withdraw/WithdrawAcceptView.swift
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-import SwiftUI
-import taler_swift
-import SymLog
-
-struct WithdrawAcceptView: View {
- private let symLog = SymLogV()
- var url: URL
- @ObservedObject var model: WithdrawURIModel
-
-// @Environment(\.dismiss) var dismiss // call dismiss() to get rid of
the sheet
- let navTitle = "Accept Withdrawal"
- var cancelButton: some View {
- Button("Cancel4") { dismissTop() }
- }
-
- let detailsForAmount: WithdrawalDetailsForAmount
- let baseURL: String
-
- var body: some View {
- symLog { Group {
- switch model.withdrawState {
- case .receivedAmountDetails, .receivedTOS, .receivedTOSAck:
- let raw = detailsForAmount.amountRaw
- let effective = detailsForAmount.amountEffective
- let fee = try! Amount.diff(raw, effective) // TODO:
different currencies
- Form {
- AmountView(title: "Chosen amount to withdraw:",
- value: raw.readableDescription, color:
Color(UIColor.label))
- .padding(.bottom)
- AmountView(title: "Exchange fee:",
- value: "- " + fee.readableDescription,
color: Color("Outgoing"))
- .padding(.bottom)
- AmountView(title: "Coins to be withdrawn:",
- value: effective.readableDescription,
color: Color("Incoming"))
- }
- AwesomeButton(title: "Accept") {
- Task {
- do {
- let bankConfirmationUrl = try await
model.sendAcceptIntWithdrawal(baseURL, withdrawURL: url.absoluteString)
- symLog.log(bankConfirmationUrl as Any)
- // TODO: Show Hints that User should Confirm
on bank website
- } catch {
- symLog.log(error.localizedDescription)
- }
- dismissTop()
- }
- }
- default:
- ErrorView()
- }
- }
- .navigationBarItems(leading: cancelButton)
- .navigationTitle(navTitle)
- }
- }
-}
diff --git a/TalerWallet1/Views/Withdraw/WithdrawProgressView.swift
b/TalerWallet1/Views/Withdraw/WithdrawProgressView.swift
deleted file mode 100644
index 2c08b75..0000000
--- a/TalerWallet1/Views/Withdraw/WithdrawProgressView.swift
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-import SwiftUI
-
-struct WithdrawProgressView: View {
- let message: String
- let action: () -> Void
-
- var cancelButton: some View {
- Button("Cancel2") {
- action()
- } // dismiss the sheet
- }
-
- var body: some View { // show Message with
cancelButton
- VStack {
- Spacer()
- ProgressView()
- Spacer()
- Text(message)
- .font(.title)
- Spacer()
- Spacer()
- }.navigationBarItems(leading: cancelButton)
- }
-}
-
-struct WithdrawProgressView_Previews: PreviewProvider {
- static var previews: some View {
- WithdrawProgressView(message: "message") {}
- }
-}
diff --git a/TalerWallet1/Views/Withdraw/WithdrawTOSView.swift
b/TalerWallet1/Views/Withdraw/WithdrawTOSView.swift
deleted file mode 100644
index b1c3cf6..0000000
--- a/TalerWallet1/Views/Withdraw/WithdrawTOSView.swift
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-import SwiftUI
-import SymLog
-
-struct WithdrawTOSView: View {
- private let symLog = SymLogV()
- var url: URL
- @ObservedObject var model: WithdrawURIModel
-
-// @Environment(\.dismiss) var dismiss // call dismiss() to get rid of
the sheet
- let navTitle = "Terms of Service"
- var cancelButton: some View {
- Button("Cancel3") { dismissTop() }
- }
-
- var detailsForUri: WithdrawalDetailsForUri
- @State var exchangeTOS: ExchangeTermsOfService?
- @Binding var didAcceptTOS: Bool
-
- var body: some View {
- let badURL = "Error in URL: \(url)"
- let baseURL = detailsForUri.defaultExchangeBaseUrl
- VStack {
- switch model.withdrawState {
- case .waitingForTOS:
- WithdrawProgressView(message: baseURL ?? badURL) {
- dismissTop()
- }.navigationTitle("Loading " + navTitle)
- case .receivedTOS:
- Content(symLog: symLog, exchangeTOS: exchangeTOS) {
- Task {
- do {
- _ = try await
model.setExchangeTOSAccepted(baseURL!, etag: exchangeTOS!.currentEtag)
- didAcceptTOS = true
- } catch {
- // TODO: Show Error
- symLog.log(error.localizedDescription)
- }
- }
- }
- .navigationBarTitleDisplayMode(.large) // .inline
- .navigationBarItems(leading: cancelButton)
- .navigationTitle(navTitle)
- default:
- ErrorView()
- }
- }.task {
- do {
- let someTOS = try await
model.loadExchangeTermsOfService(baseURL!)
- exchangeTOS = someTOS
- } catch {
- // TODO: error
- symLog.log(error.localizedDescription)
- }
- }
- }
-}
-// MARK: -
-extension WithdrawTOSView {
- struct Content: View {
- let symLog: SymLogV
- var exchangeTOS: ExchangeTermsOfService?
- var acceptAction: () -> ()
-
- var body: some View {
- Group {
- if let tos = exchangeTOS {
- let components = tos.content.components(separatedBy:"\n\n")
-
- List (components, id: \.self) { term in
- Text(term)
- }
- AwesomeButton(title: "Accept") {
- acceptAction()
- }.padding(.vertical)
- } else {
- ErrorView() // TODO: ???
- }
- }
- }
- }
-}
diff --git a/TalerWallet1/Views/Withdraw/WithdrawURIModel.swift
b/TalerWallet1/Views/Withdraw/WithdrawURIModel.swift
deleted file mode 100644
index f375a1d..0000000
--- a/TalerWallet1/Views/Withdraw/WithdrawURIModel.swift
+++ /dev/null
@@ -1,213 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-import Foundation
-import taler_swift
-import SymLog
-fileprivate let ASYNCDELAY: UInt = 0 //set e.g to 6 or 9 seconds for
debugging
-
-enum WithdrawState {
- case error
- case waitingForUriDetails
- case receivedUriDetails
- case waitingForAmountDetails
- case receivedAmountDetails
- case waitingForTOS
- case receivedTOS
- case waitingForTOSAck
- case receivedTOSAck
- case waitingForWithdrAck
- case receivedWithdrAck
-}
-
-class WithdrawURIModel: WalletModel {
- @Published var withdrawState: WithdrawState?
-}
-
-// MARK: -
-/// The result from getWithdrawalDetailsForUri
-struct WithdrawalDetailsForUri: Decodable {
- var amount: Amount
- var defaultExchangeBaseUrl: String?
- var possibleExchanges: [ExchangeListItem]
-}
-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 = WithdrawalDetailsForUri
- 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 WithdrawalDetailsForAmount: Decodable {
- var tosAccepted: Bool
- var amountRaw: Amount
- var amountEffective: Amount
-}
-/// A request to get an exchange's withdrawal details.
-fileprivate struct GetWithdrawalDetailsForAmount:
WalletBackendFormattedRequest {
- typealias Response = WithdrawalDetailsForAmount
- 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 BankConfirmation: Decodable {
- var bankConfirmationUrl: String?
-}
-/// A request to accept a bank-integrated withdrawl.
-fileprivate struct AcceptBankIntegratedWithdrawal:
WalletBackendFormattedRequest {
- typealias Response = BankConfirmation
- 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: -
-extension WithdrawURIModel {
- /// load withdrawal details. Networking involved
- @MainActor
- func loadWithdrawalDetailsForURI(_ talerWithdrawUri: String) async throws
-> WithdrawalDetailsForUri {
- do {
- withdrawState = .waitingForUriDetails
- let request = GetWithdrawalDetailsForURI(talerWithdrawUri:
talerWithdrawUri)
- let response = try await sendRequest(request, ASYNCDELAY)
- withdrawState = .receivedUriDetails
- return response
- } catch {
- withdrawState = .error
- throw error
- }
- }
- @MainActor
- func loadWithdrawalDetailsForAmount(_ detailsForUri:
WithdrawalDetailsForUri) async throws -> WithdrawalDetailsForAmount {
- do {
- withdrawState = .waitingForAmountDetails
- let baseURL = detailsForUri.defaultExchangeBaseUrl!
- let request = GetWithdrawalDetailsForAmount(exchangeBaseUrl:
baseURL, amount: detailsForUri.amount)
- let response = try await sendRequest(request, ASYNCDELAY)
- withdrawState = .receivedAmountDetails
- return response
- } catch {
- withdrawState = .error
- throw error
- }
- }
- @MainActor
- func loadExchangeTermsOfService(_ exchangeBaseUrl: String) async throws ->
ExchangeTermsOfService {
- do {
- withdrawState = .waitingForTOS
- let request = GetExchangeTermsOfService(exchangeBaseUrl:
exchangeBaseUrl)
- let response = try await sendRequest(request, ASYNCDELAY)
- withdrawState = .receivedTOS
- return response
- } catch {
- withdrawState = .error
- throw error
- }
- }
- @MainActor
- func setExchangeTOSAccepted(_ exchangeBaseUrl: String, etag: String) async
throws -> Decodable {
- do {
- withdrawState = .waitingForTOSAck
- let request = SetExchangeTOSAccepted(exchangeBaseUrl:
exchangeBaseUrl, etag: etag)
- let response = try await sendRequest(request, ASYNCDELAY)
- withdrawState = .receivedTOSAck
- return response
- } catch {
- withdrawState = .error
- throw error
- }
- }
- @MainActor
- func sendAcceptIntWithdrawal(_ exchangeBaseUrl: String, withdrawURL:
String) async throws -> String? {
- do {
- withdrawState = .waitingForWithdrAck
- let request = AcceptBankIntegratedWithdrawal(talerWithdrawUri:
withdrawURL, exchangeBaseUrl: exchangeBaseUrl)
- let response = try await sendRequest(request, ASYNCDELAY)
- withdrawState = .receivedWithdrAck
- return response.bankConfirmationUrl
- } catch {
- withdrawState = .error
- throw error
- }
- }
-}
diff --git a/TalerWallet1/Views/Withdraw/WithdrawURIView.swift
b/TalerWallet1/Views/Withdraw/WithdrawURIView.swift
deleted file mode 100644
index e928e44..0000000
--- a/TalerWallet1/Views/Withdraw/WithdrawURIView.swift
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-import SwiftUI
-import SymLog
-
-struct WithdrawURIView: View {
- private let symLog = SymLogV()
- var url: URL
- @ObservedObject var viewModel: WithdrawURIModel
-
-// @Environment(\.dismiss) var dismiss // call dismiss() to get rid of
the sheet
- let navTitle = "Withdraw"
- var cancelButton: some View {
- Button("Cancel1") { dismissTop() }
- }
-
- @State var detailsForUri: WithdrawalDetailsForUri?
- @State var detailsForAmount: WithdrawalDetailsForAmount?
- @State var didAcceptTOS: Bool = false
-
- var body: some View {
- let badURL = "Error in URL: \(url)"
- VStack {
- if viewModel.withdrawState == nil {
- LoadingView(backButtonHidden: false)
- } else { switch viewModel.withdrawState {
- case .waitingForUriDetails:
- let _ = symLog.vlog("waitingForUriDetails")
- WithdrawProgressView(message: url.host ?? badURL) {
dismissTop() }
- .navigationTitle("Contacting Exchange")
- case .waitingForAmountDetails:
- let _ = symLog.vlog("waitingForAmountDetails")
- WithdrawProgressView(message:
detailsForUri!.defaultExchangeBaseUrl ?? badURL) { dismissTop() }
- .navigationTitle("Found Exchange")
- case .receivedAmountDetails, .waitingForTOS, .receivedTOS,
.receivedTOSAck:
- let _ = symLog.vlog("waitingForTOS")
- if !didAcceptTOS {
- WithdrawTOSView(url: url, model: viewModel,
detailsForUri: detailsForUri!, didAcceptTOS: $didAcceptTOS)
- } else {
- // show Amount details and let user accept
- WithdrawAcceptView(url: url, model: viewModel,
detailsForAmount: detailsForAmount!,
- baseURL:
detailsForUri!.defaultExchangeBaseUrl!)
- }
- default:
- symLog {
- Content(symLog: symLog, viewModel: viewModel)
- .navigationBarItems(leading: cancelButton)
- .navigationTitle(navTitle)
- }
- } }
- }.task {
- do { // TODO: cancelled
- symLog.log(".task")
- detailsForUri = try await
viewModel.loadWithdrawalDetailsForURI(url.absoluteString)
- let baseURL = detailsForUri!.defaultExchangeBaseUrl
- symLog.log("amount: \(detailsForUri!.amount), baseURL:
\(String(describing: baseURL))")
- // TODO: let user choose exchange from array
- detailsForAmount = try await
viewModel.loadWithdrawalDetailsForAmount(detailsForUri!)
- symLog.log("raw: \(detailsForAmount!.amountRaw), effective:
\(detailsForAmount!.amountEffective)")
- if detailsForAmount!.tosAccepted {
- didAcceptTOS = true
- }
- } catch {
- // TODO: error
- }
- }
- }
-}
-// MARK: -
-extension WithdrawURIView {
- struct Content: View {
- let symLog: SymLogV?
- @ObservedObject var viewModel: WithdrawURIModel
-// @EnvironmentObject var controller : Controller
-
- var body: some View {
- Group {
- Text("Hello")
-// List(model.pendingOperations!, id: \.self) { pendingOp in
-// PendingOpView(pendingOp: pendingOp)
-// }
-// .navigationBarTitleDisplayMode(.large) // .inline
-// .refreshable {
-// symLog?.log("refreshing")
-// try? await reloadAction() // TODO: catch error
-// }
- }
- }
- }
-}
diff --git a/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawAcceptDone.swift
b/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawAcceptDone.swift
new file mode 100644
index 0000000..1a802f1
--- /dev/null
+++ b/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawAcceptDone.swift
@@ -0,0 +1,68 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import SwiftUI
+import taler_swift
+import SymLog
+
+struct WithdrawAcceptDone: View {
+ private let symLog = SymLogV()
+ let navTitle = String(localized: "Confirm with Bank")
+
+ let exchangeBaseUrl: String?
+ let url: URL
+
+ @EnvironmentObject private var model: WalletModel
+ @EnvironmentObject private var controller: Controller
+
+ @State private var transactionId: String? = 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
+ ScrollViewReader { scrollView in
+ VStack {
+ if let transactionId {
+ TransactionDetailView(transactionId: transactionId,
+ 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_WITHDRAW_CONFIRM)
+ }.task {
+ do {
+ if let exchangeBaseUrl {
+ let result = try await
model.sendAcceptIntWithdrawalM(exchangeBaseUrl, withdrawURL: url.absoluteString)
+ let confirmTransferUrl = result!.confirmTransferUrl
+ symLog.log(confirmTransferUrl)
+ transactionId = result!.transactionId
+ }
+ } catch { // TODO: error
+ symLog.log(error.localizedDescription)
+ controller.playSound(0)
+ }
+ }
+ }
+ }
+}
+// MARK: -
+struct WithdrawAcceptDone_Previews: PreviewProvider {
+ static var previews: some View {
+ WithdrawAcceptDone(exchangeBaseUrl: DEMOEXCHANGE,
+ url: URL(string: DEMOSHOP)!)
+ }
+}
diff --git a/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawAcceptView.swift
b/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawAcceptView.swift
new file mode 100644
index 0000000..ada134f
--- /dev/null
+++ b/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawAcceptView.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 WithdrawAcceptView: View {
+ private let symLog = SymLogV()
+ let navTitle = String(localized: "Accept Withdrawal")
+
+ let exchangeBaseUrl: String
+ let model: WithdrawModel?
+ let amount: Amount?
+ let url: URL
+
+ @State private var buttonSelected: Int? = nil
+ @State private var confirmTransferUrl: String? = nil
+ @State private var transactionId: String? = nil
+ @State var manualWithdrawalDetails: ManualWithdrawalDetails?
+
+ func acceptAction() -> () {
+ Task {
+ do {
+ if let model {
+ if let acceptWithdrawalResponse = try await
model.sendAcceptIntWithdrawalM(exchangeBaseUrl, withdrawURL:
url.absoluteString) {
+ confirmTransferUrl =
acceptWithdrawalResponse.confirmTransferUrl
+ transactionId = acceptWithdrawalResponse.transactionId
+ symLog.log(confirmTransferUrl ?? "❗️Yikes: No
confirmTransferUrl")
+ buttonSelected = 1 // trigger NavigationLink
+ } else {
+ // TODO: error sendAcceptIntWithdrawal failed
+ }
+ }
+ } catch { // TODO: error
+ symLog.log(error.localizedDescription)
+ }
+ }
+ }
+
+ var body: some View {
+ VStack {
+ if let manualWithdrawalDetails {
+ List {
+
+ HStack(spacing: 0) {
+ NavigationLink(destination: LazyView {
+ WithdrawAcceptDone(model: model,
confirmTransferUrl: confirmTransferUrl, transactionId: transactionId)
+ }, tag: 1, selection: $buttonSelected
+ ) { EmptyView() }.frame(width: 0).opacity(0).hidden()
+
+ ThreeAmountsView(topTitle: String(localized: "Chosen
amount to withdraw:"),
+ topAmount: raw, fee: fee,
+ bottomTitle: String(localized: "Coins
to be withdrawn:"),
+ bottomAmount: effective,
+ large: false, pending: false,
incoming: true,
+ baseURL: exchangeBaseUrl)
+ }
+ }
+ .safeAreaInset(edge: .bottom) {
+ Button("Confirm Withdrawal", action: acceptAction)
+ .lineLimit(2)
+ .disabled(false)
+ .buttonStyle(TalerButtonStyle(type: .prominent,
narrow: false, aligned: .center))
+ .padding()
+ }
+ .navigationTitle(navTitle)
+ } else {
+ WithdrawProgressView(message: exchangeBaseUrl.trimURL())
+ .navigationTitle("Found Exchange")
+ }
+ }
+// .overlay {
+// VStack {
+// ErrorView(errortext: "unknown state") // TODO: Error
+// }.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0,
maxHeight: .infinity)
+//
.background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all))
+// }
+ .onAppear() {
+ DebugViewC.shared.setSheetID(SHEET_WITHDRAW_ACCEPT)
+ }
+ .task { if let amount, let model {
+ do { // TODO: cancelled
+ symLog.log(".task")
+ if exchangeBaseUrl.hasPrefix(HTTPS) {
+ symLog.log("amount: \(amount), baseURL:
\(String(describing: exchangeBaseUrl))")
+ // TODO: let user choose exchange from list
+ manualWithdrawalDetails = try await
model.loadWithdrawalDetailsForAmountM(exchangeBaseUrl, amount: amount)
+ symLog.log("raw: \(manualWithdrawalDetails!.amountRaw),
effective: \(manualWithdrawalDetails!.amountEffective)")
+ } else {
+ // TODO: error no exchange!
+ }
+ } catch { // TODO: error
+ symLog.log(error.localizedDescription)
+ }
+ } else {
+ // TODO: error no amount!
+ } }
+ }
+}
+// MARK: -
+struct WithdrawAcceptView_Previews: PreviewProvider {
+ static var previews: some View {
+ let amount = try! Amount(fromString: LONGCURRENCY + ":2.4")
+ WithdrawAcceptView(exchangeBaseUrl: DEMOEXCHANGE,
+ model: nil,
+ amount: amount,
+ url: URL(string: DEMOSHOP)!)
+ }
+}
diff --git
a/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawProgressView.swift
b/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawProgressView.swift
new file mode 100644
index 0000000..f623c3a
--- /dev/null
+++ b/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawProgressView.swift
@@ -0,0 +1,32 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import SwiftUI
+
+struct WithdrawProgressView: View {
+ let message: String
+
+ var body: some View {
+ VStack {
+ Spacer()
+ ProgressView()
+ Spacer()
+ HStack {
+ Spacer()
+ Text(message)
+ .font(.title)
+ Spacer()
+ }
+ Spacer()
+ Spacer()
+ }
+ .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all))
+ }
+}
+
+struct WithdrawProgressView_Previews: PreviewProvider {
+ static var previews: some View {
+ WithdrawProgressView(message: "message")
+ }
+}
diff --git a/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawTOSView.swift
b/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawTOSView.swift
new file mode 100644
index 0000000..6197077
--- /dev/null
+++ b/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawTOSView.swift
@@ -0,0 +1,99 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import SwiftUI
+import SymLog
+
+struct WithdrawTOSView: View {
+ private let symLog = SymLogV()
+ @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic
+
+ let navTitle = String(localized: "Terms of Service")
+
+ var exchangeBaseUrl: String?
+
+ @EnvironmentObject private var model: WalletModel
+
+ @State var exchangeTOS: ExchangeTermsOfService?
+ let viewID: Int // either VIEW_WITHDRAW_TOS or SHEET_WITHDRAW_TOS
+
+ let acceptAction: (() -> Void)?
+ @Environment(\.presentationMode) var presentationMode
+
+ var body: some View {
+ VStack {
+ Content(symLog: symLog, exchangeTOS: exchangeTOS, myListStyle:
$myListStyle) {
+ Task {
+ do {
+ if let exchangeBaseUrl {
+ _ = try await
model.setExchangeTOSAcceptedM(exchangeBaseUrl, etag: exchangeTOS!.currentEtag)
+ if acceptAction != nil {
+ acceptAction!()
+ } else { // just go back - caller will reload
+ self.presentationMode.wrappedValue.dismiss()
+ }
+ }
+ } catch { // TODO: Show Error
+ symLog.log(error.localizedDescription)
+ }
+ }
+ }
+ .navigationBarTitleDisplayMode(.large) // .inline
+ .navigationTitle(navTitle)
+ .overlay {
+ if exchangeTOS == nil {
+ if let exchangeBaseUrl {
+ WithdrawProgressView(message:
exchangeBaseUrl.trimURL())
+ .navigationTitle("Loading " + navTitle)
+ } else {
+ // Yikes!
+ WithdrawProgressView(message: "No exchangeBaseUrl!")
+ .navigationTitle("Loading " + navTitle)
+ }
+ }
+ }
+ }.onAppear() {
+ if viewID > SHEET_WITHDRAWAL {
+ DebugViewC.shared.setSheetID(SHEET_WITHDRAW_TOS)
+ } else {
+ DebugViewC.shared.setViewID(VIEW_WITHDRAW_TOS)
+ }
+ }.task {
+ do {
+ if let exchangeBaseUrl {
+ let someTOS = try await
model.loadExchangeTermsOfServiceM(exchangeBaseUrl)
+ exchangeTOS = someTOS
+ }
+ } catch { // TODO: error
+ symLog.log(error.localizedDescription)
+ }
+ }
+ }
+}
+// MARK: -
+extension WithdrawTOSView {
+ struct Content: View {
+ let symLog: SymLogV
+ var exchangeTOS: ExchangeTermsOfService?
+ @Binding var myListStyle: MyListStyle
+ var acceptAction: () -> ()
+
+ var body: some View {
+ if let tos = exchangeTOS {
+ let components = tos.content.components(separatedBy:"\n\n")
+
+ List (components, id: \.self) { term in
+ Text(term)
+ }.safeAreaInset(edge: .bottom) {
+ Button(String(localized: "Accept ToS"), action:
acceptAction)
+ .buttonStyle(TalerButtonStyle(type: .prominent))
+ .padding(.horizontal)
+ }
+ .listStyle(myListStyle.style).anyView
+ } else {
+ ErrorView(errortext: String(localized: "unknown ToS")) //
TODO: ???
+ }
+ }
+ }
+}
diff --git a/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawURIView.swift
b/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawURIView.swift
new file mode 100644
index 0000000..da3f547
--- /dev/null
+++ b/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawURIView.swift
@@ -0,0 +1,100 @@
+/*
+ * 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, both from the bank's website
+// we show the user the withdrawal details - but first the ToS must be accepted
+// after the user confirmed the withdrawal, we remind them to return to the
bank website to confirm there, too
+struct WithdrawURIView: View {
+ private let symLog = SymLogV()
+ let navTitle = String(localized: "Accept Withdrawal")
+
+ // the URL from the bank website
+ let url: URL
+
+ @EnvironmentObject private var model: WalletModel
+
+ // the exchange used for this withdrawal.
+ @State private var exchangeBaseUrl: String? = nil
+ @State private var withdrawalAmountDetails: WithdrawalAmountDetails?
+
+ var body: some View {
+ let badURL = "Error in URL: \(url)"
+ VStack {
+ if let withdrawalAmountDetails, let exchangeBaseUrl {
+ List {
+ let raw = withdrawalAmountDetails.amountRaw
+ let effective = withdrawalAmountDetails.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: "Chosen
amount to withdraw:"),
+ topAmount: raw, fee: fee,
+ bottomTitle: String(localized: "\(currency)
to be withdrawn:"),
+ bottomAmount: effective,
+ large: false, pending: false,
incoming: true,
+ baseURL: exchangeBaseUrl)
+ let someCoins = SomeCoins(details: withdrawalAmountDetails)
+ QuiteSomeCoins(someCoins: someCoins, shouldShowFee: false,
+ currency: raw.currencyStr, amountEffective:
effective)
+ }
+ .navigationTitle(navTitle)
+ let tosAccepted = withdrawalAmountDetails.tosAccepted
+ if tosAccepted {
+ NavigationLink(destination: LazyView {
+ WithdrawAcceptDone(exchangeBaseUrl: exchangeBaseUrl,
url: url)
+ }) {
+ Text("Confirm Withdrawal") //
SHEET_WITHDRAW_ACCEPT
+ }.buttonStyle(TalerButtonStyle(type: .prominent))
+ .padding()
+ } else {
+ NavigationLink(destination: LazyView {
+ WithdrawTOSView(exchangeBaseUrl: exchangeBaseUrl,
+ viewID: SHEET_WITHDRAW_TOS,
+ acceptAction: nil) // pop
back to here
+ }) {
+ Text("Check Terms of Service") // VIEW_WITHDRAW_TOS
+ }.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_WITHDRAWAL)
+ }
+ .task {
+ do { // TODO: cancelled
+ symLog.log(".task")
+ let withdrawUriInfo = try await
model.loadWithdrawalDetailsForUriM(url.absoluteString)
+ let amount = withdrawUriInfo.amount
+ if let baseURL = withdrawUriInfo.defaultExchangeBaseUrl {
+ exchangeBaseUrl = baseURL
+ } else if let first = withdrawUriInfo.possibleExchanges.first {
+ exchangeBaseUrl = first.exchangeBaseUrl
+ }
+ if let exchangeBaseUrl {
+ let details = try await
model.loadWithdrawalDetailsForAmountM(exchangeBaseUrl, amount: amount)
+ withdrawalAmountDetails = details
+// agePicker.setAges(ages: details?.ageRestrictionOptions)
+ } else { // TODO: error
+ symLog.log("no exchangeBaseUrl")
+ withdrawalAmountDetails = nil
+ }
+ } catch { // TODO: error
+ symLog.log(error.localizedDescription)
+ withdrawalAmountDetails = nil
+ }
+ }
+ }
+}
diff --git a/taler-swift/Sources/taler-swift/Amount.swift
b/taler-swift/Sources/taler-swift/Amount.swift
index d97612e..cfd01e0 100644
--- a/taler-swift/Sources/taler-swift/Amount.swift
+++ b/taler-swift/Sources/taler-swift/Amount.swift
@@ -1,17 +1,6 @@
/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
*/
import Foundation
@@ -169,7 +158,12 @@ public class Amount: Codable, Hashable,
CustomStringConvertible {
self.integer = integer
self.fraction = fraction
}
-
+ public init(currency: String, value: UInt64) {
+ self.currency = currency
+ self.integer = value / 100 // TODO: fractional digits can be
0, 2 or 3
+ self.fraction = UInt32(value - (self.integer * 100))
+ }
+
/// Initializes an amount from a decoder.
/// - Parameters:
/// - from: The decoder to extract the amount from.
@@ -282,8 +276,10 @@ public class Amount: Codable, Hashable,
CustomStringConvertible {
}
var remainder = result.integer % UInt64(divisor)
result.integer = result.integer / UInt64(divisor)
- remainder = (remainder * UInt64(Amount.fractionalBase)) +
UInt64(result.fraction)
- result.fraction = UInt32(remainder) / divisor
+
+ let fractionalBase = UInt64(Amount.fractionalBase)
+ remainder = (remainder * fractionalBase) + UInt64(result.fraction)
+ result.fraction = UInt32(remainder / UInt64(divisor))
try result.normalize()
return result
}
@@ -392,4 +388,19 @@ public class Amount: Codable, Hashable,
CustomStringConvertible {
public static func zero(currency: String) -> Amount {
return Amount(currency: currency, integer: 0, fraction: 0)
}
+
+ public static func amountFromCents(_ currency: String, _ cents: UInt64) ->
Amount {
+ let amount100 = Amount(currency: currency, integer: cents, fraction: 0)
+ do {
+ let amount = try amount100 / 100
+ return amount
+ } catch { // shouldn't happen, but if it does then truncate
+ return Amount(currency: currency, integer: cents / 100, fraction:
0)
+ }
+ }
+}
+// MARK: -
+extension Amount: Identifiable {
+ // needed to be passed as value for .sheet
+ public var id: Amount {self}
}
diff --git a/taler-swift/Sources/taler-swift/Time.swift
b/taler-swift/Sources/taler-swift/Time.swift
index 1d76053..43ada5d 100644
--- a/taler-swift/Sources/taler-swift/Time.swift
+++ b/taler-swift/Sources/taler-swift/Time.swift
@@ -1,17 +1,6 @@
/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
*/
import Foundation
@@ -45,7 +34,7 @@ public enum Timestamp: Codable, Hashable {
} else {
self = Timestamp.milliseconds(try
container.decode(UInt64.self, forKey: .t_ms))
}
- } catch {
+ } catch { // rethrows or never
let stringValue = try container.decode(String.self, forKey: .t_s)
if stringValue == "never" {
self = Timestamp.never
@@ -79,6 +68,23 @@ extension Timestamp {
return Timestamp.milliseconds(Date().millisecondsSince1970)
}
+ public static func tomorrow() -> Timestamp {
+ return Timestamp.inSomeDays(1)
+ }
+
+ public static func inSomeDays(_ days: UInt) -> Timestamp {
+ let now = Date().millisecondsSince1970
+ let seconds: UInt64 = 60 * 60 * 24
+ return Timestamp.milliseconds(now + (UInt64(days) * seconds * 1000))
+ }
+
+ public static func inSomeMinutes(_ minutes: UInt) -> Timestamp {
+ let now = Date().millisecondsSince1970
+ let seconds: UInt64 = 60
+ return Timestamp.milliseconds(now + (UInt64(minutes) * seconds * 1000))
+ }
+
+
/// convenience initializer from UInt64 (milliseconds from January 1, 1970)
public init(from: UInt64) {
self = Timestamp.milliseconds(from)
diff --git a/taler-swift/Tests/taler-swiftTests/AmountTests.swift
b/taler-swift/Tests/taler-swiftTests/AmountTests.swift
index 429e9ef..a0b1f27 100644
--- a/taler-swift/Tests/taler-swiftTests/AmountTests.swift
+++ b/taler-swift/Tests/taler-swiftTests/AmountTests.swift
@@ -1,17 +1,6 @@
/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
*/
import XCTest
@testable import taler_swift
diff --git a/taler-swift/Tests/taler-swiftTests/TimeTests.swift
b/taler-swift/Tests/taler-swiftTests/TimeTests.swift
index 4bfd5c6..a26128a 100644
--- a/taler-swift/Tests/taler-swiftTests/TimeTests.swift
+++ b/taler-swift/Tests/taler-swiftTests/TimeTests.swift
@@ -1,17 +1,6 @@
/*
- * This file is part of GNU Taler
- * (C) 2022 Taler Systems S.A.
- *
- * GNU Taler is free software; you can redistribute it and/or modify it under
the
- * terms of the GNU General Public License as published by the Free Software
- * Foundation; either version 3, or (at your option) any later version.
- *
- * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
- * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
*/
import XCTest
@testable import taler_swift
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.
- [taler-taler-ios] branch master updated (7ce9180 -> f7f01e5),
gnunet <=
- [taler-taler-ios] 02/54: Moved AgePicker in its own file, gnunet, 2023/06/30
- [taler-taler-ios] 05/54: cleanup, back to Swift 5.8 (for now until Xcode 15 is usable), gnunet, 2023/06/30
- [taler-taler-ios] 04/54: PopToRoot instead of dismiss sheet, gnunet, 2023/06/30
- [taler-taler-ios] 03/54: Cleaned up buttons, gnunet, 2023/06/30
- [taler-taler-ios] 11/54: Accessibility, gnunet, 2023/06/30
- [taler-taler-ios] 18/54: remove loaded, gnunet, 2023/06/30
- [taler-taler-ios] 09/54: Launch animation, SideBarView, gnunet, 2023/06/30
- [taler-taler-ios] 01/54: Big update after DD37, gnunet, 2023/06/30
- [taler-taler-ios] 13/54: Overhaul withdraw + p2p, gnunet, 2023/06/30
- [taler-taler-ios] 17/54: for debugging time-outs, gnunet, 2023/06/30