summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--GNU Taler.entitlements (renamed from TalerWalletT.entitlements)0
-rw-r--r--Info.plist11
-rw-r--r--LICENSE.md12
-rw-r--r--TalerTests/WalletBackendTests.swift15
-rw-r--r--TalerUITests/TalerUITests.swift16
-rw-r--r--TalerWallet.xcodeproj/project.pbxproj341
-rw-r--r--TalerWallet1/Assets.xcassets/taler-logo-2023-red.imageset/Contents.json12
-rw-r--r--TalerWallet1/Assets.xcassets/taler-logo-2023-red.imageset/taler-logo-2023-red.svg19
-rw-r--r--TalerWallet1/Backend/Transaction.swift365
-rw-r--r--TalerWallet1/Backend/WalletBackendError.swift17
-rw-r--r--TalerWallet1/Backend/WalletBackendRequest.swift201
-rw-r--r--TalerWallet1/Backend/WalletCore.swift337
-rw-r--r--TalerWallet1/Controllers/Controller.swift61
-rw-r--r--TalerWallet1/Controllers/DebugViewC.swift163
-rw-r--r--TalerWallet1/Controllers/PublicConstants.swift35
-rw-r--r--TalerWallet1/Controllers/TalerWallet1App.swift83
-rw-r--r--TalerWallet1/Helper/AnyTransition+backslide.swift30
-rw-r--r--TalerWallet1/Helper/CurrencyFormatter.swift27
-rw-r--r--TalerWallet1/Helper/EqualIconWidthDomain.swift141
-rw-r--r--TalerWallet1/Helper/KeyboardResponder.swift45
-rw-r--r--TalerWallet1/Helper/LocalizedAlertError.swift54
-rw-r--r--TalerWallet1/Helper/PublicConstants.swift24
-rw-r--r--TalerWallet1/Helper/TalerDater.swift37
-rw-r--r--TalerWallet1/Helper/TalerStrings.swift15
-rw-r--r--TalerWallet1/Helper/URL+iban.swift37
-rw-r--r--TalerWallet1/Helper/View+dismissTop.swift90
-rw-r--r--TalerWallet1/Helper/WalletColors.swift56
-rw-r--r--TalerWallet1/Model/BalancesModel.swift (renamed from TalerWallet1/Views/Balances/BalancesModel.swift)52
-rw-r--r--TalerWallet1/Model/ExchangeModel.swift (renamed from TalerWallet1/Views/Exchange/ExchangeModel.swift)117
-rw-r--r--TalerWallet1/Model/PaymentURIModel.swift (renamed from TalerWallet1/Views/Payment/PaymentURIModel.swift)52
-rw-r--r--TalerWallet1/Model/Peer2peerModel.swift130
-rw-r--r--TalerWallet1/Model/PendingModel.swift78
-rw-r--r--TalerWallet1/Model/SettingsModel.swift (renamed from TalerWallet1/Model/ExchangeTestModel.swift)75
-rw-r--r--TalerWallet1/Model/TransactionsModel.swift157
-rw-r--r--TalerWallet1/Model/WalletInitModel.swift35
-rw-r--r--TalerWallet1/Model/WalletModel.swift82
-rw-r--r--TalerWallet1/Model/WithdrawModel.swift (renamed from TalerWallet1/Views/Withdraw/WithdrawURIModel.swift)142
-rw-r--r--TalerWallet1/Preview Content/transactions.json300
-rw-r--r--TalerWallet1/Quickjs/quickjs.swift15
-rw-r--r--TalerWallet1/Settings.bundle/Root.plist21
-rw-r--r--TalerWallet1/Settings.bundle/en.lproj/Root.stringsbin0 -> 546 bytes
-rw-r--r--TalerWallet1/Views/Balances/BalanceRow.swift46
-rw-r--r--TalerWallet1/Views/Balances/BalanceRowView.swift52
-rw-r--r--TalerWallet1/Views/Balances/BalancesListView.swift133
-rw-r--r--TalerWallet1/Views/Balances/BalancesSectionView.swift161
-rw-r--r--TalerWallet1/Views/Balances/CurrenciesListView.swift83
-rw-r--r--TalerWallet1/Views/Balances/CurrencyView.swift58
-rw-r--r--TalerWallet1/Views/Balances/PendingRow.swift61
-rw-r--r--TalerWallet1/Views/Balances/PendingRowView.swift48
-rw-r--r--TalerWallet1/Views/Balances/UncompletedRowView.swift34
-rw-r--r--TalerWallet1/Views/Balances/WalletEmptyView.swift44
-rw-r--r--TalerWallet1/Views/Exchange/ExchangeListView.swift144
-rw-r--r--TalerWallet1/Views/Exchange/ExchangeSectionView.swift112
-rw-r--r--TalerWallet1/Views/Exchange/ManualWithdraw.swift199
-rw-r--r--TalerWallet1/Views/Exchange/ManualWithdrawDone.swift79
-rw-r--r--TalerWallet1/Views/HelperViews/AmountView.swift28
-rw-r--r--TalerWallet1/Views/HelperViews/Buttons.swift246
-rw-r--r--TalerWallet1/Views/HelperViews/CopyShare.swift85
-rw-r--r--TalerWallet1/Views/HelperViews/CurrencyField.swift221
-rw-r--r--TalerWallet1/Views/HelperViews/CurrencyInputView.swift60
-rw-r--r--TalerWallet1/Views/HelperViews/LaunchAnimationView.swift (renamed from TalerWallet1/Views/Main/LaunchAnimationView.swift)18
-rw-r--r--TalerWallet1/Views/HelperViews/ListStyle.swift113
-rw-r--r--TalerWallet1/Views/HelperViews/LoadingView.swift39
-rw-r--r--TalerWallet1/Views/HelperViews/QRGeneratorView.swift62
-rw-r--r--TalerWallet1/Views/HelperViews/SelectDays.swift56
-rw-r--r--TalerWallet1/Views/HelperViews/TextFieldAlert.swift15
-rw-r--r--TalerWallet1/Views/Main/ContentView.swift92
-rw-r--r--TalerWallet1/Views/Main/ErrorView.swift22
-rw-r--r--TalerWallet1/Views/Main/MainView.swift100
-rw-r--r--TalerWallet1/Views/Main/SideBarView.swift71
-rw-r--r--TalerWallet1/Views/Main/WalletEmptyView.swift44
-rw-r--r--TalerWallet1/Views/Payment/PaymentAcceptView.swift103
-rw-r--r--TalerWallet1/Views/Payment/PaymentURIView.swift47
-rw-r--r--TalerWallet1/Views/Peer2peer/ReceivePurpose.swift129
-rw-r--r--TalerWallet1/Views/Peer2peer/RequestPayment.swift77
-rw-r--r--TalerWallet1/Views/Peer2peer/SendAmount.swift85
-rw-r--r--TalerWallet1/Views/Peer2peer/SendNow.swift87
-rw-r--r--TalerWallet1/Views/Peer2peer/SendPurpose.swift126
-rw-r--r--TalerWallet1/Views/Pending/PendingModel.swift82
-rw-r--r--TalerWallet1/Views/Pending/PendingOpsListView.swift75
-rw-r--r--TalerWallet1/Views/Settings/Pending/PendingOpView.swift (renamed from TalerWallet1/Views/Pending/PendingOpView.swift)31
-rw-r--r--TalerWallet1/Views/Settings/Pending/PendingOpsListView.swift64
-rw-r--r--TalerWallet1/Views/Settings/SettingsItem.swift35
-rw-r--r--TalerWallet1/Views/Settings/SettingsView.swift245
-rw-r--r--TalerWallet1/Views/Sheets/QRSheet.swift52
-rw-r--r--TalerWallet1/Views/Sheets/ShareSheet.swift40
-rw-r--r--TalerWallet1/Views/Sheets/Sheet.swift44
-rw-r--r--TalerWallet1/Views/Sheets/URLSheet.swift45
-rw-r--r--TalerWallet1/Views/Transactions/ManualDetails.swift69
-rw-r--r--TalerWallet1/Views/Transactions/ThreeAmounts.swift109
-rw-r--r--TalerWallet1/Views/Transactions/TransactionDetail.swift136
-rw-r--r--TalerWallet1/Views/Transactions/TransactionDetailView.swift255
-rw-r--r--TalerWallet1/Views/Transactions/TransactionRow.swift92
-rw-r--r--TalerWallet1/Views/Transactions/TransactionRowView.swift137
-rw-r--r--TalerWallet1/Views/Transactions/TransactionsEmptyView.swift37
-rw-r--r--TalerWallet1/Views/Transactions/TransactionsListView.swift153
-rw-r--r--TalerWallet1/Views/Transactions/TransactionsModel.swift61
-rw-r--r--TalerWallet1/Views/URLSheet.swift64
-rw-r--r--TalerWallet1/Views/Withdraw/WithdrawAcceptView.swift71
-rw-r--r--TalerWallet1/Views/Withdraw/WithdrawProgressView.swift45
-rw-r--r--TalerWallet1/Views/Withdraw/WithdrawTOSView.swift96
-rw-r--r--TalerWallet1/Views/Withdraw/WithdrawURIView.swift103
-rw-r--r--TalerWallet1/Views/WithdrawBankIntegrated/WithdrawAcceptView.swift71
-rw-r--r--TalerWallet1/Views/WithdrawBankIntegrated/WithdrawProgressView.swift27
-rw-r--r--TalerWallet1/Views/WithdrawBankIntegrated/WithdrawTOSView.swift90
-rw-r--r--TalerWallet1/Views/WithdrawBankIntegrated/WithdrawURIView.swift76
-rw-r--r--taler-swift/Sources/taler-swift/Amount.swift43
-rw-r--r--taler-swift/Sources/taler-swift/Time.swift28
-rw-r--r--taler-swift/Tests/taler-swiftTests/AmountTests.swift15
-rw-r--r--taler-swift/Tests/taler-swiftTests/TimeTests.swift15
110 files changed, 6583 insertions, 2603 deletions
diff --git a/TalerWalletT.entitlements b/GNU Taler.entitlements
index dfdca9d..dfdca9d 100644
--- a/TalerWalletT.entitlements
+++ b/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..66a3b04 100644
--- a/TalerWallet.xcodeproj/project.pbxproj
+++ b/TalerWallet.xcodeproj/project.pbxproj
@@ -3,11 +3,32 @@
archiveVersion = 1;
classes = {
};
- objectVersion = 52;
+ objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
+ 4E16E12329F3BB99008B9C86 /* CurrencyFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E16E12229F3BB99008B9C86 /* CurrencyFormatter.swift */; };
+ 4E363CBC2A237E0900D7E98C /* URL+iban.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E363CBB2A237E0900D7E98C /* URL+iban.swift */; };
+ 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 */; };
+ 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 */; };
+ 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 /* ReceivePurpose.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9320462A164BC700A87B0E /* ReceivePurpose.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 */; };
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,7 +44,7 @@
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 /* SettingsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095102989CBB00043A8A1 /* SettingsModel.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 */; };
@@ -38,24 +59,24 @@
4EB095542989CBFE0043A8A1 /* PaymentURIModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB0952C2989CBFE0043A8A1 /* PaymentURIModel.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 */; };
+ 4EB095572989CBFE0043A8A1 /* TransactionRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095302989CBFE0043A8A1 /* TransactionRowView.swift */; };
+ 4EB095582989CBFE0043A8A1 /* TransactionDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095312989CBFE0043A8A1 /* TransactionDetailView.swift */; };
4EB095592989CBFE0043A8A1 /* TransactionsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095322989CBFE0043A8A1 /* TransactionsModel.swift */; };
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 */; };
+ 4EB095622989CBFE0043A8A1 /* WithdrawModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB0953D2989CBFE0043A8A1 /* WithdrawModel.swift */; };
4EB095632989CBFE0043A8A1 /* WithdrawAcceptView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB0953E2989CBFE0043A8A1 /* WithdrawAcceptView.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 */; };
@@ -64,6 +85,17 @@
4EB0956E2989CBFE0043A8A1 /* PendingModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB0954C2989CBFE0043A8A1 /* PendingModel.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 */; };
+ 4EC90C782A1B528B0071DC58 /* ExchangeSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EC90C772A1B528B0071DC58 /* ExchangeSectionView.swift */; };
+ 4ECB62802A0BA6DF004ABBB7 /* Peer2peerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ECB627F2A0BA6DF004ABBB7 /* Peer2peerModel.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 +130,30 @@
/* 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+iban.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URL+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>"; };
+ 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>"; };
+ 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 /* ReceivePurpose.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReceivePurpose.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>"; };
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,7 +169,7 @@
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 /* SettingsModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsModel.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>"; };
@@ -130,24 +184,24 @@
4EB0952C2989CBFE0043A8A1 /* PaymentURIModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaymentURIModel.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>"; };
+ 4EB095302989CBFE0043A8A1 /* TransactionRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionRowView.swift; sourceTree = "<group>"; };
+ 4EB095312989CBFE0043A8A1 /* TransactionDetailView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionDetailView.swift; sourceTree = "<group>"; };
4EB095322989CBFE0043A8A1 /* TransactionsModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionsModel.swift; sourceTree = "<group>"; };
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>"; };
+ 4EB0953D2989CBFE0043A8A1 /* WithdrawModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WithdrawModel.swift; sourceTree = "<group>"; };
4EB0953E2989CBFE0043A8A1 /* WithdrawAcceptView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WithdrawAcceptView.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>"; };
@@ -156,8 +210,18 @@
4EB0954C2989CBFE0043A8A1 /* PendingModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PendingModel.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>"; };
+ 4EC90C772A1B528B0071DC58 /* ExchangeSectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExchangeSectionView.swift; sourceTree = "<group>"; };
+ 4ECB627F2A0BA6DF004ABBB7 /* Peer2peerModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Peer2peerModel.swift; sourceTree = "<group>"; };
+ 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 +235,7 @@
4EB094FD29897D280043A8A1 /* SymLog in Frameworks */,
4EB094F829897CA20043A8A1 /* FTalerWalletcore.framework in Frameworks */,
ABC13AA32859962800D23185 /* taler-swift in Frameworks */,
+ 4EEC157629F8ECBF00D46A03 /* CodeScanner in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -198,9 +263,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 +276,7 @@
isa = PBXGroup;
children = (
4EB094F329897A510043A8A1 /* Preview Assets.xcassets */,
+ 4E753A042A08E720002D9328 /* transactions.json */,
);
path = "Preview Content";
sourceTree = "<group>";
@@ -227,6 +294,8 @@
children = (
4EB094EC298979620043A8A1 /* TalerWallet1App.swift */,
4EB095012989C9BC0043A8A1 /* Controller.swift */,
+ 4EA1ABBD29A3833A008821EA /* PublicConstants.swift */,
+ 4E753A052A0952F7002D9328 /* DebugViewC.swift */,
);
path = Controllers;
sourceTree = "<group>";
@@ -234,10 +303,16 @@
4EB095052989CB7C0043A8A1 /* Helper */ = {
isa = PBXGroup;
children = (
+ 4E363CBD2A23CB2100D7E98C /* AnyTransition+backslide.swift */,
+ 4E16E12229F3BB99008B9C86 /* CurrencyFormatter.swift */,
+ 4EAD117529F672FA008EDD0B /* KeyboardResponder.swift */,
+ 4E363CC12A2621C200D7E98C /* LocalizedAlertError.swift */,
4EB095062989CB7C0043A8A1 /* TalerDater.swift */,
4EB095072989CB7C0043A8A1 /* TalerStrings.swift */,
4EB095082989CB7C0043A8A1 /* View+dismissTop.swift */,
- 4EA1ABBD29A3833A008821EA /* PublicConstants.swift */,
+ 4E363CBB2A237E0900D7E98C /* URL+iban.swift */,
+ 4E9320422A14F6EA00A87B0E /* WalletColors.swift */,
+ 4E8E25322A1CD39700A27BFA /* EqualIconWidthDomain.swift */,
);
path = Helper;
sourceTree = "<group>";
@@ -253,9 +328,16 @@
4EB0950F2989CBB00043A8A1 /* Model */ = {
isa = PBXGroup;
children = (
- 4EB095102989CBB00043A8A1 /* ExchangeTestModel.swift */,
4EB095112989CBB00043A8A1 /* WalletModel.swift */,
4EB095142989CBB00043A8A1 /* WalletInitModel.swift */,
+ 4EB095352989CBFE0043A8A1 /* BalancesModel.swift */,
+ 4EB095282989CBFE0043A8A1 /* ExchangeModel.swift */,
+ 4ECB627F2A0BA6DF004ABBB7 /* Peer2peerModel.swift */,
+ 4EB0954C2989CBFE0043A8A1 /* PendingModel.swift */,
+ 4EB0952C2989CBFE0043A8A1 /* PaymentURIModel.swift */,
+ 4EB095102989CBB00043A8A1 /* SettingsModel.swift */,
+ 4EB095322989CBFE0043A8A1 /* TransactionsModel.swift */,
+ 4EB0953D2989CBFE0043A8A1 /* WithdrawModel.swift */,
);
path = Model;
sourceTree = "<group>";
@@ -275,15 +357,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 +375,7 @@
children = (
4EB095252989CBFE0043A8A1 /* SettingsView.swift */,
4EB095262989CBFE0043A8A1 /* SettingsItem.swift */,
+ 4EB0954B2989CBFE0043A8A1 /* Pending */,
);
path = Settings;
sourceTree = "<group>";
@@ -300,8 +383,10 @@
4EB095272989CBFE0043A8A1 /* Exchange */ = {
isa = PBXGroup;
children = (
- 4EB095282989CBFE0043A8A1 /* ExchangeModel.swift */,
4EB095292989CBFE0043A8A1 /* ExchangeListView.swift */,
+ 4EC90C772A1B528B0071DC58 /* ExchangeSectionView.swift */,
+ 4E50B34F2A1BEE8000F9F01C /* ManualWithdraw.swift */,
+ 4EB431662A1E55C700C5690E /* ManualWithdrawDone.swift */,
);
path = Exchange;
sourceTree = "<group>";
@@ -309,9 +394,8 @@
4EB0952A2989CBFE0043A8A1 /* Payment */ = {
isa = PBXGroup;
children = (
- 4EB0952B2989CBFE0043A8A1 /* PaymentAcceptView.swift */,
- 4EB0952C2989CBFE0043A8A1 /* PaymentURIModel.swift */,
4EB0952D2989CBFE0043A8A1 /* PaymentURIView.swift */,
+ 4EB0952B2989CBFE0043A8A1 /* PaymentAcceptView.swift */,
);
path = Payment;
sourceTree = "<group>";
@@ -320,9 +404,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 +416,33 @@
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 */,
+ 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 */,
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 +451,16 @@
isa = PBXGroup;
children = (
4EB095472989CBFE0043A8A1 /* Buttons.swift */,
+ 4EF840A62A0B85F400EE0D47 /* CopyShare.swift */,
+ 4ECB62812A0BB01D004ABBB7 /* SelectDays.swift */,
+ 4E53A33629F50B7B00830EC2 /* CurrencyField.swift */,
+ 4EA551242A2C923600FEC9A8 /* CurrencyInputView.swift */,
+ 4EEC157229F8242800D46A03 /* QRGeneratorView.swift */,
+ 4E6EDD862A363D8D0031D520 /* ListStyle.swift */,
4EB095482989CBFE0043A8A1 /* TextFieldAlert.swift */,
4EB095492989CBFE0043A8A1 /* AmountView.swift */,
4EB0954A2989CBFE0043A8A1 /* LoadingView.swift */,
+ 4EB095432989CBFE0043A8A1 /* LaunchAnimationView.swift */,
);
path = HelperViews;
sourceTree = "<group>";
@@ -377,19 +468,41 @@
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 /* ReceivePurpose.swift */,
+ );
+ path = Peer2peer;
+ sourceTree = "<group>";
+ };
+ 4EEC157129F7188B00D46A03 /* Sheets */ = {
+ isa = PBXGroup;
+ children = (
+ 4EEC157729F9032900D46A03 /* Sheet.swift */,
+ 4EEC157929F9427F00D46A03 /* QRSheet.swift */,
+ 4E753A072A0B6A5F002D9328 /* ShareSheet.swift */,
+ 4EB095332989CBFE0043A8A1 /* URLSheet.swift */,
+ );
+ 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 +514,7 @@
D14AFD1E24D232B300C51073 /* Products */ = {
isa = PBXGroup;
children = (
- D14AFD1D24D232B300C51073 /* TalerWalletT.app */,
+ D14AFD1D24D232B300C51073 /* GNU Taler.app */,
D14AFD3324D232B500C51073 /* TalerTests.xctest */,
D14AFD3E24D232B500C51073 /* TalerUITests.xctest */,
);
@@ -430,9 +543,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 +556,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 +641,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 +660,7 @@
buildActionMask = 2147483647;
files = (
4EB094F429897A510043A8A1 /* Preview Assets.xcassets in Resources */,
+ 4E363CC02A24754200D7E98C /* Settings.bundle in Resources */,
4EB094F0298979D30043A8A1 /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -570,43 +686,65 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 4ECB62822A0BB01D004ABBB7 /* SelectDays.swift in Sources */,
4EB095512989CBFE0043A8A1 /* ExchangeModel.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 */,
+ 4EB095602989CBFE0043A8A1 /* BalancesSectionView.swift in Sources */,
+ 4EEC157329F8242800D46A03 /* QRGeneratorView.swift in Sources */,
4EB095222989CBCB0043A8A1 /* Transaction.swift in Sources */,
- 4EB0955D2989CBFE0043A8A1 /* CurrenciesListView.swift in Sources */,
+ 4E9320432A14F6EA00A87B0E /* WalletColors.swift in Sources */,
+ 4EB0955D2989CBFE0043A8A1 /* BalancesListView.swift in Sources */,
4EB095532989CBFE0043A8A1 /* PaymentAcceptView.swift in Sources */,
4EB095212989CBCB0043A8A1 /* WalletBackendError.swift in Sources */,
- 4EB0955E2989CBFE0043A8A1 /* PendingRow.swift in Sources */,
+ 4EB0955E2989CBFE0043A8A1 /* PendingRowView.swift in Sources */,
4EB0955B2989CBFE0043A8A1 /* BalancesModel.swift in Sources */,
4EB095632989CBFE0043A8A1 /* WithdrawAcceptView.swift in Sources */,
4EB0956D2989CBFE0043A8A1 /* LoadingView.swift in Sources */,
+ 4E50B3502A1BEE8000F9F01C /* ManualWithdraw.swift in Sources */,
+ 4E87C8732A31CB7F001C6406 /* TransactionsEmptyView.swift in Sources */,
+ 4E87C8752A34B411001C6406 /* UncompletedRowView.swift in Sources */,
+ 4E40E0BE29F25ABB00B85369 /* SendAmount.swift in Sources */,
+ 4E8E25332A1CD39700A27BFA /* EqualIconWidthDomain.swift in Sources */,
4EB095542989CBFE0043A8A1 /* PaymentURIModel.swift in Sources */,
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 */,
+ 4EB431672A1E55C700C5690E /* ManualWithdrawDone.swift in Sources */,
+ 4E9320472A164BC700A87B0E /* ReceivePurpose.swift in Sources */,
+ 4E753A082A0B6A5F002D9328 /* ShareSheet.swift in Sources */,
4EB0956C2989CBFE0043A8A1 /* AmountView.swift in Sources */,
+ 4E363CBE2A23CB2100D7E98C /* AnyTransition+backslide.swift in Sources */,
4EB095592989CBFE0043A8A1 /* TransactionsModel.swift in Sources */,
4EB0955F2989CBFE0043A8A1 /* WalletEmptyView.swift in Sources */,
+ 4E16E12329F3BB99008B9C86 /* CurrencyFormatter.swift in Sources */,
4EB095192989CBB00043A8A1 /* WalletInitModel.swift in Sources */,
4EB095092989CB7C0043A8A1 /* TalerDater.swift in Sources */,
+ 4E363CC22A2621C200D7E98C /* LocalizedAlertError.swift in Sources */,
4EB0950E2989CB9A0043A8A1 /* quickjs.swift in Sources */,
- 4EB095152989CBB00043A8A1 /* ExchangeTestModel.swift in Sources */,
+ 4E53A33729F50B7B00830EC2 /* CurrencyField.swift in Sources */,
+ 4EB095152989CBB00043A8A1 /* SettingsModel.swift in Sources */,
4EB095692989CBFE0043A8A1 /* ErrorView.swift in Sources */,
4EB0956E2989CBFE0043A8A1 /* PendingModel.swift in Sources */,
4EB095522989CBFE0043A8A1 /* ExchangeListView.swift in Sources */,
4EB095642989CBFE0043A8A1 /* WithdrawProgressView.swift in Sources */,
- 4EB095582989CBFE0043A8A1 /* TransactionDetail.swift in Sources */,
+ 4EEC157A29F9427F00D46A03 /* QRSheet.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 +752,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 /* WithdrawModel.swift in Sources */,
+ 4EC90C782A1B528B0071DC58 /* ExchangeSectionView.swift in Sources */,
+ 4E7940DE29FC307C00A9AEA1 /* SendPurpose.swift in Sources */,
+ 4ECB62802A0BA6DF004ABBB7 /* Peer2peerModel.swift in Sources */,
4EB0950A2989CB7C0043A8A1 /* TalerStrings.swift in Sources */,
+ 4EA551252A2C923600FEC9A8 /* CurrencyInputView.swift in Sources */,
+ 4E363CBC2A237E0900D7E98C /* URL+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 +791,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 +922,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 Distribution";
+ CODE_SIGN_STYLE = Manual;
+ CURRENT_PROJECT_VERSION = 6;
+ 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 +945,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;
@@ -812,16 +964,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 = 6;
+ 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 +987,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 +1038,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 +1060,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 +1083,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 +1105,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 +1143,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 +1167,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-2023-red.imageset/Contents.json b/TalerWallet1/Assets.xcassets/taler-logo-2023-red.imageset/Contents.json
new file mode 100644
index 0000000..50ee748
--- /dev/null
+++ b/TalerWallet1/Assets.xcassets/taler-logo-2023-red.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "taler-logo-2023-red.svg",
+ "idiom" : "universal"
+ },
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
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..b49d560 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,124 @@ 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
+}
+
struct TransactionCommon: Decodable {
- var type: String
+ enum TransactionType: String, Codable {
+ case withdrawal
+ case deposit
+ case payment
+ case refund
+ case refresh
+ case reward
+// case tip
+ 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 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 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 +140,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 +169,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 +187,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 +195,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,51 +240,126 @@ 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
+}
+
enum Transaction: Decodable, Hashable, Identifiable {
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 }
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 isDone : Bool { common.txState.major == .done }
+ var isPending : Bool { common.txState.major == .pending }
+ var isAborting : Bool { common.txState.major == .aborting }
+ var isAborted : Bool { common.txState.major == .aborted }
+ var isFailed : Bool { common.txState.major == .failed }
+ var isExpired : Bool { common.txState.major == .expired }
+ var isSpecial : Bool { isPending || isAborting || isAborted || isFailed || isExpired }
+
+ 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 .withdrawal(let withdrawalTransaction):
return withdrawalTransaction.common
@@ -177,10 +367,14 @@ enum Transaction: Decodable, Hashable, Identifiable {
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
}
}
@@ -190,46 +384,23 @@ enum Transaction: Decodable, Hashable, Identifiable {
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..c7205a7 100644
--- a/TalerWallet1/Backend/WalletCore.swift
+++ b/TalerWallet1/Backend/WalletCore.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 // FOUNDATION has no AppStorage
import AnyCodable
@@ -24,13 +13,18 @@ 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 queue: DispatchQueue
+ private var semaphore: DispatchSemaphore
+
private var 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
@@ -41,7 +35,7 @@ class WalletCore: QuickjsMessageHandler {
let id: UInt
let args: AnyEncodable
}
-
+
private struct FullResponse: Decodable {
let type: String
let operation: String
@@ -58,6 +52,21 @@ 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()
// TODO: send shutdown message to talerWalletInstance
@@ -65,95 +74,157 @@ class WalletCore: QuickjsMessageHandler {
}
init() throws {
+ symLog.log("alloc.init wallet-core")
requestsMade = 0
+ queue = DispatchQueue(label: "net.taler.myQueue", attributes: .concurrent)
+ semaphore = DispatchSemaphore(value: 1)
quickjs = Quickjs()
quickjs.messageHandler = self
+ symLog.log("wallet-core done")
}
}
// 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 {
+ symLog.log(decoded) // TODO: .error
+ throw WalletBackendError.deserializationError
+ }
+ guard let (timeSent, completion) = completions[requestId] else {
+ symLog.log("requestId \(requestId) not in list") // TODO: .error
+ 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)
- symLog.log(decoded)
- completion(id, nil, decoded.error)
- } catch {
- symLog.log(responseDict) // TODO: error
- completion(id, nil, WalletCore.parseFailureError())
+ let jsonData = try JSONEncoder().encode(walletError)
+ } catch { // JSON encoding of response.result failed / should never happen
+ symLog.log(walletError) // TODO: .error
+ completion(requestId, timeSent, nil, WalletCore.parseFailureError())
}
+
+ // TODO: decode jsonData to WalletBackendResponseError - or HTTPError
+// 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 {
+ symLog.log(decoded) // TODO: .error
+ throw WalletBackendError.deserializationError
+ }
+ guard let (timeSent, completion) = completions[requestId] else {
+ symLog.log("requestId \(requestId) not in list") // TODO: .error
+ throw WalletBackendError.deserializationError
+ }
+ completions[requestId] = nil
+ guard let result = decoded.result else {
+ symLog.log("requestId \(requestId) got no result") // TODO: .error
+ throw WalletBackendError.deserializationError
+ }
+ do {
+ let jsonData = try JSONEncoder().encode(result)
+ symLog.log(result) // TODO: .info
+ 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())
+ }
+ }
+
+ @MainActor
+ private func postNotificationM(_ aName: NSNotification.Name,
+ object anObject: Any? = nil,
+ userInfo: [AnyHashable: Any]? = nil) async {
+ symLog.log(aName.rawValue)
+ NotificationCenter.default.post(name: aName, object: anObject, userInfo: userInfo)
+ }
+ private func postNotification(_ aName: NSNotification.Name,
+ object anObject: Any? = nil,
+ userInfo: [AnyHashable: Any]? = nil) {
+ Task {
+ await postNotificationM(aName, object: anObject, userInfo: userInfo)
+ }
+ if let userInfo { symLog.log(userInfo) } else { symLog.log(aName) }
+ }
+
+ 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 {
- 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())
- }
+ // TODO: handle other pending-operation-processed
+print("\n❗️ \(pendingOp): \(id)\n") // this is a new pendingOp I haven't seen before
+ print("\n")
+ }
+ }
+ private func handleStateTransition(_ jsonData: Data) throws {
+ do {
+ let decoded = try JSONDecoder().decode(TransactionTransition.self, from: jsonData)
+ postNotification(.TransactionStateTransition,
+ userInfo: [TRANSACTIONTRANSITION: decoded])
+ } catch { // rethrows
+ symLog.log(jsonData) // TODO: .error
+ throw WalletBackendError.deserializationError
}
}
- private func handleNotification(dict responseDict: [String : Any]) throws {
+ private func handleNotification(_ anyCodable: AnyCodable?) throws {
+ guard let anyPayload = anyCodable else { throw WalletBackendError.deserializationError }
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)
- }
- }
- 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
+ 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.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)
+
case "reserve-registered-with-bank":
- symLog.log(payload)
+ symLog.log(anyPayload)
+
+ // TODO: remove these once wallet-core doesn't send them anymore
+ case "withdraw-group-finished",
+ "pay-operation-success",
+ "withdrawal-group-bank-confirmed", // replaced by transaction-state-transition
+ "withdrawal-group-reserve-ready",
+ "coin-withdrawn", // totally useless since wallet-core handles coins in bulk
+ "waiting-for-retry", // Bla Bla Bla
+ "refresh-started", "refresh-melted",
+ "refresh-revealed", "refresh-unwarranted":
break
default:
- symLog.log(payload)
+print("\n❗️ ", 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 +233,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 +244,61 @@ 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()
+ completionHandler(requestId, now, nil, WalletCore.serializeRequestError());
+ }
}
}
}
@@ -219,27 +309,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
+print("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
}
} 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..9a3db72 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 {
@@ -30,52 +20,35 @@ enum UrlCommand {
case pay
}
+// 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
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 initWalletCoreM()
+ async throws { // M for MainActor
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 walletInitModel = WalletInitModel()
+ let versionInfo = try await walletInitModel.initWalletT()
+ WalletCore.shared.versionInfo = versionInfo
+ backendState = .ready // dismiss the launch animation
+ } catch { // rethrows
+ symLog.log(error.localizedDescription)
+ backendState = .error // TODO: ❗️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: .warning
}
}
}
@@ -112,7 +85,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 {
diff --git a/TalerWallet1/Controllers/DebugViewC.swift b/TalerWallet1/Controllers/DebugViewC.swift
new file mode 100644
index 0000000..828ba08
--- /dev/null
+++ b/TalerWallet1/Controllers/DebugViewC.swift
@@ -0,0 +1,163 @@
+/* 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
+
+// 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_REFUND =
+
+
+// MARK: -
+struct DebugViewV: View {
+ private let symLog = SymLogV()
+ @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()
+ Text(viewIDString)
+ .font(.caption2)
+ .foregroundColor(.red)
+ Spacer()
+ }
+ .edgesIgnoringSafeArea(.top)
+ }
+}
+// MARK: -
+class DebugViewC: ObservableObject {
+ private let symLog = SymLogC() // 0 to switch off viewID change logging
+ public static let shared = DebugViewC()
+ @AppStorage("developerMode") var developerMode: Bool = false
+
+ @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)")
+ viewID = newID // publish ON
+ } else if viewID != newID {
+ symLog.log("switching from \(viewID) to \(newID)")
+ viewID = newID // publish new viewID
+ } else {
+ symLog.log("\(newID) 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)")
+ viewID = 0 // publish OFF
+ } else {
+ symLog.log("off, will not use \(newID)")
+ // 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)")
+ sheetID = newID // publish new sheetID
+ } else {
+ symLog.log("\(newID) stays")
+ // 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)")
+ sheetID = 0 // publish OFF
+ } else {
+ symLog.log("off, will not use \(newID)")
+ // 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..386f6f6
--- /dev/null
+++ b/TalerWallet1/Controllers/PublicConstants.swift
@@ -0,0 +1,35 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import Foundation
+
+public let DEMOBANK = "https://bAnK.dEmO.tAlEr.nEt" // should be weird to read, but still work
+public let DEMOSHOP = "https://shop.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"
+
+extension Notification.Name {
+ 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")
+}
diff --git a/TalerWallet1/Controllers/TalerWallet1App.swift b/TalerWallet1/Controllers/TalerWallet1App.swift
index 1eac830..98593ce 100644
--- a/TalerWallet1/Controllers/TalerWallet1App.swift
+++ b/TalerWallet1/Controllers/TalerWallet1App.swift
@@ -1,17 +1,12 @@
/*
- * 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
@@ -19,11 +14,15 @@ import SymLog
@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
+ private let walletCore = WalletCore.shared
// our main controller
- @StateObject private var controller = Controller.shared
+ private let controller = Controller.shared
+ private let debugViewC = DebugViewC.shared
func scheduleAppRefresh() {
let request = BGAppRefreshTaskRequest(identifier: "net.taler.refresh")
@@ -33,21 +32,41 @@ struct TalerWallet1App: App {
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()
+ .environmentObject(debugViewC) // change viewID / sheetID
+ .environmentObject(viewState) // popToRoot
+ .environmentObject(controller)
+ /// external events are taler:// or payto:// URLs passed to this app
+ /// we handle them in .onOpenURL in MainView.swift
+ .handlesExternalEvents(preferring: ["*"], allowing: ["*"])
+ .task {
+ symLog.log("task -> initWalletCore")
+ try! await controller.initWalletCoreM() // will (and should) crash on failure
+ symLog.log("task -> initWalletCore done")
+ }
+ .onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification, object: nil)) { _ in
+ print("❗️App Did Become Active notification")
+ }
+ .onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification, object: nil)) { _ in
+ print("❗️App Will Resign notification")
+ isActive = false
+ }
+ .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification, object: nil)) { _ in
+ print("❗️App Will Enter Foreground notification")
+ isActive = true
+ }
+ .onReceive(NotificationCenter.default.publisher(for: UIApplication.willTerminateNotification, object: nil)) { _ in
+ print("❗️App Will Terminate notification")
+ }
+
}
.onChange(of: phase) { newPhase in
switch newPhase {
- case .background: scheduleAppRefresh()
+ case .active:
+ print("❗️ .onChange(of: phase) ==> newPhase: Active")
+ case .background:
+ print("❗️ .onChange(of: phase) ==> newPhase: Background)")
+ scheduleAppRefresh()
default: break
}
}
@@ -76,3 +95,15 @@ struct TalerWallet1App: App {
}
}
+
+final class ViewState : ObservableObject {
+ static let shared = ViewState()
+ @Published var rootViewId = UUID()
+
+ public func popToRootView() -> Void {
+ let _ = symLog()
+ rootViewId = UUID() // setting a new ID will cause tableView popToRootView behaviour
+ }
+
+ private init() { }
+}
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+iban.swift b/TalerWallet1/Helper/URL+iban.swift
new file mode 100644
index 0000000..ca4689c
--- /dev/null
+++ b/TalerWallet1/Helper/URL+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+dismissTop.swift b/TalerWallet1/Helper/View+dismissTop.swift
index bf6721e..ffd9c31 100644
--- a/TalerWallet1/Helper/View+dismissTop.swift
+++ b/TalerWallet1/Helper/View+dismissTop.swift
@@ -1,20 +1,80 @@
-/*
- * 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
+// John Sundell
+extension View {
+ func onNotification(
+ _ notificationName: Notification.Name,
+ perform action: @escaping () -> Void
+ ) -> some View {
+ onReceive(NotificationCenter.default
+ .publisher(for: notificationName)
+ ) { _ in
+ action()
+ }
+ }
+ func onNotification(
+ _ notificationName: Notification.Name,
+ perform action: @escaping (_ notification: Notification) -> Void
+ ) -> some View {
+ onReceive(NotificationCenter.default
+ .publisher(for: notificationName)
+ ) { notification in
+ action(notification)
+ }
+ }
+
+ func onNotificationM( // M for main thread
+ _ notificationName: Notification.Name,
+ perform action: @escaping () -> Void
+ ) -> some View {
+ onReceive(NotificationCenter.default
+ .publisher(for: notificationName)
+ .receive(on: RunLoop.main)
+ ) { _ in
+ action()
+ }
+ }
+
+ func onNotificationM( // M for main thread
+ _ notificationName: Notification.Name,
+ perform action: @escaping (_ notification: Notification) -> Void
+ ) -> some View {
+ onReceive(NotificationCenter.default
+ .publisher(for: notificationName)
+ .receive(on: RunLoop.main)
+ ) { notification in
+ action(notification)
+ }
+ }
+
+ func onAppEnteredBackground(
+ perform action: @escaping () -> Void
+ ) -> some View {
+ onNotification(
+ UIApplication.didEnterBackgroundNotification,
+ perform: action
+ )
+ }
+}
+
/// This is just a workaround for a SwiftUI bug
/// A presented sheet (SwiftUI view) doesn't always close when calling "dismiss()" provided by @Environment(\.dismiss),
/// so we are walking the view stack to find the top presentedViewController (UIKit) and dismiss it.
diff --git a/TalerWallet1/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/Views/Balances/BalancesModel.swift b/TalerWallet1/Model/BalancesModel.swift
index 80197d9..525aabf 100644
--- a/TalerWallet1/Views/Balances/BalancesModel.swift
+++ b/TalerWallet1/Model/BalancesModel.swift
@@ -1,25 +1,19 @@
/*
- * 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
+// MARK: -
class BalancesModel: WalletModel {
- @Published var balances: [Balance]? // update view
+ @Published var balances: [Balance]
+
+ override init(_ symbol: Int = -1) {
+ balances = [] // empty, but not nil
+ super.init(symbol)
+ }
}
// MARK: -
/// A request to get the balances held in the wallet.
@@ -41,6 +35,7 @@ struct Balance: Decodable, Hashable {
var pendingOutgoing: Amount
var hasPendingTransactions: Bool
var requiresUserInput: Bool
+ var scopeInfo: ScopeInfo
public static func == (lhs: Balance, rhs: Balance) -> Bool {
return lhs.available == rhs.available &&
@@ -61,13 +56,32 @@ struct Balance: Decodable, Hashable {
// MARK: -
extension BalancesModel {
/// fetch Balances from Wallet-Core. No networking involved
- @MainActor func fetchBalances() async throws {
+ @MainActor func fetchBalancesM()
+ async { // M for MainActor
do {
let request = GetBalances()
- let response = try await sendRequest(request, ASYNCDELAY)
- balances = response.balances // trigger view update in CurrenciesListView
+ let response = try await sendRequestM(request, ASYNCDELAY)
+ balances = response.balances // trigger view update in BalancesListView
} catch {
- throw error
+ balances = []
+ }
+ }
+}
+
+// MARK: -
+extension BalancesModel {
+ private static var currencies: [String] = [] // names of currencies
+ private static var models: [BalancesModel] = [] // one model per currency
+
+ static func model(currency: String) -> BalancesModel {
+ if let index = BalancesModel.currencies.firstIndex(of:currency) {
+ let model = BalancesModel.models[index]
+ return model
+ } else { // new currency
+ let model = BalancesModel()
+ BalancesModel.models.append(model)
+ BalancesModel.currencies.append(currency)
+ return model
}
}
}
diff --git a/TalerWallet1/Views/Exchange/ExchangeModel.swift b/TalerWallet1/Model/ExchangeModel.swift
index c16e720..fd2a5d5 100644
--- a/TalerWallet1/Views/Exchange/ExchangeModel.swift
+++ b/TalerWallet1/Model/ExchangeModel.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,36 +8,16 @@ 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
+ @Published var exchanges: [Exchange]
- struct Response: Decodable { // list of known exchanges
- var exchanges: [Exchange]
+ override init(_ symbol: Int = -1) {
+ exchanges = [] // empty, but not nil
+ super.init(symbol)
}
}
-
-/// 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 +26,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 +45,88 @@ 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 {
+ func operation() -> String { return "addExchange" }
+ func args() -> Args { return Args(exchangeBaseUrl: exchangeBaseUrl) }
+
+ var exchangeBaseUrl: String
+
+ struct Args: Encodable {
+ var exchangeBaseUrl: String
+ }
+
+ struct Response: Decodable {}
+}
// MARK: -
extension ExchangeModel {
/// ask wallet-core for its list of known exchanges
- @MainActor func updateList() async throws {
+ @MainActor func updateListM()
+ async throws { // M for MainActor
do {
let request = ListExchanges()
- let response = try await sendRequest(request, ASYNCDELAY)
+ let response = try await sendRequestM(request, ASYNCDELAY)
exchanges = response.exchanges // trigger view update in ExchangeListView
- } catch { // TODO: Error
+ } catch { // rethrows
symLog?.log(error.localizedDescription)
throw error
}
}
- /// add a new exchange with URL to wallet's list of known exchanges
+ /// add a new exchange with URL to the wallet's list of known exchanges
func add(url: String) async throws {
do {
symLog?.log("adding exchange: \(url)") // TODO: notice
let request = AddExchange(exchangeBaseUrl: url)
- _ = try await sendRequest(request)
+ _ = try await sendRequestT(request) // TODO: MainActor?
symLog?.log("added exchange: \(url)")
- try await updateList()
- } catch { // TODO: Error
+ try await updateListM()
+ } catch { // rethrows
symLog?.log(error.localizedDescription)
throw error
}
}
}
+
+// MARK: -
+extension ExchangeModel {
+ private static var models: [ExchangeModel] = [] // a list of models even though I currently need only one
+
+ static func model() -> ExchangeModel {
+ if ExchangeModel.models.count > 0 {
+ let model = ExchangeModel.models[0]
+ return model
+ } else { // new model
+ let model = ExchangeModel()
+ ExchangeModel.models.append(model)
+ return model
+ }
+ }
+}
diff --git a/TalerWallet1/Views/Payment/PaymentURIModel.swift b/TalerWallet1/Model/PaymentURIModel.swift
index 8fd4142..dea406c 100644
--- a/TalerWallet1/Views/Payment/PaymentURIModel.swift
+++ b/TalerWallet1/Model/PaymentURIModel.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
@@ -29,6 +18,9 @@ enum PaymentState {
class PaymentURIModel: WalletModel {
@Published var paymentState: PaymentState?
+ override init(_ symbol: Int = -1) { // init with 0 to disable logging for this class
+ super.init(symbol)
+ }
}
@@ -114,7 +106,7 @@ struct Extra: Codable {
// 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 +126,7 @@ fileprivate struct PreparePayForUri: WalletBackendFormattedRequest {
}
}
// MARK: -
-/// The result from getPaymentDetailsForAmount
+/// The result from confirmPayForUri
struct ConfirmPayResult: Decodable {
var type: String
var contractTerms: ContractTerms
@@ -155,29 +147,47 @@ fileprivate struct confirmPayForUri: WalletBackendFormattedRequest {
extension PaymentURIModel {
/// load payment details. Networking involved
@MainActor
- func preparePayForUri(_ talerPayUri: String) async throws -> PaymentDetailsForUri {
+ func preparePayForUriM(_ talerPayUri: String) // M for MainActor
+ async throws -> PaymentDetailsForUri {
do {
paymentState = .waitingForUriDetails
let request = PreparePayForUri(talerPayUri: talerPayUri)
- let response = try await sendRequest(request, ASYNCDELAY)
+ let response = try await sendRequestM(request, ASYNCDELAY) // TODO: MainActor ?
paymentState = .receivedUriDetails
return response
- } catch {
+ } catch { // rethrows
paymentState = .error
throw error
}
}
@MainActor
- func confirmPay(_ proposalId: String) async throws -> ConfirmPayResult {
+ func confirmPayM(_ proposalId: String) // M for MainActor
+ async throws -> ConfirmPayResult {
do {
paymentState = .waitingForPaymentAck
let request = confirmPayForUri(proposalId: proposalId)
- let response = try await sendRequest(request, ASYNCDELAY)
+ let response = try await sendRequestM(request, ASYNCDELAY) // TODO: MainActor ?
paymentState = .receivedPaymentAck
return response
- } catch {
+ } catch { // rethrows
paymentState = .error
throw error
}
}
}
+
+// MARK: -
+extension PaymentURIModel {
+ private static var models: [PaymentURIModel] = [] // a list of models even though I currently need only one
+
+ static func model() -> PaymentURIModel {
+ if PaymentURIModel.models.count > 0 {
+ let model = PaymentURIModel.models[0]
+ return model
+ } else { // new model
+ let model = PaymentURIModel()
+ PaymentURIModel.models.append(model)
+ return model
+ }
+ }
+}
diff --git a/TalerWallet1/Model/Peer2peerModel.swift b/TalerWallet1/Model/Peer2peerModel.swift
new file mode 100644
index 0000000..490915c
--- /dev/null
+++ b/TalerWallet1/Model/Peer2peerModel.swift
@@ -0,0 +1,130 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import Foundation
+import taler_swift
+import AnyCodable
+//import SymLog
+fileprivate let ASYNCDELAY: UInt = 0 //set e.g to 6 or 9 seconds for debugging
+
+class Peer2peerModel: WalletModel {
+ override init(_ symbol: Int = -1) { // init with 0 to disable logging for this class
+ super.init(symbol)
+ }
+}
+// MARK: - PeerContractTerms
+struct PeerContractTerms: Codable {
+ let amount: Amount
+ let summary: String
+ let purse_expiration: Timestamp
+}
+// MARK: -
+/// The result from CheckPeerPushDebit
+struct CheckPeerPushDebitResponse: Codable {
+ let amountEffective: Amount
+ let amountRaw: Amount
+// let maxExpirationDate: Timestamp // TODO: limit expiration (30 days or 7 days)
+}
+/// A request to check fees before sending coins to another wallet.
+fileprivate struct CheckPeerPushDebit: WalletBackendFormattedRequest {
+ typealias Response = CheckPeerPushDebitResponse
+ func operation() -> String { return "checkPeerPushDebit" }
+ func args() -> Args { return Args(amount: amount) }
+
+ var amount: Amount
+ struct Args: Encodable {
+ var amount: Amount
+ }
+}
+// MARK: -
+/// The result from CheckPeerPullCredit
+struct CheckPeerPullCreditResponse: Codable {
+ let scopeInfo: ScopeInfo?
+ let exchangeBaseUrl: String?
+ let amountEffective: Amount
+ let amountRaw: Amount
+}
+/// A request to check fees before invoicing another wallet.
+fileprivate struct CheckPeerPullCredit: WalletBackendFormattedRequest {
+ typealias Response = CheckPeerPullCreditResponse
+ func operation() -> String { return "checkPeerPullCredit" }
+ func args() -> Args { return Args(exchangeBaseUrl: exchangeBaseUrl, scopeInfo: scopeInfo, amount: amount) }
+
+ var exchangeBaseUrl: String?
+ var scopeInfo: ScopeInfo?
+ var amount: Amount
+ struct Args: Encodable {
+ var exchangeBaseUrl: String?
+ var scopeInfo: ScopeInfo?
+ var amount: Amount
+ }
+}
+// MARK: -
+/// The result from InitiatePeerPushDebit
+struct PeerPushResponse: Codable {
+ let contractPriv: String
+ let mergePriv: String
+ let pursePub: String
+ let exchangeBaseUrl: String
+ let talerUri: String
+ let transactionId: String
+}
+/// A request to send coins to another wallet.
+fileprivate struct InitiatePeerPushDebit: WalletBackendFormattedRequest {
+ typealias Response = PeerPushResponse
+ func operation() -> String { return "initiatePeerPushDebit" }
+ func args() -> Args { return Args(exchangeBaseUrl: exchangeBaseUrl,
+ partialContractTerms: partialContractTerms) }
+
+ var exchangeBaseUrl: String?
+ var partialContractTerms: PeerContractTerms
+ struct Args: Encodable {
+ var exchangeBaseUrl: String?
+ var partialContractTerms: PeerContractTerms
+ }
+}
+// MARK: -
+extension Peer2peerModel {
+ /// query exchange for fees (sending coins). Networking involved
+ @MainActor
+ func checkPeerPushDebitM(_ amount: Amount) // M for MainActor
+ async throws -> CheckPeerPushDebitResponse {
+ let request = CheckPeerPushDebit(amount: amount)
+ let response = try await sendRequestM(request, ASYNCDELAY)
+ return response
+ }
+ /// query exchange for fees (invoice coins). Networking involved
+ @MainActor
+ func checkPeerPullCreditM(_ amount: Amount, exchangeBaseUrl: String?) // M for MainActor
+ async throws -> CheckPeerPullCreditResponse {
+ let request = CheckPeerPullCredit(exchangeBaseUrl: exchangeBaseUrl, amount: amount)
+ let response = try await sendRequestM(request, ASYNCDELAY)
+ return response
+ }
+ /// generate peer-push. Networking involved
+ @MainActor
+ func initiatePeerPushDebitM(_ baseURL: String, terms: PeerContractTerms) // M for MainActor
+ async throws -> PeerPushResponse {
+ let request = InitiatePeerPushDebit(exchangeBaseUrl: baseURL,
+ partialContractTerms: terms)
+ let response = try await sendRequestM(request, ASYNCDELAY)
+ return response
+ }
+}
+
+// MARK: -
+extension Peer2peerModel {
+ private static var models: [Peer2peerModel] = [] // a list of models even though I currently need only one
+
+ static func model() -> Peer2peerModel {
+ if Peer2peerModel.models.count > 0 {
+ let model = Peer2peerModel.models[0]
+ return model
+ } else { // new model
+ let model = Peer2peerModel()
+ Peer2peerModel.models.append(model)
+ return model
+ }
+ }
+}
diff --git a/TalerWallet1/Model/PendingModel.swift b/TalerWallet1/Model/PendingModel.swift
new file mode 100644
index 0000000..764f799
--- /dev/null
+++ b/TalerWallet1/Model/PendingModel.swift
@@ -0,0 +1,78 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import Foundation
+import AnyCodable
+import taler_swift
+import SymLog
+fileprivate let ASYNCDELAY: UInt = 0 //set e.g to 6 or 9 seconds for debugging
+
+class PendingModel: WalletModel {
+ @Published var pendingOperations: [PendingOperation]
+
+ override init(_ symbol: Int = -1) {
+ pendingOperations = [] // empty, but not nil
+ super.init(symbol)
+ }
+}
+// MARK: -
+/// A request to list the backend's currently pending operations.
+fileprivate struct GetPendingOperations: WalletBackendFormattedRequest {
+ func operation() -> String { return "getPendingOperations" }
+ func args() -> Args { Args() }
+
+ struct Args: Encodable {}
+
+ struct Response: Decodable {
+ var pendingOperations: [PendingOperation]
+ }
+}
+// MARK: -
+struct PendingOperation: Codable, Hashable {
+ var type: String
+ var exchangeBaseUrl: String?
+ var id: String
+ var isLongpolling: Bool
+ var givesLifeness: Bool
+ var isDue: Bool
+ var timestampDue: Timestamp
+
+ public func hash(into hasher: inout Hasher) {
+ hasher.combine(type)
+ hasher.combine(exchangeBaseUrl)
+ hasher.combine(id)
+ hasher.combine(isLongpolling)
+ hasher.combine(givesLifeness)
+ hasher.combine(isDue)
+ hasher.combine(timestampDue)
+ }
+
+}
+// MARK: -
+extension PendingModel {
+ @MainActor func updateM()
+ async throws { // M for MainActor
+ do {
+ let request = GetPendingOperations()
+ let response = try await sendRequestM(request, ASYNCDELAY)
+ pendingOperations = response.pendingOperations
+ }
+ }
+}
+
+// MARK: -
+extension PendingModel {
+ private static var models: [PendingModel] = [] // a list of models even though I currently need only one
+
+ static func model() -> PendingModel {
+ if PendingModel.models.count > 0 {
+ let model = PendingModel.models[0]
+ return model
+ } else { // new model
+ let model = PendingModel()
+ PendingModel.models.append(model)
+ return model
+ }
+ }
+}
diff --git a/TalerWallet1/Model/ExchangeTestModel.swift b/TalerWallet1/Model/SettingsModel.swift
index 98702d5..0172cf6 100644
--- a/TalerWallet1/Model/ExchangeTestModel.swift
+++ b/TalerWallet1/Model/SettingsModel.swift
@@ -1,61 +1,59 @@
/*
- * 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
-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_EXCHANGEBASEURL = DEMOEXCHANGE // "https://exchange.demo.taler.net/"
+fileprivate let DEMO_BANKBASEURL = DEMOBANK // "https://bank.demo.taler.net/"
+fileprivate let DEMO_BANKAPIBASEURL = DEMOBANK + "/demobanks/default/access-api/"
fileprivate let DEMO_MERCHANTBASEURL = "https://backend.demo.taler.net/"
fileprivate let DEMO_MERCHANTAUTHTOKEN = "secret-token:sandbox"
// MARK: -
-class ExchangeTestModel: WalletModel {
+class SettingsModel: WalletModel {
+ override init(_ symbol: Int = -1) { // init with 0 to disable logging for this class
+ super.init(symbol)
+ }
}
// MARK: -
-extension ExchangeTestModel {
- @MainActor func loadTestKudos() async throws {
+extension SettingsModel {
+ @MainActor func loadTestKudosM()
+ async throws { // M for MainActor
do {
- let amount = Amount(currency: "KUDOS", integer: 11, fraction: 0)
+ 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)
+ let response = try await sendRequestM(request, ASYNCDELAY)
symLog?.log("received: \(response)")
- } catch {
+ } catch { // rethrows
+ symLog?.log(error.localizedDescription)
throw error
}
}
- @MainActor func runIntegrationTest() async throws {
+ @MainActor func runIntegrationTestM(newVersion: Bool)
+ async throws { // M for MainActor
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 {
+ let amountW = Amount(currency: DEMOCURRENCY, integer: 3, fraction: 0)
+ let amountS = Amount(currency: DEMOCURRENCY, integer: 1, fraction: 0)
+ let request = WalletBackendRunIntegration(newVersion: newVersion,
+ amountToWithdraw: amountW,
+ amountToSpend: amountS,
+ bankBaseUrl: DEMO_BANKAPIBASEURL,
+ bankAccessApiBaseUrl: DEMO_BANKAPIBASEURL,
+ exchangeBaseUrl: DEMO_EXCHANGEBASEURL,
+ merchantBaseUrl: DEMO_MERCHANTBASEURL,
+ merchantAuthToken: DEMO_MERCHANTAUTHTOKEN)
+ let _ = try await sendRequestT(request, ASYNCDELAY)
+ symLog?.log("runIntegrationTest finished")
+ } catch { // rethrows
+ symLog?.log(error.localizedDescription)
throw error
}
}
@@ -85,21 +83,25 @@ fileprivate struct WalletBackendWithdrawTestBalance: WalletBackendFormattedReque
/// A request to add a test balance to the wallet.
fileprivate struct WalletBackendRunIntegration: WalletBackendFormattedRequest {
- typealias Response = String
- func operation() -> String { return "runIntegrationTest" }
+ struct Response: Decodable {}
+ func operation() -> String { return newVersion ? "runIntegrationTestV2" : "runIntegrationTest" }
func args() -> Args {
return Args(amountToWithdraw: amountToWithdraw,
amountToSpend: amountToSpend,
bankBaseUrl: bankBaseUrl,
+ bankAccessApiBaseUrl: bankAccessApiBaseUrl,
exchangeBaseUrl: exchangeBaseUrl,
merchantBaseUrl: merchantBaseUrl,
merchantAuthToken: merchantAuthToken
)
}
+ let newVersion: Bool
+
var amountToWithdraw: Amount
var amountToSpend: Amount
var bankBaseUrl: String
+ var bankAccessApiBaseUrl: String
var exchangeBaseUrl: String
var merchantBaseUrl: String
var merchantAuthToken: String
@@ -108,6 +110,7 @@ fileprivate struct WalletBackendRunIntegration: WalletBackendFormattedRequest {
var amountToWithdraw: Amount
var amountToSpend: Amount
var bankBaseUrl: String
+ var bankAccessApiBaseUrl: String
var exchangeBaseUrl: String
var merchantBaseUrl: String
var merchantAuthToken: String
diff --git a/TalerWallet1/Model/TransactionsModel.swift b/TalerWallet1/Model/TransactionsModel.swift
new file mode 100644
index 0000000..668f3c3
--- /dev/null
+++ b/TalerWallet1/Model/TransactionsModel.swift
@@ -0,0 +1,157 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import Foundation
+import taler_swift
+import SymLog
+fileprivate let ASYNCDELAY: UInt = 0 //set e.g to 6 or 9 seconds for debugging
+
+// MARK: -
+class TransactionsModel: WalletModel {
+ @Published var transactions: [Transaction]
+
+ static func specialTransactions(_ transactions: [Transaction]) -> [Transaction] {
+ transactions.filter { transaction in
+ transaction.isSpecial
+ }
+ }
+ var specialTransactions: [Transaction] {
+ transactions.filter { transaction in
+ transaction.isSpecial
+ }
+ }
+
+ static func completedTransactions(_ transactions: [Transaction]) -> [Transaction] {
+ transactions.filter { transaction in
+ transaction.isDone
+ }
+ }
+ static func pendingTransactions(_ transactions: [Transaction]) -> [Transaction] {
+ transactions.filter { transaction in
+ transaction.isPending
+ }
+ }
+ static func uncompletedTransactions(_ transactions: [Transaction]) -> [Transaction] {
+ transactions.filter { transaction in
+ !transaction.isDone && !transaction.isPending
+ }
+ }
+
+ override init(_ symbol: Int = -1) {
+ transactions = [] // empty, but not nil
+ super.init(symbol)
+ }
+}
+
+// MARK: -
+/// A request to get the transactions in the wallet's history.
+fileprivate struct GetTransactions: WalletBackendFormattedRequest {
+ func operation() -> String { return "getTransactions" }
+// func operation() -> String { return "testingGetSampleTransactions" }
+ func args() -> Args { return Args(currency: currency, search: search) }
+
+ var currency: String?
+ var search: String?
+
+ struct Args: Encodable {
+ var currency: String?
+ var search: String?
+ }
+
+ struct Response: Decodable { // list of transactions
+ var transactions: [Transaction]
+ }
+}
+/// A request to abort a wallet transaction by ID.
+struct AbortTransaction: WalletBackendFormattedRequest {
+ func operation() -> String { return "abortTransaction" }
+ func args() -> Args { return Args(transactionId: transactionId) }
+
+ var transactionId: String
+
+ struct Args: Encodable {
+ var transactionId: String
+ }
+
+ struct Response: Decodable {}
+}
+/// A request to delete a wallet transaction by ID.
+struct DeleteTransaction: WalletBackendFormattedRequest {
+ struct Response: Decodable {}
+ func operation() -> String { return "deleteTransaction" }
+ func args() -> Args { return Args(transactionId: transactionId) }
+
+ var transactionId: String
+
+ struct Args: Encodable {
+ var transactionId: String
+ }
+}
+
+// MARK: -
+extension TransactionsModel {
+ /// ask wallet-core for its list of transactions filtered by searchString
+ func fetchTransactions(currency: String?) async { // might be called from a background thread itself
+ await fetchTransactionsM(currency: currency, searchString: nil)
+ }
+ /// fetch transactions from Wallet-Core. No networking involved
+ @MainActor func fetchTransactionsM(currency: String? = nil, searchString: String? = nil)
+ async { // M for MainActor
+ do {
+ let request = GetTransactions(currency: currency, search: searchString)
+ let response = try await sendRequestM(request, ASYNCDELAY)
+ transactions = response.transactions // trigger view update in TransactionsListView
+ } catch {
+ transactions = []
+ }
+ }
+
+ func abortTransaction(transactionId: String) async throws { // might be called from a background thread itself
+ try await abortTransactionM(transactionId: transactionId) // call deleteTransactionM on main thread
+ }
+ /// delete the specified transaction from Wallet-Core. No networking involved
+ @MainActor func abortTransactionM(transactionId: String)
+ async throws { // M for MainActor
+ do {
+ let request = AbortTransaction(transactionId: transactionId)
+ let _ = try await sendRequestT(request, ASYNCDELAY)
+ } catch { // rethrows
+ symLog?.log(error.localizedDescription)
+ throw error
+ }
+ }
+
+ func deleteTransaction(transactionId: String) async throws { // might be called from a background thread itself
+ try await deleteTransactionM(transactionId: transactionId) // call deleteTransactionM on main thread
+ }
+ /// delete the specified transaction from Wallet-Core. No networking involved
+ @MainActor func deleteTransactionM(transactionId: String)
+ async throws { // M for MainActor
+ do {
+ let request = DeleteTransaction(transactionId: transactionId)
+ let _ = try await sendRequestT(request, ASYNCDELAY)
+ } catch { // rethrows
+ symLog?.log(error.localizedDescription)
+ throw error
+ }
+ }
+}
+
+// MARK: -
+extension TransactionsModel {
+ private static var currencies: [String] = [] // names of currencies
+ private static var models: [TransactionsModel] = [] // one model per currency
+
+ static func model(currency: String) -> TransactionsModel {
+ if let index = TransactionsModel.currencies.firstIndex(of:currency) {
+ let model = TransactionsModel.models[index]
+ return model
+ } else { // new currency
+ let model = TransactionsModel()
+ TransactionsModel.models.append(model)
+ TransactionsModel.currencies.append(currency)
+ return model
+ }
+ }
+}
diff --git a/TalerWallet1/Model/WalletInitModel.swift b/TalerWallet1/Model/WalletInitModel.swift
index 7be0dff..d977890 100644
--- a/TalerWallet1/Model/WalletInitModel.swift
+++ b/TalerWallet1/Model/WalletInitModel.swift
@@ -1,25 +1,16 @@
/*
- * 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
-let DATABASE = "talerwalletdb-v30"
+private let DATABASE = "talerwalletdb-v30"
class WalletInitModel: WalletModel {
-
+ override init(_ symbol: Int = -1) { // init with 0 to disable logging for this class
+ super.init(symbol)
+ }
}
// MARK: -
/// A request to initialize Wallet-core
@@ -39,11 +30,8 @@ fileprivate struct WalletBackendInitRequest: WalletBackendFormattedRequest {
var persistentStoragePath: String
- struct Response: Decodable { // versioninfo
+ struct Response: Decodable {
var versionInfo: VersionInfo
- enum CodingKeys: String, CodingKey {
- case versionInfo = "versionInfo"
- }
}
}
// MARK: -
@@ -59,14 +47,15 @@ struct VersionInfo: Decodable {
// MARK: -
extension WalletInitModel {
/// initalize Wallet-Core. Will do networking
- func initWallet() async throws -> VersionInfo? {
+ func initWalletT() // T for any Thread
+ async throws -> VersionInfo? {
do {
let docPath = try docPath()
let request = WalletBackendInitRequest(persistentStoragePath: docPath)
symLog?.log("info: not main thread")
- let response = try await sendRequest(request, 0) // no Delay
+ let response = try await sendRequestT(request, 0) // no Delay
return response.versionInfo
- } catch {
+ } catch { // rethrows
symLog?.log("error: \(error)")
throw error
}
@@ -80,7 +69,7 @@ extension WalletInitModel {
storageDir.appendPathExtension("json")
return storageDir.path
} else { // should never happen
- symLog?.log("Yikes! documentURLs empty") // TODO: symLog.error
+ 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..9fb0622 100644
--- a/TalerWallet1/Model/WalletModel.swift
+++ b/TalerWallet1/Model/WalletModel.swift
@@ -1,55 +1,87 @@
/*
- * 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
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 {
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
+ init(_ symbol: Int) { // init with 0 to disable logging for this class
+ self.symLog = SymLogC(symbol == 0 ? 0 : -1, funcName: Self.className())
}
- @MainActor func sendRequest<T: WalletBackendFormattedRequest> (_ request: T, _ delay: UInt = 0)
- async throws -> T.Response {
+ @MainActor func sendRequestM<T: WalletBackendFormattedRequest> (_ request: T, _ delay: UInt = 0)
+ async throws -> T.Response { // M for MainActor
loading = true // enter progressView
do {
+ let response = try await sendRequestT(request, delay)
+ loading = false // exit progressView
+ return response
+ } catch { // rethrows
+ loading = false // exit progressView
+ throw error
+ }
+ }
+
+ func sendRequestT<T: WalletBackendFormattedRequest> (_ request: T, _ delay: UInt = 0)
+ async throws -> T.Response { // T for any Thread
+ 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)
+ let timeUsed = Date.now - sendTime
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...")
+ symLog?.log("received: (\(id)) after \(timeUsed.milliseconds) ms, 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)")
+ symLog?.log("(\(id)) waking up again after \(asyncDelay) seconds, will deliver \(response)")
} else {
- symLog?.log("received: \(response)")
+ symLog?.log("received: (\(id)) after \(timeUsed.milliseconds) ms, \(response)")
}
- loading = false // exit progressView
return response
- } catch {
+ } catch { // rethrows
+ let timeUsed = Date.now - sendTime
+ symLog?.log("Yikes❗️ \(request.operation()) failed after \(timeUsed.milliseconds) ms")
throw error
}
}
+ func getTransactionById(transactionId: String) async throws -> Transaction { // might be called from a background thread itself
+ return try await getTransactionByIdM(transactionId: transactionId) // call deleteTransactionM on main thread
+ }
+ /// get the specified transaction from Wallet-Core. No networking involved
+ @MainActor func getTransactionByIdM(transactionId: String)
+ async throws -> Transaction { // M for MainActor
+ do {
+ let request = GetTransactionById(transactionId: transactionId)
+ let response = try await sendRequestT(request, ASYNCDELAY)
+ return response
+ } catch { // rethrows
+ throw error
+ }
+ }
}
+// MARK: -
+/// A request to get a wallet transaction by ID.
+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
+ }
+}
+
diff --git a/TalerWallet1/Views/Withdraw/WithdrawURIModel.swift b/TalerWallet1/Model/WithdrawModel.swift
index f375a1d..b3a2161 100644
--- a/TalerWallet1/Views/Withdraw/WithdrawURIModel.swift
+++ b/TalerWallet1/Model/WithdrawModel.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
@@ -32,16 +21,19 @@ enum WithdrawState {
case receivedWithdrAck
}
-class WithdrawURIModel: WalletModel {
+class WithdrawModel: WalletModel {
@Published var withdrawState: WithdrawState?
+ override init(_ symbol: Int = -1) { // init with 0 to disable logging for this class
+ super.init(symbol)
+ }
}
// MARK: -
/// The result from getWithdrawalDetailsForUri
-struct WithdrawalDetailsForUri: Decodable {
+struct WithdrawUriInfoResponse: Decodable {
var amount: Amount
- var defaultExchangeBaseUrl: String?
- var possibleExchanges: [ExchangeListItem]
+ var defaultExchangeBaseUrl: String? // TODO: might be nil ❗️Yikes
+ var possibleExchanges: [ExchangeListItem] // TODO: query these for fees?
}
struct ExchangeListItem: Codable, Hashable {
var exchangeBaseUrl: String
@@ -62,7 +54,7 @@ struct ExchangeListItem: Codable, Hashable {
}
/// A request to get an exchange's withdrawal details.
fileprivate struct GetWithdrawalDetailsForURI: WalletBackendFormattedRequest {
- typealias Response = WithdrawalDetailsForUri
+ typealias Response = WithdrawUriInfoResponse
func operation() -> String { return "getWithdrawalDetailsForUri" }
func args() -> Args { return Args(talerWithdrawUri: talerWithdrawUri) }
@@ -73,14 +65,17 @@ fileprivate struct GetWithdrawalDetailsForURI: WalletBackendFormattedRequest {
}
// MARK: -
/// The result from getWithdrawalDetailsForAmount
-struct WithdrawalDetailsForAmount: Decodable {
- var tosAccepted: Bool
+struct ManualWithdrawalDetails: Decodable {
var amountRaw: Amount
var amountEffective: Amount
+ var paytoUris: [String]
+ var tosAccepted: Bool
+ var ageRestrictionOptions: [Int]?
+ var numCoins: Int?
}
/// A request to get an exchange's withdrawal details.
fileprivate struct GetWithdrawalDetailsForAmount: WalletBackendFormattedRequest {
- typealias Response = WithdrawalDetailsForAmount
+ typealias Response = ManualWithdrawalDetails
func operation() -> String { return "getWithdrawalDetailsForAmount" }
func args() -> Args { return Args(exchangeBaseUrl: exchangeBaseUrl, amount: amount) }
@@ -124,12 +119,14 @@ fileprivate struct SetExchangeTOSAccepted: WalletBackendFormattedRequest {
}
}
// MARK: -
-struct BankConfirmation: Decodable {
- var bankConfirmationUrl: String?
+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 = BankConfirmation
+ typealias Response = AcceptWithdrawalResponse
func operation() -> String { return "acceptBankIntegratedWithdrawal" }
func args() -> Args { return Args(talerWithdrawUri: talerWithdrawUri, exchangeBaseUrl: exchangeBaseUrl) }
@@ -142,72 +139,131 @@ fileprivate struct AcceptBankIntegratedWithdrawal: WalletBackendFormattedRequest
}
}
// MARK: -
-extension WithdrawURIModel {
+struct AcceptManualWithdrawalResult: Decodable {
+ var reservePub: String
+ var exchangePaytoUris: [String]
+ var transactionId: String
+}
+/// A request to accept a manual withdrawl.
+fileprivate struct AcceptManualWithdrawal: WalletBackendFormattedRequest {
+ typealias Response = AcceptManualWithdrawalResult
+ func operation() -> String { return "acceptManualWithdrawal" }
+ func args() -> Args { return Args(exchangeBaseUrl: exchangeBaseUrl, amount: amount, restrictAge: restrictAge) }
+
+ var exchangeBaseUrl: String
+ var amount: Amount
+ var restrictAge: Int?
+
+ struct Args: Encodable {
+ var exchangeBaseUrl: String
+ var amount: Amount
+ var restrictAge: Int?
+ }
+}
+// MARK: -
+extension WithdrawModel {
/// load withdrawal details. Networking involved
@MainActor
- func loadWithdrawalDetailsForURI(_ talerWithdrawUri: String) async throws -> WithdrawalDetailsForUri {
+ func loadWithdrawalDetailsForUriM(_ talerWithdrawUri: String) // M for MainActor
+ async throws -> WithdrawUriInfoResponse {
do {
withdrawState = .waitingForUriDetails
let request = GetWithdrawalDetailsForURI(talerWithdrawUri: talerWithdrawUri)
- let response = try await sendRequest(request, ASYNCDELAY)
+ let response = try await sendRequestM(request, ASYNCDELAY) // TODO: MainActor ?
withdrawState = .receivedUriDetails
return response
- } catch {
+ } catch { // rethrows
withdrawState = .error
throw error
}
}
@MainActor
- func loadWithdrawalDetailsForAmount(_ detailsForUri: WithdrawalDetailsForUri) async throws -> WithdrawalDetailsForAmount {
+ func loadWithdrawalDetailsForAmountM(_ exchangeBaseUrl: String, amount: Amount) // M for MainActor
+ async throws -> ManualWithdrawalDetails {
do {
withdrawState = .waitingForAmountDetails
- let baseURL = detailsForUri.defaultExchangeBaseUrl!
- let request = GetWithdrawalDetailsForAmount(exchangeBaseUrl: baseURL, amount: detailsForUri.amount)
- let response = try await sendRequest(request, ASYNCDELAY)
+ let request = GetWithdrawalDetailsForAmount(exchangeBaseUrl: exchangeBaseUrl,
+ amount: amount)
+ let response = try await sendRequestM(request, ASYNCDELAY) // TODO: MainActor ?
withdrawState = .receivedAmountDetails
return response
- } catch {
+ } catch { // rethrows
withdrawState = .error
throw error
}
}
@MainActor
- func loadExchangeTermsOfService(_ exchangeBaseUrl: String) async throws -> ExchangeTermsOfService {
+ func loadExchangeTermsOfServiceM(_ exchangeBaseUrl: String) // M for MainActor
+ async throws -> ExchangeTermsOfService {
do {
withdrawState = .waitingForTOS
let request = GetExchangeTermsOfService(exchangeBaseUrl: exchangeBaseUrl)
- let response = try await sendRequest(request, ASYNCDELAY)
+ let response = try await sendRequestM(request, ASYNCDELAY) // TODO: MainActor ?
withdrawState = .receivedTOS
return response
- } catch {
+ } catch { // rethrows
withdrawState = .error
throw error
}
}
@MainActor
- func setExchangeTOSAccepted(_ exchangeBaseUrl: String, etag: String) async throws -> Decodable {
+ func setExchangeTOSAcceptedM(_ exchangeBaseUrl: String, etag: String) // M for MainActor
+ async throws -> Decodable {
do {
withdrawState = .waitingForTOSAck
let request = SetExchangeTOSAccepted(exchangeBaseUrl: exchangeBaseUrl, etag: etag)
- let response = try await sendRequest(request, ASYNCDELAY)
+ let response = try await sendRequestM(request, ASYNCDELAY) // TODO: MainActor ?
withdrawState = .receivedTOSAck
return response
- } catch {
+ } catch { // rethrows
withdrawState = .error
throw error
}
}
@MainActor
- func sendAcceptIntWithdrawal(_ exchangeBaseUrl: String, withdrawURL: String) async throws -> String? {
+ func sendAcceptIntWithdrawalM(_ exchangeBaseUrl: String, withdrawURL: String) // M for MainActor
+ async throws -> String? {
do {
withdrawState = .waitingForWithdrAck
let request = AcceptBankIntegratedWithdrawal(talerWithdrawUri: withdrawURL, exchangeBaseUrl: exchangeBaseUrl)
- let response = try await sendRequest(request, ASYNCDELAY)
+ let response = try await sendRequestM(request, ASYNCDELAY) // TODO: MainActor ?
withdrawState = .receivedWithdrAck
- return response.bankConfirmationUrl
- } catch {
+ return response.confirmTransferUrl
+ } catch { // rethrows
withdrawState = .error
throw error
}
}
+ @MainActor
+ func sendAcceptManualWithdrawalM(_ exchangeBaseUrl: String, amount: Amount, restrictAge: Int?) // M for MainActor
+ async throws -> AcceptManualWithdrawalResult? {
+ do {
+ withdrawState = .waitingForWithdrAck
+ let request = AcceptManualWithdrawal(exchangeBaseUrl: exchangeBaseUrl, amount: amount, restrictAge: restrictAge)
+ let response = try await sendRequestM(request, ASYNCDELAY) // TODO: MainActor ?
+ withdrawState = .receivedWithdrAck
+ return response
+ } catch { // rethrows
+ withdrawState = .error
+ throw error
+ }
+ }
+}
+
+// MARK: -
+extension WithdrawModel {
+ private static var exchanges: [String] = [] // names of exchanges
+ private static var models: [WithdrawModel] = [] // one model per exchange
+
+ static func model(baseURL: String) -> WithdrawModel {
+ if let index = WithdrawModel.exchanges.firstIndex(of:baseURL) {
+ let model = WithdrawModel.models[index]
+ return model
+ } else { // new model
+ let model = WithdrawModel()
+ WithdrawModel.models.append(model)
+ WithdrawModel.exchanges.append(baseURL)
+ return model
+ }
+ }
}
diff --git a/TalerWallet1/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
--- /dev/null
+++ b/TalerWallet1/Settings.bundle/en.lproj/Root.strings
Binary files 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/BalanceRowView.swift b/TalerWallet1/Views/Balances/BalanceRowView.swift
new file mode 100644
index 0000000..cb847ec
--- /dev/null
+++ b/TalerWallet1/Views/Balances/BalanceRowView.swift
@@ -0,0 +1,52 @@
+/*
+ * 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 BalanceRowView: View {
+ let amount: Amount
+ let sendAction: () -> Void
+ let recvAction: () -> Void
+ let rowAction: () -> Void
+ var body: some View {
+ HStack {
+ Button("Request\nPayment", action: recvAction)
+ .lineLimit(2)
+ .disabled(false)
+ .buttonStyle(TalerButtonStyle(type: .bordered, narrow: true, aligned: .leading))
+ Button("Send\nCoins", action: sendAction)
+ .lineLimit(2)
+ .disabled(amount.isZero)
+ .buttonStyle(TalerButtonStyle(type: .bordered, narrow: true, aligned: .leading))
+ Button(action: rowAction) {
+ VStack(alignment: .trailing) {
+ Text("Balance")
+ .font(.footnote)
+ .accessibility(sortPriority: 2)
+ Text("\(amount.valueStr)") // TODO: CurrencyFormatter
+ .font(.title)
+ .accessibility(sortPriority: 1)
+ }
+ } .disabled(false)
+ .buttonStyle(TalerButtonStyle(type: .plain, aligned: .trailing))
+ }
+ .fixedSize(horizontal: false, 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: LONGCURRENCY + ":0.1"),
+ sendAction: {}, recvAction: {}, rowAction: {})
+ }
+ }
+}
+#endif
diff --git a/TalerWallet1/Views/Balances/BalancesListView.swift b/TalerWallet1/Views/Balances/BalancesListView.swift
new file mode 100644
index 0000000..15d0f1c
--- /dev/null
+++ b/TalerWallet1/Views/Balances/BalancesListView.swift
@@ -0,0 +1,133 @@
+/*
+ * 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()
+ let navTitle = String(localized: "GNU Taler") // + Wallet
+ @AppStorage("listStyle") var myListStyle = MyListStyle.automatic
+
+ @ObservedObject var model: BalancesModel
+ var hamburgerAction: () -> Void
+
+ @State private var centsToTransfer: UInt64 = 0 // TODO: maybe Decimal?
+ @State private var showQRScanner: Bool = false
+ @State private var showCameraAlert: Bool = false
+
+ var ClosingAnnouncement = AttributedString(localized: "Closing Camera")
+ private var openSettingsButton: some View {
+ Button("Open Settings") {
+ showCameraAlert = false
+ UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
+ }
+ }
+ private var dismissAlertButton: some View {
+ Button("Cancel", role: .cancel) {
+// AccessibilityNotification.Announcement(ClosingAnnouncement).post()
+ showCameraAlert = false
+ }
+ }
+
+ var defaultPriorityAnnouncement = AttributedString(localized: "Opening Camera")
+ var lowPriorityAnnouncement: AttributedString {
+ var lowPriorityString = AttributedString ("Camera Loading")
+ // lowPriorityString.accessibilitySpeechAnnouncementPriority = .low
+ return lowPriorityString
+ }
+ var highPriorityAnnouncement: AttributedString {
+ var highPriorityString = AttributedString("Camera Active")
+ // highPriorityString.accessibilitySpeechAnnouncementPriority = .high
+ return highPriorityString
+ }
+ private func checkCameraAvailable() -> Void {
+ /// Open Camera Code
+// AccessibilityNotification.Announcement(defaultPriorityAnnouncement).post()
+ AVCaptureDevice.requestAccess(for: .video, completionHandler: { (granted: Bool) -> Void in
+ if granted {
+ showQRScanner = true
+ } else {
+ // Camera Loaded Code
+// AccessibilityNotification.Announcement(highPriorityAnnouncement).post()
+ showCameraAlert = true
+ }
+ })
+ }
+
+ var body: some View {
+#if DEBUG
+ let _ = Self._printChanges()
+ let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear
+#endif
+ let reloadAction = model.fetchBalancesM
+ Content(symLog: symLog, model: model, centsToTransfer: $centsToTransfer,
+ reloadAction: reloadAction, myListStyle: $myListStyle)
+ .navigationTitle(navTitle)
+ .navigationBarItems(leading: HamburgerButton(action: hamburgerAction),
+ trailing: QRButton(action: checkCameraAvailable))
+ .alert("Scanning QR-codes requires access to the camera",
+ isPresented: $showCameraAlert,
+ actions: { openSettingsButton
+ dismissAlertButton },
+ message: { Text("Please allow camera access in settings.") })
+ .sheet(isPresented: $showQRScanner) {
+ let sheet = AnyView(QRSheet())
+ Sheet(sheetView: sheet)
+ } // sheet
+ .task {
+ symLog.log(".task fetchBalancesM")
+ await reloadAction()
+ } // task
+ }
+}
+// MARK: -
+extension BalancesListView {
+ struct Content: View {
+ let symLog: SymLogV?
+ @ObservedObject var model: BalancesModel
+ @Binding var centsToTransfer: UInt64
+ var reloadAction: () async -> ()
+ @Binding var myListStyle: MyListStyle
+
+ var body: some View {
+#if DEBUG
+ let _ = Self._printChanges()
+ let _ = symLog?.vlog() // just to get the # to compare it with .onAppear & onDisappear
+#endif
+ Group {
+ List (model.balances, id: \.self) { balance in
+ let model = TransactionsModel.model(currency: balance.available.currencyStr)
+ BalancesSectionView(balance: balance, centsToTransfer: $centsToTransfer, model: model)
+ }
+ .refreshable {
+ symLog?.log("refreshing")
+ await reloadAction() // this closure is already async, no need for a Task
+ }
+ .listStyle(myListStyle.style)
+ .anyView
+ }
+ .navigationBarTitleDisplayMode(.automatic)
+ .onAppear() {
+ DebugViewC.shared.setViewID(VIEW_BALANCES)
+ }
+ .overlay {
+ if model.balances.isEmpty {
+ WalletEmptyView()
+ .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all))
+ }
+ }
+ // automatically fetch balances after receiving transaction-state-transition ...
+ .onNotification(.TransactionStateTransition) { notification in
+ // doesn't need to be received on main thread because we just reload in the background anyway
+ symLog?.log(".onNotification(.TransactionStateTransition) ==> reloading balances")
+ Task { await reloadAction() }
+ }
+ } // body
+ } // Content
+}
diff --git a/TalerWallet1/Views/Balances/BalancesSectionView.swift b/TalerWallet1/Views/Balances/BalancesSectionView.swift
new file mode 100644
index 0000000..d9535c1
--- /dev/null
+++ b/TalerWallet1/Views/Balances/BalancesSectionView.swift
@@ -0,0 +1,161 @@
+/*
+ * 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
+ @ObservedObject var model: TransactionsModel
+
+ @State private var isShowingDetailView = false
+ @State private var buttonSelected: Int? = nil
+ @State private var completedTransactions: [Transaction] = []
+ @State private var pendingTransactions: [Transaction] = []
+ @State private var uncompletedTransactions: [Transaction] = []
+
+ var body: some View {
+ let currency = balance.available.currencyStr
+ let reloadCompleted = {
+ await model.fetchTransactions(currency: currency)
+ completedTransactions = TransactionsModel.completedTransactions(model.transactions)
+ }
+ let reloadPending = {
+ await model.fetchTransactions(currency: currency)
+ pendingTransactions = TransactionsModel.pendingTransactions(model.transactions)
+ }
+ let reloadUncompleted = {
+ await model.fetchTransactions(currency: currency)
+ uncompletedTransactions = TransactionsModel.uncompletedTransactions(model.transactions)
+ }
+ let deleteAction = model.deleteTransaction
+ let abortAction = model.abortTransaction
+
+
+ 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)
+ }, tag: 1, selection: $buttonSelected
+ ) { EmptyView() }.frame(width: 0).opacity(0).hidden()
+
+ NavigationLink(destination: LazyView {
+ RequestPayment(model: Peer2peerModel.model(),
+ scopeInfo: balance.scopeInfo,
+ centsToTransfer: $centsToTransfer)
+ .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all))
+ }, tag: 2, selection: $buttonSelected
+ ) { EmptyView() }.frame(width: 0).opacity(0).hidden()
+
+ NavigationLink(destination: LazyView {
+ TransactionsListView(navTitle: String(localized: "Transactions"), currency: currency,
+ transactions: completedTransactions,
+ reloadAction: reloadCompleted,
+ deleteAction: deleteAction,
+ abortAction: abortAction)
+ }, tag: 3, selection: $buttonSelected
+ ) { EmptyView() }.frame(width: 0).opacity(0).hidden()
+
+ BalanceRowView(amount: balance.available, sendAction: {
+print("button: Send Coins: \(currency)")
+ buttonSelected = 1 // will trigger SendAmount NavigationLink
+ }, recvAction: {
+print("button: Request Payment: \(currency)")
+ buttonSelected = 2 // will trigger RequestPayment NavigationLink
+ }, rowAction: {
+print("button: Transactions: \(currency)")
+ buttonSelected = 3 // will trigger TransactionList NavigationLink
+ })
+ }
+ let hasPending = pendingTransactions.count > 0
+ if hasPending {
+ let hasPendingIn = !balance.pendingIncoming.isZero
+ let hasPendingOut = !balance.pendingOutgoing.isZero
+ NavigationLink {
+let _ = print("button: Pending Transactions: \(currency)")
+ LazyView {
+ TransactionsListView(navTitle: String(localized: "Pending"), currency: currency,
+ transactions: pendingTransactions,
+ reloadAction: reloadPending,
+ deleteAction: deleteAction,
+ abortAction: abortAction)
+ }
+ } label: {
+ VStack(spacing: 6) {
+ if hasPendingIn {
+ PendingRowView(amount: balance.pendingIncoming, incoming: true)
+ }
+ if hasPendingOut {
+ PendingRowView(amount: balance.pendingOutgoing, incoming: false)
+ }
+ }
+ }
+ }
+ let hasUncompleted = uncompletedTransactions.count > 0
+ if hasUncompleted {
+ NavigationLink {
+let _ = print("button: Uncompleted Transactions: \(currency)")
+ LazyView {
+ TransactionsListView(navTitle: String(localized: "Uncompleted"), currency: currency,
+ transactions: uncompletedTransactions,
+ reloadAction: reloadUncompleted,
+ deleteAction: deleteAction,
+ abortAction: abortAction)
+ }
+ } label: {
+ UncompletedRowView(uncompletedTransactions: uncompletedTransactions)
+ }
+
+ }
+ } header: {
+ Text(currency)
+ .font(.title)
+ } .task {
+ await model.fetchTransactions(currency: currency)
+ pendingTransactions = TransactionsModel.pendingTransactions(model.transactions)
+ uncompletedTransactions = TransactionsModel.uncompletedTransactions(model.transactions)
+ }
+ } // body
+}
+// MARK: -
+#if DEBUG
+fileprivate struct BindingViewContainer : View {
+ @State var centsToTransfer: UInt64 = 333
+ let model = TransactionsModel.model(currency: LONGCURRENCY)
+
+ var body: some View {
+ let scopeInfo = ScopeInfo(type: ScopeInfo.ScopeInfoType.exchange, exchangeBaseUrl: DEMOEXCHANGE, currency: LONGCURRENCY)
+ let balance = Balance(available: try! Amount(fromString: LONGCURRENCY + ":0.1"),
+ pendingIncoming: try! Amount(fromString: LONGCURRENCY + ":4.8"),
+ pendingOutgoing: try! Amount(fromString: LONGCURRENCY + ":3.25"),
+ hasPendingTransactions: true,
+ requiresUserInput: false,
+ scopeInfo: scopeInfo)
+ List {
+ BalancesSectionView(balance: balance, centsToTransfer: $centsToTransfer, model: model)
+ }
+ }
+}
+
+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..b7642dc
--- /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 {
+ 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..e7fd4fe 100644
--- a/TalerWallet1/Views/Exchange/ExchangeListView.swift
+++ b/TalerWallet1/Views/Exchange/ExchangeListView.swift
@@ -1,103 +1,129 @@
/*
- * 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"
+ let navTitle = String(localized: "Exchanges")
- @ObservedObject var viewModel: ExchangeModel
+ @ObservedObject var model: ExchangeModel
+ var hamburgerAction: () -> Void
+
+
+ // source of truth for the value the user enters in currencyField
+ @State private var centsToTransfer: UInt64 = 0 // TODO: different values for different currencies
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)
- }
- }.task {
- symLog.log(".task")
- do {
- try await reloadAction()
- } catch {
- // TODO: show error
- symLog.log(error.localizedDescription)
+#if DEBUG
+ let _ = Self._printChanges()
+ let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear
+#endif
+ let reloadAction = model.updateListM
+ Content(symLog: symLog, model: model,
+ centsToTransfer: $centsToTransfer,
+ reloadAction: reloadAction)
+ .navigationBarItems(leading: HamburgerButton(action: hamburgerAction))
+ .navigationTitle(navTitle)
+ .task {
+ symLog.log(".task")
+ do {
+ try await reloadAction()
+ } catch { // TODO: show error
+ symLog.log(error.localizedDescription)
+ }
}
- }
+ }
+}
+// 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
+ let symLog: SymLogV?
+ @AppStorage("listStyle") var myListStyle = MyListStyle.automatic
+ @ObservedObject var model: ExchangeModel
+ @Binding var centsToTransfer: UInt64
var reloadAction: () async throws -> ()
+
@State var showAlert: Bool = false
@State var newExchange: String = "https://exchange-age.taler.ar/"
func addExchange(_ exUrl: String) -> Void {
Task {
do {
- symLog.log("adding: \(exUrl)")
- try await viewModel.add(url: exUrl)
- symLog.log("added: \(exUrl)")
- } catch {
- symLog.log("error: \(error)")
- // TODO: error handling - couldn't add exchangeURL
+ symLog?.log("adding: \(exUrl)")
+ try await model.add(url: exUrl)
+ symLog?.log("added: \(exUrl)")
+ } catch { // TODO: error handling - couldn't add exchangeURL
+ symLog?.log("error: \(error)")
}
}
}
+ func currenciesDict(_ exchanges: [Exchange]) -> [String : [Exchange]] {
+ var currencies: [String : [Exchange]] = [:]
+
+ 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 }
+// withAnimation { showAlert = true }
showAlert = true
}
VStack {
- if viewModel.exchanges!.isEmpty {
+ if model.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()
+ ScrollViewReader { scrollView in //
+ let dict = currenciesDict(model.exchanges)
+ let sortedDict = dict.sorted{ $0.key < $1.key}
+ List {
+ ForEach(sortedDict, id: \.key) { key, value in
+ ExchangeSectionView(currency: key, exchanges: value, centsToTransfer: $centsToTransfer)
+ }
}
- }
- .navigationBarTitleDisplayMode(.large) // .inline
.refreshable {
do {
- symLog.log("refreshing")
+ symLog?.log("refreshing")
try await reloadAction()
- } catch {
- // TODO: error
- symLog.log(error.localizedDescription)
+ } catch { // TODO: error
+ symLog?.log(error.localizedDescription)
}
}
- }
+ .listStyle(myListStyle.style)
+ .anyView
+ }
+ } // else
+ }.onAppear() {
+ DebugViewC.shared.setViewID(VIEW_EXCHANGES)
}
- .navigationBarItems(trailing: PlusButton(action: plusAction))
- .textFieldAlert(isPresented: $showAlert, title: "Add Exchange",
- doneText: "Add", text: $newExchange, action: addExchange)
+ .navigationBarTitleDisplayMode(.automatic)
+ .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..cdcacbe
--- /dev/null
+++ b/TalerWallet1/Views/Exchange/ExchangeSectionView.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 ExchangeRowView: View {
+ let exchange: Exchange
+ @Binding var centsToTransfer: UInt64
+
+ @State private var buttonSelected: Int? = nil
+ var body: some View {
+ let baseURL = exchange.exchangeBaseUrl
+ let model = WithdrawModel.model(baseURL: baseURL)
+
+ HStack(spacing: 0) { // can't use the built in Label because it adds the accessory arrow
+ Text(baseURL.trimURL())
+
+ NavigationLink(destination: LazyView {
+ EmptyView()
+ }, tag: 1, selection: $buttonSelected
+ ) {
+ EmptyView()
+ } .frame(width: 0)
+ .opacity(0)
+ NavigationLink(destination: LazyView {
+ ManualWithdraw(exchange: exchange,
+ model: model,
+ centsToTransfer: $centsToTransfer)
+ .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all))
+ }, tag: 2, selection: $buttonSelected
+ ) {
+ EmptyView()
+ } .frame(width: 0)
+ .opacity(0)
+ }.listRowSeparator(.hidden)
+
+ HStack { // buttons just set "buttonSelected" so the NavigationLink will trigger
+ Button("Deposit\nCoins") { buttonSelected = 1 }
+ .multilineTextAlignment(.center)
+ .lineLimit(2)
+ .buttonStyle(TalerButtonStyle(type: .bordered))
+ .disabled(true) // TODO: after implementing Deposit check available
+
+ Button("Withdraw\nCoins") { buttonSelected = 2 }
+ .multilineTextAlignment(.center)
+ .lineLimit(2)
+ .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, centsToTransfer: $centsToTransfer)
+ }
+ .accessibilityElement(children: .combine)
+ } header: {
+ Text(currency)
+ .font(.title)
+ }
+ }
+}
+// MARK: -
+#if DEBUG
+struct ExchangeRow_Container : View {
+ @State private var centsToTransfer: UInt64 = 100 // TODO: maybe Decimal?
+
+ 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..db32716
--- /dev/null
+++ b/TalerWallet1/Views/Exchange/ManualWithdraw.swift
@@ -0,0 +1,199 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import SwiftUI
+import taler_swift
+import SymLog
+
+struct ManualWithdraw: View {
+ private let symLog = SymLogV()
+ let navTitle = String(localized: "Withdraw Coins")
+
+ var exchange: Exchange
+ @ObservedObject var model: WithdrawModel
+ @Binding var centsToTransfer: UInt64
+
+ @State var manualWithdrawalDetails: ManualWithdrawalDetails? = nil
+// @State var numCoins: Int = 0
+
+ // returns numCoins, 0 if invalid, -1 if unknown
+ // either fees or empty string
+ private func numAndFee(detailsForAmount: ManualWithdrawalDetails?) -> (Int, String) {
+ do {
+ if let details = detailsForAmount {
+ let fee = try details.amountRaw - details.amountEffective
+ return (details.numCoins ?? -1, // either the number of coins, or unknown
+ fee.isZero ? "" : fee.readableDescription)
+ }
+ } catch {}
+ symLog.log("invalid")
+ return (0, "") // invalid
+ }
+ @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 currencyField = CurrencyField(value: $centsToTransfer, currency: currency) // becomeFirstResponder
+
+ ScrollView {
+ Text("from \(exchange.exchangeBaseUrl.trimURL())")
+ .padding(.top)
+ .font(.title3)
+ CurrencyInputView(currencyField: currencyField, title: String(localized: "Amount to withdraw:"))
+
+ let (numCoins, fee) = numAndFee(detailsForAmount: manualWithdrawalDetails)
+ let unknown = (numCoins < 0)
+ let invalid = (numCoins == 0)
+ let manyCoins = (numCoins > 99)
+ let quiteSome = (numCoins > 199)
+ let tooMany = (numCoins > 999)
+ let hasFee = (fee.count > 0)
+ let shownFee = hasFee ? String(localized: "- \(fee)") : String(localized: "No")
+ Text(invalid ? "invalid amount"
+ : tooMany ? "too many coins for a single withdrawal"
+ : "\(shownFee) withdrawal fee")
+ .foregroundColor((invalid || tooMany || hasFee) ? .red : .primary)
+ .padding()
+
+ if !invalid {
+ HStack {
+ Text(unknown ? "Some" : "\(numCoins)")
+ .foregroundColor(quiteSome ? .red : .primary)
+ Text(tooMany ? "coins" : "coins to obtain:")
+ .foregroundColor(tooMany ? .red : .primary)
+
+ Spacer()
+ if !tooMany {
+ let effective = manualWithdrawalDetails?.amountEffective ?? Amount(currency: currency, value: 0)
+ Text(effective.readableDescription)
+ }
+ } // xx coins to obtain: YYY currency
+ .padding(.top)
+// .font(.title3)
+
+ if !tooMany {
+ if manyCoins {
+ Text(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(quiteSome ? .red : .primary)
+ } // warnings
+
+ if ageMenuList.count > 1 {
+ 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) ? "unlimited"
+ : "\(index) years").tag(index)
+ }
+ }
+ }
+ if let tosAcc = manualWithdrawalDetails?.tosAccepted {
+ if tosAcc {
+ let restrictAge: Int? = (selectedAge == 0) ? nil
+ : selectedAge
+let _ = print(selectedAge, restrictAge)
+ NavigationLink(destination: LazyView {
+ ManualWithdrawDone(exchange: exchange,
+ model: model,
+ centsToTransfer: centsToTransfer,
+ restrictAge: restrictAge)
+ }) {
+ Text("Confirm Withdrawal") // VIEW_WITHDRAW_ACCEPT
+ }.buttonStyle(TalerButtonStyle(type: .prominent))
+ } else {
+ NavigationLink(destination: LazyView {
+ WithdrawTOSView(exchangeBaseUrl: exchange.exchangeBaseUrl,
+ model: model,
+ 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)
+ .navigationTitle(navTitle)
+ .padding(.horizontal)
+ .onAppear {
+ symLog.log("onAppear")
+ DebugViewC.shared.setViewID(VIEW_WITHDRAWAL)
+ }
+ .task(id: centsToTransfer) {
+ let amount = Amount.amountFromCents(currency, centsToTransfer)
+ do {
+ manualWithdrawalDetails = try await model.loadWithdrawalDetailsForAmountM(exchange.exchangeBaseUrl, amount: amount)
+ if let ageRestrictions = manualWithdrawalDetails?.ageRestrictionOptions {
+ var ages = ageRestrictions
+ let nonzero = ages.count > 0 // need at least 1 value from exchange which is not 0
+ if nonzero {
+ if ages[0] != 0 { // ensure that the first age is "0"
+ ages.insert(0, at: 0) // if not, insert "0" at position 0
+ }
+ if selectedAge >= ages.count { // check for out of bounds
+ selectedAge = 0
+ }
+ } else {
+ selectedAge = 0 // first ensure that selected is not out of bounds
+ }
+print(ages)
+ ageMenuList = ages // set State (will update view)
+ }
+ } catch { // TODO: error
+ symLog.log(error.localizedDescription)
+ manualWithdrawalDetails = nil
+ }
+ }
+ }
+}
+// MARK: -
+#if DEBUG
+struct ManualWithdraw_Container : View {
+ @State private var centsToTransfer: UInt64 = 510
+ @State private var manualWithdrawalDetails = ManualWithdrawalDetails(amountRaw: try! Amount(fromString: LONGCURRENCY + ":5.1"),
+ amountEffective: try! Amount(fromString: LONGCURRENCY + ":5.0"),
+ paytoUris: [],
+ tosAccepted: false,
+ ageRestrictionOptions: [],
+ numCoins: 6)
+
+ var body: some View {
+ let model = WithdrawModel.model(baseURL: DEMOEXCHANGE)
+ let exchange = Exchange(exchangeBaseUrl: DEMOEXCHANGE,
+ currency: LONGCURRENCY,
+ paytoUris: [],
+ tosStatus: "tosStatus",
+ exchangeStatus: "exchangeStatus",
+ ageRestrictionOptions: [],
+ permanent: false)
+ ManualWithdraw(exchange: exchange,
+ model: model,
+ centsToTransfer: $centsToTransfer,
+ manualWithdrawalDetails: manualWithdrawalDetails)
+ }
+}
+
+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..3d698bf
--- /dev/null
+++ b/TalerWallet1/Views/Exchange/ManualWithdrawDone.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 SymLog
+
+struct ManualWithdrawDone: View {
+ private let symLog = SymLogV()
+ let navTitle = String(localized: "Wire Transfer")
+
+ var exchange: Exchange
+ @ObservedObject var model: WithdrawModel
+ var centsToTransfer: UInt64
+ var restrictAge: Int?
+ @State var acceptManualWithdrawalResult: AcceptManualWithdrawalResult?
+ @State var withdrawalTransaction: Transaction?
+
+ var body: some View {
+#if DEBUG
+ let _ = Self._printChanges()
+ let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear
+#endif
+ VStack {
+ if let transaction = withdrawalTransaction {
+ TransactionDetailView(transaction: transaction,
+ 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: restrictAge)
+print(result as Any)
+ let transaction = try await model.getTransactionById(transactionId: result!.transactionId)
+ withdrawalTransaction = transaction
+// acceptManualWithdrawalResult = result
+ } 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 model = WithdrawModel.model(baseURL: DEMOEXCHANGE)
+ let exchange = Exchange(exchangeBaseUrl: DEMOEXCHANGE,
+ currency: LONGCURRENCY,
+ paytoUris: [],
+ tosStatus: "tosStatus",
+ exchangeStatus: "exchangeStatus",
+ ageRestrictionOptions: [],
+ permanent: false)
+ ManualWithdrawDone(exchange: exchange,
+ model: model,
+ centsToTransfer: centsToTransfer)
+ }
+}
+
+struct ManualWithdrawDone_Previews: PreviewProvider {
+ static var previews: some View {
+ ManualWithdrawDone_Container()
+ }
+}
+#endif
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..68d17eb 100644
--- a/TalerWallet1/Views/HelperViews/Buttons.swift
+++ b/TalerWallet1/Views/HelperViews/Buttons.swift
@@ -1,90 +1,248 @@
/*
- * 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
+import AVFoundation
+
+
+
+
+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 {
+struct BigBlueButton: View {
let title: String
+ var font: Font?
+ let disabled: Bool
let action: () -> Void
var body: some View {
Button(action: action) {
+ let colors: [Color] = disabled ? [Color.gray, Color.blue]
+ : [Color.red, Color.blue]
+ let back = LinearGradient(gradient: Gradient(colors: colors),
+ startPoint: .leading, endPoint: .trailing)
Text(title)
- .frame(minWidth: 0, maxWidth: 300)
+ .frame(minWidth: 50, maxWidth: 500)
.padding()
.foregroundColor(.white)
- .background(LinearGradient(gradient: Gradient(colors: [Color.red, Color.blue]), startPoint: .leading, endPoint: .trailing))
- .cornerRadius(40)
- .font(.title)
+ .background(disabled ? .gray : .blue) // back
+ .cornerRadius(20)
+ .font(font ?? .title)
+ }
+ .disabled(disabled)
+ }
+}
+struct BorderedButton: View {
+ let title: String
+ var font: Font?
+ let action: () -> Void
+
+ var body: some View {
+ Button(action: action) {
+ Text(title)
+ .frame(minWidth: 50, maxWidth: 500)
+ .padding()
+ .font(font ?? .title)
+ }
+ .buttonStyle(.bordered)
+ }
+}
+struct ProminentButton: View {
+ let title: String
+ var font: Font?
+ let action: () -> Void
+
+ var body: some View {
+ Button(action: action) {
+ Text(title)
+ .frame(minWidth: 50, maxWidth: 500)
+ .padding()
+ .font(font ?? .title)
}
+ .buttonStyle(.borderedProminent)
+ }
+}
+
+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)
+ }
+ }
+ func backColor(type: TalerButtonStyleType, pressed: Bool) -> Color {
+ return if type == .plain {
+ Color.clear
+ } else {
+ 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(.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 GrowingButton: ButtonStyle {
+ func makeBody(configuration: Configuration) -> some View {
+ configuration.label
+ .padding()
+ .background(.blue)
+ .foregroundColor(.white)
+ .clipShape(Capsule())
+ .scaleEffect(configuration.isPressed ? 1.2 : 1)
+ .animation(.easeOut(duration: 0.2), value: configuration.isPressed)
}
}
@@ -93,6 +251,8 @@ struct Buttons_Previews: PreviewProvider {
VStack {
HamburgerButton() {}
.padding()
+ QRButton() {}
+ .padding()
PlusButton() {}
.padding()
HStack {
@@ -101,8 +261,46 @@ struct Buttons_Previews: PreviewProvider {
ReloadButton(disabled: true) {}
.padding()
}
- AwesomeButton(title: "AwesomeButton") {}
+ BigBlueButton(title: "DisabledButton", disabled: true) { AudioServicesPlaySystemSound(1015) }
+ .padding()
+ BigBlueButton(title: "BigBlueButton", disabled: false) { AudioServicesPlaySystemSound(1000) }
+ .padding()
+ }
+ }
+}
+
+#if DEBUG
+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() + 1) {
+ 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 }
}
}
}
+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..19e06a1
--- /dev/null
+++ b/TalerWallet1/Views/HelperViews/CopyShare.swift
@@ -0,0 +1,85 @@
+/*
+ * 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
+ let dismissFirst: Bool
+
+ func shareAction() -> Void {
+ if dismissFirst {
+ dismissTop() // cannot open another sheet from within a sheet
+ }
+ 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
+ let dismissFirst: Bool
+
+ var body: some View {
+ HStack {
+ CopyButton(textToCopy: textToCopy, vertical: false)
+ .buttonStyle(TalerButtonStyle(type: .bordered))
+ ShareButton(textToShare: textToCopy, dismissFirst: dismissFirst)
+ .buttonStyle(TalerButtonStyle(type: .bordered))
+ } // two buttons
+ }
+}
+// MARK: -
+struct CopyShare_Previews: PreviewProvider {
+ static var previews: some View {
+ CopyShare(textToCopy: "Hallö", dismissFirst: false)
+ }
+}
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
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..409e275
--- /dev/null
+++ b/TalerWallet1/Views/HelperViews/ListStyle.swift
@@ -0,0 +1,113 @@
+/* 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 = "Automatic"
+ case grouped = "Grouped"
+ case inset = "Inset"
+ case insetGrouped = "InsetGrouped"
+ case plain = "Plain"
+ case sidebar = "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", "Lunch", "Dinner"]
+ let breakfast = ["pancakes", "bacon", "orange juice"]
+ var body: some View {
+ VStack {
+ Picker("List Style", selection: $selectedStyle) {
+ ForEach(MyListStyle.allCases, id: \.self) {
+ Text($0.displayName).tag($0)
+ }
+ }
+
+ List(sections, id: \.self) { section in
+ Section(section) {
+ if "Breakfast" == section {
+ ForEach(breakfast, id: \.self) { item in
+ Text(item)
+ }
+ } else {
+ Text("row")
+ }
+ }
+// Section("Breakfast") {
+// Text("pancakes")
+// Text("bacon")
+// Text("orange juice")
+// }
+// Section("Lunch") {
+// Text("sandwich")
+// Text("chips")
+// Text("lemonade")
+// }
+// Section("Dinner") {
+// Text("spaghetti")
+// Text("bread")
+// Text("water")
+// }
+ }
+ .refreshable {
+ print("refreshing")
+ // this closure is already async, no need for a Task
+ }
+ .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..0601ae0 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)
}
}
-
+// MARK: -
struct LoadingView_Previews: PreviewProvider {
static var previews: some View {
- LoadingView(backButtonHidden: true)
+ NavigationView {
+ LoadingView(backButtonHidden: true)
+ }
}
}
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..8fb1905
--- /dev/null
+++ b/TalerWallet1/Views/HelperViews/SelectDays.swift
@@ -0,0 +1,56 @@
+/*
+ * 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
+
+ @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("1 Day")
+ }.buttonStyle(TalerButtonStyle(type: (selected == 1) ? .prominent : .bordered, dimmed: true))
+ .disabled(!isEnabled)
+
+ Button(action: sevenDayAction) {
+ Text("7 Days")
+ }.buttonStyle(TalerButtonStyle(type: (selected == 7) ? .prominent : .bordered, dimmed: true))
+ .disabled(!isEnabled || maxExpiration < 7)
+
+ Button(action: thirtyDayAction) {
+ Text("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/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..50e0515 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()
+
+ 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..e6ce3f1
--- /dev/null
+++ b/TalerWallet1/Views/Main/MainView.swift
@@ -0,0 +1,100 @@
+/*
+ * 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()
+ @EnvironmentObject private var viewState: ViewState // popToRootView()
+ @EnvironmentObject private var controller: Controller
+ @State private var sheetPresented = false
+ @State private var urlToOpen: URL? = nil
+
+ func sheetDismiss() -> 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
+ NavigationView { // the one and only for all non-sheet views
+ if controller.backendState == .ready {
+ Content(symLog: symLog)
+ // any change to rootViewId triggers popToRootView behaviour
+ .id(viewState.rootViewId)
+ .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
+ }
+ } else if controller.backendState == .error {
+ ErrorView(errortext: nil) // TODO: show Error View
+ } else {
+ LaunchAnimationView()
+ }
+ }.navigationViewStyle(.stack)
+ .overlay(alignment: .top) {
+ // Show the viewID on top of the app's NavigationView
+ DebugViewV()
+ .id("ViewID")
+ }
+ .sheet(item: $urlToOpen, onDismiss: sheetDismiss) { 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
+ @State var currentView: Int = 0
+
+ var views: [SidebarItem] {[
+ SidebarItem(name: String(localized: "Balances"),
+ sysImage: "creditcard.fill", // TODO: Wallet Icon
+ view: AnyView(BalancesListView(model: BalancesModel.model(currency: "*"))
+ { sidebarVisible = !sidebarVisible } // hamburgerAction
+ )),
+ SidebarItem(name: String(localized: "Exchanges"),
+ sysImage: "building.columns",
+ view: AnyView(ExchangeListView(model: ExchangeModel.model())
+ { sidebarVisible = !sidebarVisible } // hamburgerAction
+ )),
+ SidebarItem(name: String(localized: "Settings"), // TODO: "About"?
+ sysImage: "gearshape.fill",
+ view: AnyView(SettingsView()
+ { sidebarVisible = !sidebarVisible } // hamburgerAction
+ ))
+ ]}
+ 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) {
+ views[currentView].view
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
+ .id(views[currentView].name)
+ .transition(.backslide)
+ SideBarView(views: views, currentView: $currentView, sidebarVisible: $sidebarVisible)
+ }
+ .background(WalletColors().backgroundColor)
+ }
+ }
+}
diff --git a/TalerWallet1/Views/Main/SideBarView.swift b/TalerWallet1/Views/Main/SideBarView.swift
index bec2b2c..db50a2c 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
@@ -31,15 +20,21 @@ struct SideBarView: View {
@Binding var sidebarVisible: Bool
var body: some View {
- symLog {
- HStack {
+ HStack { // sideView left, clear dismiss target right
+ EqualIconWidthDomain {
VStack {
Spacer()
+ Image("taler-logo-2023-red")
+ .resizable()
+ .scaledToFit()
+ .frame(width: 100, height: 100)
+ .padding(.top, 30)
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
+// withAnimation(.easeInOut(duration: 0.5)) {currentView = i} // switch to the view the user selected
} label: {
if let sysImage = views[i].sysImage {
Label(views[i].name, systemImage: sysImage)
@@ -51,14 +46,24 @@ struct SideBarView: View {
}
.buttonStyle(.bordered)
.font(.title)
-// .padding(.vertical)
-
-// Divider()
+ // .padding(.vertical)
}
Spacer()
Spacer()
+ Spacer()
+// Button {
+// symLog.log("Scan QR selected")
+// sidebarVisible = false // slide sidebar to the left
+ // TODO: show scan sheet
+// } label: {
+// Label("Scan QR", systemImage: "qrcode.viewfinder")
+// .frame(maxWidth: sidebarWidth, alignment: .leading)
+// }
+// .buttonStyle(.bordered)
+// .font(.title)
+// .padding(.bottom, 26.0)
}
- .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)
@@ -67,23 +72,23 @@ struct SideBarView: View {
// .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
- }
}
+ 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
+ }
}
}
}
-
+// MARK: -
#if DEBUG
-struct BindingViewContainer : View {
+fileprivate struct BindingViewContainer : View {
@State var currentView: Int = 0
@State var sidebarVisible: Bool = true
var views: [SidebarItem]
@@ -99,8 +104,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..a6a810a
--- /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()
+ @AppStorage("listStyle") 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
+ .onAppear() {
+ DebugViewC.shared.setViewID(VIEW_EMPTY) // 10
+ }
+ }
+}
+
+struct WalletEmptyView_Previews: PreviewProvider {
+ static var previews: some View {
+ WalletEmptyView()
+ }
+}
diff --git a/TalerWallet1/Views/Payment/PaymentAcceptView.swift b/TalerWallet1/Views/Payment/PaymentAcceptView.swift
index 26eb916..3b15583 100644
--- a/TalerWallet1/Views/Payment/PaymentAcceptView.swift
+++ b/TalerWallet1/Views/Payment/PaymentAcceptView.swift
@@ -1,71 +1,74 @@
/*
- * 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 AVFoundation
import SymLog
struct PaymentAcceptView: View {
private let symLog = SymLogV()
- @ObservedObject var viewModel: PaymentURIModel
+ @ObservedObject var model: PaymentURIModel
- var detailsForAmount: PaymentDetailsForUri
+ let 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() }
- }
+ let navTitle = String(localized: "Accept Payment")
+ @State private var confirmPayResult: ConfirmPayResult?
- @State var confirmPayResult: ConfirmPayResult?
+ func playSound(success: Bool) {
+// let url = URL(fileURLWithPath: "/System/Library/Audio/UISounds/PaymentReceived.caf")
+ let url = URL(fileURLWithPath: "/System/Library/Audio/UISounds/payment_" + (success ? "success.caf"
+ : "failure.caf"))
+ var soundID: SystemSoundID = 0
+ AudioServicesCreateSystemSoundID(url as CFURL, &soundID)
+ print(soundID)
+ AudioServicesPlaySystemSound(soundID);
+ }
+ func acceptAction() {
+ Task {
+ do {
+ confirmPayResult = try await model.confirmPayM(detailsForAmount.proposalId)
+ symLog.log(confirmPayResult as Any)
+ if confirmPayResult?.type == "done" {
+ // TODO: Show Hints that Payment was successfull
+ playSound(success: true)
+ } else {
+ // TODO: show error
+ playSound(success: false)
+ }
+ } catch { // TODO: error
+ symLog.log(error.localizedDescription)
+ }
+ dismissTop()
+ }
+ }
+ @State private var disabled = false
var body: some View {
- symLog { Group {
+ 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()
- }
+ ThreeAmountsView(topTitle: "Amount to pay:",
+ topAmount: raw, fee: fee,
+ bottomTitle: "Coins to be spent:",
+ bottomAmount: effective,
+ large: true, pending: false, incoming: false,
+ baseURL: detailsForAmount.contractTerms.exchanges.first?.url)
+ // TODO: payment: popup with all possible exchanges, check fees
+ .safeAreaInset(edge: .bottom) {
+ ProminentButton(title: "Accept", action: acceptAction)
+ .padding(.horizontal)
}
}
- .navigationBarItems(leading: cancelButton)
.navigationTitle(navTitle)
- }
}
}
+
+//struct PaymentAccept_Previews: PreviewProvider {
+// static var previews: some View {
+// let model: PaymentURIModel =
+// PaymentAcceptView(model: <#PaymentURIModel#>, detailsForAmount: <#PaymentDetailsForUri#>)
+// }
+//}
diff --git a/TalerWallet1/Views/Payment/PaymentURIView.swift b/TalerWallet1/Views/Payment/PaymentURIView.swift
index 38ad04c..8815be7 100644
--- a/TalerWallet1/Views/Payment/PaymentURIView.swift
+++ b/TalerWallet1/Views/Payment/PaymentURIView.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 SymLog
@@ -19,46 +8,38 @@ import SymLog
struct PaymentURIView: View {
private let symLog = SymLogV()
var url: URL
- @ObservedObject var viewModel: PaymentURIModel
-
-// @Environment(\.dismiss) var dismiss // call dismiss() to get rid of the sheet
- let navTitle = "Payment"
- var cancelButton: some View {
- Button("Cancel5") { dismissTop() }
- }
-
+ @ObservedObject var model: PaymentURIModel
@State var detailsForUri: PaymentDetailsForUri?
+ let navTitle = String(localized: "Payment")
+
var body: some View {
let badURL = "Error in URL: \(url)"
VStack {
- if viewModel.paymentState == nil {
+ if model.paymentState == nil {
LoadingView(backButtonHidden: false)
- } else { switch viewModel.paymentState {
+ } else { switch model.paymentState {
case .waitingForUriDetails:
let _ = symLog.vlog("waitingForUriDetails")
- WithdrawProgressView(message: url.host ?? badURL) { dismissTop() }
+ WithdrawProgressView(message: url.host ?? badURL)
.navigationTitle("Contacting Exchange")
case .receivedUriDetails:
let _ = symLog.vlog("waitingForUser")
- PaymentAcceptView(viewModel: viewModel, detailsForAmount: detailsForUri!)
+ PaymentAcceptView(model: model, detailsForAmount: detailsForUri!)
default:
- symLog {
- Text("Payment")
- .navigationBarItems(leading: cancelButton)
- .navigationTitle(navTitle)
- }
+ Text("Payment")
+ .navigationTitle(navTitle)
} }
}.task {
do { // TODO: cancelled
symLog.log(".task")
- detailsForUri = try await viewModel.preparePayForUri(url.absoluteString)
+ detailsForUri = try await model.preparePayForUriM(url.absoluteString)
// print(detailsForUri?.status)
// print(detailsForUri?.amountRaw.description)
// print(detailsForUri?.amountEffective.description)
// print(detailsForUri?.proposalId)
- } catch {
- symLog.log(error.localizedDescription) // TODO: error
+ } catch { // TODO: error
+ symLog.log(error.localizedDescription)
}
}
}
diff --git a/TalerWallet1/Views/Peer2peer/ReceivePurpose.swift b/TalerWallet1/Views/Peer2peer/ReceivePurpose.swift
new file mode 100644
index 0000000..38be62b
--- /dev/null
+++ b/TalerWallet1/Views/Peer2peer/ReceivePurpose.swift
@@ -0,0 +1,129 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import SwiftUI
+import taler_swift
+import SymLog
+
+struct ReceivePurpose: View {
+ private let symLog = SymLogV()
+ @FocusState private var isFocused: Bool
+ @StateObject var model = Peer2peerModel.model()
+ @State var peerPullCheck: CheckPeerPullCreditResponse?
+
+ var scopeInfo: ScopeInfo
+ var centsToTransfer: UInt64
+ @Binding var purpose: String
+ @Binding var expireDays: UInt
+ var deactivateAction: () -> Void
+
+ let formatter = CurrencyFormatter.shared // TODO: based on currency
+
+ let buttonFont: Font = .title2
+ private var label: String {
+ let mag = pow(10, formatter.maximumFractionDigits)
+ return formatter.string(for: Decimal(centsToTransfer) / mag) ?? ""
+ }
+
+ func pullFee(ppCheck: CheckPeerPullCreditResponse?) -> String {
+ do {
+ if let p2pcheck = ppCheck {
+ let fee = try p2pcheck.amountRaw - p2pcheck.amountEffective
+ return fee.readableDescription
+ }
+ } catch {}
+ return ""
+ }
+
+ var body: some View {
+ let amount = Amount.amountFromCents(scopeInfo.currency, centsToTransfer)
+
+ let fee = pullFee(ppCheck: peerPullCheck)
+ VStack (spacing: 6) {
+ Text(amount.readableDescription)
+ Text("+ \(fee) payment fee")
+ .foregroundColor(.red)
+ VStack(alignment: .leading, spacing: 6) {
+ Text("Purpose:")
+ .padding(.top)
+ .font(.title3)
+
+ TextField("Purpose", text: $purpose)
+ .font(.title)
+ .foregroundColor(WalletColors().fieldForeground) // text color
+ .background(WalletColors().fieldBackground)
+ .border(.primary)
+ .focused($isFocused)
+ .onAppear {
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
+ isFocused = true // make first responder - raise keybord
+ }
+ }
+
+ HStack{
+ Spacer()
+ Text("\(purpose.count)/100")
+ } // maximum 100 characters
+
+ Text("Expires in:")
+ .font(.title3)
+
+ SelectDays(selected: $expireDays, maxExpiration: 35)
+ .disabled(false)
+ .padding(.bottom)
+
+ let buttonTitle = "Invoice \(label) \(scopeInfo.currency)"
+ let disabled = (expireDays == 0) || (purpose.count < 1)
+
+ NavigationLink(destination: LazyView {
+ SendNow(amountToSend: amount, purpose: purpose, expireDays: expireDays, model: model)
+ .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all))
+ }) {
+ Text(buttonTitle)
+ .font(buttonFont)
+ }
+ .buttonStyle(TalerButtonStyle(type: .prominent))
+ .disabled(disabled)
+
+ Spacer()
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(.horizontal)
+ }
+ .navigationTitle("Invoice another Wallet")
+ .onAppear {
+ DebugViewC.shared.setSheetID(VIEW_INVOICE_PURPOSE)
+ print("❗️ ReceivePurpose onAppear")
+ }
+ .onDisappear {
+ print("❗️ ReceivePurpose onDisappear")
+ deactivateAction()
+ }
+ .task {
+ symLog.log(".task")
+ do {
+ peerPullCheck = try await model.checkPeerPullCreditM(amount, exchangeBaseUrl: scopeInfo.exchangeBaseUrl ?? nil) // TODO: exchangeBaseUrl!
+ } catch { // TODO: error
+ symLog.log(error.localizedDescription)
+ }
+ }
+ }
+
+}
+// MARK: -
+#if DEBUG
+struct ReceivePurpose_Previews: PreviewProvider {
+ static var previews: some View {
+ let scopeInfo = ScopeInfo(type: ScopeInfo.ScopeInfoType.exchange, exchangeBaseUrl: DEMOEXCHANGE, currency: LONGCURRENCY)
+ @State var purpose: String = "pUrPoSe"
+ @State var expireDays: UInt = 0
+ ReceivePurpose(scopeInfo: scopeInfo,
+ centsToTransfer: 5,
+ purpose: $purpose,
+ expireDays: $expireDays) {
+ print("deactivateAction")
+ }
+ }
+}
+#endif
diff --git a/TalerWallet1/Views/Peer2peer/RequestPayment.swift b/TalerWallet1/Views/Peer2peer/RequestPayment.swift
new file mode 100644
index 0000000..0c37649
--- /dev/null
+++ b/TalerWallet1/Views/Peer2peer/RequestPayment.swift
@@ -0,0 +1,77 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import SwiftUI
+import taler_swift
+import SymLog
+
+struct RequestPayment: View {
+ private let symLog = SymLogV()
+ let navTitle = String(localized: "Request Payment")
+
+ @ObservedObject var model: Peer2peerModel
+ var scopeInfo: ScopeInfo
+ @Binding var centsToTransfer: UInt64
+
+ @State private var peerPullCheck: CheckPeerPullCreditResponse? = nil
+ @State private var purpose: String = ""
+ @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 currencyField = CurrencyField(value: $centsToTransfer, currency: scopeInfo.currency)
+
+ VStack(alignment: .leading, spacing: 6) {
+ CurrencyInputView(currencyField: currencyField, title: "Amount to receive:")
+
+ HStack {
+ let disabled = centsToTransfer == 0
+
+ let deactivateAction = {
+ print("❗️ deactivateAction")
+ }
+
+ NavigationLink(destination: LazyView {
+ ReceivePurpose(scopeInfo: scopeInfo,
+ centsToTransfer: centsToTransfer,
+ purpose: $purpose,
+ expireDays: $expireDays
+ ) {
+ deactivateAction()
+ }
+ .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all))
+ }) {
+ Text("title2")
+ }
+ .buttonStyle(TalerButtonStyle(type: .prominent))
+ .disabled(disabled)
+ }
+
+ Spacer()
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .navigationTitle(navTitle)
+ .padding(.horizontal)
+ .onAppear { // make CurrencyField show the keyboard
+ DebugViewC.shared.setViewID(VIEW_INVOICE_P2P)
+ print("❗️Yikes \(navTitle) onAppear")
+ }
+ .onDisappear {
+ print("❗️Yikes \(navTitle) onDisappear")
+ }
+ }
+}
+// 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..e95aab9
--- /dev/null
+++ b/TalerWallet1/Views/Peer2peer/SendAmount.swift
@@ -0,0 +1,85 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import SwiftUI
+import taler_swift
+import SymLog
+
+struct SendAmount: View {
+ private let symLog = SymLogV()
+ let navTitle = String(localized: "Send Coins")
+// @ObservedObject private var keyboardResponder = KeyboardResponder()
+// @FocusState private var isFocused: Bool
+
+ let amountAvailable: Amount
+ let buttonFont: Font = .title2
+
+ @State private var centsToTransfer: UInt64 = 0 // TODO: maybe Decimal?
+ @State private var purpose: String = ""
+ @State private var expireDays: UInt = 0
+
+ var body: some View {
+ let currencyField = CurrencyField(value: $centsToTransfer, currency: amountAvailable.currencyStr)
+
+ VStack(alignment: .leading, spacing: 6) {
+ CurrencyInputView(currencyField: currencyField, title: "Amount to send:")
+
+ let available = amountAvailable.readableDescription
+ Text("Available: \(available)")
+
+ Text("Choose where to send to:")
+ .padding(.top)
+ .font(.title3)
+ HStack {
+ let kbdShown: Bool = false // keyboardResponder.keyboardHeight > 0
+ let title1 = kbdShown ? "To bank" : "To a bank\naccount"
+ let title2 = kbdShown ? "To wallet" : "To another\nwallet"
+ let disabled = centsToTransfer == 0 // TODO: check amountAvailable
+
+ // Left button: To bank
+ NavigationLink(destination: LazyView {
+ Text("Bank pressed")
+ }) {
+ Text(title1)
+ }
+ .buttonStyle(TalerButtonStyle(type: .bordered))
+ .disabled(disabled)
+
+ NavigationLink(destination: LazyView {
+ SendPurpose(amountAvailable: amountAvailable,
+ centsToSend: centsToTransfer,
+ purpose: $purpose,
+ expireDays: $expireDays,
+ deactivateAction: {})
+ .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all))
+ }) {
+ Text(title2)
+ }
+ .buttonStyle(TalerButtonStyle(type: .prominent))
+ .disabled(disabled)
+ }
+
+ Spacer()
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .navigationTitle(navTitle)
+ .padding(.horizontal)
+ .onAppear { // make CurrencyField show the keyboard
+ DebugViewC.shared.setSheetID(SHEET_PAY_P2P)
+ print("❗️Yikes SendAmount onAppear")
+ }
+ .onDisappear {
+ print("❗️Yikes SendAmount onDisappear")
+ }
+ }
+}
+// MARK: -
+#if DEBUG
+struct SendAmount_Previews: PreviewProvider {
+ static var previews: some View {
+ let amount = Amount(currency: "TaLeR", integer: 10, fraction: 0)
+ SendAmount(amountAvailable: amount)
+ }
+}
+#endif
diff --git a/TalerWallet1/Views/Peer2peer/SendNow.swift b/TalerWallet1/Views/Peer2peer/SendNow.swift
new file mode 100644
index 0000000..8c81271
--- /dev/null
+++ b/TalerWallet1/Views/Peer2peer/SendNow.swift
@@ -0,0 +1,87 @@
+/*
+ * 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()
+
+ var amountToSend: Amount
+ var purpose: String
+ var expireDays: UInt
+
+ @ObservedObject var model: Peer2peerModel
+ @State var peerPushResponse: PeerPushResponse?
+
+ @State var talerURI: String? = nil
+
+ var body: some View {
+ ScrollView {
+ if talerURI == nil {
+ LoadingView(backButtonHidden: true)
+ } else {
+ VStack() {
+ Text("Let the payee scan this QR code to receive:")
+ .fixedSize(horizontal: false, vertical: true)
+ .padding(.top, 30)
+ .font(.title3)
+
+ QRGeneratorView(text: talerURI!)
+
+ Text("Alternatively, copy and send this URI:")
+ .fixedSize(horizontal: false, vertical: true)
+ .font(.title3)
+ .padding(.vertical)
+
+ Text(talerURI!)
+ .padding(.bottom)
+
+ CopyShare(textToCopy: talerURI!, dismissFirst: true)
+ .disabled(false)
+
+ Text("The QR code can also be copied and shared from Transactions later")
+ .fixedSize(horizontal: false, vertical: true)
+ .font(.subheadline)
+ .padding(.vertical, 20)
+
+ Button("Done") {
+ dismissTop()
+ }
+ .buttonStyle(TalerButtonStyle(type: .prominent))
+ .padding(.vertical)
+
+ }
+ .interactiveDismissDisabled() // can only use "Done" button to dismiss
+ .navigationBarHidden(true) // no back button, no title
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
+ .padding(.horizontal)
+ }
+ }
+ .task {
+ symLog.log(".task")
+ do {
+ // generate talerURI
+ let timestamp = Timestamp.inSomeDays(expireDays)
+ let terms = PeerContractTerms(amount: amountToSend, summary: purpose, purse_expiration: timestamp)
+ let baseURL = DEMOEXCHANGE // TODO: use correct baseURL
+ peerPushResponse = try await model.initiatePeerPushDebitM(baseURL, terms: terms)
+ talerURI = peerPushResponse?.talerUri
+ } catch { // TODO: error
+ symLog.log(error.localizedDescription)
+ }
+ } // task
+ }
+}
+// MARK: -
+//struct SendNow_Previews: PreviewProvider {
+// static var previews: some View {
+// Group {
+// SendNow()
+// SendNow(talerURI: "taler://pay-push/exchange.demo.taler.net")
+// }
+// }
+//}
diff --git a/TalerWallet1/Views/Peer2peer/SendPurpose.swift b/TalerWallet1/Views/Peer2peer/SendPurpose.swift
new file mode 100644
index 0000000..cabeea8
--- /dev/null
+++ b/TalerWallet1/Views/Peer2peer/SendPurpose.swift
@@ -0,0 +1,126 @@
+/*
+ * 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
+ @StateObject var model = Peer2peerModel.model()
+ @State var peerPushCheck: CheckPeerPushDebitResponse?
+
+ var amountAvailable: Amount
+ var centsToSend: UInt64
+ @Binding var purpose: String
+ @Binding var expireDays: UInt
+ var deactivateAction: () -> Void
+
+ let formatter = CurrencyFormatter.shared // TODO: based on currency
+
+ let buttonFont: Font = .title2
+ private var label: String {
+ let mag = pow(10, formatter.maximumFractionDigits)
+ return formatter.string(for: Decimal(centsToSend) / mag) ?? ""
+ }
+
+ private func fee(ppCheck: CheckPeerPushDebitResponse?) -> String {
+ do {
+ if let p2pcheck = ppCheck {
+ let fee = try p2pcheck.amountEffective - p2pcheck.amountRaw
+ return fee.readableDescription
+ }
+ } catch {}
+ return ""
+ }
+
+ var body: some View {
+ let amount = Amount.amountFromCents(amountAvailable.currencyStr, centsToSend)
+
+ let fee = fee(ppCheck: peerPushCheck)
+ VStack (spacing: 6) {
+ Text(amount.readableDescription)
+ Text("+ \(fee) payment fee")
+ .foregroundColor(.red)
+ VStack(alignment: .leading, spacing: 6) {
+ Text("Purpose:")
+ .padding(.top)
+ .font(.title3)
+
+ TextField("Purpose", text: $purpose)
+ .font(.title)
+ .foregroundColor(WalletColors().fieldForeground) // text color
+ .background(WalletColors().fieldBackground)
+ .border(.primary)
+ .focused($isFocused)
+ .onAppear {
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
+ isFocused = true // make first responder - raise keybord
+ }
+ }
+
+ HStack{
+ Spacer()
+ Text("\(purpose.count)/100")
+ } // maximum 100 characters
+
+ Text("Expires in:")
+ .font(.title3)
+
+ // TODO: compute max Expiration day from peerPushCheck to disable 30 (and even 7)
+ SelectDays(selected: $expireDays, maxExpiration: 35)
+ .disabled(false)
+ .padding(.bottom)
+
+ let buttonTitle = "Send \(label) \(amountAvailable.currencyStr) now"
+ let disabled = (expireDays == 0) || (purpose.count < 1) // TODO: check amountAvailable
+
+ NavigationLink(destination: LazyView {
+ SendNow(amountToSend: amount, purpose: purpose, expireDays: expireDays, model: model)
+ .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all))
+ }) {
+ Text(buttonTitle)
+ .font(buttonFont)
+ }
+ .buttonStyle(TalerButtonStyle(type: .prominent))
+ .disabled(disabled)
+
+ Spacer()
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(.horizontal)
+ }
+ .navigationTitle("To another Wallet")
+ .onAppear {
+ DebugViewC.shared.setSheetID(VIEW_SEND_PURPOSE)
+ print("❗️ SendPurpose onAppear")
+ }
+ .onDisappear {
+ print("❗️ SendPurpose onDisappear")
+ deactivateAction()
+ }
+ .task {
+ symLog.log(".task")
+ do {
+ peerPushCheck = try await model.checkPeerPushDebitM(amount)
+ } catch { // TODO: error
+ symLog.log(error.localizedDescription)
+ }
+ }
+ }
+
+}
+// MARK: -
+struct SendPurpose_Previews: PreviewProvider {
+ static var previews: some View {
+ @State var purpose: String = ""
+ @State var expireDays: UInt = 0
+ let amount = Amount(currency: "TaLeR", integer: 10, fraction: 0)
+ SendPurpose(amountAvailable: amount, centsToSend: 5,
+ purpose: $purpose, expireDays: $expireDays) {
+ print("deactivateAction")
+ }
+ }
+}
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
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..3a35398
--- /dev/null
+++ b/TalerWallet1/Views/Settings/Pending/PendingOpsListView.swift
@@ -0,0 +1,64 @@
+/*
+ * 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")
+
+ @ObservedObject var model: PendingModel
+
+ var body: some View {
+#if DEBUG
+ let _ = Self._printChanges()
+ let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear
+#endif
+ let reloadAction = model.updateM
+ Content(symLog: symLog, model: model, reloadAction: reloadAction)
+ .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 model: PendingModel
+ var reloadAction: () async throws -> ()
+
+ var body: some View {
+#if DEBUG
+ let _ = Self._printChanges()
+ let _ = symLog?.vlog() // just to get the # to compare it with .onAppear & onDisappear
+#endif
+ ScrollViewReader { scrollView in
+ List(model.pendingOperations, id: \.self) { pendingOp in
+ PendingOpView(pendingOp: pendingOp)
+ }
+ .listStyle(SidebarListStyle())
+ .navigationBarTitleDisplayMode(.large)
+ .onAppear() {
+ DebugViewC.shared.setViewID(VIEW_PENDING)
+ }
+ .refreshable {
+ do {
+ symLog?.log("refreshing")
+ try await reloadAction()
+ } catch { // TODO: error
+ symLog?.log(error.localizedDescription)
+ }
+ }
+ }
+ }
+ }
+}
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..db63b5a 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,173 @@ 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
+ private let symLog = SymLogV()
+ let navTitle = String(localized: "Settings")
+
@AppStorage("developerMode") var developerMode: Bool = false
@AppStorage("developDelay") var developDelay: Bool = false
+ @AppStorage("listStyle") var myListStyle = MyListStyle.automatic
- var showSidebar: () -> Void
- init(showSidebar: @escaping () -> Void) {
- self.showSidebar = showSidebar
- }
+ var hamburgerAction: () -> Void
@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") {}
+ HStack {
+ Text("Liststyle:")
+ .font(.title2)
+ Spacer()
+ Picker(selection: $myListStyle) {
+ ForEach(MyListStyle.allCases, id: \.self) {
+ Text($0.displayName).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(model: PendingModel.model()) }
+ } 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 {
+ let model: SettingsModel = SettingsModel()
+ 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 {
+ let model: SettingsModel = SettingsModel()
+ 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 {
+ let model: SettingsModel = SettingsModel()
+ 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 {
@@ -144,10 +191,10 @@ extension Bundle {
}
}
-struct SettingsView_Previews: PreviewProvider {
- static var previews: some View {
- SettingsView {
-
- }
- }
-}
+//struct SettingsView_Previews: PreviewProvider {
+// static var previews: some View {
+// SettingsView {
+//
+// }
+// }
+//}
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..e2e8a9a
--- /dev/null
+++ b/TalerWallet1/Views/Sheets/Sheet.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
+
+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
+
+ var cancelButton: some View {
+ Button("Cancel") {
+ print(dismiss) // TODO: delete this line
+ dismissTop()
+ }
+ }
+
+ var body: some View {
+ let idString = debugViewC.sheetID > 0 ? String(debugViewC.sheetID) : ""
+ 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")
+ }
+ }
+}
+// MARK: -
+//struct Sheet_Previews: PreviewProvider {
+// static var previews: some View {
+//
+// }
+//}
diff --git a/TalerWallet1/Views/Sheets/URLSheet.swift b/TalerWallet1/Views/Sheets/URLSheet.swift
new file mode 100644
index 0000000..342447b
--- /dev/null
+++ b/TalerWallet1/Views/Sheets/URLSheet.swift
@@ -0,0 +1,45 @@
+/*
+ * 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: "Invalid URL")
+ var urlToOpen: URL
+ @EnvironmentObject private var controller: Controller
+
+ @State private var urlCommand: UrlCommand? = nil
+
+ var body: some View {
+ Group {
+ if urlCommand == UrlCommand.withdraw {
+ let model = WithdrawModel.model(baseURL: "global") // TODO: get baseURL from command
+ WithdrawURIView(url: urlToOpen, model: model)
+ } else if urlCommand == UrlCommand.pay {
+ let model = PaymentURIModel.model()
+ PaymentURIView(url: urlToOpen, model: model)
+ } 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..4940e18
--- /dev/null
+++ b/TalerWallet1/Views/Transactions/ManualDetails.swift
@@ -0,0 +1,69 @@
+/*
+ * 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)
+ .disabled(false)
+ } .padding(.leading)
+ .padding(.vertical, -8)
+ .listRowSeparator(.hidden)
+ Text("and use the transaction subject:")
+ .listRowSeparator(.hidden)
+ HStack {
+ Text(details.reservePub)
+ Spacer()
+ CopyButton(textToCopy: details.reservePub, vertical: true)
+ .disabled(false)
+ } .padding(.leading)
+ .padding(.vertical, -8)
+ .listRowSeparator(.hidden)
+ HStack {
+ Spacer()
+ ShareButton(textToShare: payto, dismissFirst: false)
+ .disabled(false)
+ Spacer()
+ } .listRowSeparator(.hidden)
+ Text("Payto URL")
+ .font(.footnote)
+ .foregroundColor(Color.yellow) // clear
+ .padding(.vertical, -8)
+ .listRowSeparator(.automatic)
+ }
+ }
+}
+// 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..d016089
--- /dev/null
+++ b/TalerWallet1/Views/Transactions/ThreeAmounts.swift
@@ -0,0 +1,109 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import SwiftUI
+import taler_swift
+
+struct ThreeAmounts: 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)") // TODO: localize
+ .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 {
+ ThreeAmounts(common: common, topTitle: "Withdrawal", baseURL: DEMOEXCHANGE, large: 1==1)
+ .safeAreaInset(edge: .bottom) {
+ ProminentButton(title: "Accept", action: {})
+ .padding(.horizontal)
+ }
+ }
+ }
+ }
+}
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..7de0960
--- /dev/null
+++ b/TalerWallet1/Views/Transactions/TransactionDetailView.swift
@@ -0,0 +1,255 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import SwiftUI
+import taler_swift
+import SymLog
+
+struct TransactionDetailView: View {
+ private let symLog = SymLogV()
+ @AppStorage("listStyle") var myListStyle = MyListStyle.automatic
+
+ var transaction: Transaction
+ var deleteAction: ((_ transactionId: String) async throws -> Void)?
+ var abortAction: ((_ transactionId: String) async throws -> Void)?
+ var doneAction: (() -> 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 navTitle = (pending ? String(localized: "Pending ") : "") + "\(common.type)" // TODO: localize type
+
+ Group {
+ List {
+ Text("\(dateString)")
+ .font(.title2)
+// .listRowSeparator(.hidden)
+ SwitchCase(transaction: transaction, common: common)
+
+ if transaction.isAbortable { if let abortAction {
+ AbortButton(common: common, abortAction: abortAction)
+ } }
+ if transaction.isDeleteable { if let deleteAction {
+ DeleteButton(common: common, deleteAction: deleteAction)
+ } }
+
+ if let doneAction {
+ DoneButton(doneAction: doneAction)
+ .onNotification(.TransactionStateTransition) { notification in
+print(notification.userInfo)
+ if let transition = notification.userInfo?[TRANSACTIONTRANSITION] as? TransactionTransition {
+print(transition.newTxState.major)
+ if transition.transactionId == common.transactionId {
+ doneAction()
+ }
+ }
+ }
+ }
+// if transaction.isSuspendable { if let suspendAction {
+// SuspendButton(common: common, suspendAction: suspendAction)
+// } }
+// if transaction.isResumable { if let resumeAction {
+// ResumeButton(common: common, resumeAction: resumeAction)
+// } }
+ }
+ .listStyle(myListStyle.style)
+ .anyView
+ }
+ .navigationTitle(navTitle)
+ .onAppear {
+ symLog.log("onAppear")
+ DebugViewC.shared.setViewID(VIEW_TRANSACTIONDETAIL)
+ }
+ .onDisappear {
+ symLog.log("onDisappear")
+ }
+ }
+//}
+//
+//extension TransactionDetail {
+ struct SwitchCase: View {
+ let transaction: Transaction
+ let common: TransactionCommon
+
+ var body: some View {
+ let pending = (common.txState.major == TransactionMajorState.pending)
+ switch transaction {
+ case .withdrawal(let withdrawalTransaction):
+ let details = withdrawalTransaction.details
+ if pending {
+ switch details.withdrawalDetails.type {
+ case .manual:
+ ManualDetails(common: common, details: details.withdrawalDetails)
+
+ case .bankIntegrated:
+ QRCodeDetails(transaction: transaction)
+ }
+ }
+ ThreeAmounts(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)
+ ThreeAmounts(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
+ ThreeAmounts(common: common, topTitle: String(localized: "Refunded amount:"),
+ baseURL: nil, large: true) // TODO: baseURL
+ case .reward(let rewardTransaction):
+ let details = rewardTransaction.details // TODO: more details
+ ThreeAmounts(common: common, topTitle: String(localized: "Received Reward:"),
+ baseURL: details.exchangeBaseUrl, large: true)
+// case .tip(let tipTransaction):
+// let details = tipTransaction.details // TODO: details
+// ThreeAmounts(common: common, topTitle: String(localized: "Received Tip:"), large: true)
+ case .refresh(let refreshTransaction):
+ let details = refreshTransaction.details // TODO: details
+ ThreeAmounts(common: common, topTitle: String(localized: "Refreshed amount:"),
+ baseURL: nil, large: true) // TODO: baseURL
+ case .peer2peer(let p2pTransaction):
+ let details = p2pTransaction.details // TODO: details
+ QRCodeDetails(transaction: transaction)
+ ThreeAmounts(common: common, topTitle: String(localized: "Peer to Peer:"),
+ baseURL: details.exchangeBaseUrl, large: false)
+ }
+ }
+ }
+ 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 {
+ VStack {
+ QRGeneratorView(text: talerURI)
+ Text(talerURI)
+ }
+ }
+ }
+ } else if keys.contains(EXCHANGEBASEURL) {
+ if let baseURL = details[EXCHANGEBASEURL] {
+ Text("from \(baseURL.trimURL())")
+ .font(.title2)
+ .padding(.bottom)
+ }
+ }
+ }
+ }
+ struct DeleteButton: View {
+ var common : TransactionCommon
+ var deleteAction: (_ transactionId: String) async throws -> Void
+
+ @State var disabled: Bool = false
+ @State var deleted: Bool = false
+ var body: some View {
+ Button(role: .destructive, action: {
+ Task { // delete from wallet-core
+ disabled = true // don't try this more than once
+ do {
+ try await deleteAction(common.transactionId)
+// symLog.log("deleted \(common.transactionId)")
+ deleted = true
+ } catch { // TODO: error
+// symLog.log(error.localizedDescription)
+ }
+ }
+ }, label: {
+ HStack {
+ if deleted {
+ Text("Deleted from list")
+ } else {
+ Text("Delete from list" + " ")
+ Image(systemName: "trash")
+ }
+ }
+ .font(.title)
+ .frame(maxWidth: .infinity)
+ })
+ .buttonStyle(.bordered)
+ .controlSize(.large)
+ .padding(.horizontal)
+ .disabled(disabled)
+ }
+ }
+ struct AbortButton: View {
+ var common : TransactionCommon
+ var abortAction: (_ transactionId: String) async throws -> Void
+
+ @State var disabled: Bool = false
+ @State var aborting: Bool = false
+ var body: some View {
+ Button(role: .cancel, action: {
+ Task { // delete from wallet-core
+ disabled = true // don't try this more than once
+ do {
+ try await abortAction(common.transactionId)
+// symLog.log("aborted \(common.transactionId)")
+ aborting = true
+ } catch { // TODO: error
+// symLog.log(error.localizedDescription)
+ }
+ }
+ }, label: {
+ HStack {
+ if aborting {
+ Text("Abort pending...")
+ } else {
+ Text("Abort" + " ")
+ Image(systemName: "x.circle")
+ }
+ }
+ .font(.title)
+ .frame(maxWidth: .infinity)
+ })
+ .buttonStyle(.bordered)
+ .controlSize(.large)
+ .padding(.horizontal)
+ .disabled(disabled)
+ }
+ }
+ 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 var previews: some View {
+ Group {
+ TransactionDetailView(transaction: withdrawal, doneAction: doneActionDummy)
+ TransactionDetailView(transaction: payment, 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..98349c8
--- /dev/null
+++ b/TalerWallet1/Views/Transactions/TransactionRowView.swift
@@ -0,0 +1,137 @@
+/*
+ * 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 {
+ Image(systemName: incoming ? "text.badge.plus" : "text.badge.minus")
+ .foregroundColor(foreColor)
+ .padding(.trailing)
+ .font(.largeTitle)
+ .accessibility(hidden: true)
+
+ if transaction.isWithdrawal {
+ TransactionRowCenter(centerTop: String(localized: "Withdrawal"), centerBottom: dateString)
+ } else if transaction.isPayment {
+ TransactionRowCenter(centerTop: String(localized: "Payment"), centerBottom: dateString)
+ } else if transaction.isP2pOutgoing {
+ TransactionRowCenter(centerTop: String(localized: "P2P Send"), centerBottom: dateString)
+ } else if transaction.isP2pIncoming {
+ TransactionRowCenter(centerTop: String(localized: "P2P Receive"), centerBottom: dateString)
+ } else if transaction.isRefund {
+ TransactionRowCenter(centerTop: String(localized: "Refund"), centerBottom: dateString)
+ } else if transaction.isRefresh {
+ TransactionRowCenter(centerTop: String(localized: "Refresh"), centerBottom: dateString)
+ } else if keys.contains(EXCHANGEBASEURL) {
+ if let baseURL = details[EXCHANGEBASEURL] {
+ TransactionRowCenter(centerTop: baseURL.trimURL(), 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..1721663
--- /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("listStyle") 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
+ .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..e2e5ad1 100644
--- a/TalerWallet1/Views/Transactions/TransactionsListView.swift
+++ b/TalerWallet1/Views/Transactions/TransactionsListView.swift
@@ -1,105 +1,112 @@
/*
- * 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"
+ @AppStorage("listStyle") var myListStyle = MyListStyle.automatic
+ let navTitle: String
+
+ let currency: String
+ let transactions: [Transaction]
+ var reloadAction: () async -> ()
+ let deleteAction: (_ transactionId: String) async throws -> ()
+ let abortAction: (_ transactionId: String) async throws -> ()
- @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
+// let title = AttributedString(localized: "^[\(count) Ticket](inflect: true)")
+ let title: String = "\(count) \(navTitle)"
+ Content(symLog: symLog, currency: currency, transactions: transactions, myListStyle: $myListStyle,
+ reloadAction: reloadAction, deleteAction: deleteAction, abortAction: abortAction)
+ .navigationTitle(title)
+ .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 reloadAction()
}
- }
}
}
// MARK: -
extension TransactionsListView {
struct Content: View {
let symLog: SymLogV?
- @ObservedObject var viewModel: TransactionsModel
-
- var reloadAction: () async throws -> ()
+ let currency: String
+ let transactions: [Transaction]
+ @Binding var myListStyle: MyListStyle
+ let reloadAction: () async -> ()
+ let deleteAction: (_ transactionId: String) async throws -> ()
+ let abortAction: (_ transactionId: String) async throws -> ()
@State private var upAction: () -> Void = {}
@State private var downAction: () -> Void = {}
+// 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)
+// }
+// }
+// }
+// }
+
var body: some View {
- let transactions = viewModel.transactions!
+#if DEBUG
+ let _ = Self._printChanges()
+ let _ = symLog?.vlog() // just to get the # to compare it with .onAppear & onDisappear
+#endif
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
+// let common = transaction.common
+ NavigationLink { LazyView { // whole row like in a tableView
+ // pending may not be deleted, but only aborted
+ TransactionDetailView(transaction: transaction,
+ deleteAction: deleteAction,
+ abortAction: abortAction)
+ }} 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)
+ await reloadAction()
+ }
+ .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)
+ .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all))
}
}
}
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/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/WithdrawAcceptView.swift b/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawAcceptView.swift
new file mode 100644
index 0000000..d0a1269
--- /dev/null
+++ b/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawAcceptView.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
+
+struct WithdrawAcceptView: View {
+ private let symLog = SymLogV()
+ let url: URL
+ @ObservedObject var model: WithdrawModel
+
+ let navTitle = String(localized: "Accept Withdrawal")
+ let detailsForAmount: ManualWithdrawalDetails
+ let baseURL: String
+
+ func acceptAction() -> () {
+ Task {
+ do {
+ let confirmTransferUrl = try await model.sendAcceptIntWithdrawalM(baseURL, withdrawURL: url.absoluteString)
+ symLog.log(confirmTransferUrl as Any)
+ // TODO: Show Hints that User should Confirm on bank website
+ // update balances to show pending withdrawal
+ await BalancesModel.model(currency: "*").fetchBalancesM()
+ } catch { // TODO: error
+ symLog.log(error.localizedDescription)
+ }
+ }
+ }
+
+ var body: some View {
+ Group {
+ let currState = model.withdrawState
+ switch currState {
+ case .receivedAmountDetails, .receivedTOS, .receivedTOSAck:
+ let raw = detailsForAmount.amountRaw
+ let effective = detailsForAmount.amountEffective
+ let fee = try! Amount.diff(raw, effective) // TODO: different currencies
+ let outColor = WalletColors().transactionColor(false)
+ let inColor = WalletColors().transactionColor(true)
+
+ ThreeAmountsView(topTitle: String(localized: "Chosen amount to withdraw:"),
+ topAmount: raw, fee: fee,
+ bottomTitle: String(localized: "Coins to be withdrawn:"),
+ bottomAmount: effective,
+ large: false, pending: false, incoming: true,
+ baseURL: baseURL)
+ .safeAreaInset(edge: .bottom) {
+ ProminentButton(title: String(localized: "Accept"), action: acceptAction)
+ .padding(.horizontal)
+ }
+ case .waitingForWithdrAck, .receivedWithdrAck:
+ // TODO: SHEET_WITHDRAW_CONFIRM
+ Text("waiting for bank confirmation")
+ .navigationTitle("Confirm with Bank")
+ .onAppear() {
+ DebugViewC.shared.setSheetID(SHEET_WITHDRAW_CONFIRM)
+ }
+
+ default:
+ let _ = symLog.vlog(currState as Any)
+ ErrorView(errortext: "unknown state") // TODO: Error
+ }
+ }
+ .navigationTitle(navTitle)
+ .onAppear() {
+ DebugViewC.shared.setSheetID(SHEET_WITHDRAW_ACCEPT)
+ }
+ }
+}
diff --git a/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawProgressView.swift b/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawProgressView.swift
new file mode 100644
index 0000000..c872021
--- /dev/null
+++ b/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawProgressView.swift
@@ -0,0 +1,27 @@
+/*
+ * 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()
+ Text(message)
+ .font(.title)
+ Spacer()
+ Spacer()
+ }
+ }
+}
+
+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..48a8c0e
--- /dev/null
+++ b/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawTOSView.swift
@@ -0,0 +1,90 @@
+/*
+ * 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("listStyle") var myListStyle = MyListStyle.automatic
+
+ let navTitle = String(localized: "Terms of Service")
+
+ var exchangeBaseUrl: String
+ @ObservedObject var model: WithdrawModel
+
+ @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 {
+ switch model.withdrawState {
+ case .receivedAmountDetails, .waitingForTOS:
+ WithdrawProgressView(message: exchangeBaseUrl.trimURL())
+ .navigationTitle("Loading " + navTitle)
+ case .receivedTOS, .waitingForTOSAck, .receivedTOSAck:
+ Content(symLog: symLog, exchangeTOS: exchangeTOS, myListStyle: $myListStyle) {
+ Task {
+ do {
+ _ = try await model.setExchangeTOSAcceptedM(exchangeBaseUrl, etag: exchangeTOS!.currentEtag)
+ if acceptAction != nil {
+ acceptAction!()
+ } else {
+ self.presentationMode.wrappedValue.dismiss()
+ }
+ } catch { // TODO: Show Error
+ symLog.log(error.localizedDescription)
+ }
+ }
+ }
+ .navigationBarTitleDisplayMode(.large) // .inline
+ .navigationTitle(navTitle)
+ default:
+ ErrorView(errortext: "unknown state") // TODO: ???
+ }
+ }.onAppear() {
+ if viewID > SHEET_WITHDRAWAL {
+ DebugViewC.shared.setSheetID(SHEET_WITHDRAW_TOS)
+ } else {
+ DebugViewC.shared.setViewID(VIEW_WITHDRAW_TOS)
+ }
+ }.task {
+ do {
+ 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) {
+ ProminentButton(title: String(localized: "Accept"), action: acceptAction)
+ .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..246daaa
--- /dev/null
+++ b/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawURIView.swift
@@ -0,0 +1,76 @@
+/*
+ * This file is part of GNU Taler, ©2022-23 Taler Systems S.A.
+ * See LICENSE.md
+ */
+import SwiftUI
+import SymLog
+
+struct WithdrawURIView: View {
+ private let symLog = SymLogV()
+ var url: URL
+ @ObservedObject var model: WithdrawModel
+
+// @State var withdrawUriInfo: WithdrawUriInfoResponse?
+ @State var exchangeBaseUrl: String?
+ @State var manualWithdrawalDetails: ManualWithdrawalDetails?
+ @State var didAcceptTOS: Bool = false
+
+ var body: some View {
+ let badURL = "Error in URL: \(url)"
+ VStack {
+ let currState = model.withdrawState
+ if currState == nil {
+ LoadingView(backButtonHidden: false)
+ } else {
+ let _ = symLog.vlog(currState as Any)
+ switch currState {
+ case .waitingForUriDetails, .receivedUriDetails:
+ WithdrawProgressView(message: url.host ?? badURL)
+ .navigationTitle("Contacting Exchange")
+ case .waitingForAmountDetails:
+ WithdrawProgressView(message: exchangeBaseUrl?.trimURL() ?? badURL)
+ .navigationTitle("Found Exchange")
+ default:
+ // .receivedAmountDetails, .waitingForTOS, .receivedTOS, .waitingForTOSAck, .receivedTOSAck
+ // waitingForWithdrAck, receivedWithdrAck
+ if !didAcceptTOS {
+ // user must accept ToS first
+ WithdrawTOSView(exchangeBaseUrl: exchangeBaseUrl!,
+ model: model,
+ viewID: SHEET_WITHDRAW_TOS) {
+ didAcceptTOS = true
+ }
+ } else {
+ // show Amount details and let user accept
+ WithdrawAcceptView(url: url, model: model,
+ detailsForAmount: manualWithdrawalDetails!,
+ baseURL: exchangeBaseUrl!)
+ }
+ }
+ }
+ }.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 {
+ exchangeBaseUrl = withdrawUriInfo.possibleExchanges.first?.exchangeBaseUrl
+ }
+ symLog.log("amount: \(amount), baseURL: \(String(describing: exchangeBaseUrl))")
+ // TODO: let user choose exchange from list
+ manualWithdrawalDetails = try await model.loadWithdrawalDetailsForAmountM(exchangeBaseUrl!, amount: amount)
+
+ symLog.log("raw: \(manualWithdrawalDetails!.amountRaw), effective: \(manualWithdrawalDetails!.amountEffective)")
+ if manualWithdrawalDetails!.tosAccepted {
+ didAcceptTOS = true
+ }
+ } catch { // TODO: error
+ symLog.log(error.localizedDescription)
+ }
+ }
+ }
+}
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..dffb53d 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,17 @@ 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))
+ }
+
+
/// 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