[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[taler-taler-ios] 06/09: App sources
From: |
gnunet |
Subject: |
[taler-taler-ios] 06/09: App sources |
Date: |
Wed, 01 Feb 2023 09:28:55 +0100 |
This is an automated email from the git hooks/post-receive script.
marc-stibane pushed a commit to branch master
in repository taler-ios.
commit 86364f0257c44882202e46d7b784c4cee4cab3dd
Author: Marc Stibane <marc@taler.net>
AuthorDate: Wed Feb 1 00:29:56 2023 +0100
App sources
---
Taler/Model/BalancesModel.swift | 48 -
Taler/Model/ExchangeManager.swift | 69 --
Taler/Model/PendingManager.swift | 49 -
Taler/Model/TransactionsModel.swift | 48 -
Taler/Model/WithdrawModel.swift | 179 ---
Taler/Views/BalancesView.swift | 106 --
Taler/Views/ContentView.swift | 85 --
Taler/Views/PendingView.swift | 68 --
Taler/Views/SettingsView.swift | 252 -----
Taler/Views/WithdrawView.swift | 142 ---
Taler/WalletBackend.swift | 1167 --------------------
TalerWallet1/Backend/Transaction.swift | 314 ++++++
TalerWallet1/Backend/WalletBackendError.swift | 54 +
TalerWallet1/Backend/WalletBackendRequest.swift | 434 ++++++++
TalerWallet1/Backend/WalletCore.swift | 262 +++++
TalerWallet1/Controllers/Controller.swift | 128 +++
TalerWallet1/Controllers/TalerWallet1App.swift | 81 ++
TalerWallet1/Helper/TalerDater.swift | 102 ++
.../Helper/TalerStrings.swift | 16 +-
TalerWallet1/Helper/View+dismissTop.swift | 41 +
TalerWallet1/Model/ExchangeTestModel.swift | 138 +++
TalerWallet1/Model/WalletInitModel.swift | 86 ++
TalerWallet1/Model/WalletModel.swift | 55 +
TalerWallet1/Quickjs/quickjs.swift | 81 ++
TalerWallet1/Views/Balances/BalanceRow.swift | 46 +
TalerWallet1/Views/Balances/BalancesModel.swift | 73 ++
.../Views/Balances/CurrenciesListView.swift | 78 ++
TalerWallet1/Views/Balances/CurrencyView.swift | 58 +
TalerWallet1/Views/Balances/PendingRow.swift | 61 +
.../Views/Balances/WalletEmptyView.swift | 36 +-
TalerWallet1/Views/Exchange/ExchangeListView.swift | 103 ++
TalerWallet1/Views/Exchange/ExchangeModel.swift | 115 ++
TalerWallet1/Views/HelperViews/AmountView.swift | 43 +
TalerWallet1/Views/HelperViews/Buttons.swift | 86 ++
TalerWallet1/Views/HelperViews/LoadingView.swift | 45 +
.../Views/HelperViews/TextFieldAlert.swift | 73 ++
TalerWallet1/Views/Main/ContentView.swift | 92 ++
.../Views/Main/ErrorView.swift | 23 +-
TalerWallet1/Views/Main/LaunchAnimationView.swift | 33 +
TalerWallet1/Views/Main/SideBarView.swift | 110 ++
TalerWallet1/Views/Payment/PaymentAcceptView.swift | 71 ++
TalerWallet1/Views/Payment/PaymentURIModel.swift | 183 +++
TalerWallet1/Views/Payment/PaymentURIView.swift | 65 ++
TalerWallet1/Views/Pending/PendingModel.swift | 82 ++
TalerWallet1/Views/Pending/PendingOpView.swift | 64 ++
.../Views/Pending/PendingOpsListView.swift | 65 ++
TalerWallet1/Views/Settings/SettingsItem.swift | 93 ++
TalerWallet1/Views/Settings/SettingsView.swift | 139 +++
.../Views/Transactions/TransactionDetail.swift | 79 ++
.../Views/Transactions/TransactionRow.swift | 81 ++
.../Views/Transactions/TransactionsListView.swift | 91 ++
.../Views/Transactions/TransactionsModel.swift | 69 ++
TalerWallet1/Views/URLSheet.swift | 64 ++
.../Views/Withdraw/WithdrawAcceptView.swift | 71 ++
.../Views/Withdraw/WithdrawProgressView.swift | 37 +-
TalerWallet1/Views/Withdraw/WithdrawTOSView.swift | 96 ++
TalerWallet1/Views/Withdraw/WithdrawURIModel.swift | 213 ++++
TalerWallet1/Views/Withdraw/WithdrawURIView.swift | 103 ++
58 files changed, 4493 insertions(+), 2253 deletions(-)
diff --git a/Taler/Model/BalancesModel.swift b/Taler/Model/BalancesModel.swift
deleted file mode 100644
index 29a6656..0000000
--- a/Taler/Model/BalancesModel.swift
+++ /dev/null
@@ -1,48 +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
-
-class BalancesModel: ObservableObject {
- var backend: WalletBackend
-
- @Published var loading: Bool = false
- @Published var balances: [Balance]?
-
- init(backend: WalletBackend) {
- self.backend = backend
- }
-
- func getBalances() {
- self.loading = true
- let req = WalletBackendGetBalancesRequest()
- backend.sendFormattedRequest(request: req) { response, err in
- // TODO: Use Combine instead
- DispatchQueue.main.async {
- self.loading = false
- if let res = response {
- self.balances = res.balances
- } else {
- // TODO: Handle error.
- }
- }
- }
- }
-
- func getTransactionsModel() -> TransactionsModel {
- return TransactionsModel(backend: self.backend, currency: nil)
- }
-}
diff --git a/Taler/Model/ExchangeManager.swift
b/Taler/Model/ExchangeManager.swift
deleted file mode 100644
index ca4942e..0000000
--- a/Taler/Model/ExchangeManager.swift
+++ /dev/null
@@ -1,69 +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
-
-typealias ExchangeItem = WalletBackendListExchanges.ExchangeListItem
-
-class ExchangeManager: ObservableObject {
- var backend: WalletBackend
-
- @Published var loading: Bool
- @Published var exchanges: [ExchangeItem]?
-
- init(_backend: WalletBackend) {
- self.backend = _backend
- self.loading = false
- self.exchanges = nil
- }
-
- func updateList() {
- let listRequest = WalletBackendListExchanges()
- backend.sendFormattedRequest(request: listRequest) { response, err in
- // TODO: Use Combine instead.
- DispatchQueue.main.async {
- self.loading = false
- if let result = response {
- self.exchanges = result.exchanges
- } else {
- // TODO: Show error.
- }
- }
- }
- self.loading = true
- }
-
- func add(url: String) {
- let addRequest = WalletBackendAddExchangeRequest(exchangeBaseUrl: url)
- backend.sendFormattedRequest(request: addRequest) { response, err in
- // TODO: Use Combine instead.
- DispatchQueue.main.async {
- self.loading = false
- if let _ = response {
- self.updateList()
- } else {
- // TODO: Show error.
- }
- }
- }
- self.loading = true
- }
-
- func withdraw(exchange: ExchangeItem) -> WithdrawModel {
- return WithdrawModel(backend: self.backend, exchange: exchange)
- }
-}
diff --git a/Taler/Model/PendingManager.swift b/Taler/Model/PendingManager.swift
deleted file mode 100644
index 02a7e37..0000000
--- a/Taler/Model/PendingManager.swift
+++ /dev/null
@@ -1,49 +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
-
-class PendingManager: ObservableObject {
- var backend: WalletBackend
-
- @Published var loading: Bool
- @Published var items: [String]?
-
- init(_backend: WalletBackend) {
- self.backend = _backend
- self.loading = false
- self.items = nil
- }
-
- func update() {
- let req = WalletBackendPendingRequest()
- backend.sendFormattedRequest(request: req) { response, err in
- // TODO: Use Combine instead.
- DispatchQueue.main.async {
- self.loading = false
- if let x = response {
- self.items = x.pendingOperations.map({ op in
- let encoded = try! JSONEncoder().encode(op)
- let str = String(data: encoded, encoding: .utf8)!
- return str
- })
- }
- }
- }
- self.loading = true
- }
-}
diff --git a/Taler/Model/TransactionsModel.swift
b/Taler/Model/TransactionsModel.swift
deleted file mode 100644
index 753d5cd..0000000
--- a/Taler/Model/TransactionsModel.swift
+++ /dev/null
@@ -1,48 +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
-
-class TransactionsModel: ObservableObject {
- var backend: WalletBackend
- var currency: String?
-
- @Published var loading: Bool = false
- @Published var transactions: [Transaction]?
-
- init(backend: WalletBackend, currency: String?) {
- self.backend = backend
- self.currency = currency
- }
-
- func loadTransactions(searchString: String? = nil) {
- self.loading = true
- let req = WalletBackendGetTransactionsRequest(currency: self.currency,
- search: searchString)
- backend.sendFormattedRequest(request: req) { response, err in
- // TODO: Use Combine instead
- DispatchQueue.main.async {
- self.loading = false
- if let res = response {
- print("x")
- self.transactions = res.transactions
- } else {
- // TODO: Handle error.
- }
- }
- }
- }
-}
diff --git a/Taler/Model/WithdrawModel.swift b/Taler/Model/WithdrawModel.swift
deleted file mode 100644
index 02027cc..0000000
--- a/Taler/Model/WithdrawModel.swift
+++ /dev/null
@@ -1,179 +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
-
-func paytoUriGetIban(uri: String) -> String {
- let url = URL(string: uri)!
- return url.lastPathComponent
-}
-
-func paytoUriGetSubject(uri: String) -> String {
- let url = URLComponents(string: uri)!
- return url.queryItems!.first(where: { item in
- item.name == "message"
- })!.value!.removingPercentEncoding!.replacingOccurrences(of: "+", with: "
")
-}
-
-typealias WithdrawDetails =
WalletBackendGetWithdrawalDetailsForAmountRequest.Response
-typealias TOSDetails = WalletBackendGetExchangeTermsOfService.Response
-
-class ManualTransferModel: ObservableObject {
- let backend: WalletBackend
- let exchange: ExchangeItem
- var details: WithdrawDetails!
- var paytoUri: String!
- @Published var loading: Bool = false
- @Published var nav: Bool = false
-
- init(backend: WalletBackend, exchange: ExchangeItem) {
- self.backend = backend
- self.exchange = exchange
- }
-
- func loadDetails(_ newDetails: WithdrawDetails, _ newPaytoUri: String) {
- self.details = newDetails
- self.paytoUri = newPaytoUri
- }
-}
-
-class PromptWithdrawModel: ObservableObject {
- let backend: WalletBackend
- let exchange: ExchangeItem
- var details: WithdrawDetails!
- var tosDetails: TOSDetails?
- @Published var tosAccepted: Bool!
- @Published var loading: Bool = false
- @Published var navTos: Bool = false
- @Published var nav: Bool = false
-
- var manualTransferModel: ManualTransferModel
-
- init(backend: WalletBackend, exchange: ExchangeItem) {
- self.backend = backend
- self.exchange = exchange
- self.manualTransferModel = ManualTransferModel(backend: backend,
exchange: exchange)
- }
-
- func loadDetails(_ newDetails: WithdrawDetails) {
- self.details = newDetails
- self.tosAccepted = details.tosAccepted
- }
-
- func acceptTos() {
- self.loading = true
- let req =
WalletBackendSetExchangeTermsOfServiceAccepted(exchangeBaseUrl:
exchange.exchangeBaseUrl,
- etag:
tosDetails!.currentEtag)
- backend.sendFormattedRequest(request: req) { response, err in
- // TODO: Use Combine instead
- DispatchQueue.main.async {
- self.loading = false
- if let _ = response {
- self.tosAccepted = true
- self.navTos = false
- } else {
- // TODO: Handle error.
- }
- }
- }
- }
-
- func acceptWithdraw() {
- // TODO: Include an option for a withdraw payto uri.
- self.loading = true
- let req = WalletBackendAcceptManualWithdrawalRequest(exchangeBaseUrl:
exchange.exchangeBaseUrl,
- amount:
details.amountRaw)
- backend.sendFormattedRequest(request: req) { response, err in
- // TODO: Use Combine instead
- DispatchQueue.main.async {
- self.loading = false
- if let res = response {
- self.manualTransferModel.loadDetails(self.details,
res.exchangePaytoUris[0])
- self.nav = true
- } else {
- // TODO: Show error.
- self.loading = false
- }
- }
- }
- }
-}
-
-class WithdrawModel: ObservableObject {
- let backend: WalletBackend
- let exchange: ExchangeItem
- var details: WithdrawDetails?
- @Published var loading: Bool = false
- @Published var nav: Bool = false
-
- var promptModel: PromptWithdrawModel
-
- init(backend: WalletBackend, exchange: ExchangeItem) {
- self.backend = backend
- self.exchange = exchange
- self.promptModel = PromptWithdrawModel(backend: backend, exchange:
exchange)
- }
-
- func getWithdrawDetails(amountStr: String) {
- self.loading = true
- do {
- let amount = try Amount(fromString: amountStr)
- let req =
WalletBackendGetWithdrawalDetailsForAmountRequest(exchangeBaseUrl:
exchange.exchangeBaseUrl,
-
amount: amount)
- backend.sendFormattedRequest(request: req) { response, err in
- // TODO: Use Combine instead.
- DispatchQueue.main.async {
- if let res = response {
- self.details = res
- self.promptModel.loadDetails(res)
- if res.tosAccepted {
- self.loading = false
- self.nav = true
- } else {
- self.getTos()
- }
- } else {
- // TODO: Show error.
- self.loading = false
- }
- }
- }
- } catch {
- // TODO: Show error.
- self.loading = false
- }
- }
-
- private func getTos() {
- self.loading = true
- let req = WalletBackendGetExchangeTermsOfService(exchangeBaseUrl:
exchange.exchangeBaseUrl)
- backend.sendFormattedRequest(request: req) { response, err in
- // TODO: Use Combine instead
- DispatchQueue.main.async {
- self.loading = false
- if let res = response {
- self.promptModel.tosDetails = res
- self.loading = false
- self.nav = true
- } else {
- // TODO: Show error.
- self.loading = false
- }
- }
- }
- }
-}
diff --git a/Taler/Views/BalancesView.swift b/Taler/Views/BalancesView.swift
deleted file mode 100644
index a8eab64..0000000
--- a/Taler/Views/BalancesView.swift
+++ /dev/null
@@ -1,106 +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 TransactionsView: View {
- @ObservedObject var model: TransactionsModel
-
- var body: some View {
- VStack {
- if model.transactions == nil {
- ProgressView()
- .onAppear {
- model.loadTransactions()
- }
- } else if model.loading {
- ProgressView()
- } else {
- List(model.transactions!, id: \.self) { tx in
- VStack {
- Text("Transaction: \(tx.transactionId)")
- }
- }
- Text("Loaded")
- /*VStack {
- Text("Balances")
- NavigationLink {
- TransactionsView(model:
self.balancesModel.getTransactionsModel())
- } label: {
- Text("Transactions")
- }
-
- }
- .padding(16)
- .navigationTitle("Balances")
- .navigationBarItems(
- leading: Button(action: self.showSidebar, label: {
- Image(systemName: "line.3.horizontal")
- })
- )*/
- }
- }
- .navigationTitle("Transactions")
- }
-}
-
-struct BalancesView: View {
- @ObservedObject var balancesModel: BalancesModel
- @EnvironmentObject var backend: BackendManager
- var showSidebar: () -> Void
-
- var body: some View {
- NavigationView {
- if balancesModel.balances == nil {
- ProgressView()
- .navigationTitle("Balances")
- .navigationBarItems(
- leading: Button(action: self.showSidebar, label: {
- Image(systemName: "line.3.horizontal")
- })
- )
- .onAppear {
- balancesModel.getBalances()
- }
- } else if balancesModel.loading {
- ProgressView()
- .navigationTitle("Balances")
- .navigationBarItems(
- leading: Button(action: self.showSidebar, label: {
- Image(systemName: "line.3.horizontal")
- })
- )
- } else {
- VStack {
- Text("Balances")
- NavigationLink {
- TransactionsView(model:
self.balancesModel.getTransactionsModel())
- } label: {
- Text("Transactions")
- }
-
- }
- .padding(16)
- .navigationTitle("Balances")
- .navigationBarItems(
- leading: Button(action: self.showSidebar, label: {
- Image(systemName: "line.3.horizontal")
- })
- )
- }
- }
- }
-}
diff --git a/Taler/Views/ContentView.swift b/Taler/Views/ContentView.swift
deleted file mode 100644
index 237b33a..0000000
--- a/Taler/Views/ContentView.swift
+++ /dev/null
@@ -1,85 +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 SidebarItem {
- var name: String
- var view: AnyView
-}
-
-struct ContentView: View {
- @StateObject var backend: BackendManager = BackendManager()
-
- @State var sidebarVisible: Bool = false
- var views: [SidebarItem] {[
- SidebarItem(name: "Balances",
- view: AnyView(BalancesView(balancesModel:
BalancesModel(backend: self.backend.backend)) {
- self.sidebarVisible = true
- }.environmentObject(backend))),
- SidebarItem(name: "Settings",
- view: AnyView(SettingsView {
- self.sidebarVisible = true
- }.environmentObject(backend))),
- SidebarItem(name: "Pending Operations",
- view: AnyView(PendingView(_showSidebar: {
- self.sidebarVisible = true
- }, pending:
backend.pendingManager).environmentObject(backend)))
- ]}
- @State var currentView: Int = 0
-
- var body: some View {
- ZStack(alignment: .leading) {
-
- views[currentView].view
- .frame(maxWidth: .infinity, maxHeight: .infinity, alignment:
.center)
-
- VStack {
- Spacer()
-
- Button {
- self.sidebarVisible = false
- } label: {
- Text("Close")
- }
- Divider()
-
- ForEach(0..<views.count, id: \.self) { i in
- Button {
- self.sidebarVisible = false
- self.currentView = i
- } label: {
- Text(views[i].name)
- }
- Divider()
- }
-
- Spacer()
- }
- .background(Color.gray)
- .frame(width: 100, alignment: .center)
- .offset(x: sidebarVisible ? 0 : -100)
- .animation(.easeInOut, value: sidebarVisible)
- .ignoresSafeArea()
- }
- }
-}
-
-struct ContentView_Previews: PreviewProvider {
- static var previews: some View {
- ContentView()
- }
-}
diff --git a/Taler/Views/PendingView.swift b/Taler/Views/PendingView.swift
deleted file mode 100644
index 40fecd0..0000000
--- a/Taler/Views/PendingView.swift
+++ /dev/null
@@ -1,68 +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 PendingView: View {
- @ObservedObject var pendingManager: PendingManager
-
- var showSidebar: () -> Void
- var body: some View {
- NavigationView {
- if pendingManager.items == nil {
- ProgressView()
- .navigationTitle("Pending")
- .navigationBarItems(
- leading: Button(action: self.showSidebar, label: {
- Image(systemName: "line.3.horizontal")
- }))
- .onAppear {
- pendingManager.update()
- }
- } else if pendingManager.loading {
- ProgressView()
- .navigationTitle("Pending")
- .navigationBarItems(
- leading: Button(action: self.showSidebar, label: {
- Image(systemName: "line.3.horizontal")
- }))
- } else {
- let items = pendingManager.items!
- List(items, id: \.self) { item in
- VStack {
- Text(item)
- .font(.system(size: 14, design: .monospaced))
- }
- }
- .navigationTitle("Pending")
- .navigationBarItems(
- leading: Button(action: self.showSidebar, label: {
- Image(systemName: "line.3.horizontal")
- }),
- trailing: Button(action: {
- pendingManager.update()
- }, label: {
- Image(systemName: "arrow.clockwise")
- }))
- }
- }
- }
-
- init(_showSidebar: @escaping () -> Void, pending: PendingManager) {
- self.showSidebar = _showSidebar
- self.pendingManager = pending
- }
-}
diff --git a/Taler/Views/SettingsView.swift b/Taler/Views/SettingsView.swift
deleted file mode 100644
index a86778f..0000000
--- a/Taler/Views/SettingsView.swift
+++ /dev/null
@@ -1,252 +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 TextInputPopup: ViewModifier {
- @State var exchangeUrl: String = "https://"
- var onCancel: () -> Void
- var onOk: (String) -> Void
-
- init(cancel: @escaping () -> Void, ok: @escaping (String) -> Void) {
- self.onCancel = cancel
- self.onOk = ok
- }
-
- func body(content: Content) -> some View {
- return content
- .overlay(
- VStack {
- Text("Add Exchange")
- TextField("Exchange URL", text: $exchangeUrl)
- HStack {
- Button {
- self.onCancel()
- } label: {
- Text("Cancel")
- }
- Button {
- self.onOk(exchangeUrl)
- } label: {
- Text("Ok")
- }
- }
- }
- .padding(8)
- .frame(width: UIScreen.main.bounds.width - 100, height:
150, alignment: .center)
- .background(Color.green)
- .cornerRadius(8)
- , alignment: .center)
- .animation(.easeIn)
- }
-}
-
-extension View {
- func textInputPopup(cancel: @escaping () -> Void, ok: @escaping (String)
-> Void, showing: Bool) -> some View {
- if showing {
- return AnyView(modifier(TextInputPopup(cancel: cancel, ok: ok)))
- } else {
- return AnyView(self)
- }
- }
-}
-
-struct ExchangeListView: View {
- @ObservedObject var exchangeManager: ExchangeManager
- @State var showPopup: Bool = false
-
- var body: some View {
- if exchangeManager.exchanges == nil {
- ProgressView()
- .navigationTitle("Exchanges")
- .onAppear {
- exchangeManager.updateList()
- }
- } else if exchangeManager.loading {
- ProgressView()
- .navigationTitle("Exchanges")
- } else {
- let exchanges = exchangeManager.exchanges!
- if exchanges.count == 0 {
- Text("No Exchanges")
- .navigationTitle("Exchanges")
- .navigationBarItems(trailing: Button(action: {
- withAnimation {
- showPopup = true
- }
- }, label: {
- Image(systemName: "plus")
- }))
- .textInputPopup(cancel: {
- self.showPopup = false
- }, ok: { exchangeUrl in
- self.showPopup = false
- exchangeManager.add(url: exchangeUrl)
- print(exchangeUrl)
- }, showing: showPopup)
- } else {
- List(exchanges, id: \.self) { exchange in
- VStack {
- Text(exchange.exchangeBaseUrl)
- .frame(maxWidth: .infinity)
- Text("Currency: " + exchange.currency)
- .frame(maxWidth: .infinity)
- NavigationLink {
- WithdrawView(model:
exchangeManager.withdraw(exchange: exchange))
- } label: {
- Text("Withdraw")
- }
- }
- }
- .navigationTitle("Exchanges")
- .navigationBarItems(trailing: Button(action: {
- withAnimation {
- showPopup = true
- }
- }, label: {
- Image(systemName: "plus")
- }))
- .textInputPopup(cancel: {
- self.showPopup = false
- }, ok: { exchangeUrl in
- self.showPopup = false
- exchangeManager.add(url: exchangeUrl)
- print(exchangeUrl)
- }, showing: showPopup)
- }
- }
- }
-}
-
-/*
- * Exchanges
- * Manage list of exchanges known to this wallet
- *
- * Backup
- * Last backup: 5 hr. ago
- *
- * Developer Mode [toggle]
- * Shows more information intended for debugging
- *
- * Withdraw TESTKUDOS
- * Get money for testing
- *
- * Debug log
- * View/send internal log
- *
- * App Version
- * v0.9.0-dev.11 (fdroid 11)
- *
- * Wallet Core Version
- * v0.9.0-dev.11
- *
- * Supported Exchange Versions
- * 12:0:0
- *
- * Supported Merchant Versions
- * 2:0:1
- *
- * Reset Wallet (dangerous!)
- * Throws away your money
- */
-
-struct SettingsItem<Content: View>: View {
- var name: String
- var description: String?
- var content: () -> Content
-
- init(name: String, description: String? = nil, @ViewBuilder content:
@escaping () -> Content) {
- self.name = name
- self.description = description
- self.content = content
- }
-
- var body: some View {
- HStack {
- Image(systemName: "line.3.horizontal")
- VStack {
- Text(name)
- .frame(maxWidth: .infinity, alignment: .leading)
- .font(.title2)
- if let desc = description {
- Text(desc)
- .frame(maxWidth: .infinity, alignment: .leading)
- .font(.caption)
- }
- }
- content()
- }
- .padding([.bottom], 8)
- }
-}
-
-struct SettingsView: View {
- @EnvironmentObject var backend: BackendManager
- @AppStorage("developerMode") var developerMode: Bool = false
-
- var showSidebar: () -> Void
- var body: some View {
- NavigationView {
- VStack {
- SettingsItem(name: "Exchanges", description: "Manage list of
exchanges known to this wallet") {
- NavigationLink {
- ExchangeListView(exchangeManager:
backend.exchangeManager)
- } label: {
- Text("View")
- }
- }
- SettingsItem(name: "Developer Mode", description: "Shows more
information intended for debugging") {
- Toggle(isOn: $developerMode) { }
- }
- if developerMode {
- SettingsItem(name: "App Version") {
- Text("v0.9.0-dev.11")
- }
- SettingsItem(name: "Wallet Core Version") {
- Text("v0.9.0-dev.11")
- }
- SettingsItem(name: "Supported Exchange Versions") {
- Text("12:0:0")
- }
- SettingsItem(name: "Supported Merchant Versions") {
- Text("2:0:1")
- }
- }
- Spacer()
- }
- .padding(16)
- .navigationTitle("Settings")
- .navigationBarItems(
- leading: Button(action: self.showSidebar, label: {
- Image(systemName: "line.3.horizontal")
- })
- )
- }
- }
-
- init(_showSidebar: @escaping () -> Void) {
- self.showSidebar = _showSidebar
- }
-}
-
-struct SettingsView_Previews: PreviewProvider {
- static var previews: some View {
- SettingsView {
-
- }
- }
-}
diff --git a/Taler/Views/WithdrawView.swift b/Taler/Views/WithdrawView.swift
deleted file mode 100644
index 946249e..0000000
--- a/Taler/Views/WithdrawView.swift
+++ /dev/null
@@ -1,142 +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 TransferView: View {
- @ObservedObject var model: ManualTransferModel
-
- var body: some View {
- VStack {
- Text("Exchange is ready for withdrawal!")
- Text("To complete the process you need to wire
\(self.model.details.amountRaw.readableDescription) to the exchange bank
account.")
- HStack {
- Text("IBAN: ")
- Text(paytoUriGetIban(uri: self.model.paytoUri))
- }
- HStack {
- Text("Subject: ")
- Text(paytoUriGetSubject(uri: self.model.paytoUri))
- }
- HStack {
- Text("Chosen Amount: ")
- Text(self.model.details.amountRaw.readableDescription)
- }
- HStack {
- Text("Exchange: ")
- Text(self.model.exchange.exchangeBaseUrl)
- }
- Text("Make sure to use the correct subject, otherwise the money
will not arrive in this wallet.")
- }
- .navigationTitle("Manual Transfer")
- }
-}
-
-struct PromptWithdrawView: View {
- @ObservedObject var model: PromptWithdrawModel
-
- var body: some View {
- VStack {
- NavigationLink("", isActive: $model.nav) {
- TransferView(model: model.manualTransferModel)
- .onDisappear {
- self.model.nav = false
- }
- }
- if model.loading {
- ProgressView()
- } else {
- if model.tosAccepted {
- Text("Withdraw")
-
Text(self.model.details.amountEffective.readableDescription)
- Text("Chosen Amount")
- Text(self.model.details.amountRaw.readableDescription)
- Text("Fee")
- Text("- \((try! self.model.details.amountRaw -
self.model.details.amountEffective).readableDescription)")
- Text("Exchange")
- Text(model.exchange.name)
- Button {
- self.model.acceptWithdraw()
- } label: {
- Text("Confirm Withdraw")
- }
- } else {
- Text("Withdraw")
-
Text(self.model.details.amountEffective.readableDescription)
- Text("Chosen Amount")
- Text(self.model.details.amountRaw.readableDescription)
- Text("Fee")
- Text("- \((try! self.model.details.amountRaw -
self.model.details.amountEffective).readableDescription)")
- Text("Exchange")
- Text(model.exchange.name)
- NavigationLink(isActive: $model.navTos) {
- VStack {
- ScrollView {
- Text(model.tosDetails!.content)
- }
- Button {
- model.acceptTos()
- } label: {
- Text("Accept Terms of Service")
- }
- }
- .navigationTitle("Review Terms of Service")
- } label: {
- Text("Review Terms")
- }
- }
- }
- }
- .navigationTitle("Review Withdraw")
- }
-}
-
-struct WithdrawView: View {
- @ObservedObject var model: WithdrawModel
- @State var amount: String = ""
-
- var body: some View {
- VStack {
- NavigationLink("", isActive: $model.nav) {
- PromptWithdrawView(model: model.promptModel)
- .onDisappear {
- self.model.nav = false
- }
- }
- if self.model.loading {
- ProgressView()
- } else {
- Button {
-
- } label: {
- Text("Scan Taler QR Code")
- }
- Text("Or transfer manually:")
- HStack {
- TextField(model.exchange.currency, text: $amount)
- }
- Button {
- // TODO: Handle when the user inputs a non-valid amount
- model.getWithdrawDetails(amountStr:
model.exchange.currency + ":" + amount)
- } label: {
- Text("Check Fees")
- }
- }
- }
- .navigationTitle("Withdraw")
- }
-}
diff --git a/Taler/WalletBackend.swift b/Taler/WalletBackend.swift
deleted file mode 100644
index e6df597..0000000
--- a/Taler/WalletBackend.swift
+++ /dev/null
@@ -1,1167 +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 iono
-import taler_swift
-import AnyCodable
-
-/// Information supplied by the backend describing an error.
-struct WalletBackendResponseError: Decodable {
- /// Numeric error code defined defined in the GANA gnu-taler-error-codes
registry.
- var talerErrorCode: Int
-
- /// English description of the error code.
- var talerErrorHint: String
-
- /// English diagnostic message that can give details for the instance of
the error.
- var message: String
-
- /// Error details, type depends on `talerErrorCode`.
- var details: Data?
-}
-
-/// A request sent to the wallet backend.
-struct WalletBackendRequest: Encodable {
- /// The operation name of the request.
- var operation: String
-
- /// The body of the request as JSON.
- var args: AnyEncodable
-}
-
-protocol WalletBackendFormattedRequest {
- associatedtype Args: Encodable
- associatedtype Response: Decodable
-
- func operation() -> String
- func args() -> Args
-}
-
-fileprivate struct WalletBackendInitRequest: WalletBackendFormattedRequest {
- var persistentStoragePath: String
-
- struct Args: Encodable {
- var persistentStoragePath: String
- }
-
- struct Response: Codable {
- struct SupportedProtocolVersions: Codable {
- var exchange: String
- var merchant: String
- }
- var supportedProtocolVersions: SupportedProtocolVersions
- enum CodingKeys: String, CodingKey {
- case supportedProtocolVersions = "supported_protocol_versions"
- }
- }
-
- func operation() -> String {
- return "init"
- }
-
- func args() -> Args {
- return Args(persistentStoragePath: persistentStoragePath)
- }
-}
-
-/// An balance on a wallet.
-struct Balance: Decodable {
- var available: Amount
- var pendingIncoming: Amount
- var pendingOutgoing: Amount
- var requiresUserInput: Bool
-}
-
-/// A request to get the balances held in the wallet.
-struct WalletBackendGetBalancesRequest: WalletBackendFormattedRequest {
- struct Args: Encodable {
-
- }
-
- struct Response: Decodable {
- var balances: [Balance]
- }
-
- func operation() -> String {
- return "getBalances"
- }
-
- func args() -> Args {
- return Args()
- }
-}
-
-/// A billing or mailing location.
-struct Location: Codable {
- var country: String?
- var country_subdivision: String?
- var district: String?
- var town: String?
- var town_location: String?
- var post_code: String?
- var street: String?
- var building_name: String?
- var building_number: String?
- var address_lines: [String]?
-}
-
-/// Information identifying a merchant.
-struct Merchant: Codable {
- var name: String
- var address: Location?
- var jurisdiction: Location?
-}
-
-/// A tax made on a payment.
-struct Tax: Codable {
- var name: String
- var tax: Amount
-}
-
-/// A product being purchased from a merchant.
-struct Product: Codable {
- var product_id: String?
- var description: String
- // description_i18n?
- var quantity: Int
- var unit: String
- var price: Amount?
- var image: String // URL to a product image
- var taxes: [Tax]?
- var delivery_date: Timestamp?
-}
-
-/// Brief information about an order.
-struct OrderShortInfo: Codable {
- var orderId: String
- var merchant: Merchant
- var summary: String
- // summary_i18n?
- var products: [Product]
- var fulfillmentUrl: String?
- var fulfillmentMessage: String?
- // fulfillmentMessage_i18n?
-}
-
-enum TransactionTypeError: Error {
- case unknownTypeError
-}
-
-/// Different types of transactions.
-enum TransactionType: Codable {
- case withdrawal
- case payment
- case refund
- case tip
- case refresh
-
- init(from decoder: Decoder) throws {
- let value = try decoder.singleValueContainer()
- let str = try value.decode(String.self)
- let codingNames = [
- "TransactionWithdrawal" : TransactionType.withdrawal,
- "TransactionPayment" : TransactionType.payment,
- "TransactionRefund" : TransactionType.refund,
- "TransactionTip" : TransactionType.tip,
- "TransactionRefresh" : TransactionType.refresh
- ]
- if let type = codingNames[str] {
- self = type
- } else {
- throw TransactionTypeError.unknownTypeError
- }
- }
-
- func encode(to encoder: Encoder) throws {
- var value = encoder.singleValueContainer()
- switch self {
- case .withdrawal:
- try value.encode("TransactionWithdrawal")
- case .payment:
- try value.encode("TransactionPayment")
- case .refund:
- try value.encode("TransactionRefund")
- case .tip:
- try value.encode("TransactionTip")
- case .refresh:
- try value.encode("TransactionRefresh")
- }
- }
-}
-
-enum TransactionDecodingError: Error {
- case invalidStringValue
-}
-
-/// Details for a manual withdrawal.
-struct ManualWithdrawalDetails: Codable {
- /// The payto URIs that the exchange supports.
- var exchangePaytoUris: [String]
-
- /// The public key of the newly created reserve.
- var reservePub: String
-}
-
-/// Details for a bank-integrated withdrawal.
-struct BankIntegratedWithdrawalDetails: Codable {
- /// Whether the bank has confirmed the withdrawal.
- var confirmed: Bool
-
- /// URL for user-initiated confirmation
- var bankConfirmationUrl: String?
-}
-
-/// A withdrawal transaction.
-struct TransactionWithdrawal: Decodable {
- enum WithdrawalDetails {
- case manual(ManualWithdrawalDetails)
- case bankIntegrated(BankIntegratedWithdrawalDetails)
- }
-
- /// The exchange that was withdrawn from.
- var exchangeBaseUrl: String
-
- /// The amount of the withdrawal, including fees.
- var amountRaw: Amount
-
- /// The amount that will be added to the withdrawer's account.
- var amountEffective: Amount
-
- /// The details of the withdrawal.
- var withdrawalDetails: WithdrawalDetails
-
- init(from decoder: Decoder) throws {
- enum CodingKeys: String, CodingKey {
- case exchangeBaseUrl
- case amountRaw
- case amountEffective
- case withdrawalDetails
- case type
- case exchangePaytoUris
- case reservePub
- case confirmed
- case bankConfirmationUrl
- }
-
- let value = try decoder.container(keyedBy: CodingKeys.self)
- self.exchangeBaseUrl = try value.decode(String.self, forKey:
.exchangeBaseUrl)
- self.amountRaw = try value.decode(Amount.self, forKey: .amountRaw)
- self.amountEffective = try value.decode(Amount.self, forKey:
.amountEffective)
-
- let detail = try value.nestedContainer(keyedBy: CodingKeys.self,
forKey: .withdrawalDetails)
- let detailType = try detail.decode(String.self, forKey: .type)
- if detailType == "manual-transfer" {
- let paytoUris = try detail.decode([String].self, forKey:
.exchangePaytoUris)
- let reservePub = try detail.decode(String.self, forKey:
.reservePub)
- let manual = ManualWithdrawalDetails(exchangePaytoUris: paytoUris,
reservePub: reservePub)
- self.withdrawalDetails = .manual(manual)
- } else if detailType == "taler-bank-integration-api" {
- let confirmed = try detail.decode(Bool.self, forKey: .confirmed)
- var bankConfirmationUrl: String? = nil
- if detail.contains(.bankConfirmationUrl) {
- bankConfirmationUrl = try detail.decode(String.self, forKey:
.bankConfirmationUrl)
- }
- let bankDetails = BankIntegratedWithdrawalDetails(confirmed:
confirmed, bankConfirmationUrl: bankConfirmationUrl)
- self.withdrawalDetails = .bankIntegrated(bankDetails)
- } else {
- throw TransactionDecodingError.invalidStringValue
- }
- }
-}
-
-/// A payment transaction.
-struct TransactionPayment: Codable {
- /// Additional information about the payment.
- // TODO
-
- /// An identifier for the payment.
- var proposalId: String
-
- /// The current status of the payment.
- // TODO
-
- /// The amount that must be paid.
- var amountRaw: Amount
-
- /// The amount that was paid.
- var amountEffective: Amount
-}
-
-/// A refund transaction.
-struct TransactionRefund: Codable {
- /// Identifier for the refund.
- var refundedTransactionId: String
-
- /// Additional information about the refund
- // TODO
-
- /// The amount that couldn't be applied because refund permissions expired.
- var amountInvalid: Amount
-
- /// The amount refunded by the merchant.
- var amountRaw: Amount
-
- /// The amount paid to the wallet after fees.
- var amountEffective: Amount
-}
-
-/// A tip transaction.
-struct TransactionTip: Codable {
- /// The current status of the tip.
- // TODO
-
- /// The exchange that the tip will be withdrawn from
- var exchangeBaseUrl: String
-
- /// More information about the merchant sending the tip.
- // TODO
-
- /// The raw amount of the tip without fees.
- var amountRaw: Amount
-
- /// The amount added to the recipient's wallet.
- var amountEffective: Amount
-}
-
-/// A refresh transaction.
-struct TransactionRefresh: Codable {
- /// The exchange that the coins are refreshed with.
- var exchangeBaseUrl: String
-
- /// The raw amount to refresh.
- var amountRaw: Amount
-
- /// The amount to be paid as fees for the refresh.
- var amountEffective: Amount
-}
-
-/// A wallet transaction.
-struct Transaction: Decodable, Hashable {
- enum TransactionDetail {
- case withdrawal(TransactionWithdrawal)
- }
-
- var transactionId: String
- var timestamp: Timestamp
- var pending: Bool
- var error: AnyCodable?
- var amountRaw: Amount
- var amountEffective: Amount
- var detail: TransactionDetail
-
- init(from decoder: Decoder) throws {
- enum CodingKeys: String, CodingKey {
- case transactionId
- case timestamp
- case pending
- case error
- case amountRaw
- case amountEffective
- case type
- }
-
- let value = try decoder.container(keyedBy: CodingKeys.self)
- self.transactionId = try value.decode(String.self, forKey:
.transactionId)
- self.timestamp = try value.decode(Timestamp.self, forKey: .timestamp)
- self.pending = try value.decode(Bool.self, forKey: .pending)
- if value.contains(.error) {
- self.error = try value.decode(AnyCodable.self, forKey: .error)
- }
- self.amountRaw = try value.decode(Amount.self, forKey: .amountRaw)
- self.amountEffective = try value.decode(Amount.self, forKey:
.amountEffective)
-
- let type = try value.decode(String.self, forKey: .type)
- if type == "withdrawal" {
- let withdrawDetail = try TransactionWithdrawal.init(from: decoder)
- self.detail = .withdrawal(withdrawDetail)
- } else {
- throw TransactionDecodingError.invalidStringValue
- }
- }
-
- static func == (lhs: Transaction, rhs: Transaction) -> Bool {
- return lhs.transactionId == rhs.transactionId
- }
-
- func hash(into hasher: inout Hasher) {
- transactionId.hash(into: &hasher)
- }
-}
-
-/// A request to get the transactions in the wallet's history.
-struct WalletBackendGetTransactionsRequest: WalletBackendFormattedRequest {
- var currency: String?
- var search: String?
-
- struct Args: Encodable {
- var currency: String?
- var search: String?
- }
-
- struct Response: Decodable {
- var transactions: [Transaction]
- }
-
- func operation() -> String {
- return "getTransactions"
- }
-
- func args() -> Args {
- return Args(currency: currency, search: search)
- }
-}
-
-/// 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)
- }
-}
-
-/// A request to process a refund.
-struct WalletBackendApplyRefundRequest: WalletBackendFormattedRequest {
- var talerRefundUri: String
-
- struct Args: Encodable {
- var talerRefundUri: String
- }
-
- struct Response: Decodable {
- var contractTermsHash: String
- var amountEffectivePaid: Amount
- var amountRefundGranted: Amount
- var amountRefundGone: Amount
- var pendingAtExchange: Bool
- var info: OrderShortInfo
- }
-
- func operation() -> String {
- return "applyRefund"
- }
-
- func args() -> Args {
- return Args(talerRefundUri: talerRefundUri)
- }
-}
-
-/// A request to list exchanges.
-struct WalletBackendListExchanges: WalletBackendFormattedRequest {
- struct Args: Encodable {
-
- }
-
- struct ExchangeListItem: Decodable, Hashable {
- var exchangeBaseUrl: String
- var currency: String
- var paytoUris: [String]
-
- var name: String {
- let url = URL(string: exchangeBaseUrl)!
- return url.host!
- }
- }
-
- struct Response: Decodable {
- var exchanges: [ExchangeListItem]
- }
-
- func operation() -> String {
- return "listExchanges"
- }
-
- func args() -> Args {
- return Args()
- }
-}
-
-/// A request to add an exchange.
-struct WalletBackendAddExchangeRequest: WalletBackendFormattedRequest {
- var exchangeBaseUrl: String
-
- struct Args: Encodable {
- var exchangeBaseUrl: String
- }
-
- struct Response: Decodable {
-
- }
-
- func operation() -> String {
- return "addExchange"
- }
-
- func args() -> Args {
- return Args(exchangeBaseUrl: exchangeBaseUrl)
- }
-}
-
-/// A request to force update an exchange.
-struct WalletBackendForceUpdateRequest: WalletBackendFormattedRequest {
- var exchangeBaseUrl: String
-
- struct Args: Encodable {
- var exchangeBaseUrl: String
- }
-
- struct Response: Decodable {
-
- }
-
- func operation() -> String {
- return "addRequest"
- }
-
- func args() -> Args {
- return Args(exchangeBaseUrl: exchangeBaseUrl)
- }
-}
-
-/// A request to query an exchange's terms of service.
-struct WalletBackendGetExchangeTermsOfService: WalletBackendFormattedRequest {
- var exchangeBaseUrl: String
-
- struct Args: Encodable {
- var exchangeBaseUrl: String
- }
-
- struct Response: Decodable {
- var content: String
- var currentEtag: String
- var acceptedEtag: String?
- }
-
- func operation() -> String {
- return "getExchangeTos"
- }
-
- func args() -> Args {
- return Args(exchangeBaseUrl: exchangeBaseUrl)
- }
-}
-
-/// A request to mark an exchange's terms of service as accepted.
-struct WalletBackendSetExchangeTermsOfServiceAccepted:
WalletBackendFormattedRequest {
- var exchangeBaseUrl: String
- var etag: String
-
- struct Args: Encodable {
- var exchangeBaseUrl: String
- var etag: String
- }
-
- struct Response: Decodable {
-
- }
-
- func operation() -> String {
- return "setExchangeTosAccepted"
- }
-
- func args() -> Args {
- return Args(exchangeBaseUrl: exchangeBaseUrl, etag: etag)
- }
-}
-
-struct ExchangeListItem: Codable {
- var exchangeBaseUrl: String
- var currency: String
- var paytoUris: [String]
-}
-
-/// A request to get an exchange's withdrawal details.
-struct WalletBackendGetWithdrawalDetailsForURIRequest:
WalletBackendFormattedRequest {
- var talerWithdrawUri: String
-
- struct Args: Encodable {
- var talerWithdrawUri: String
- }
-
- struct Response: Decodable {
- var amount: Amount
- var defaultExchangeBaseUrl: String?
- var possibleExchanges: [ExchangeListItem]
- }
-
- func operation() -> String {
- return "getWithdrawalDetailsForUri"
- }
-
- func args() -> Args {
- return Args(talerWithdrawUri: talerWithdrawUri)
- }
-}
-
-/// A request to get an exchange's withdrawal details.
-struct WalletBackendGetWithdrawalDetailsForAmountRequest:
WalletBackendFormattedRequest {
- var exchangeBaseUrl: String
- var amount: Amount
-
- struct Args: Encodable {
- var exchangeBaseUrl: String
- var amount: Amount
- }
-
- struct Response: Decodable {
- var tosAccepted: Bool
- var amountRaw: Amount
- var amountEffective: Amount
- }
-
- func operation() -> String {
- return "getWithdrawalDetailsForAmount"
- }
-
- func args() -> Args {
- return Args(exchangeBaseUrl: exchangeBaseUrl, amount: amount)
- }
-}
-
-/// 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
- var amount: Amount
-
- struct Args: Encodable {
- var depositPayToUri: String
- var amount: Amount
- }
-
- struct Response: Decodable {
- var depositGroupId: String
- }
-
- func operation() -> String {
- return "createDepositGroup"
- }
-
- func args() -> Args {
- return Args(depositPayToUri: depositePayToUri, amount: amount)
- }
-}
-
-/// A request to get information about a payment request.
-struct WalletBackendPreparePayRequest: WalletBackendFormattedRequest {
- var talerPayUri: String
-
- struct Args: Encodable {
- var talerPayUri: String
- }
-
- struct Response: Decodable {
-
- }
-
- func operation() -> String {
- return "preparePay"
- }
-
- func args() -> Args {
- return Args(talerPayUri: talerPayUri)
- }
-}
-
-/// A request to confirm a payment.
-struct WalletBackendConfirmPayRequest: WalletBackendFormattedRequest {
- var proposalId: String
-
- struct Args: Encodable {
- var proposalId: String
- }
-
- struct Response: Decodable {
-
- }
-
- func operation() -> String {
- return "abortFailedPayWithRefund"
- }
-
- func args() -> Args {
- return Args(proposalId: proposalId)
- }
-}
-
-/// 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)
- }
-}
-
-/// A request to accept a tip.
-struct WalletBackendAcceptTipRequest: WalletBackendFormattedRequest {
- var walletTipId: String
-
- struct Args: Encodable {
- var walletTipId: String
- }
-
- struct Response: Decodable {
-
- }
-
- func operation() -> String {
- return "acceptTip"
- }
-
- func args() -> Args {
- return Args(walletTipId: walletTipId)
- }
-}
-
-/// A request to abort a failed payment.
-struct WalletBackendAbortFailedPaymentRequest: WalletBackendFormattedRequest {
- var proposalId: String
-
- struct Args: Encodable {
- var proposalId: String
- }
-
- struct Response: Decodable {
-
- }
-
- func operation() -> String {
- return "confirmPay"
- }
-
- func args() -> Args {
- return Args(proposalId: proposalId)
- }
-}
-
-/// A request to withdraw a balance from the TESTKUDOS environment.
-struct WalletBackendWithdrawTestkudosRequest: WalletBackendFormattedRequest {
- struct Args: Encodable {
-
- }
-
- struct Response: Decodable {
-
- }
-
- func operation() -> String {
- return "withdrawTestkudos"
- }
-
- func args() -> Args {
- return Args()
- }
-}
-
-/// A request to add a test balance to the wallet.
-struct WalletBackendWithdrawTestBalance: WalletBackendFormattedRequest {
- var amount: Amount
- var bankBaseUrl: String
- var exchangeBaseUrl: String
-
- struct Args: Encodable {
- var amount: Amount
- var bankBaseUrl: String
- var exchangeBaseUrl: String
- }
- typealias Response = String
-
- func operation() -> String {
- return "withdrawTestBalance"
- }
-
- func args() -> Args {
- return Args(amount: amount, bankBaseUrl: bankBaseUrl, exchangeBaseUrl:
exchangeBaseUrl)
- }
-}
-
-struct IntegrationTestArgs: Codable {
- var exchangeBaseUrl: String
- var bankBaseUrl: String
- var merchantBaseUrl: String
- var merchantApiKey: String
- var amountToWithdraw: String
- var amountToSpend: String
-}
-
-/// A request to run a basic integration test.
-struct WalletBackendRunIntegrationTestRequest: WalletBackendFormattedRequest {
- var integrationTestArgs: IntegrationTestArgs
-
- typealias Args = IntegrationTestArgs
-
- struct Response: Decodable {
-
- }
-
- func operation() -> String {
- return "runIntegrationTest"
- }
-
- func args() -> Args {
- return integrationTestArgs
- }
-}
-
-struct TestPayArgs: Codable {
- var merchantBaseUrl: String
- var merchantApiKey: String
- var amount: String
- var summary: String
-}
-
-/// A request to make a test payment.
-struct WalletBackendTestPayRequest: WalletBackendFormattedRequest {
- var testPayArgs: TestPayArgs
-
- typealias Args = TestPayArgs
-
- struct Response: Decodable {
-
- }
-
- func operation() -> String {
- return "testPay"
- }
-
- func args() -> Args {
- return testPayArgs
- }
-}
-
-struct Coin: Codable {
- var denom_pub: String
- var denom_pub_hash: String
- var denom_value: String
- var coin_pub: String
- var exchange_base_url: String
- var remaining_value: String
- var refresh_parent_coin_pub: String
- var withdrawal_reserve_pub: String
- var coin_suspended: Bool
-}
-
-/// A request to dump all coins to JSON.
-struct WalletBackendDumpCoinsRequest: WalletBackendFormattedRequest {
- struct Args: Encodable {
-
- }
-
- struct Response: Decodable {
- var coins: [Coin]
- }
-
- func operation() -> String {
- return "dumpCoins"
- }
-
- func args() -> Args {
- return Args()
- }
-}
-
-/// A request to suspend or unsuspend a coin.
-struct WalletBackendSuspendCoinRequest: WalletBackendFormattedRequest {
- var coinPub: String
- var suspended: Bool
-
- struct Args: Encodable {
- var coinPub: String
- var suspended: Bool
- }
-
- struct Response: Decodable {
-
- }
-
- func operation() -> String {
- return "setCoinSuspended"
- }
-
- func args() -> Args {
- return Args(coinPub: coinPub, suspended: suspended)
- }
-}
-
-typealias PendingOperation = AnyCodable
-
-/// A request to list the backend's currently pending operations.
-struct WalletBackendPendingRequest: WalletBackendFormattedRequest {
- struct Args: Encodable {
-
- }
-
- struct Response: Decodable {
- var pendingOperations: [PendingOperation]
- }
-
- func operation() -> String {
- return "getPendingOperations"
- }
-
- func args() -> Args {
- Args()
- }
-}
-
-/// Errors for `WalletBackend`.
-enum WalletBackendError: Error {
- /// An error that prevented the wallet from being initialized occurred.
- case initializationError
- case serializationError
- case deserializationError
-}
-
-/// Delegate for the wallet backend.
-protocol WalletBackendDelegate {
- /// Called when the backend interface receives a message it does not know
how to handle.
- func walletBackendReceivedUnknownMessage(_ walletBackend: WalletBackend,
message: String)
-}
-
-/// An interface to the wallet backend.
-class WalletBackend: IonoMessageHandler {
- private var iono: Iono
- private var requestsMade: UInt
- private var backendReady: Bool
- private var backendReadyCondition: NSCondition
- private var requests: [UInt : (AnyCodable?, WalletBackendResponseError?)
-> Void] = [:]
- var delegate: WalletBackendDelegate?
-
- private struct FullRequest: Encodable {
- let operation: String
- let id: UInt
- let args: AnyEncodable
- }
-
- private struct FullResponse: Decodable {
- let type: String
- let operation: String
- let id: UInt
- let result: AnyCodable
- }
-
- private struct FullError: Decodable {
- let type: String
- let operation: String
- let id: UInt
- let error: WalletBackendResponseError
- }
-
- init() throws {
- iono = Iono()
- requestsMade = 0
- self.backendReady = false
- self.backendReadyCondition = NSCondition()
-
- iono.messageHandler = self
-
- let js_path = URL(fileURLWithPath: Bundle.main.path(forResource:
"taler-wallet-embedded", ofType: "js")!)
- do {
- let js = try String(contentsOf: js_path, encoding: .utf8)
- iono.putModuleCode(modName: "@gnu-taler/taler-wallet-embedded",
code: js)
- iono.evalNodeCode(source: "require('iono');")
- iono.evalNodeCode(source: "tw =
require('@gnu-taler/taler-wallet-embedded');")
- iono.evalNodeCode(source: "tw.installNativeWalletListener();")
- } catch {
- throw WalletBackendError.initializationError
- }
-
- // Send the init message
- let documentUrls = FileManager.default.urls(for: .documentDirectory,
in: .userDomainMask)
- if (documentUrls.count > 0) {
- var storageDir = documentUrls[0]
- storageDir.appendPathComponent("talerwalletdb-v30", isDirectory:
false)
- storageDir.appendPathExtension("json")
- sendFormattedRequest(request:
WalletBackendInitRequest(persistentStoragePath: storageDir.path),
completionHandler: { (resp: WalletBackendInitRequest.Response?, err:
WalletBackendResponseError?) in
- self.backendReady = true
- self.backendReadyCondition.broadcast()
- })
- }
-
- waitUntilReady()
- }
-
- deinit {
- iono.waitStopped()
- }
-
- func waitUntilReady() {
- backendReadyCondition.lock()
- while (!self.backendReady) {
- backendReadyCondition.wait()
- }
- backendReadyCondition.unlock()
- }
-
- func handleMessage(message: String) {
- print(message)
- do {
- guard let messageData = message.data(using: .utf8) else { throw
WalletBackendError.deserializationError }
- let data = try JSONSerialization.jsonObject(with: messageData,
options: .allowFragments) as? [String : Any]
- if let responseData = data {
- let type = (responseData["type"] as? String) ?? ""
- if type == "response" {
- guard let id = responseData["id"] as? UInt else { throw
WalletBackendError.deserializationError }
- guard let request = requests[id] else { throw
WalletBackendError.deserializationError }
- do {
- let decoded = try
JSONDecoder().decode(FullResponse.self, from: messageData)
- request(decoded.result, nil)
- } catch {
- request(nil, WalletBackend.parseResponseError())
- }
- requests[id] = nil
- } else if type == "tunnelHttp" {
- // TODO: Handle
- } else if type == "notification" {
- // TODO: Handle
- } else if type == "error" {
- guard let id = responseData["id"] as? UInt else { throw
WalletBackendError.deserializationError }
- guard let request = requests[id] else { throw
WalletBackendError.deserializationError }
- do {
- let decoded = try JSONDecoder().decode(FullError.self,
from: messageData)
- request(nil, decoded.error)
- } catch {
- request(nil, WalletBackend.parseFailureError())
- }
- requests[id] = nil
- } else {
- throw WalletBackendError.deserializationError
- }
- }
- } catch {
- self.delegate?.walletBackendReceivedUnknownMessage(self, message:
message)
- }
- }
-
- func sendRequest(request: WalletBackendRequest, completionHandler:
@escaping (AnyCodable?, WalletBackendResponseError?) -> Void) {
- /* Encode the request and send it to the backend. */
- do {
- let full = FullRequest(operation: request.operation, id:
requestsMade, args: request.args)
- let encoded = try JSONEncoder().encode(full)
- guard let jsonString = String(data: encoded, encoding: .utf8) else
{ throw WalletBackendError.serializationError }
- requests[full.id] = completionHandler
- requestsMade += 1
- iono.sendMessage(message: jsonString)
- } catch {
- completionHandler(nil, WalletBackend.serializeRequestError());
- }
- }
-
- func sendFormattedRequest<T: WalletBackendFormattedRequest>(request: T,
completionHandler: @escaping (T.Response?, WalletBackendResponseError?) ->
Void) {
- let reqData = WalletBackendRequest(operation: request.operation(),
args: AnyEncodable(request.args()))
- sendRequest(request: reqData) { (result: AnyCodable?, err:
WalletBackendResponseError?) in
- if let res = result {
- do {
- /* TODO: Don't use a hack (there is no reason to pass to
JSON): */
- let jsonStr = try JSONEncoder().encode(res)
- let decoded = try JSONDecoder().decode(T.Response.self,
from: jsonStr)
- completionHandler(decoded, err)
- } catch {
- completionHandler(nil, WalletBackend.parseResponseError())
- }
- } else {
- completionHandler(nil, err)
- }
- }
- }
-
- static func serializeRequestError() -> WalletBackendResponseError {
- return WalletBackendResponseError(talerErrorCode: -1, talerErrorHint:
"Could not serialize request.", message: "")
- }
-
- static func parseResponseError() -> WalletBackendResponseError {
- return WalletBackendResponseError(talerErrorCode: -2, talerErrorHint:
"Could not parse response.", message: "")
- }
-
- static func parseFailureError() -> WalletBackendResponseError {
- return WalletBackendResponseError(talerErrorCode: -3, talerErrorHint:
"Could not parse error detail.", message: "")
- }
-}
diff --git a/TalerWallet1/Backend/Transaction.swift
b/TalerWallet1/Backend/Transaction.swift
new file mode 100644
index 0000000..86a0ae9
--- /dev/null
+++ b/TalerWallet1/Backend/Transaction.swift
@@ -0,0 +1,314 @@
+/*
+ * 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
+
+enum TransactionTypeError: Error {
+ case unknownTypeError
+}
+
+/// Different types of transactions.
+enum TransactionType: Codable {
+ case withdrawal
+ case payment
+ case refund
+ case tip
+ case refresh
+
+ init(from decoder: Decoder) throws {
+ let value = try decoder.singleValueContainer()
+ let str = try value.decode(String.self)
+ let codingNames = [
+ "TransactionWithdrawal" : TransactionType.withdrawal,
+ "TransactionPayment" : TransactionType.payment,
+ "TransactionRefund" : TransactionType.refund,
+ "TransactionTip" : TransactionType.tip,
+ "TransactionRefresh" : TransactionType.refresh
+ ]
+ if let type = codingNames[str] {
+ self = type
+ } else {
+ throw TransactionTypeError.unknownTypeError
+ }
+ }
+
+ func encode(to encoder: Encoder) throws {
+ var value = encoder.singleValueContainer()
+ switch self {
+ case .withdrawal:
+ try value.encode("TransactionWithdrawal")
+ case .payment:
+ try value.encode("TransactionPayment")
+ case .refund:
+ try value.encode("TransactionRefund")
+ case .tip:
+ try value.encode("TransactionTip")
+ case .refresh:
+ try value.encode("TransactionRefresh")
+ }
+ }
+}
+
+enum TransactionDecodingError: Error {
+ case invalidStringValue
+}
+
+/// Details for a manual withdrawal.
+struct ManualWithdrawalDetails: Codable {
+ /// The payto URIs that the exchange supports.
+ var exchangePaytoUris: [String]
+
+ /// The public key of the newly created reserve.
+ var reservePub: String
+}
+
+/// Details for a bank-integrated withdrawal.
+struct BankIntegratedWithdrawalDetails: Codable {
+ /// Whether the bank has confirmed the withdrawal.
+ var confirmed: Bool
+
+ /// The public key of the newly created reserve.
+ var reservePub: String?
+
+ /// URL for user-initiated confirmation
+ var bankConfirmationUrl: String?
+}
+
+/// A withdrawal transaction.
+struct TransactionWithdrawal: Decodable {
+ enum WithdrawalDetails {
+ case manual(ManualWithdrawalDetails)
+ case bankIntegrated(BankIntegratedWithdrawalDetails)
+ }
+
+ /// The exchange that was withdrawn from.
+ var exchangeBaseUrl: String
+
+ /// The amount of the withdrawal, including fees.
+ var amountRaw: Amount
+
+ /// The amount that will be added to the withdrawer's account.
+ var amountEffective: Amount
+
+ /// The details of the withdrawal.
+ var withdrawalDetails: WithdrawalDetails
+
+ init(from decoder: Decoder) throws {
+ enum CodingKeys: String, CodingKey {
+ case exchangeBaseUrl
+ case amountRaw
+ case amountEffective
+ case withdrawalDetails
+ case type
+ case exchangePaytoUris
+ case reservePub
+ case confirmed
+ case bankConfirmationUrl
+ }
+
+ let value = try decoder.container(keyedBy: CodingKeys.self)
+ self.exchangeBaseUrl = try value.decode(String.self, forKey:
.exchangeBaseUrl)
+ self.amountRaw = try value.decode(Amount.self, forKey: .amountRaw)
+ self.amountEffective = try value.decode(Amount.self, forKey:
.amountEffective)
+
+ let detail = try value.nestedContainer(keyedBy: CodingKeys.self,
forKey: .withdrawalDetails)
+ let detailType = try detail.decode(String.self, forKey: .type)
+ if detailType == "manual-transfer" {
+ let paytoUris = try detail.decode([String].self, forKey:
.exchangePaytoUris)
+ let reservePub = try detail.decode(String.self, forKey:
.reservePub)
+ let manual = ManualWithdrawalDetails(exchangePaytoUris: paytoUris,
reservePub: reservePub)
+ self.withdrawalDetails = .manual(manual)
+ } else if detailType == "taler-bank-integration-api" {
+ let confirmed = try detail.decode(Bool.self, forKey: .confirmed)
+ var bankConfirmationUrl: String? = nil
+ if detail.contains(.bankConfirmationUrl) {
+ bankConfirmationUrl = try detail.decode(String.self, forKey:
.bankConfirmationUrl)
+ }
+ var reservePub : String? = nil
+ if detail.contains(.reservePub) {
+ reservePub = try detail.decode(String.self, forKey:
.reservePub)
+ }
+ let bankDetails = BankIntegratedWithdrawalDetails(confirmed:
confirmed, reservePub: reservePub,
+
bankConfirmationUrl: bankConfirmationUrl)
+ self.withdrawalDetails = .bankIntegrated(bankDetails)
+ } else {
+ throw TransactionDecodingError.invalidStringValue
+ }
+ }
+}
+#if DEBUG
+extension TransactionWithdrawal { // for PreViews
+ init(url: String) {
+ self.exchangeBaseUrl = url
+ self.amountRaw = try! Amount(fromString: "Taler:5")
+ self.amountEffective = try! Amount(fromString: "Taler:4.8")
+ let bankDetails = BankIntegratedWithdrawalDetails(confirmed: true,
reservePub: nil,
+ bankConfirmationUrl:
nil)
+ self.withdrawalDetails = .bankIntegrated(bankDetails)
+ }
+}
+#endif
+
+/// A payment transaction.
+struct TransactionPayment: Codable {
+ /// Additional information about the payment.
+ // TODO
+
+ /// An identifier for the payment.
+ var proposalId: String
+
+ /// The current status of the payment.
+ // TODO
+
+ /// The amount that must be paid.
+ var amountRaw: Amount
+
+ /// The amount that was paid.
+ var amountEffective: Amount
+}
+
+/// A refund transaction.
+struct TransactionRefund: Codable {
+ /// Identifier for the refund.
+ var refundedTransactionId: String
+
+ /// Additional information about the refund
+ // TODO
+
+ /// The amount that couldn't be applied because refund permissions expired.
+ var amountInvalid: Amount
+
+ /// The amount refunded by the merchant.
+ var amountRaw: Amount
+
+ /// The amount paid to the wallet after fees.
+ var amountEffective: Amount
+}
+
+/// A tip transaction.
+struct TransactionTip: Codable {
+ /// The current status of the tip.
+ // TODO
+
+ /// The exchange that the tip will be withdrawn from
+ var exchangeBaseUrl: String
+
+ /// More information about the merchant sending the tip.
+ // TODO
+
+ /// The raw amount of the tip without fees.
+ var amountRaw: Amount
+
+ /// The amount added to the recipient's wallet.
+ var amountEffective: Amount
+}
+
+/// A refresh transaction.
+struct TransactionRefresh: Codable {
+ /// The exchange that the coins are refreshed with.
+ var exchangeBaseUrl: String
+
+ /// The raw amount to refresh.
+ var amountRaw: Amount
+
+ /// The amount to be paid as fees for the refresh.
+ var amountEffective: Amount
+}
+
+/// A wallet transaction.
+struct Transaction: Decodable, Hashable {
+// private let symLog = SymLogC(0)
+
+ var type: String
+ var amountRaw: Amount
+ var amountEffective: Amount
+ var transactionId: String
+ var timestamp: Timestamp
+ var extendedStatus: String
+ var pending: Bool
+ var frozen: Bool
+
+ var error: AnyCodable?
+ var exchangeBaseUrl: String?
+
+
+
+// enum TransactionDetail {
+// case withdrawal(TransactionWithdrawal)
+// }
+
+// var detail: TransactionDetail
+
+// init(from decoder: Decoder) throws {
+// enum CodingKeys: String, CodingKey {
+// case transactionId
+// case timestamp
+// case pending
+// case error
+// case amountRaw
+// case amountEffective
+// case type
+// }
+//
+// let value = try decoder.container(keyedBy: CodingKeys.self)
+// self.transactionId = try value.decode(String.self, forKey:
.transactionId)
+// self.timestamp = try value.decode(Timestamp.self, forKey: .timestamp)
+// self.pending = try value.decode(Bool.self, forKey: .pending)
+// if value.contains(.error) {
+// self.error = try value.decode(AnyCodable.self, forKey: .error)
+// }
+// self.amountRaw = try value.decode(Amount.self, forKey: .amountRaw)
+// self.amountEffective = try value.decode(Amount.self, forKey:
.amountEffective)
+//
+// let type = try value.decode(String.self, forKey: .type)
+// if type == "withdrawal" {
+// let withdrawDetail = try TransactionWithdrawal.init(from:
decoder)
+// self.detail = .withdrawal(withdrawDetail)
+// } else {
+// throw TransactionDecodingError.invalidStringValue
+// }
+// symLog.log("\(self)")
+// }
+
+ static func == (lhs: Transaction, rhs: Transaction) -> Bool {
+ return lhs.transactionId == rhs.transactionId
+ }
+
+ func hash(into hasher: inout Hasher) {
+ transactionId.hash(into: &hasher)
+ }
+}
+
+#if DEBUG
+extension Transaction { // for PreViews
+ init(id: String, time: Timestamp) {
+ self.type = "withdrawal"
+ self.amountRaw = try! Amount(fromString: "Taler:5")
+ self.amountEffective = try! Amount(fromString: "Taler:4.8")
+ self.transactionId = id
+ self.timestamp = time
+ self.extendedStatus = "done"
+ self.pending = false
+ self.frozen = false
+ self.error = nil
+ self.exchangeBaseUrl = "Exchange.Demo.Taler.net"
+// let withdrawDetail = TransactionWithdrawal(url:
"Exchange.Demo.Taler.net")
+// self.detail = .withdrawal(withdrawDetail)
+ }
+}
+#endif
diff --git a/TalerWallet1/Backend/WalletBackendError.swift
b/TalerWallet1/Backend/WalletBackendError.swift
new file mode 100644
index 0000000..cb0bfeb
--- /dev/null
+++ b/TalerWallet1/Backend/WalletBackendError.swift
@@ -0,0 +1,54 @@
+/*
+ * 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
+
+/// Errors for `WalletBackend`.
+enum WalletBackendError: Error {
+ /// An error that prevented the wallet from being initialized occurred.
+ case initializationError
+ case serializationError
+ case deserializationError
+ case walletCoreError
+}
+
+/// Information supplied by the backend describing an error.
+struct WalletBackendResponseError: Decodable {
+ /// Numeric error code defined defined in the GANA gnu-taler-error-codes
registry.
+ var talerErrorCode: Int
+
+ /// English description of the error code.
+ var talerErrorHint: String
+
+ /// English diagnostic message that can give details for the instance of
the error.
+ var message: String
+
+ /// Error details, type depends on `talerErrorCode`.
+ var details: Data?
+}
+
+extension WalletCore {
+ static func serializeRequestError() -> WalletBackendResponseError {
+ return WalletBackendResponseError(talerErrorCode: -1, talerErrorHint:
"Could not serialize request.", message: "")
+ }
+
+ static func parseResponseError() -> WalletBackendResponseError {
+ return WalletBackendResponseError(talerErrorCode: -2, talerErrorHint:
"Could not parse response.", message: "")
+ }
+
+ static func parseFailureError() -> WalletBackendResponseError {
+ return WalletBackendResponseError(talerErrorCode: -3, talerErrorHint:
"Could not parse error detail.", message: "")
+ }
+}
diff --git a/TalerWallet1/Backend/WalletBackendRequest.swift
b/TalerWallet1/Backend/WalletBackendRequest.swift
new file mode 100644
index 0000000..ad48879
--- /dev/null
+++ b/TalerWallet1/Backend/WalletBackendRequest.swift
@@ -0,0 +1,434 @@
+/*
+ * 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
+
+/// A request sent to the wallet backend.
+struct WalletBackendRequest: Encodable {
+ /// The operation name of the request.
+ var operation: String
+
+ /// The body of the request as JSON.
+ var args: AnyEncodable
+}
+
+protocol WalletBackendFormattedRequest {
+ associatedtype Args: Encodable
+ associatedtype Response: Decodable
+
+ func operation() -> String
+ func args() -> Args
+}
+// MARK: -
+
+
+/// A billing or mailing location.
+struct Location: Codable {
+ var country: String?
+ var country_subdivision: String?
+ var district: String?
+ var town: String?
+ var town_location: String?
+ var post_code: String?
+ var street: String?
+ var building_name: String?
+ var building_number: String?
+ var address_lines: [String]?
+}
+
+/// Information identifying a merchant.
+struct Merchant: Codable {
+ var name: String
+ var address: Location?
+ var jurisdiction: Location?
+}
+
+/// A tax made on a payment.
+struct Tax: Codable {
+ var name: String
+ var tax: Amount
+}
+
+/// A product being purchased from a merchant.
+struct Product: Codable {
+ var product_id: String?
+ var description: String
+ // description_i18n?
+ var quantity: Int
+ var unit: String
+ var price: Amount?
+ var image: String // URL to a product image
+ var taxes: [Tax]?
+ var delivery_date: Timestamp?
+}
+
+/// Brief information about an order.
+struct OrderShortInfo: Codable {
+ var orderId: String
+ var merchant: Merchant
+ var summary: String
+ // summary_i18n?
+ var products: [Product]
+ var fulfillmentUrl: String?
+ var fulfillmentMessage: String?
+ // fulfillmentMessage_i18n?
+}
+
+
+/// 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)
+ }
+}
+
+/// A request to process a refund.
+struct WalletBackendApplyRefundRequest: WalletBackendFormattedRequest {
+ var talerRefundUri: String
+
+ struct Args: Encodable {
+ var talerRefundUri: String
+ }
+
+ struct Response: Decodable {
+ var contractTermsHash: String
+ var amountEffectivePaid: Amount
+ var amountRefundGranted: Amount
+ var amountRefundGone: Amount
+ var pendingAtExchange: Bool
+ var info: OrderShortInfo
+ }
+
+ func operation() -> String {
+ return "applyRefund"
+ }
+
+ func args() -> Args {
+ return Args(talerRefundUri: talerRefundUri)
+ }
+}
+
+
+/// A request to force update an exchange.
+struct WalletBackendForceUpdateRequest: WalletBackendFormattedRequest {
+ var exchangeBaseUrl: String
+
+ struct Args: Encodable {
+ var exchangeBaseUrl: String
+ }
+
+ struct Response: Decodable {}
+
+ func operation() -> String {
+ return "addRequest"
+ }
+
+ func args() -> Args {
+ return Args(exchangeBaseUrl: exchangeBaseUrl)
+ }
+}
+
+
+
+
+/// 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
+ var amount: Amount
+
+ struct Args: Encodable {
+ var depositPayToUri: String
+ var amount: Amount
+ }
+
+ struct Response: Decodable {
+ var depositGroupId: String
+ }
+
+ func operation() -> String {
+ return "createDepositGroup"
+ }
+
+ func args() -> Args {
+ return Args(depositPayToUri: depositePayToUri, amount: amount)
+ }
+}
+
+/// A request to get information about a payment request.
+struct WalletBackendPreparePayRequest: WalletBackendFormattedRequest {
+ var talerPayUri: String
+
+ struct Args: Encodable {
+ var talerPayUri: String
+ }
+
+ struct Response: Decodable {}
+
+ func operation() -> String {
+ return "preparePay"
+ }
+
+ func args() -> Args {
+ return Args(talerPayUri: talerPayUri)
+ }
+}
+
+/// A request to confirm a payment.
+struct WalletBackendConfirmPayRequest: WalletBackendFormattedRequest {
+ var proposalId: String
+
+ struct Args: Encodable {
+ var proposalId: String
+ }
+
+ struct Response: Decodable {}
+
+ func operation() -> String {
+ return "confirmPay"
+ }
+
+ func args() -> Args {
+ return Args(proposalId: proposalId)
+ }
+}
+
+/// 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)
+ }
+}
+
+/// A request to accept a tip.
+struct WalletBackendAcceptTipRequest: WalletBackendFormattedRequest {
+ var walletTipId: String
+
+ struct Args: Encodable {
+ var walletTipId: String
+ }
+
+ struct Response: Decodable {}
+
+ func operation() -> String {
+ return "acceptTip"
+ }
+
+ func args() -> Args {
+ return Args(walletTipId: walletTipId)
+ }
+}
+
+/// A request to abort a failed payment.
+struct WalletBackendAbortFailedPaymentRequest: WalletBackendFormattedRequest {
+ var proposalId: String
+
+ struct Args: Encodable {
+ var proposalId: String
+ }
+
+ struct Response: Decodable {}
+
+ func operation() -> String {
+ return "abortFailedPayWithRefund"
+ }
+
+ func args() -> Args {
+ return Args(proposalId: proposalId)
+ }
+}
+
+
+struct IntegrationTestArgs: Codable {
+ var exchangeBaseUrl: String
+ var bankBaseUrl: String
+ var merchantBaseUrl: String
+ var merchantApiKey: String
+ var amountToWithdraw: String
+ var amountToSpend: String
+}
+
+/// A request to run a basic integration test.
+struct WalletBackendRunIntegrationTestRequest: WalletBackendFormattedRequest {
+ var integrationTestArgs: IntegrationTestArgs
+
+ typealias Args = IntegrationTestArgs
+
+ struct Response: Decodable {}
+
+ func operation() -> String {
+ return "runIntegrationTest"
+ }
+
+ func args() -> Args {
+ return integrationTestArgs
+ }
+}
+
+struct TestPayArgs: Codable {
+ var merchantBaseUrl: String
+ var merchantApiKey: String
+ var amount: String
+ var summary: String
+}
+
+/// A request to make a test payment.
+struct WalletBackendTestPayRequest: WalletBackendFormattedRequest {
+ var testPayArgs: TestPayArgs
+
+ typealias Args = TestPayArgs
+
+ struct Response: Decodable {}
+
+ func operation() -> String {
+ return "testPay"
+ }
+
+ func args() -> Args {
+ return testPayArgs
+ }
+}
+
+struct Coin: Codable {
+ var denom_pub: String
+ var denom_pub_hash: String
+ var denom_value: String
+ var coin_pub: String
+ var exchange_base_url: String
+ var remaining_value: String
+ var refresh_parent_coin_pub: String
+ var withdrawal_reserve_pub: String
+ var coin_suspended: Bool
+}
+
+/// A request to dump all coins to JSON.
+struct WalletBackendDumpCoinsRequest: WalletBackendFormattedRequest {
+ struct Args: Encodable {
+
+ }
+
+ struct Response: Decodable {
+ var coins: [Coin]
+ }
+
+ func operation() -> String {
+ return "dumpCoins"
+ }
+
+ func args() -> Args {
+ return Args()
+ }
+}
+
+/// A request to suspend or unsuspend a coin.
+struct WalletBackendSuspendCoinRequest: WalletBackendFormattedRequest {
+ var coinPub: String
+ var suspended: Bool
+
+ struct Args: Encodable {
+ var coinPub: String
+ var suspended: Bool
+ }
+
+ struct Response: Decodable {}
+
+ func operation() -> String {
+ return "setCoinSuspended"
+ }
+
+ func args() -> Args {
+ return Args(coinPub: coinPub, suspended: suspended)
+ }
+}
+
+
diff --git a/TalerWallet1/Backend/WalletCore.swift
b/TalerWallet1/Backend/WalletCore.swift
new file mode 100644
index 0000000..ff67b15
--- /dev/null
+++ b/TalerWallet1/Backend/WalletCore.swift
@@ -0,0 +1,262 @@
+/*
+ * 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 // FOUNDATION has no AppStorage
+import AnyCodable
+import FTalerWalletcore
+import SymLog
+
+/// Delegate for the wallet backend.
+protocol WalletBackendDelegate {
+ /// Called when the backend interface receives a message it does not know
how to handle.
+ func walletBackendReceivedUnknownMessage(_ walletCore: WalletCore,
message: String)
+}
+
+/// An interface to the wallet backend.
+class WalletCore: QuickjsMessageHandler {
+ private let symLog = SymLogC()
+
+ private var quickjs: Quickjs
+ private var requestsMade: UInt // counter for array of completion
closures
+ private var completions: [UInt : (UInt, Data?,
WalletBackendResponseError?) -> Void] = [:]
+ var delegate: WalletBackendDelegate?
+
+ var versionInfo: VersionInfo? // shown in SettingsView
+ var developDelay: Bool? // if set in SettingsView will
delay wallet-core after each action
+
+ private struct FullRequest: Encodable {
+ let operation: String
+ let id: UInt
+ let args: AnyEncodable
+ }
+
+ private struct FullResponse: Decodable {
+ let type: String
+ let operation: String
+ let id: UInt
+ let result: AnyCodable
+ }
+
+ struct FullError: Decodable {
+ let type: String
+ let operation: String
+ let id: UInt
+ let error: WalletBackendResponseError
+ }
+
+ var lastError: FullError?
+
+ deinit {
+ symLog.log()
+ // TODO: send shutdown message to talerWalletInstance
+// quickjs.waitStopped()
+ }
+
+ init() throws {
+ requestsMade = 0
+ quickjs = Quickjs()
+ quickjs.messageHandler = self
+ }
+}
+// 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 {
+ 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())
+ }
+ } 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())
+ }
+ }
+ }
+
+ private func handleNotification(dict responseDict: [String : Any]) throws {
+ do {
+ guard let payload = (responseDict["payload"] as? [String : Any])
else { throw WalletBackendError.deserializationError }
+ guard let type = (payload["type"] as? String) else { throw
WalletBackendError.deserializationError }
+ switch type {
+ case "pending-operation-processed":
+ guard let id = (payload["id"] as? String) else { throw
WalletBackendError.deserializationError }
+ if id.hasPrefix("exchange-update:") {
+ // TODO: handle exchange-update
+ } else {
+ symLog.log(id)
+ // TODO: handle other pending-operation-processed
+ }
+ case "coin-withdrawn", "withdraw-group-finished",
"pay-operation-success":
+ symLog.log(payload)
+ Task {
+ do {
+ 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
+ case "reserve-registered-with-bank":
+ symLog.log(payload)
+ break
+ default:
+ symLog.log(payload)
+ break
+ }
+ } catch let error {
+ symLog.log("Error \(error) parsing notification: \(responseDict)")
// TODO: .error
+ // TODO: if DevMode then should log into file for user
+ }
+ }
+
+ /// here not only responses, but also notifications from wallet-core will
be received
+ func handleMessage(message: String) {
+ do {
+ var asyncDelay = 0
+ if let delay = developDelay {
+ if delay {
+ asyncDelay = 2
+ }
+ }
+ if asyncDelay > 0 {
+ symLog.log(message)
+ symLog.log("...going to sleep for \(asyncDelay) seconds...")
+ 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 {
+ case "error":
+ try handleResponse(dict: responseDict, data: messageData,
isError: true)
+ case "response":
+ try handleResponse(dict: responseDict, data: messageData)
+ case "notification":
+ try handleNotification(dict: responseDict)
+ case "tunnelHttp": // TODO: Handle tunnelHttp
+ break
+ default:
+ symLog.log("Unknown response type: \(responseDict)") //
TODO: .error
+ throw WalletBackendError.deserializationError
+ }
+ } catch {
+ delegate?.walletBackendReceivedUnknownMessage(self, message:
message)
+ }
+ }
+
+ private func sendRequest(request: WalletBackendRequest, completionHandler:
@escaping (UInt, 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());
+ }
+ }
+
+ /// call this to send requests to wallet-core
+ func sendFormattedRequest<T: WalletBackendFormattedRequest>
+ (request: T, completionHandler: @escaping (T.Response?,
WalletBackendResponseError?) -> Void)
+ {
+ let reqData = WalletBackendRequest(operation: request.operation(),
+ args:
AnyEncodable(request.args()))
+ sendRequest(request: reqData) { (id: UInt, result: Data?, err:
WalletBackendResponseError?) in
+ guard let json = result else { completionHandler(nil, err); return
}
+ do {
+ let decoded = try JSONDecoder().decode(T.Response.self, from:
json)
+ completionHandler(decoded, err)
+ } catch {
+ completionHandler(nil, WalletCore.parseResponseError())
+ }
+ }
+ }
+}
+// MARK: - async / await function
+extension WalletCore {
+ /// send async requests to wallet-core
+ func sendFormattedRequest<T: WalletBackendFormattedRequest> (request: T)
async throws -> (T.Response, UInt) {
+ let reqData = WalletBackendRequest(operation: request.operation(),
+ args: AnyEncodable(request.args()))
+ return try await withCheckedThrowingContinuation { continuation in
+ sendRequest(request: reqData) { id, result, error in
+ if let json = result {
+ do {
+ let decoded = try
JSONDecoder().decode(T.Response.self, from: json)
+ continuation.resume(returning: (decoded, id))
+ } catch {
+ 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)
+ }
+ } else {
+ if let error = error {
+ self.lastError = FullError(type: "error", operation:
request.operation(), id: id, error: error)
+ } else {
+ self.lastError = nil
+ }
+ continuation.resume(throwing:
WalletBackendError.walletCoreError)
+ }
+ }
+ }
+ }
+}
diff --git a/TalerWallet1/Controllers/Controller.swift
b/TalerWallet1/Controllers/Controller.swift
new file mode 100644
index 0000000..513472a
--- /dev/null
+++ b/TalerWallet1/Controllers/Controller.swift
@@ -0,0 +1,128 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under
the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import Foundation
+import SymLog
+
+enum BackendState {
+ case none
+ case instantiated
+ case initing
+ case ready
+ case error
+}
+
+enum UrlCommand {
+ case unknown
+ case withdraw
+ case pay
+}
+
+class Controller: ObservableObject {
+ public static var 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
+
+ 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 {
+ 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
+ throw error
+ }
+ } else {
+ symLog.log("Yikes\(logSymbol(-1)) wallet-core already
initialized") // TODO: .warning
+ }
+ }
+}
+
+// MARK: -
+extension Controller {
+ func openURL(_ url:URL) -> UrlCommand {
+ guard let scheme = url.scheme else {return UrlCommand.unknown}
+ var uncrypted = false
+ switch scheme {
+ case "taler+http":
+ uncrypted = true
+ fallthrough
+ case "taler":
+ return talerScheme(url, uncrypted)
+ case "payto":
+ messageForSheet = url.absoluteString
+ return paytoScheme(url)
+ default:
+ symLog.log("unknown scheme: <\(scheme)>") // should
never happen
+ }
+ return UrlCommand.unknown
+ }
+}
+// MARK: -
+extension Controller {
+ func paytoScheme(_ url:URL) -> UrlCommand {
+ let logItem = "scheme payto:// is not yet implemented"
+ // TODO: write logItem to somewhere in Debug section of SettingsView
+ symLog.log(logItem) // TODO: symLog.error(logItem)
+ return UrlCommand.unknown
+ }
+
+ func talerScheme(_ url:URL,_ uncrypted: Bool = false) -> UrlCommand {
+ guard let command = url.host else {return UrlCommand.unknown}
+ if uncrypted {
+ print("uncrypted")
+ // TODO: uncrypted
+ }
+ switch command {
+ case "withdraw":
+ return UrlCommand.withdraw
+ case "pay":
+ return UrlCommand.pay
+ default:
+ symLog.log("unknown command taler://\(command)")
+ }
+ return UrlCommand.unknown
+ }
+}
diff --git a/TalerWallet1/Controllers/TalerWallet1App.swift
b/TalerWallet1/Controllers/TalerWallet1App.swift
new file mode 100644
index 0000000..11f8645
--- /dev/null
+++ b/TalerWallet1/Controllers/TalerWallet1App.swift
@@ -0,0 +1,81 @@
+/*
+ * 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/>
+ */
+import BackgroundTasks
+import SwiftUI
+import SymLog
+#if DEBUG
+let schemes: Set = ["taler", "payto", "taler+http"]
+#else
+let schemes: Set = ["taler", "payto"]
+#endif
+
+@main
+struct TalerWallet1App: App {
+ private let symLog = SymLogV(0)
+ @Environment(\.scenePhase) private var phase
+
+ // our main controller
+ @StateObject private var controller = Controller.shared
+
+ func scheduleAppRefresh() {
+ let request = BGAppRefreshTaskRequest(identifier: "net.taler.refresh")
+ request.earliestBeginDate = .now.addingTimeInterval(24 * 3600)
+ try? BGTaskScheduler.shared.submit(request)
+ }
+
+ var body: some Scene {
+ WindowGroup {
+ symLog { ContentView()
+ .environmentObject(controller)
+ .handlesExternalEvents(preferring: ["*"], allowing: ["*"])
+ .task {
+ symLog.log("task -> initWalletCore")
+ try? await controller.initWalletCore()
+ symLog.log("task done")
+ }
+ }
+ }
+ .onChange(of: phase) { newPhase in
+ switch newPhase {
+ case .background: scheduleAppRefresh()
+ default: break
+ }
+ }
+// if #available(iOS 16.0, *) {
+// .backgroundTask(.appRefresh("net.taler.refresh")) {
+// symLog.log("backgroundTask running")
+//#if 0
+// let request = URLRequest(url: URL(string: "your_backend")!)
+// guard let data = try? await URLSession.shared.data(for:
request).0 else {
+// return
+// }
+//
+// let decoder = JSONDecoder()
+// guard let products = try? decoder.decode([Product].self,
from: data) else {
+// return
+// }
+//
+// if !products.isEmpty && !Task.isCancelled {
+// await notifyUser(for: products)
+// }
+//#endif
+// }
+// } else {
+// // Fallback on earlier versions
+// }
+
+ }
+}
diff --git a/TalerWallet1/Helper/TalerDater.swift
b/TalerWallet1/Helper/TalerDater.swift
new file mode 100644
index 0000000..5a7abdb
--- /dev/null
+++ b/TalerWallet1/Helper/TalerDater.swift
@@ -0,0 +1,102 @@
+/*
+ * 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
+
+public class TalerDater: DateFormatter {
+ public static var shared = TalerDater()
+
+ static func relativeDate(from: TimeInterval) -> String? {
+ if from > 0 { // transactions should always be in the past
+ let minute = from / 60.0 // from is in seconds
+ if minute < 1 { return "Right now" }
+ if minute < 2 { return "1 minute ago" }
+ if minute < 55 { return "\(Int(minute)) minutes ago" }
+ if minute < 60 { return "About an hour ago" }
+ if minute < 80 { return "1 hour ago" }
+ if minute < 105 { return "About 1½ hours ago" }
+ if minute < 125 { return "About 2 hours ago" }
+ let hour = minute / 60.0
+ let calendar = Calendar.current
+ let now = Date.now
+ let currHour = Double(calendar.component(.hour, from: now))
+ let currMin = Double(calendar.component(.minute, from: now))
+ let currTime = currHour + currMin/60
+ if hour < currTime { return "\(Int(hour)) hours ago" }
+ if hour < currTime + 24 { return "Yesterday" }
+ let day = (hour - currTime) / 24.0
+ if day < 7 { return "\(Int(day+1)) days ago" }
+ if day < 14 { return "More than a week ago" }
+ // will fall thru...
+ return nil
+ } else { // Yikes! transaction date is in the future
+ return nil
+ }
+ }
+
+ /// produces a random date string between `now` and m+h+d (edit values
after 60x)
+ public static func randomDateStr() -> String {
+ let m = 60*15
+ let h = 60*60*9
+ let d = 24*60*60*22
+ let t = m+h+d
+ let randomTime = Int.random(in:1...t)
+ if let randomDateStr = relativeDate(from: Double(randomTime)) {
+ return randomDateStr
+ } else { // t is too large for a relative date
+ // return absolute date with random locale
+ let localeStr = (randomTime&1 == 1) ? "de_DE" : "en_US"
+ shared.locale = NSLocale(localeIdentifier: localeStr) as Locale
+ let randomDate = Date(timeIntervalSinceNow: Double(-t))
+ return shared.string(from: randomDate)
+ }
+ }
+
+ /// converts a timestamp into a formatted date string
+ public static func dateString(from: Timestamp, relative: Bool = false) ->
String {
+ do {
+ let milliseconds = try from.milliseconds()
+ let date = Date(milliseconds: milliseconds)
+ if relative {
+ let now = Date.now
+ let timeInterval = now.timeIntervalSince(date)
+ if let relativeDate = relativeDate(from: timeInterval) {
+ return relativeDate
+ }
+ }
+ return shared.string(from: date)
+ } catch {
+ return "Never"
+ }
+ }
+
+ public static func dateString() -> String {
+ return shared.string(from: Date())
+ }
+
+ private override init() {
+ super.init()
+ self.setLocalizedDateFormatFromTemplate("EEEdMMM") //
abbreviated day of week
+ self.dateStyle = .medium
+ self.timeStyle = .short
+// self.timeZone = TimeZone(abbreviation: "UTC") // UTC prints
GMT
+// self.dateFormat = "z yyyy-MM-dd HH:mm" // "GMT
2022-11-09 18:00"
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+}
diff --git a/Taler/TalerApp.swift b/TalerWallet1/Helper/TalerStrings.swift
similarity index 72%
rename from Taler/TalerApp.swift
rename to TalerWallet1/Helper/TalerStrings.swift
index 2eb3154..b642798 100644
--- a/Taler/TalerApp.swift
+++ b/TalerWallet1/Helper/TalerStrings.swift
@@ -1,6 +1,6 @@
/*
* This file is part of GNU Taler
- * (C) 2021 Taler Systems S.A.
+ * (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
@@ -13,14 +13,16 @@
* 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 SwiftUI
+extension StringProtocol {
-@main
-struct TalerApp: App {
- var body: some Scene {
- WindowGroup {
- ContentView()
+ func trimURL() -> String {
+ if let url = URL(string: String(self)) {
+ if let host = url.host {
+ return host
+ }
}
+ return String(self)
}
}
diff --git a/TalerWallet1/Helper/View+dismissTop.swift
b/TalerWallet1/Helper/View+dismissTop.swift
new file mode 100644
index 0000000..bf6721e
--- /dev/null
+++ b/TalerWallet1/Helper/View+dismissTop.swift
@@ -0,0 +1,41 @@
+/*
+ * 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
+
+/// 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.
+extension View {
+ public func dismissTop(animated: Bool = true) {
+ let windows = UIApplication.shared.connectedScenes.compactMap {
+ ($0 as? UIWindowScene)?.keyWindow // TODO: iPad might have
more than 1 window
+ }
+ if var topController = windows.first?.rootViewController {
+ var gotPresented = false
+ while let presentedViewController =
topController.presentedViewController {
+ topController = presentedViewController
+ gotPresented = true
+ }
+ if gotPresented {
+ topController.dismiss(animated: animated)
+ } else {
+ print("Yikes❗️ Trying to dismiss the rootViewController!")
+ }
+ } else {
+ print("Yikes❗️ There is no window/rootViewController!")
+ }
+ }
+}
diff --git a/TalerWallet1/Model/ExchangeTestModel.swift
b/TalerWallet1/Model/ExchangeTestModel.swift
new file mode 100644
index 0000000..f73592a
--- /dev/null
+++ b/TalerWallet1/Model/ExchangeTestModel.swift
@@ -0,0 +1,138 @@
+/*
+ * 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 EXCHANGEBASEURL = "https://exchange.demo.taler.net/"
+fileprivate let BANKBASEURL = "https://bank.demo.taler.net/"
+fileprivate let BANKACCESSAPIBASEURL =
"https://bank.demo.taler.net/demobanks/default/access-api/"
+fileprivate let MERCHANTBASEURL = "https://backend.demo.taler.net/"
+fileprivate let MERCHANTAUTHTOKEN = "secret-token:sandbox"
+
+// MARK: -
+class ExchangeTestModel: ObservableObject {
+ private let symLog = SymLogC(0)
+
+ var walletCore: WalletCore
+
+ @Published var loading: Bool = false
+
+ init(walletCore: WalletCore) {
+ self.walletCore = walletCore
+ }
+}
+// MARK: -
+extension ExchangeTestModel {
+ func loadTestKudos() {
+ loading = true
+
+ let amount = Amount(currency: "KUDOS", integer: 11, fraction: 0)
+ let req = WalletBackendWithdrawTestBalance(amount: amount,
bankBaseUrl: BANKBASEURL,
+ exchangeBaseUrl: EXCHANGEBASEURL,
bankAccessApiBaseUrl: BANKACCESSAPIBASEURL)
+ symLog.log("sending: \(req)")
+ walletCore.sendFormattedRequest(request: req) { response, err in
+ DispatchQueue.main.async {
+ self.loading = false
+ if let res = response {
+ // TODO: ?
+ self.symLog.log("received: \(res)")
+ } else {
+ // TODO: Handle error
+ }
+ }
+ }
+ }
+
+ func runIntegrationTest() {
+ loading = true
+
+ let amountW = Amount(currency: "KUDOS", integer: 3, fraction: 0)
+ let amountS = Amount(currency: "KUDOS", integer: 1, fraction: 0)
+ let req = WalletBackendRunIntegration(amountToWithdraw: amountW,
+ amountToSpend: amountS,
+ bankBaseUrl:
BANKACCESSAPIBASEURL,
+ exchangeBaseUrl: EXCHANGEBASEURL,
+ merchantBaseUrl: MERCHANTBASEURL,
+ merchantAuthToken:
MERCHANTAUTHTOKEN
+ )
+ symLog.log("sending: \(req)")
+ walletCore.sendFormattedRequest(request: req) { response, err in
+ DispatchQueue.main.async {
+ self.loading = false
+ if let res = response {
+ // TODO: ?
+ self.symLog.log("received: \(res)")
+ } else {
+ // TODO: Handle error
+ }
+ }
+ }
+ }
+}
+
+/// A request to add a test balance to the wallet.
+fileprivate struct WalletBackendWithdrawTestBalance:
WalletBackendFormattedRequest {
+ typealias Response = String
+ func operation() -> String { return "withdrawTestBalance" }
+ func args() -> Args {
+ return Args(amount: amount, bankBaseUrl: bankBaseUrl,
+ exchangeBaseUrl: exchangeBaseUrl, bankAccessApiBaseUrl:
bankAccessApiBaseUrl)
+ }
+
+ var amount: Amount
+ var bankBaseUrl: String
+ var exchangeBaseUrl: String
+ var bankAccessApiBaseUrl: String
+
+ struct Args: Encodable {
+ var amount: Amount
+ var bankBaseUrl: String
+ var exchangeBaseUrl: String
+ var bankAccessApiBaseUrl: String
+ }
+}
+
+/// A request to add a test balance to the wallet.
+fileprivate struct WalletBackendRunIntegration: WalletBackendFormattedRequest {
+ typealias Response = String
+ func operation() -> String { return "runIntegrationTest" }
+ func args() -> Args {
+ return Args(amountToWithdraw: amountToWithdraw,
+ amountToSpend: amountToSpend,
+ bankBaseUrl: bankBaseUrl,
+ exchangeBaseUrl: exchangeBaseUrl,
+ merchantBaseUrl: merchantBaseUrl,
+ merchantAuthToken: merchantAuthToken
+ )
+ }
+
+ var amountToWithdraw: Amount
+ var amountToSpend: Amount
+ var bankBaseUrl: String
+ var exchangeBaseUrl: String
+ var merchantBaseUrl: String
+ var merchantAuthToken: String
+
+ struct Args: Encodable {
+ var amountToWithdraw: Amount
+ var amountToSpend: Amount
+ var bankBaseUrl: String
+ var exchangeBaseUrl: String
+ var merchantBaseUrl: String
+ var merchantAuthToken: String
+ }
+}
diff --git a/TalerWallet1/Model/WalletInitModel.swift
b/TalerWallet1/Model/WalletInitModel.swift
new file mode 100644
index 0000000..e254d41
--- /dev/null
+++ b/TalerWallet1/Model/WalletInitModel.swift
@@ -0,0 +1,86 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under
the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import Foundation
+import SymLog
+
+let DATABASE = "talerwalletdb-v30"
+
+class WalletInitModel: WalletModel {
+
+}
+// MARK: -
+/// A request to initialize Wallet-core
+fileprivate struct WalletBackendInitRequest: WalletBackendFormattedRequest {
+ func operation() -> String { return "init" }
+ func args() -> Args {
+ return Args(persistentStoragePath: persistentStoragePath,
+ cryptoWorkerType: "sync")
+ }
+
+ struct Args: Encodable {
+ var persistentStoragePath: String
+ var cryptoWorkerType: String?
+ }
+
+ var persistentStoragePath: String
+
+ struct Response: Decodable { // versioninfo
+ var versionInfo: VersionInfo
+ enum CodingKeys: String, CodingKey {
+ case versionInfo = "versionInfo"
+ }
+ }
+}
+// MARK: -
+/// The info returned from Wallet-core init
+struct VersionInfo: Decodable {
+ var hash: String
+ var version: String
+ var exchange: String
+ var merchant: String
+ var bank: String
+ var devMode: Bool
+}
+// MARK: -
+extension WalletInitModel {
+ /// initalize Wallet-Core. Will do networking
+ func initWallet() async throws -> VersionInfo? {
+ do {
+ let docPath = try docPath()
+ let request = WalletBackendInitRequest(persistentStoragePath:
docPath)
+ symLog?.log("info: not main thread")
+ let response = try await sendRequest(request, 0) // no Delay
+ return response.versionInfo
+ } catch {
+ symLog?.log("error: \(error)")
+ throw error
+ }
+ }
+
+ private func docPath () throws -> String {
+ let documentUrls = FileManager.default.urls(for: .documentDirectory,
in: .userDomainMask)
+ if (documentUrls.count > 0) {
+ var storageDir = documentUrls[0]
+ storageDir.appendPathComponent(DATABASE, isDirectory: false)
+ storageDir.appendPathExtension("json")
+ return storageDir.path
+ } else { // should never happen
+ symLog?.log("Yikes! documentURLs empty") // TODO: symLog.error
+ throw WalletBackendError.initializationError
+ }
+ }
+}
+
diff --git a/TalerWallet1/Model/WalletModel.swift
b/TalerWallet1/Model/WalletModel.swift
new file mode 100644
index 0000000..d340278
--- /dev/null
+++ b/TalerWallet1/Model/WalletModel.swift
@@ -0,0 +1,55 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under
the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import Foundation
+import SymLog
+
+fileprivate let ASYNCDELAY: UInt = 0 //set e.g to 6 or 9 seconds for
debugging
+
+/// 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
+ }
+
+ @MainActor func sendRequest<T: WalletBackendFormattedRequest> (_ request:
T, _ delay: UInt = 0)
+ async throws -> T.Response {
+ loading = true // enter progressView
+ do {
+ symLog?.log("sending: \(request)")
+ let (response, id) = try await
walletCore.sendFormattedRequest(request: request)
+ let asyncDelay: UInt = delay > 0 ? delay : UInt(ASYNCDELAY)
+ if asyncDelay > 0 { // test LoadingView, sleep some seconds
+ symLog?.log("received: (\(id)), going to sleep for
\(asyncDelay) seconds...")
+ try? await Task.sleep(nanoseconds: 1_000_000_000 *
UInt64(asyncDelay))
+ symLog?.log("waking up again after \(asyncDelay) seconds, will
deliver \(response)")
+ } else {
+ symLog?.log("received: \(response)")
+ }
+ loading = false // exit progressView
+ return response
+ } catch {
+ throw error
+ }
+ }
+
+}
diff --git a/TalerWallet1/Quickjs/quickjs.swift
b/TalerWallet1/Quickjs/quickjs.swift
new file mode 100644
index 0000000..040bfcb
--- /dev/null
+++ b/TalerWallet1/Quickjs/quickjs.swift
@@ -0,0 +1,81 @@
+/*
+ * 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/>
+ */
+import Foundation
+
+import FTalerWalletcore
+
+public protocol QuickjsMessageHandler: AnyObject {
+ func handleMessage(message: String)
+}
+
+func notification_callback(userdata: Optional<UnsafeMutableRawPointer>,
+ payload: Optional<UnsafePointer<Int8>>) {
+ let native = Unmanaged<Quickjs>.fromOpaque(userdata!).takeUnretainedValue()
+ let string = String(cString: payload!)
+ native.internalOnNotify(payload: string)
+}
+
+public class Quickjs {
+ var talerWalletInstance: OpaquePointer!
+ public weak var messageHandler: QuickjsMessageHandler?
+
+ public init() {
+ self.talerWalletInstance = TALER_WALLET_create()
+ TALER_WALLET_set_message_handler(talerWalletInstance,
+ notification_callback,
+
Unmanaged.passUnretained(self).toOpaque())
+ TALER_WALLET_run(talerWalletInstance);
+ }
+
+ deinit {
+ // FIXME: TALER_WALLET_destroy
+// TALER_WALLET_destroy(talerWalletInstance)
+ }
+
+
+ public func internalOnNotify(payload: String) {
+ if let handler = messageHandler {
+ handler.handleMessage(message: payload)
+ }
+ }
+
+// public func notifyNative() {
+// __notifyNative(instance)
+// }
+
+// public func evalNodeCode(source: String) {
+// scheduleNodeThreadAsync {
+// __makeCallbackNative(self.instance, source.cString(using: .utf8))
+// }
+// }
+
+ public func sendMessage(message: String) {
+ TALER_WALLET_send_request(talerWalletInstance, message)
+ }
+
+ /// Note: This *must* be called before releasing the object, or else the
thread will keep going.
+// public func waitStopped() {
+// scheduleNodeThreadSync {
+// self.stopped = true
+// }
+// thread.cancel()
+// }
+
+// public func putModuleCode(modName: String, code: String) {
+// __putModuleCodeNative(self.instance, modName.cString(using: .utf8),
+// code.cString(using: .utf8))
+// }
+}
diff --git a/TalerWallet1/Views/Balances/BalanceRow.swift
b/TalerWallet1/Views/Balances/BalanceRow.swift
new file mode 100644
index 0000000..9c8aeee
--- /dev/null
+++ b/TalerWallet1/Views/Balances/BalanceRow.swift
@@ -0,0 +1,46 @@
+/*
+ * 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/BalancesModel.swift
b/TalerWallet1/Views/Balances/BalancesModel.swift
new file mode 100644
index 0000000..80197d9
--- /dev/null
+++ b/TalerWallet1/Views/Balances/BalancesModel.swift
@@ -0,0 +1,73 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under
the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import Foundation
+import taler_swift
+import SymLog
+fileprivate let ASYNCDELAY: UInt = 0 //set e.g to 6 or 9 seconds for
debugging
+
+class BalancesModel: WalletModel {
+ @Published var balances: [Balance]? // update view
+}
+// MARK: -
+/// A request to get the balances held in the wallet.
+fileprivate struct GetBalances: WalletBackendFormattedRequest {
+ func operation() -> String { return "getBalances" }
+ func args() -> Args { return Args() }
+
+ struct Args: Encodable {} // no arguments needed
+
+ struct Response: Decodable { // list of balances
+ var balances: [Balance]
+ }
+}
+// MARK: -
+/// A currency balance
+struct Balance: Decodable, Hashable {
+ var available: Amount
+ var pendingIncoming: Amount
+ var pendingOutgoing: Amount
+ var hasPendingTransactions: Bool
+ var requiresUserInput: Bool
+
+ public static func == (lhs: Balance, rhs: Balance) -> Bool {
+ return lhs.available == rhs.available &&
+ lhs.pendingIncoming == rhs.pendingIncoming &&
+ lhs.pendingOutgoing == rhs.pendingOutgoing &&
+ lhs.hasPendingTransactions == rhs.hasPendingTransactions &&
+ lhs.requiresUserInput == rhs.requiresUserInput
+ }
+
+ public func hash(into hasher: inout Hasher) {
+ hasher.combine(available)
+ hasher.combine(pendingIncoming)
+ hasher.combine(pendingOutgoing)
+ hasher.combine(hasPendingTransactions)
+ hasher.combine(requiresUserInput)
+ }
+}
+// MARK: -
+extension BalancesModel {
+ /// fetch Balances from Wallet-Core. No networking involved
+ @MainActor func fetchBalances() async throws {
+ do {
+ let request = GetBalances()
+ let response = try await sendRequest(request, ASYNCDELAY)
+ balances = response.balances // trigger view update
in CurrenciesListView
+ } catch {
+ throw error
+ }
+ }
+}
diff --git a/TalerWallet1/Views/Balances/CurrenciesListView.swift
b/TalerWallet1/Views/Balances/CurrenciesListView.swift
new file mode 100644
index 0000000..a40b074
--- /dev/null
+++ b/TalerWallet1/Views/Balances/CurrenciesListView.swift
@@ -0,0 +1,78 @@
+/*
+ * 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 {
+ symLog?.log("refreshing")
+ try? await reloadAction() // TODO: catch error
+ }
+ }
+ }
+ }
+}
diff --git a/TalerWallet1/Views/Balances/CurrencyView.swift
b/TalerWallet1/Views/Balances/CurrencyView.swift
new file mode 100644
index 0000000..0978de0
--- /dev/null
+++ b/TalerWallet1/Views/Balances/CurrencyView.swift
@@ -0,0 +1,58 @@
+/*
+ * 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
new file mode 100644
index 0000000..d6321d2
--- /dev/null
+++ b/TalerWallet1/Views/Balances/PendingRow.swift
@@ -0,0 +1,61 @@
+/*
+ * 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/Taler/Model/BackendManager.swift
b/TalerWallet1/Views/Balances/WalletEmptyView.swift
similarity index 51%
copy from Taler/Model/BackendManager.swift
copy to TalerWallet1/Views/Balances/WalletEmptyView.swift
index d0c262b..72d5192 100644
--- a/Taler/Model/BackendManager.swift
+++ b/TalerWallet1/Views/Balances/WalletEmptyView.swift
@@ -13,18 +13,32 @@
* 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 Foundation
+struct WalletEmptyView: View {
-class BackendManager: ObservableObject {
- var backend: WalletBackend
-
- @Published var exchangeManager: ExchangeManager
- @Published var pendingManager: PendingManager
-
- init() {
- self.backend = try! WalletBackend()
- self.exchangeManager = ExchangeManager(_backend: self.backend)
- self.pendingManager = PendingManager(_backend: self.backend)
+ 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
new file mode 100644
index 0000000..b4b8b9f
--- /dev/null
+++ b/TalerWallet1/Views/Exchange/ExchangeListView.swift
@@ -0,0 +1,103 @@
+/*
+ * 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 ExchangeListView: View {
+ private let symLog = SymLogV()
+ let navTitle = "Exchanges"
+
+ @ObservedObject var viewModel: ExchangeModel
+
+ 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)
+ }
+ }
+ }
+}
+// MARK: -
+extension ExchangeListView {
+ struct Content: View {
+ let symLog: SymLogV
+ @ObservedObject var viewModel: ExchangeModel
+ var reloadAction: () async throws -> ()
+ @State var showAlert: Bool = false
+ @State var newExchange: String = "https://exchange-age.taler.ar/"
+
+ 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
+ }
+ }
+ }
+
+ var body: some View {
+ let plusAction: () -> Void = {
+// withAnimation { showPopup = true }
+ showAlert = true
+ }
+ VStack {
+ if viewModel.exchanges!.isEmpty {
+ Text("No Exchanges yet...")
+ } else {
+ List(viewModel.exchanges!, id: \.self) { exchange in
+ VStack {
+ Text(exchange.exchangeBaseUrl)
+ .frame(maxWidth: .infinity)
+ .padding()
+ Text("Currency: " + (exchange.currency ?? "?"))
+ .frame(maxWidth: .infinity)
+ .padding()
+ }
+ }
+ .navigationBarTitleDisplayMode(.large) // .inline
+ .refreshable {
+ symLog.log("refreshing")
+ do {
+ try await reloadAction()
+ } catch {
+ // TODO: catch error
+ symLog.log(error.localizedDescription)
+ }
+ }
+ }
+ }
+ .navigationBarItems(trailing: PlusButton(action: plusAction))
+ .textFieldAlert(isPresented: $showAlert, title: "Add Exchange",
+ doneText: "Add", text: $newExchange, action:
addExchange)
+ } // body
+ }
+}
diff --git a/TalerWallet1/Views/Exchange/ExchangeModel.swift
b/TalerWallet1/Views/Exchange/ExchangeModel.swift
new file mode 100644
index 0000000..c16e720
--- /dev/null
+++ b/TalerWallet1/Views/Exchange/ExchangeModel.swift
@@ -0,0 +1,115 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under
the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import Foundation
+import taler_swift
+import SymLog
+fileprivate let ASYNCDELAY: UInt = 0 //set e.g to 6 or 9 seconds for
debugging
+
+class ExchangeModel: WalletModel {
+ @Published var exchanges: [Exchange]?
+}
+// MARK: -
+/// A request to list exchanges.
+fileprivate struct ListExchanges: WalletBackendFormattedRequest {
+ func operation() -> String { return "listExchanges" }
+ func args() -> Args { return Args() }
+
+ struct Args: Encodable {} // no arguments needed
+
+ struct Response: Decodable { // list of known exchanges
+ var exchanges: [Exchange]
+ }
+}
+
+/// A request to add an exchange.
+fileprivate struct AddExchange: WalletBackendFormattedRequest {
+ func operation() -> String { return "addExchange" }
+ func args() -> Args { return Args(exchangeBaseUrl: exchangeBaseUrl) }
+
+ var exchangeBaseUrl: String
+
+ struct Args: Encodable {
+ var exchangeBaseUrl: String
+ }
+
+ struct Response: Decodable {}
+}
+// MARK: -
+struct Exchange: Codable, Hashable {
+ static func == (lhs: Exchange, rhs: Exchange) -> Bool {
+ return lhs.exchangeBaseUrl == rhs.exchangeBaseUrl &&
+ lhs.exchangeStatus == rhs.exchangeStatus &&
+ lhs.permanent == rhs.permanent
+ }
+
+ var exchangeBaseUrl: String
+ var currency: String?
+ var tosStatus: String
+ var paytoUris: [String]
+ var exchangeStatus: String
+ var permanent: Bool
+ var ageRestrictionOptions: [Int]
+ var lastUpdateErrorInfo: ExchangeError?
+
+ var name: String? {
+ if let url = URL(string: exchangeBaseUrl) {
+ if let host = url.host {
+ return host
+ }
+ }
+ return nil
+ }
+}
+struct ExchangeError: Codable, Hashable {
+ var error: HTTPError
+}
+
+struct HTTPError: Codable, Hashable {
+ var code: Int
+ var requestUrl: String
+ var hint: String
+ var requestMethod: String
+ var httpStatusCode: Int?
+}
+
+// MARK: -
+extension ExchangeModel {
+ /// ask wallet-core for its list of known exchanges
+ @MainActor func updateList() async throws {
+ do {
+ let request = ListExchanges()
+ let response = try await sendRequest(request, ASYNCDELAY)
+ exchanges = response.exchanges // trigger view update
in ExchangeListView
+ } catch { // TODO: Error
+ symLog?.log(error.localizedDescription)
+ throw error
+ }
+ }
+
+ /// add a new exchange with URL to wallet's list of known exchanges
+ func add(url: String) async throws {
+ do {
+ symLog?.log("adding exchange: \(url)") // TODO: notice
+ let request = AddExchange(exchangeBaseUrl: url)
+ _ = try await sendRequest(request)
+ symLog?.log("added exchange: \(url)")
+ try await updateList()
+ } catch { // TODO: Error
+ symLog?.log(error.localizedDescription)
+ throw error
+ }
+ }
+}
diff --git a/TalerWallet1/Views/HelperViews/AmountView.swift
b/TalerWallet1/Views/HelperViews/AmountView.swift
new file mode 100644
index 0000000..4249876
--- /dev/null
+++ b/TalerWallet1/Views/HelperViews/AmountView.swift
@@ -0,0 +1,43 @@
+/*
+ * 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 AmountView: View {
+ let title: String
+ let value: String
+ let color: Color
+ var body: some View {
+ VStack {
+ Text(title)
+ .font(.title3)
+ Text(value)
+ .font(.largeTitle)
+ .fontWeight(.medium)
+ .foregroundColor(color)
+ }
+ .frame(maxWidth: .infinity, alignment: .center)
+ .listRowSeparator(.hidden)
+ }
+}
+
+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"))
+ }
+ }
+}
diff --git a/TalerWallet1/Views/HelperViews/Buttons.swift
b/TalerWallet1/Views/HelperViews/Buttons.swift
new file mode 100644
index 0000000..ea509a8
--- /dev/null
+++ b/TalerWallet1/Views/HelperViews/Buttons.swift
@@ -0,0 +1,86 @@
+/*
+ * 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 HamburgerButton : View {
+ let action: () -> Void
+
+ var body: some View {
+ Button(action: action) {
+ Image(systemName: "line.3.horizontal")
+ }
+ .font(.title)
+ }
+}
+
+struct PlusButton : View {
+ let action: () -> Void
+
+ var body: some View {
+ Button(action: action) {
+ Image(systemName: "plus")
+ }
+ .font(.title)
+ }
+}
+
+struct ReloadButton : View {
+ let disabled: Bool
+ let action: () -> Void
+
+ var body: some View {
+ Button(action: action) {
+ Image(systemName: "arrow.clockwise")
+ }
+ .font(.title)
+ .disabled(disabled)
+ }
+}
+
+struct AwesomeButton: View {
+ let title: String
+ let action: () -> Void
+ var body: some View {
+ Button(action: action) {
+ Text(title)
+ .frame(minWidth: 0, maxWidth: 300)
+ .padding()
+ .foregroundColor(.white)
+ .background(LinearGradient(gradient: Gradient(colors:
[Color.red, Color.blue]), startPoint: .leading, endPoint: .trailing))
+ .cornerRadius(40)
+ .font(.title)
+ }
+ }
+}
+
+struct Buttons_Previews: PreviewProvider {
+ static var previews: some View {
+ VStack {
+ HamburgerButton() {}
+ .padding()
+ PlusButton() {}
+ .padding()
+ HStack {
+ ReloadButton(disabled: false) {}
+ .padding()
+ ReloadButton(disabled: true) {}
+ .padding()
+ }
+ AwesomeButton(title: "AwesomeButton") {}
+ .padding()
+ }
+ }
+}
diff --git a/TalerWallet1/Views/HelperViews/LoadingView.swift
b/TalerWallet1/Views/HelperViews/LoadingView.swift
new file mode 100644
index 0000000..10c245e
--- /dev/null
+++ b/TalerWallet1/Views/HelperViews/LoadingView.swift
@@ -0,0 +1,45 @@
+/*
+ * 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 LoadingView: View {
+ private let symLog = SymLogV(0)
+ let backButtonHidden: Bool
+
+ var body: some View {
+ symLog { NavigationView {
+ VStack {
+ Spacer()
+ ProgressView()
+ Spacer()
+ Spacer()
+ Spacer()
+ }
+ .navigationBarBackButtonHidden(backButtonHidden)
+ .navigationTitle("Loading...")
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment:
.center)
+ .background(Color(.systemGray6))
+ }
+ }
+}
+
+struct LoadingView_Previews: PreviewProvider {
+ static var previews: some View {
+ LoadingView(backButtonHidden: true)
+ }
+}
diff --git a/TalerWallet1/Views/HelperViews/TextFieldAlert.swift
b/TalerWallet1/Views/HelperViews/TextFieldAlert.swift
new file mode 100644
index 0000000..de2eaf6
--- /dev/null
+++ b/TalerWallet1/Views/HelperViews/TextFieldAlert.swift
@@ -0,0 +1,73 @@
+/*
+ * 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 TextFieldAlert: ViewModifier {
+ @Binding var isPresented: Bool
+ let title: String
+ let doneText: String
+ @Binding var text: String
+ let placeholder: String
+ let action: (String) -> Void
+ func body(content: Content) -> some View {
+ ZStack(alignment: .center) {
+ content
+ .disabled(isPresented)
+ if isPresented {
+ VStack {
+ Text(title).font(.headline).padding()
+ TextField(placeholder, text:
$text).textFieldStyle(.roundedBorder).padding()
+ Divider()
+ HStack {
+ Spacer()
+ Button(role: .cancel) {
+ withAnimation { isPresented.toggle() }
+ } label: {
+ Text("Cancel")
+ }
+ Spacer()
+ Divider()
+ Spacer()
+ Button(doneText) {
+ action(text)
+ withAnimation { isPresented.toggle() }
+ }
+ Spacer()
+ }
+ }
+ .background(.background)
+ .frame(width: 300, height: 200)
+ .cornerRadius(20)
+ .overlay {
+ RoundedRectangle(cornerRadius: 20)
+ .stroke(.quaternary, lineWidth: 1)
+ }
+ }
+ }
+ }
+}
+
+extension View {
+ public func textFieldAlert(isPresented: Binding<Bool>,
+ title: String,
+ doneText: String,
+ text: Binding<String>,
+ placeholder: String = "",
+ action: @escaping (String) -> Void
+ ) -> some View {
+ self.modifier(TextFieldAlert(isPresented: isPresented, title: title,
doneText: doneText, text: text, placeholder: placeholder, action: action))
+ }
+}
diff --git a/TalerWallet1/Views/Main/ContentView.swift
b/TalerWallet1/Views/Main/ContentView.swift
new file mode 100644
index 0000000..1609351
--- /dev/null
+++ b/TalerWallet1/Views/Main/ContentView.swift
@@ -0,0 +1,92 @@
+/*
+ * 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/Taler/Model/BackendManager.swift
b/TalerWallet1/Views/Main/ErrorView.swift
similarity index 64%
copy from Taler/Model/BackendManager.swift
copy to TalerWallet1/Views/Main/ErrorView.swift
index d0c262b..25fc2f0 100644
--- a/Taler/Model/BackendManager.swift
+++ b/TalerWallet1/Views/Main/ErrorView.swift
@@ -13,18 +13,19 @@
* 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
-import Foundation
+struct ErrorView: View {
+ private let symLog = SymLogV()
+ var body: some View {
+
+ Text("Couldn't load Wallet-Core!")
+ }
+}
-class BackendManager: ObservableObject {
- var backend: WalletBackend
-
- @Published var exchangeManager: ExchangeManager
- @Published var pendingManager: PendingManager
-
- init() {
- self.backend = try! WalletBackend()
- self.exchangeManager = ExchangeManager(_backend: self.backend)
- self.pendingManager = PendingManager(_backend: self.backend)
+struct ErrorView_Previews: PreviewProvider {
+ static var previews: some View {
+ ErrorView()
}
}
diff --git a/TalerWallet1/Views/Main/LaunchAnimationView.swift
b/TalerWallet1/Views/Main/LaunchAnimationView.swift
new file mode 100644
index 0000000..cc20fa4
--- /dev/null
+++ b/TalerWallet1/Views/Main/LaunchAnimationView.swift
@@ -0,0 +1,33 @@
+
+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)
+ .autoconnect()
+
+ var body: some View {
+ ZStack {
+ Color.teal.ignoresSafeArea()
+ Image(systemName: "hurricane")
+ .resizable()
+ .scaledToFit()
+ .frame(width: 200, height: 200)
+ .rotationEffect(rotationDirection ? Angle(degrees: 0) :
Angle(degrees: 1080))
+ }
+ .onReceive(animationTimer) { timerValue in
+ withAnimation(.easeInOut(duration: 1.9)) {
+ rotationDirection.toggle()
+ }
+ }
+ }
+}
+struct LaunchAnimationView_Previews: PreviewProvider {
+ static var previews: some View {
+ LaunchAnimationView()
+ }
+}
diff --git a/TalerWallet1/Views/Main/SideBarView.swift
b/TalerWallet1/Views/Main/SideBarView.swift
new file mode 100644
index 0000000..bec2b2c
--- /dev/null
+++ b/TalerWallet1/Views/Main/SideBarView.swift
@@ -0,0 +1,110 @@
+/*
+ * 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
+
+fileprivate let sidebarWidth = 250.0
+
+struct SidebarItem {
+ var name: String
+ var sysImage: String?
+ var view: AnyView
+}
+
+struct SideBarView: View {
+ private let symLog = SymLogV(0)
+ var views: [SidebarItem]
+ @Binding var currentView: Int
+ @Binding var sidebarVisible: Bool
+
+ var body: some View {
+ symLog {
+ HStack {
+ VStack {
+ Spacer()
+ 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
+ } label: {
+ if let sysImage = views[i].sysImage {
+ Label(views[i].name, systemImage: sysImage)
+ .frame(maxWidth: sidebarWidth, alignment:
.leading)
+ } else {
+ Text(views[i].name)
+ .frame(maxWidth: sidebarWidth)
+ }
+ }
+ .buttonStyle(.bordered)
+ .font(.title)
+// .padding(.vertical)
+
+// Divider()
+ }
+ Spacer()
+ Spacer()
+ }
+ .background(Color(.systemGray5))
+ .frame(width: sidebarWidth, alignment: .center)
+ // TODO: use leading instead of sidebarWidth for right-to-left
+ .offset(x: sidebarVisible ? 0 : -sidebarWidth)
+ .animation(.easeInOut, value: sidebarVisible)
+ .ignoresSafeArea()
+ // .onAppear can NOT be used here, because we don't show or
dismiss this view,
+ // but only slide it left or right - so it is always there.
+ // .onAppear {} would be called once even before
LaunchScreen is dismissed and then never again
+
+ // this is just a target for a tap gesture outside the sidebar
to dismiss it
+ Color.clear
+ .frame(maxWidth: sidebarVisible ? .infinity : 0,
maxHeight: .infinity, alignment: .leading)
+ // TODO: right-to-left ?
+ .offset(x: sidebarVisible ? sidebarWidth : 0)
+ .contentShape(Rectangle())
+ .onTapGesture {
+ sidebarVisible = false
+ }
+ }
+ }
+ }
+}
+
+#if DEBUG
+struct BindingViewContainer : View {
+ @State var currentView: Int = 0
+ @State var sidebarVisible: Bool = true
+ var views: [SidebarItem]
+
+ var body: some View {
+ ZStack(alignment: .leading) {
+ views[currentView].view
+ SideBarView(views: views, currentView: $currentView,
sidebarVisible: $sidebarVisible)
+ }
+ }
+}
+
+struct SideBarView_Previews: PreviewProvider {
+ static var views: [SidebarItem] {[
+ SidebarItem(name: "Balances",
+ view: AnyView(WalletEmptyView())),
+ SidebarItem(name: "Settings",
+ view: AnyView(WalletEmptyView()))
+ ]}
+ static var previews: some View {
+ BindingViewContainer(views: views)
+ }
+}
+#endif
diff --git a/TalerWallet1/Views/Payment/PaymentAcceptView.swift
b/TalerWallet1/Views/Payment/PaymentAcceptView.swift
new file mode 100644
index 0000000..7920b87
--- /dev/null
+++ b/TalerWallet1/Views/Payment/PaymentAcceptView.swift
@@ -0,0 +1,71 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under
the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import SwiftUI
+import taler_swift
+import SymLog
+
+struct PaymentAcceptView: View {
+ private let symLog = SymLogV()
+ @ObservedObject var viewModel: PaymentURIModel
+
+ var detailsForAmount: PaymentDetailsForUri
+
+// @Environment(\.dismiss) var dismiss // call dismiss() to get rid of
the sheet
+ let navTitle = "Accept Payment"
+ var cancelButton: some View {
+ Button("Cancel6") { dismissTop() }
+ }
+
+ @State var confirmPayResult: ConfirmPayResult?
+
+ var body: some View {
+ symLog { Group {
+ let raw = detailsForAmount.amountRaw
+ let effective = detailsForAmount.amountEffective
+ let fee = try! effective - raw
+ Form {
+ AmountView(title: "Amount to pay:",
+ value: raw.readableDescription, color:
Color(UIColor.label))
+ .padding(.bottom)
+ AmountView(title: "Exchange fee:",
+ value: fee.readableDescription, color:
Color("Outgoing"))
+ .padding(.bottom)
+ AmountView(title: "Coins to be spent:",
+ value: effective.readableDescription, color:
Color("Outgoing"))
+ }
+ AwesomeButton(title: "Accept") {
+ Task {
+ do {
+ confirmPayResult = try await
viewModel.confirmPay(detailsForAmount.proposalId)
+ symLog.log(confirmPayResult as Any)
+ if confirmPayResult?.type == "done" {
+ // TODO: Show Hints that Payment was successfull
+ // success
+ } else {
+ // TODO: show error
+ }
+ } catch {
+ symLog.log(error.localizedDescription)
// TODO: error
+ }
+ dismissTop()
+ }
+ }
+ }
+ .navigationBarItems(leading: cancelButton)
+ .navigationTitle(navTitle)
+ }
+ }
+}
diff --git a/TalerWallet1/Views/Payment/PaymentURIModel.swift
b/TalerWallet1/Views/Payment/PaymentURIModel.swift
new file mode 100644
index 0000000..8fd4142
--- /dev/null
+++ b/TalerWallet1/Views/Payment/PaymentURIModel.swift
@@ -0,0 +1,183 @@
+/*
+ * 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 AnyCodable
+//import SymLog
+fileprivate let ASYNCDELAY: UInt = 0 //set e.g to 6 or 9 seconds for
debugging
+
+enum PaymentState {
+ case error
+ case waitingForUriDetails
+ case receivedUriDetails
+ case waitingForPaymentAck
+ case receivedPaymentAck
+}
+
+class PaymentURIModel: WalletModel {
+ @Published var paymentState: PaymentState?
+}
+
+
+// MARK: - ContractTerms
+struct ContractTerms: Codable {
+ let amount: Amount
+ let maxFee: Amount
+ let maxWireFee: Amount
+ let merchant: Merchant
+ let extra: Extra
+ let summary: String
+ let timestamp: Timestamp
+ let payDeadline: Timestamp
+ let refundDeadline: Timestamp
+ let wireTransferDeadline: Timestamp
+ let merchantBaseURL: String
+ let fulfillmentURL: String
+ let publicReorderURL: String
+ let auditors: [Auditor]
+ let exchanges: [ExchangeForPay]
+ let orderID, nonce, merchantPub: String
+ let products: [Product]
+ let hWire: String
+ let wireMethod: String
+ let wireFeeAmortization: Int
+
+ enum CodingKeys: String, CodingKey {
+ case amount
+ case maxFee = "max_fee"
+ case maxWireFee = "max_wire_fee"
+ case merchant, extra, summary
+ case timestamp
+ case payDeadline = "pay_deadline"
+ case refundDeadline = "refund_deadline"
+ case wireTransferDeadline = "wire_transfer_deadline"
+ case merchantBaseURL = "merchant_base_url"
+ case fulfillmentURL = "fulfillment_url"
+ case publicReorderURL = "public_reorder_url"
+ case auditors, exchanges
+ case orderID = "order_id"
+ case nonce
+ case merchantPub = "merchant_pub"
+ case products
+ case hWire = "h_wire"
+ case wireMethod = "wire_method"
+ case wireFeeAmortization = "wire_fee_amortization"
+ }
+}
+
+// MARK: - Auditor
+struct Auditor: Codable {
+ let name: String
+ let auditorPub: String
+ let url: String
+
+ enum CodingKeys: String, CodingKey {
+ case name
+ case auditorPub = "auditor_pub"
+ case url
+ }
+}
+
+// MARK: - Exchange
+struct ExchangeForPay: Codable {
+ let url: String
+ let masterPub: String
+
+ enum CodingKeys: String, CodingKey {
+ case url
+ case masterPub = "master_pub"
+ }
+}
+
+// MARK: - Extra
+struct Extra: Codable {
+ let articleName: String
+
+ enum CodingKeys: String, CodingKey {
+ case articleName = "article_name"
+ }
+}
+
+// MARK: -
+/// The result from PreparePayForUri
+struct PaymentDetailsForUri: Codable {
+ let status: String
+ let amountRaw: Amount
+ let amountEffective: Amount
+ let noncePriv: String
+ let proposalId: String
+ let contractTerms: ContractTerms
+ let contractTermsHash: String
+}
+/// A request to get an exchange's payment contract terms.
+fileprivate struct PreparePayForUri: WalletBackendFormattedRequest {
+ typealias Response = PaymentDetailsForUri
+ func operation() -> String { return "preparePayForUri" }
+ func args() -> Args { return Args(talerPayUri: talerPayUri) }
+
+ var talerPayUri: String
+ struct Args: Encodable {
+ var talerPayUri: String
+ }
+}
+// MARK: -
+/// The result from getPaymentDetailsForAmount
+struct ConfirmPayResult: Decodable {
+ var type: String
+ var contractTerms: ContractTerms
+ var transactionId: String
+}
+/// A request to get an exchange's payment details.
+fileprivate struct confirmPayForUri: WalletBackendFormattedRequest {
+ typealias Response = ConfirmPayResult
+ func operation() -> String { return "confirmPay" }
+ func args() -> Args { return Args(proposalId: proposalId) }
+
+ var proposalId: String
+ struct Args: Encodable {
+ var proposalId: String
+ }
+}
+// MARK: -
+extension PaymentURIModel {
+ /// load payment details. Networking involved
+ @MainActor
+ func preparePayForUri(_ talerPayUri: String) async throws ->
PaymentDetailsForUri {
+ do {
+ paymentState = .waitingForUriDetails
+ let request = PreparePayForUri(talerPayUri: talerPayUri)
+ let response = try await sendRequest(request, ASYNCDELAY)
+ paymentState = .receivedUriDetails
+ return response
+ } catch {
+ paymentState = .error
+ throw error
+ }
+ }
+ @MainActor
+ func confirmPay(_ proposalId: String) async throws -> ConfirmPayResult {
+ do {
+ paymentState = .waitingForPaymentAck
+ let request = confirmPayForUri(proposalId: proposalId)
+ let response = try await sendRequest(request, ASYNCDELAY)
+ paymentState = .receivedPaymentAck
+ return response
+ } catch {
+ paymentState = .error
+ throw error
+ }
+ }
+}
diff --git a/TalerWallet1/Views/Payment/PaymentURIView.swift
b/TalerWallet1/Views/Payment/PaymentURIView.swift
new file mode 100644
index 0000000..38ad04c
--- /dev/null
+++ b/TalerWallet1/Views/Payment/PaymentURIView.swift
@@ -0,0 +1,65 @@
+/*
+ * 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 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() }
+ }
+
+ @State var detailsForUri: PaymentDetailsForUri?
+
+ var body: some View {
+ let badURL = "Error in URL: \(url)"
+ VStack {
+ if viewModel.paymentState == nil {
+ LoadingView(backButtonHidden: false)
+ } else { switch viewModel.paymentState {
+ case .waitingForUriDetails:
+ let _ = symLog.vlog("waitingForUriDetails")
+ WithdrawProgressView(message: url.host ?? badURL) {
dismissTop() }
+ .navigationTitle("Contacting Exchange")
+ case .receivedUriDetails:
+ let _ = symLog.vlog("waitingForUser")
+ PaymentAcceptView(viewModel: viewModel, detailsForAmount:
detailsForUri!)
+ default:
+ symLog {
+ Text("Payment")
+ .navigationBarItems(leading: cancelButton)
+ .navigationTitle(navTitle)
+ }
+ } }
+ }.task {
+ do { // TODO: cancelled
+ symLog.log(".task")
+ detailsForUri = try await
viewModel.preparePayForUri(url.absoluteString)
+// print(detailsForUri?.status)
+// print(detailsForUri?.amountRaw.description)
+// print(detailsForUri?.amountEffective.description)
+// print(detailsForUri?.proposalId)
+ } catch {
+ symLog.log(error.localizedDescription) // TODO:
error
+ }
+ }
+ }
+}
diff --git a/TalerWallet1/Views/Pending/PendingModel.swift
b/TalerWallet1/Views/Pending/PendingModel.swift
new file mode 100644
index 0000000..cb88523
--- /dev/null
+++ b/TalerWallet1/Views/Pending/PendingModel.swift
@@ -0,0 +1,82 @@
+/*
+ * 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/PendingOpView.swift
b/TalerWallet1/Views/Pending/PendingOpView.swift
new file mode 100644
index 0000000..12c2924
--- /dev/null
+++ b/TalerWallet1/Views/Pending/PendingOpView.swift
@@ -0,0 +1,64 @@
+/*
+ * 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 PendingOpView: View {
+ var pendingOp: PendingOperation
+ @State var polling: Bool = false
+ @State var liveliness: Bool = false
+ @State var isDue: Bool = false
+
+ var body: some View {
+ Section {
+ Text(pendingOp.exchangeBaseUrl)
+ Text(pendingOp.id)
+ .font(.caption)
+ Toggle("isLongPolling", isOn: $polling)
+ .disabled(true)
+ Toggle("givesLifeness", isOn: $liveliness)
+ .disabled(true)
+ Toggle("isDue", isOn: $isDue)
+ .disabled(true)
+ let dateString = TalerDater.dateString(from:
pendingOp.timestampDue)
+ Text("\(dateString)")
+ } header: {
+ Text(pendingOp.type)
+ .font(.title2)
+ }
+// .textCase(nil) // don't capitalize
+ .onAppear {
+ polling = pendingOp.isLongpolling
+ liveliness = pendingOp.givesLifeness
+ isDue = pendingOp.isDue
+ }
+ }
+}
+
+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/",
+ isLongpolling: false,
+ givesLifeness: true,
+ isDue: false,
+ timestampDue: Timestamp(from:1669931055000))
+ static var previews: some View {
+ Form {
+ PendingOpView(pendingOp: pending1)
+ }
+ }
+}
diff --git a/TalerWallet1/Views/Pending/PendingOpsListView.swift
b/TalerWallet1/Views/Pending/PendingOpsListView.swift
new file mode 100644
index 0000000..30a5de3
--- /dev/null
+++ b/TalerWallet1/Views/Pending/PendingOpsListView.swift
@@ -0,0 +1,65 @@
+/*
+ * 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")
+ try? await reloadAction() // TODO: catch error
+ }
+ }
+}
+// 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 {
+ symLog?.log("refreshing")
+ try? await reloadAction() // TODO: catch error
+ }
+ }
+ }
+ }
+}
diff --git a/TalerWallet1/Views/Settings/SettingsItem.swift
b/TalerWallet1/Views/Settings/SettingsItem.swift
new file mode 100644
index 0000000..7735c97
--- /dev/null
+++ b/TalerWallet1/Views/Settings/SettingsItem.swift
@@ -0,0 +1,93 @@
+/*
+ * 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 SettingsItem<Content: View>: View {
+ var name: String
+ var description: String?
+ var content: () -> Content
+
+ init(name: String, description: String? = nil, @ViewBuilder content:
@escaping () -> Content) {
+ self.name = name
+ self.description = description
+ self.content = content
+ }
+
+ var body: some View {
+ HStack {
+ VStack {
+ Text(name)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .font(.title2)
+ .padding([.bottom], 0.01)
+ if let desc = description {
+ Text(desc)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .font(.caption)
+ }
+ }
+ content()
+ }.padding([.bottom], 4)
+ }
+}
+
+struct SettingsToggle: View {
+ var name: String
+ @Binding var value: Bool
+ var description: String?
+
+ var body: some View {
+ VStack {
+ Toggle(name, isOn: $value.animation(.spring()))
+ .font(.title2)
+ if let desc = description {
+ Text(desc)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .font(.caption)
+ }
+ }.padding([.bottom], 4)
+ }
+}
+
+
+
+struct SettingsItemPreview : View {
+ @State var developerMode: Bool = false
+
+ var body: some View {
+ VStack {
+ SettingsToggle(name: "Developer Mode", value: $developerMode,
description: "More information intended for debugging")
+ }
+ }
+}
+
+struct SettingsItem_Previews: PreviewProvider {
+ static var previews: some View {
+ List {
+ NavigationLink { } label: {
+ SettingsItem (name: "Exchanges", description: "Manage list of
exchanges known to this wallet") {}
+ }
+ SettingsItemPreview()
+ SettingsItem(name: "Save Logfile", description: "Help debugging
wallet-core") {
+ Button("Save") {
+ }
+ .buttonStyle(.bordered)
+ .disabled(true)
+ }
+ }
+ }
+}
diff --git a/TalerWallet1/Views/Settings/SettingsView.swift
b/TalerWallet1/Views/Settings/SettingsView.swift
new file mode 100644
index 0000000..a2f3ea5
--- /dev/null
+++ b/TalerWallet1/Views/Settings/SettingsView.swift
@@ -0,0 +1,139 @@
+/*
+ * 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
+
+/*
+ * 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
+ @AppStorage("developerMode") var developerMode: Bool = false
+ @AppStorage("developDelay") var developDelay: Bool = false
+
+ var showSidebar: () -> Void
+ init(showSidebar: @escaping () -> Void) {
+ self.showSidebar = showSidebar
+ }
+
+ @State private var checkDisabled = false
+ @State private var withDrawDisabled = false
+
+ var body: some View {
+ symLog { NavigationView {
+ List {
+ NavigationLink {
+ ExchangeListView(viewModel: controller.exchangeModel)
+ } label: {
+ SettingsItem(name: "Exchanges", description: "Manage list
of exchanges known to this wallet") {}
+ }
+ 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
+ })
+
+ SettingsItem(name: "Withdraw KUDOS", description: "Get
money for testing") {
+ Button("Withdraw") {
+ withDrawDisabled = true // don't run twice
+ let testModel: ExchangeTestModel =
ExchangeTestModel(walletCore: walletCore)
+ symLog.log("Withdrawing ")
+ testModel.loadTestKudos()
+ }
+ .buttonStyle(.bordered)
+ .disabled(withDrawDisabled)
+ }
+ SettingsItem(name: "Run Integration Test", description:
"Check if wallet-core works") {
+ Button("Check") {
+ checkDisabled = true // don't run twice
+ let testModel: ExchangeTestModel =
ExchangeTestModel(walletCore: walletCore)
+ symLog.log("running integration test ")
+ testModel.runIntegrationTest()
+ }
+ .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: "Used Bank") {
+ Text("\(walletCore.versionInfo!.bank)")
+ }
+ }
+ }
+ }
+ .navigationTitle("Settings")
+ .navigationBarItems(leading: HamburgerButton(action:
showSidebar))
+ } } // symLog
+ }
+}
+extension Bundle {
+ var releaseVersionNumber: String? {
+ return infoDictionary?["CFBundleShortVersionString"] as? String
+ }
+ var buildVersionNumber: String? {
+ return infoDictionary?["CFBundleVersion"] as? String
+ }
+ var releaseVersionNumberPretty: String {
+ return "v\(releaseVersionNumber ?? "1.0.0")"
+ }
+}
+
+struct SettingsView_Previews: PreviewProvider {
+ static var previews: some View {
+ SettingsView {
+
+ }
+ }
+}
diff --git a/TalerWallet1/Views/Transactions/TransactionDetail.swift
b/TalerWallet1/Views/Transactions/TransactionDetail.swift
new file mode 100644
index 0000000..84b442a
--- /dev/null
+++ b/TalerWallet1/Views/Transactions/TransactionDetail.swift
@@ -0,0 +1,79 @@
+/*
+ * 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 raw = transaction.amountRaw
+ let effective = transaction.amountEffective
+ let fee = try! Amount.diff(raw, effective)
+ let dateString = TalerDater.dateString(from: transaction.timestamp)
+
+ VStack() {
+ Spacer()
+ Text("\(dateString)")
+ .font(.title)
+ .fontWeight(.medium)
+ .padding(.bottom)
+ 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: "Obtained coins:",
+ value: effective.readableDescription, color:
Color("Incoming"))
+ .padding(.bottom)
+ if let baseURL = transaction.exchangeBaseUrl {
+ VStack {
+ Text("From exchange:")
+ .font(.title3)
+ Text("\(baseURL.trimURL())")
+ .font(.title)
+ .fontWeight(.medium)
+ }
+ .frame(maxWidth: .infinity, alignment: .center)
+ }
+ Spacer()
+ Button(role: .destructive, action: {
+ // TODO: delete from wallet-core
+ print("Should delete \(transaction.transactionId)")
+ }, label: {
+ HStack {
+ Text("Delete from list" + " ")
+ Image(systemName: "trash")
+ }
+ .font(.title)
+ .frame(maxWidth: .infinity)
+ })
+ .buttonStyle(.bordered)
+ .controlSize(.large)
+// Spacer()
+ }
+ }
+}
+
+#if DEBUG
+struct TransactionDetail_Previews: PreviewProvider {
+ static var transaction = Transaction(id:"some transActionID", time:
Timestamp(from: 1_666_000_000_000))
+ static var previews: some View {
+ TransactionDetail(transaction: transaction)
+ }
+}
+#endif
diff --git a/TalerWallet1/Views/Transactions/TransactionRow.swift
b/TalerWallet1/Views/Transactions/TransactionRow.swift
new file mode 100644
index 0000000..63df274
--- /dev/null
+++ b/TalerWallet1/Views/Transactions/TransactionRow.swift
@@ -0,0 +1,81 @@
+/*
+ * 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 amount = transaction.amountEffective
+ let withdraw: Bool = transaction.type == "withdrawal"
+ let payment: Bool = transaction.type == "payment"
+ let refund: Bool = transaction.type == "refund"
+ let incoming = withdraw || refund
+ let counterparty = transaction.exchangeBaseUrl
+ let dateString = TalerDater.dateString(from: transaction.timestamp,
relative: true)
+
+ HStack {
+ Image(systemName: incoming ? "text.badge.plus" :
"text.badge.minus")
+ .foregroundColor(incoming ? Color("Incoming") :
Color("Outgoing"))
+ .padding(.trailing)
+ .font(.largeTitle)
+
+ if withdraw {
+ if let baseURL = counterparty {
+ 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 transaction = Transaction(id:"some transActionID", time:
Timestamp(from: 1_666_000_000_000))
+ static var previews: some View {
+ TransactionRow(transaction: transaction)
+ }
+}
+#endif
diff --git a/TalerWallet1/Views/Transactions/TransactionsListView.swift
b/TalerWallet1/Views/Transactions/TransactionsListView.swift
new file mode 100644
index 0000000..8f9a6a3
--- /dev/null
+++ b/TalerWallet1/Views/Transactions/TransactionsListView.swift
@@ -0,0 +1,91 @@
+/*
+ * 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 TransactionsListView: View {
+ private let symLog = SymLogV()
+ let navTitle = "Transactions"
+
+ @ObservedObject var viewModel: TransactionsModel
+
+ var body: some View {
+ let reloadAction = viewModel.fetchTransactions
+ VStack {
+ if viewModel.transactions == nil {
+ symLog { LoadingView(backButtonHidden: false) }
+ } else {
+ symLog { 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)
+ }
+ }
+ }
+}
+// MARK: -
+extension TransactionsListView {
+ struct Content: View {
+ let symLog: SymLogV?
+ @ObservedObject var viewModel: TransactionsModel
+
+ var reloadAction: () async throws -> ()
+
+ var body: some View {
+ let transactions = viewModel.transactions!
+ List(transactions, id: \.transactionId) { transaction in
+ NavigationLink {
+ TransactionDetail(transaction: transaction)
+ } label: {
+ TransactionRow(transaction: transaction)
+ }
+ .swipeActions(edge: .leading, allowsFullSwipe: true) {
+ Button {
+ symLog?.log("bookmarked
\(transaction.transactionId)")
+ // TODO: Bookmark
+ } label: {
+ Label("Bookmark", systemImage: "bookmark")
+ }.tint(.indigo)
+ }
+ .swipeActions(edge: .trailing, allowsFullSwipe: true) {
+ Button(role: .destructive) {
+ symLog?.log("deleted \(transaction.transactionId)")
+ // TODO: delete from Model. SwiftUI deletes this
row from view already :-)
+ } label: {
+ Label("Delete", systemImage: "trash")
+ }
+ }
+ }
+ .navigationBarTitleDisplayMode(.large) // .inline
+ .refreshable {
+ symLog?.log("refreshing")
+ try? await reloadAction() // TODO: catch error
+ }
+ }
+ }
+}
+//struct TransactionsView_Previews: PreviewProvider {
+// static var previews: some View {
+// TransactionsView()
+// }
+//}
diff --git a/TalerWallet1/Views/Transactions/TransactionsModel.swift
b/TalerWallet1/Views/Transactions/TransactionsModel.swift
new file mode 100644
index 0000000..9786c1e
--- /dev/null
+++ b/TalerWallet1/Views/Transactions/TransactionsModel.swift
@@ -0,0 +1,69 @@
+/*
+ * 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
+}
+//extension Transaction {
+// func exchangeBaseUrl() -> String {
+// switch detail {
+// case .withdrawal(let transactionWithdrawal):
+// return transactionWithdrawal.exchangeBaseUrl
+// }
+// }
+//}
+
+// 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
new file mode 100644
index 0000000..1aaab6b
--- /dev/null
+++ b/TalerWallet1/Views/URLSheet.swift
@@ -0,0 +1,64 @@
+/*
+ * 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
new file mode 100644
index 0000000..becc6dd
--- /dev/null
+++ b/TalerWallet1/Views/Withdraw/WithdrawAcceptView.swift
@@ -0,0 +1,71 @@
+/*
+ * 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! raw - effective
+ 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/Taler/Model/BackendManager.swift
b/TalerWallet1/Views/Withdraw/WithdrawProgressView.swift
similarity index 50%
rename from Taler/Model/BackendManager.swift
rename to TalerWallet1/Views/Withdraw/WithdrawProgressView.swift
index d0c262b..2c08b75 100644
--- a/Taler/Model/BackendManager.swift
+++ b/TalerWallet1/Views/Withdraw/WithdrawProgressView.swift
@@ -13,18 +13,33 @@
* 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 Foundation
+struct WithdrawProgressView: View {
+ let message: String
+ let action: () -> Void
-class BackendManager: ObservableObject {
- var backend: WalletBackend
-
- @Published var exchangeManager: ExchangeManager
- @Published var pendingManager: PendingManager
-
- init() {
- self.backend = try! WalletBackend()
- self.exchangeManager = ExchangeManager(_backend: self.backend)
- self.pendingManager = PendingManager(_backend: self.backend)
+ 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
new file mode 100644
index 0000000..b1c3cf6
--- /dev/null
+++ b/TalerWallet1/Views/Withdraw/WithdrawTOSView.swift
@@ -0,0 +1,96 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under
the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import SwiftUI
+import SymLog
+
+struct WithdrawTOSView: View {
+ private let symLog = SymLogV()
+ var url: URL
+ @ObservedObject var model: WithdrawURIModel
+
+// @Environment(\.dismiss) var dismiss // call dismiss() to get rid of
the sheet
+ let navTitle = "Terms of Service"
+ var cancelButton: some View {
+ Button("Cancel3") { dismissTop() }
+ }
+
+ var detailsForUri: WithdrawalDetailsForUri
+ @State var exchangeTOS: ExchangeTermsOfService?
+ @Binding var didAcceptTOS: Bool
+
+ var body: some View {
+ let badURL = "Error in URL: \(url)"
+ let baseURL = detailsForUri.defaultExchangeBaseUrl
+ VStack {
+ switch model.withdrawState {
+ case .waitingForTOS:
+ WithdrawProgressView(message: baseURL ?? badURL) {
+ dismissTop()
+ }.navigationTitle("Loading " + navTitle)
+ case .receivedTOS:
+ Content(symLog: symLog, exchangeTOS: exchangeTOS) {
+ Task {
+ do {
+ _ = try await
model.setExchangeTOSAccepted(baseURL!, etag: exchangeTOS!.currentEtag)
+ didAcceptTOS = true
+ } catch {
+ // TODO: Show Error
+ symLog.log(error.localizedDescription)
+ }
+ }
+ }
+ .navigationBarTitleDisplayMode(.large) // .inline
+ .navigationBarItems(leading: cancelButton)
+ .navigationTitle(navTitle)
+ default:
+ ErrorView()
+ }
+ }.task {
+ do {
+ let someTOS = try await
model.loadExchangeTermsOfService(baseURL!)
+ exchangeTOS = someTOS
+ } catch {
+ // TODO: error
+ symLog.log(error.localizedDescription)
+ }
+ }
+ }
+}
+// MARK: -
+extension WithdrawTOSView {
+ struct Content: View {
+ let symLog: SymLogV
+ var exchangeTOS: ExchangeTermsOfService?
+ var acceptAction: () -> ()
+
+ var body: some View {
+ Group {
+ if let tos = exchangeTOS {
+ let components = tos.content.components(separatedBy:"\n\n")
+
+ List (components, id: \.self) { term in
+ Text(term)
+ }
+ AwesomeButton(title: "Accept") {
+ acceptAction()
+ }.padding(.vertical)
+ } else {
+ ErrorView() // TODO: ???
+ }
+ }
+ }
+ }
+}
diff --git a/TalerWallet1/Views/Withdraw/WithdrawURIModel.swift
b/TalerWallet1/Views/Withdraw/WithdrawURIModel.swift
new file mode 100644
index 0000000..f375a1d
--- /dev/null
+++ b/TalerWallet1/Views/Withdraw/WithdrawURIModel.swift
@@ -0,0 +1,213 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under
the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import Foundation
+import taler_swift
+import SymLog
+fileprivate let ASYNCDELAY: UInt = 0 //set e.g to 6 or 9 seconds for
debugging
+
+enum WithdrawState {
+ case error
+ case waitingForUriDetails
+ case receivedUriDetails
+ case waitingForAmountDetails
+ case receivedAmountDetails
+ case waitingForTOS
+ case receivedTOS
+ case waitingForTOSAck
+ case receivedTOSAck
+ case waitingForWithdrAck
+ case receivedWithdrAck
+}
+
+class WithdrawURIModel: WalletModel {
+ @Published var withdrawState: WithdrawState?
+}
+
+// MARK: -
+/// The result from getWithdrawalDetailsForUri
+struct WithdrawalDetailsForUri: Decodable {
+ var amount: Amount
+ var defaultExchangeBaseUrl: String?
+ var possibleExchanges: [ExchangeListItem]
+}
+struct ExchangeListItem: Codable, Hashable {
+ var exchangeBaseUrl: String
+ var currency: String
+ var paytoUris: [String]
+
+ public static func == (lhs: ExchangeListItem, rhs: ExchangeListItem) ->
Bool {
+ return lhs.exchangeBaseUrl == rhs.exchangeBaseUrl &&
+ lhs.currency == rhs.currency &&
+ lhs.paytoUris == rhs.paytoUris
+ }
+
+ public func hash(into hasher: inout Hasher) {
+ hasher.combine(exchangeBaseUrl)
+ hasher.combine(currency)
+ hasher.combine(paytoUris)
+ }
+}
+/// A request to get an exchange's withdrawal details.
+fileprivate struct GetWithdrawalDetailsForURI: WalletBackendFormattedRequest {
+ typealias Response = WithdrawalDetailsForUri
+ func operation() -> String { return "getWithdrawalDetailsForUri" }
+ func args() -> Args { return Args(talerWithdrawUri: talerWithdrawUri) }
+
+ var talerWithdrawUri: String
+ struct Args: Encodable {
+ var talerWithdrawUri: String
+ }
+}
+// MARK: -
+/// The result from getWithdrawalDetailsForAmount
+struct WithdrawalDetailsForAmount: Decodable {
+ var tosAccepted: Bool
+ var amountRaw: Amount
+ var amountEffective: Amount
+}
+/// A request to get an exchange's withdrawal details.
+fileprivate struct GetWithdrawalDetailsForAmount:
WalletBackendFormattedRequest {
+ typealias Response = WithdrawalDetailsForAmount
+ func operation() -> String { return "getWithdrawalDetailsForAmount" }
+ func args() -> Args { return Args(exchangeBaseUrl: exchangeBaseUrl,
amount: amount) }
+
+ var exchangeBaseUrl: String
+ var amount: Amount
+ struct Args: Encodable {
+ var exchangeBaseUrl: String
+ var amount: Amount
+ }
+}
+// MARK: -
+struct ExchangeTermsOfService: Decodable {
+ var content: String
+ var currentEtag: String
+ var acceptedEtag: String?
+}
+/// A request to query an exchange's terms of service.
+fileprivate struct GetExchangeTermsOfService: WalletBackendFormattedRequest {
+ typealias Response = ExchangeTermsOfService
+ func operation() -> String { return "getExchangeTos" }
+ func args() -> Args { return Args(exchangeBaseUrl: exchangeBaseUrl) }
+
+ var exchangeBaseUrl: String
+ struct Args: Encodable {
+ var exchangeBaseUrl: String
+ }
+}
+// MARK: -
+/// A request to mark an exchange's terms of service as accepted.
+fileprivate struct SetExchangeTOSAccepted: WalletBackendFormattedRequest {
+ struct Response: Decodable {}
+ func operation() -> String { return "setExchangeTosAccepted" }
+ func args() -> Args { return Args(exchangeBaseUrl: exchangeBaseUrl, etag:
etag) }
+
+ var exchangeBaseUrl: String
+ var etag: String
+
+ struct Args: Encodable {
+ var exchangeBaseUrl: String
+ var etag: String
+ }
+}
+// MARK: -
+struct BankConfirmation: Decodable {
+ var bankConfirmationUrl: String?
+}
+/// A request to accept a bank-integrated withdrawl.
+fileprivate struct AcceptBankIntegratedWithdrawal:
WalletBackendFormattedRequest {
+ typealias Response = BankConfirmation
+ func operation() -> String { return "acceptBankIntegratedWithdrawal" }
+ func args() -> Args { return Args(talerWithdrawUri: talerWithdrawUri,
exchangeBaseUrl: exchangeBaseUrl) }
+
+ var talerWithdrawUri: String
+ var exchangeBaseUrl: String
+
+ struct Args: Encodable {
+ var talerWithdrawUri: String
+ var exchangeBaseUrl: String
+ }
+}
+// MARK: -
+extension WithdrawURIModel {
+ /// load withdrawal details. Networking involved
+ @MainActor
+ func loadWithdrawalDetailsForURI(_ talerWithdrawUri: String) async throws
-> WithdrawalDetailsForUri {
+ do {
+ withdrawState = .waitingForUriDetails
+ let request = GetWithdrawalDetailsForURI(talerWithdrawUri:
talerWithdrawUri)
+ let response = try await sendRequest(request, ASYNCDELAY)
+ withdrawState = .receivedUriDetails
+ return response
+ } catch {
+ withdrawState = .error
+ throw error
+ }
+ }
+ @MainActor
+ func loadWithdrawalDetailsForAmount(_ detailsForUri:
WithdrawalDetailsForUri) async throws -> WithdrawalDetailsForAmount {
+ do {
+ withdrawState = .waitingForAmountDetails
+ let baseURL = detailsForUri.defaultExchangeBaseUrl!
+ let request = GetWithdrawalDetailsForAmount(exchangeBaseUrl:
baseURL, amount: detailsForUri.amount)
+ let response = try await sendRequest(request, ASYNCDELAY)
+ withdrawState = .receivedAmountDetails
+ return response
+ } catch {
+ withdrawState = .error
+ throw error
+ }
+ }
+ @MainActor
+ func loadExchangeTermsOfService(_ exchangeBaseUrl: String) async throws ->
ExchangeTermsOfService {
+ do {
+ withdrawState = .waitingForTOS
+ let request = GetExchangeTermsOfService(exchangeBaseUrl:
exchangeBaseUrl)
+ let response = try await sendRequest(request, ASYNCDELAY)
+ withdrawState = .receivedTOS
+ return response
+ } catch {
+ withdrawState = .error
+ throw error
+ }
+ }
+ @MainActor
+ func setExchangeTOSAccepted(_ exchangeBaseUrl: String, etag: String) async
throws -> Decodable {
+ do {
+ withdrawState = .waitingForTOSAck
+ let request = SetExchangeTOSAccepted(exchangeBaseUrl:
exchangeBaseUrl, etag: etag)
+ let response = try await sendRequest(request, ASYNCDELAY)
+ withdrawState = .receivedTOSAck
+ return response
+ } catch {
+ withdrawState = .error
+ throw error
+ }
+ }
+ @MainActor
+ func sendAcceptIntWithdrawal(_ exchangeBaseUrl: String, withdrawURL:
String) async throws -> String? {
+ do {
+ withdrawState = .waitingForWithdrAck
+ let request = AcceptBankIntegratedWithdrawal(talerWithdrawUri:
withdrawURL, exchangeBaseUrl: exchangeBaseUrl)
+ let response = try await sendRequest(request, ASYNCDELAY)
+ withdrawState = .receivedWithdrAck
+ return response.bankConfirmationUrl
+ } catch {
+ withdrawState = .error
+ throw error
+ }
+ }
+}
diff --git a/TalerWallet1/Views/Withdraw/WithdrawURIView.swift
b/TalerWallet1/Views/Withdraw/WithdrawURIView.swift
new file mode 100644
index 0000000..939c24d
--- /dev/null
+++ b/TalerWallet1/Views/Withdraw/WithdrawURIView.swift
@@ -0,0 +1,103 @@
+/*
+ * 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:
\(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
+// }
+ }
+ }
+ }
+}
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.
- [taler-taler-ios] branch master updated (78ef82c -> df4fe35), gnunet, 2023/02/01
- [taler-taler-ios] 02/09: removed iono, gnunet, 2023/02/01
- [taler-taler-ios] 03/09: most of Info.plist moved into the project, gnunet, 2023/02/01
- [taler-taler-ios] 04/09: Assets, gnunet, 2023/02/01
- [taler-taler-ios] 01/09: enhanced Amount+Time, gnunet, 2023/02/01
- [taler-taler-ios] 05/09: Testing, gnunet, 2023/02/01
- [taler-taler-ios] 07/09: Project uses QuickJS instead of iono, gnunet, 2023/02/01
- [taler-taler-ios] 06/09: App sources,
gnunet <=
- [taler-taler-ios] 08/09: more to ignore..., gnunet, 2023/02/01
- [taler-taler-ios] 09/09: Build instructions, gnunet, 2023/02/01