gnunet-svn
[Top][All Lists]
Advanced

[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.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]