taler-ios

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

commit 7db23acf4f69f07a0fda302b4909284741401fe9
parent 17a428ec093c5f1a426c0041f95be05b612ca7fe
Author: Jonathan Buchanan <jonathan.russ.buchanan@gmail.com>
Date:   Mon,  8 Aug 2022 17:59:59 -0400

basic exchange list

Diffstat:
MTaler.xcodeproj/project.pbxproj | 10++++++++++
ATaler/BackendManager.swift | 28++++++++++++++++++++++++++++
MTaler/ContentView.swift | 4+++-
ATaler/ExchangeManager.swift | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
MTaler/SettingsView.swift | 227++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
MTaler/WalletBackend.swift | 7++++---
6 files changed, 319 insertions(+), 12 deletions(-)

diff --git a/Taler.xcodeproj/project.pbxproj b/Taler.xcodeproj/project.pbxproj @@ -10,6 +10,8 @@ AB1F87C82887C94700AB82A0 /* TalerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1F87C72887C94700AB82A0 /* TalerApp.swift */; }; AB1F87CA2887D2F400AB82A0 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1F87C92887D2F400AB82A0 /* ContentView.swift */; }; AB8C3807286A88A600E0A1DD /* WalletBackendTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB8C3806286A88A500E0A1DD /* WalletBackendTests.swift */; }; + ABB33065289C5BBB00668B42 /* ExchangeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABB33064289C5BBB00668B42 /* ExchangeManager.swift */; }; + ABB33067289C658900668B42 /* BackendManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABB33066289C658900668B42 /* BackendManager.swift */; }; ABB762AD2891059600E88634 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABB762AC2891059600E88634 /* SettingsView.swift */; }; ABC13AA32859962800D23185 /* taler-swift in Frameworks */ = {isa = PBXBuildFile; productRef = ABC13AA22859962800D23185 /* taler-swift */; }; ABE97B1D286D82BF00580772 /* AnyCodable in Frameworks */ = {isa = PBXBuildFile; productRef = ABE97B1C286D82BF00580772 /* AnyCodable */; }; @@ -56,6 +58,8 @@ AB1F87C92887D2F400AB82A0 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; }; AB710490285995B6008B04F0 /* taler-swift */ = {isa = PBXFileReference; lastKnownFileType = text; path = "taler-swift"; sourceTree = SOURCE_ROOT; }; AB8C3806286A88A500E0A1DD /* WalletBackendTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletBackendTests.swift; sourceTree = "<group>"; }; + ABB33064289C5BBB00668B42 /* ExchangeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExchangeManager.swift; sourceTree = "<group>"; }; + ABB33066289C658900668B42 /* BackendManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackendManager.swift; sourceTree = "<group>"; }; ABB762AC2891059600E88634 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; }; D11250FF26B12E3200D02E00 /* taler-wallet-embedded.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = "taler-wallet-embedded.js"; sourceTree = "<group>"; }; D14AFD1D24D232B300C51073 /* Taler.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Taler.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -136,6 +140,8 @@ D14AFD2E24D232B500C51073 /* Info.plist */, AB1F87C72887C94700AB82A0 /* TalerApp.swift */, ABB762AC2891059600E88634 /* SettingsView.swift */, + ABB33064289C5BBB00668B42 /* ExchangeManager.swift */, + ABB33066289C658900668B42 /* BackendManager.swift */, ); path = Taler; sourceTree = "<group>"; @@ -349,6 +355,8 @@ files = ( AB1F87C82887C94700AB82A0 /* TalerApp.swift in Sources */, AB1F87CA2887D2F400AB82A0 /* ContentView.swift in Sources */, + ABB33067289C658900668B42 /* BackendManager.swift in Sources */, + ABB33065289C5BBB00668B42 /* ExchangeManager.swift in Sources */, D1D65B9826992E4600C1012A /* WalletBackend.swift in Sources */, ABB762AD2891059600E88634 /* SettingsView.swift in Sources */, D14CE1B426C3A2D400612DBE /* BalanceList.swift in Sources */, @@ -535,6 +543,7 @@ "$(PROJECT_DIR)/iono/ios-node-v8/deps/uv/include", ); INFOPLIST_FILE = Taler/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -608,6 +617,7 @@ "$(PROJECT_DIR)/iono/ios-node-v8/deps/uv/include", ); INFOPLIST_FILE = Taler/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Taler/BackendManager.swift b/Taler/BackendManager.swift @@ -0,0 +1,28 @@ +/* + * This file is part of GNU Taler + * (C) 2022 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import Foundation + +class BackendManager: ObservableObject { + var backend: WalletBackend + + @Published var exchangeManager: ExchangeManager + + init() { + self.backend = try! WalletBackend() + self.exchangeManager = ExchangeManager(_backend: self.backend) + } +} diff --git a/Taler/ContentView.swift b/Taler/ContentView.swift @@ -22,6 +22,8 @@ struct SidebarItem { } struct ContentView: View { + @StateObject var backend: BackendManager = BackendManager() + @State var sidebarVisible: Bool = false var views: [SidebarItem] {[ SidebarItem(name: "Main", @@ -33,7 +35,7 @@ struct ContentView: View { SidebarItem(name: "Settings", view: AnyView(SettingsView { self.sidebarVisible = true - })) + }.environmentObject(backend))) ]} @State var currentView: Int = 0 diff --git a/Taler/ExchangeManager.swift b/Taler/ExchangeManager.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 + +typealias ExchangeItem = WalletBackendListExchanges.ExchangeListItem + +class ExchangeManager: ObservableObject { + var backend: WalletBackend + + 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 + self.loading = false + if let result = response { + // TODO: Use Combine instead. + DispatchQueue.main.async { + 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: Show error. + } + } +} diff --git a/Taler/SettingsView.swift b/Taler/SettingsView.swift @@ -1,18 +1,229 @@ -// -// SettingsView.swift -// Taler -// -// Created by Jonathan Buchanan on 7/27/22. -// Copyright © 2022 Taler. All rights reserved. -// +/* + * 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 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 ExchangeItemView: View { + var exchange: ExchangeItem + var body: some View { + VStack { + Text(exchange.exchangeBaseUrl) + Text("Currency: " + exchange.currency) + } + } +} + +struct ExchangeListView: View { + @ObservedObject var exchangeManager: ExchangeManager + @State var showPopup: Bool = false + + var body: some View { + if 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 + ExchangeItemView(exchange: exchange) + } + .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 { + ProgressView() + .navigationTitle("Exchanges") + .onAppear { + exchangeManager.updateList() + } + } + } +} + +/* + * 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 { - Text("Settings") + 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: { diff --git a/Taler/WalletBackend.swift b/Taler/WalletBackend.swift @@ -296,7 +296,7 @@ struct WalletBackendListExchanges: WalletBackendFormattedRequest { } - struct ExchangeListItem: Decodable { + struct ExchangeListItem: Decodable, Hashable { var exchangeBaseUrl: String var currency: String var paytoUris: [String] @@ -316,7 +316,7 @@ struct WalletBackendListExchanges: WalletBackendFormattedRequest { } /// A request to add an exchange. -struct WalletBackendAddRequest: WalletBackendFormattedRequest { +struct WalletBackendAddExchangeRequest: WalletBackendFormattedRequest { var exchangeBaseUrl: String struct Args: Encodable { @@ -328,7 +328,7 @@ struct WalletBackendAddRequest: WalletBackendFormattedRequest { } func operation() -> String { - return "addRequest" + return "addExchange" } func args() -> Args { @@ -875,6 +875,7 @@ class WalletBackend: IonoMessageHandler { } func handleMessage(message: String) { + //print("got message: \(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]