summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Taler.xcodeproj/project.pbxproj44
-rw-r--r--Taler/Model/ExchangeManager.swift5
-rw-r--r--Taler/Model/WithdrawModel.swift107
-rw-r--r--Taler/Views/SettingsView.swift100
-rw-r--r--Taler/WalletBackend.swift10
-rw-r--r--taler-swift/Sources/taler-swift/Amount.swift25
6 files changed, 238 insertions, 53 deletions
diff --git a/Taler.xcodeproj/project.pbxproj b/Taler.xcodeproj/project.pbxproj
index cf2bdb7..3c5ceba 100644
--- a/Taler.xcodeproj/project.pbxproj
+++ b/Taler.xcodeproj/project.pbxproj
@@ -9,16 +9,17 @@
/* Begin PBXBuildFile section */
AB1F87C82887C94700AB82A0 /* TalerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1F87C72887C94700AB82A0 /* TalerApp.swift */; };
AB1F87CA2887D2F400AB82A0 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1F87C92887D2F400AB82A0 /* ContentView.swift */; };
+ AB69F9FA28AAED53005CCC2E /* WithdrawModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB69F9F928AAED53005CCC2E /* WithdrawModel.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 */; };
+ ABC4AC3B28A4619C0047A56F /* PendingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC4AC3A28A4619C0047A56F /* PendingView.swift */; };
+ ABC4AC3F28A473070047A56F /* PendingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC4AC3E28A473070047A56F /* PendingManager.swift */; };
ABE97B1D286D82BF00580772 /* AnyCodable in Frameworks */ = {isa = PBXBuildFile; productRef = ABE97B1C286D82BF00580772 /* AnyCodable */; };
D112510026B12E3200D02E00 /* taler-wallet-embedded.js in CopyFiles */ = {isa = PBXBuildFile; fileRef = D11250FF26B12E3200D02E00 /* taler-wallet-embedded.js */; };
D14AFD4324D232B500C51073 /* TalerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D14AFD4224D232B500C51073 /* TalerUITests.swift */; };
- D14CE1B226C39E5D00612DBE /* BalanceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D14CE1B126C39E5D00612DBE /* BalanceRow.swift */; };
- D14CE1B426C3A2D400612DBE /* BalanceList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D14CE1B326C3A2D400612DBE /* BalanceList.swift */; };
D1AFF0F3268D59C200FBB744 /* libiono.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D1AFF0F2268D59A500FBB744 /* libiono.a */; };
D1D65B9826992E4600C1012A /* WalletBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1D65B9726992E4600C1012A /* WalletBackend.swift */; };
/* End PBXBuildFile section */
@@ -56,11 +57,14 @@
/* Begin PBXFileReference section */
AB1F87C72887C94700AB82A0 /* TalerApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TalerApp.swift; sourceTree = "<group>"; };
AB1F87C92887D2F400AB82A0 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
+ AB69F9F928AAED53005CCC2E /* WithdrawModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WithdrawModel.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>"; };
+ ABC4AC3A28A4619C0047A56F /* PendingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PendingView.swift; sourceTree = "<group>"; };
+ ABC4AC3E28A473070047A56F /* PendingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PendingManager.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; };
D14AFD2624D232B500C51073 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@@ -71,8 +75,6 @@
D14AFD3E24D232B500C51073 /* TalerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TalerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
D14AFD4224D232B500C51073 /* TalerUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TalerUITests.swift; sourceTree = "<group>"; };
D14AFD4424D232B500C51073 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
- D14CE1B126C39E5D00612DBE /* BalanceRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceRow.swift; sourceTree = "<group>"; };
- D14CE1B326C3A2D400612DBE /* BalanceList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceList.swift; sourceTree = "<group>"; };
D1AFF0F2268D59A500FBB744 /* libiono.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libiono.a; path = iono/compiled/x64/libiono.a; sourceTree = "<group>"; };
D1D65B9726992E4600C1012A /* WalletBackend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletBackend.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -105,6 +107,27 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
+ ABC4AC3C28A470C40047A56F /* Views */ = {
+ isa = PBXGroup;
+ children = (
+ AB1F87C92887D2F400AB82A0 /* ContentView.swift */,
+ ABB762AC2891059600E88634 /* SettingsView.swift */,
+ ABC4AC3A28A4619C0047A56F /* PendingView.swift */,
+ );
+ path = Views;
+ sourceTree = "<group>";
+ };
+ ABC4AC3D28A4729E0047A56F /* Model */ = {
+ isa = PBXGroup;
+ children = (
+ ABB33066289C658900668B42 /* BackendManager.swift */,
+ ABB33064289C5BBB00668B42 /* ExchangeManager.swift */,
+ ABC4AC3E28A473070047A56F /* PendingManager.swift */,
+ AB69F9F928AAED53005CCC2E /* WithdrawModel.swift */,
+ );
+ path = Model;
+ sourceTree = "<group>";
+ };
D14AFD1424D232B300C51073 = {
isa = PBXGroup;
children = (
@@ -131,17 +154,13 @@
D14AFD1F24D232B300C51073 /* Taler */ = {
isa = PBXGroup;
children = (
+ ABC4AC3D28A4729E0047A56F /* Model */,
+ ABC4AC3C28A470C40047A56F /* Views */,
D1D65B9726992E4600C1012A /* WalletBackend.swift */,
- D14CE1B126C39E5D00612DBE /* BalanceRow.swift */,
- D14CE1B326C3A2D400612DBE /* BalanceList.swift */,
- AB1F87C92887D2F400AB82A0 /* ContentView.swift */,
D14AFD2624D232B500C51073 /* Assets.xcassets */,
D14AFD2B24D232B500C51073 /* LaunchScreen.storyboard */,
D14AFD2E24D232B500C51073 /* Info.plist */,
AB1F87C72887C94700AB82A0 /* TalerApp.swift */,
- ABB762AC2891059600E88634 /* SettingsView.swift */,
- ABB33064289C5BBB00668B42 /* ExchangeManager.swift */,
- ABB33066289C658900668B42 /* BackendManager.swift */,
);
path = Taler;
sourceTree = "<group>";
@@ -356,11 +375,12 @@
AB1F87C82887C94700AB82A0 /* TalerApp.swift in Sources */,
AB1F87CA2887D2F400AB82A0 /* ContentView.swift in Sources */,
ABB33067289C658900668B42 /* BackendManager.swift in Sources */,
+ AB69F9FA28AAED53005CCC2E /* WithdrawModel.swift in Sources */,
ABB33065289C5BBB00668B42 /* ExchangeManager.swift in Sources */,
D1D65B9826992E4600C1012A /* WalletBackend.swift in Sources */,
ABB762AD2891059600E88634 /* SettingsView.swift in Sources */,
- D14CE1B426C3A2D400612DBE /* BalanceList.swift in Sources */,
- D14CE1B226C39E5D00612DBE /* BalanceRow.swift in Sources */,
+ ABC4AC3B28A4619C0047A56F /* PendingView.swift in Sources */,
+ ABC4AC3F28A473070047A56F /* PendingManager.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
diff --git a/Taler/Model/ExchangeManager.swift b/Taler/Model/ExchangeManager.swift
index 003d2eb..ca4942e 100644
--- a/Taler/Model/ExchangeManager.swift
+++ b/Taler/Model/ExchangeManager.swift
@@ -15,6 +15,7 @@
*/
import Foundation
+import taler_swift
typealias ExchangeItem = WalletBackendListExchanges.ExchangeListItem
@@ -61,4 +62,8 @@ class ExchangeManager: ObservableObject {
}
self.loading = true
}
+
+ func withdraw(exchange: ExchangeItem) -> WithdrawModel {
+ return WithdrawModel(backend: self.backend, exchange: exchange)
+ }
}
diff --git a/Taler/Model/WithdrawModel.swift b/Taler/Model/WithdrawModel.swift
new file mode 100644
index 0000000..f423923
--- /dev/null
+++ b/Taler/Model/WithdrawModel.swift
@@ -0,0 +1,107 @@
+/*
+ * 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
+
+class WithdrawModel: ObservableObject {
+ enum State {
+ case begin
+ case loading
+ case prompt(rawAmount: Amount,
+ effectiveAmount: Amount)
+ case promptTOS(rawAmount: Amount,
+ effectiveAmount: Amount,
+ tos: String,
+ etag: String)
+ }
+
+ var backend: WalletBackend
+ let exchange: ExchangeItem
+ @Published var state: State
+
+ init(backend: WalletBackend, exchange: ExchangeItem) {
+ self.backend = backend
+ self.exchange = exchange
+ self.state = .begin
+ }
+
+ func getWithdrawDetails(amountStr: String) {
+ self.state = .loading
+ 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 {
+ if res.tosAccepted {
+ self.state = .prompt(rawAmount: res.amountRaw, effectiveAmount: res.amountEffective)
+ } else {
+ self.getTos(rawAmount: res.amountRaw, effectiveAmount: res.amountEffective)
+ }
+ } else {
+ self.state = .begin
+ // TODO: Show error.
+ }
+ }
+ }
+ } catch {
+ self.state = .begin
+ // TODO: Show error.
+ }
+ }
+
+ private func getTos(rawAmount: Amount, effectiveAmount: Amount) {
+ self.state = .loading
+ let req = WalletBackendGetExchangeTermsOfService(exchangeBaseUrl: exchange.exchangeBaseUrl)
+ backend.sendFormattedRequest(request: req) { response, err in
+ // TODO: Use Combine instead
+ DispatchQueue.main.async {
+ if let res = response {
+ self.state = .promptTOS(rawAmount: rawAmount,
+ effectiveAmount: effectiveAmount,
+ tos: res.content,
+ etag: res.currentEtag)
+ } else {
+ self.state = .begin
+ // TODO: Show error.
+ }
+ }
+ }
+ }
+
+ func acceptTos() {
+ let oldState = self.state
+ self.state = .loading
+ switch oldState {
+ case .promptTOS(let rawAmount, let effectiveAmount, let tos, let etag):
+ let req = WalletBackendSetExchangeTermsOfServiceAccepted(exchangeBaseUrl: exchange.exchangeBaseUrl,
+ acceptedEtag: etag)
+ backend.sendFormattedRequest(request: req) { response, err in
+ // TODO: Use Combine instead
+ DispatchQueue.main.async {
+ self.state = oldState
+ // TODO: Handle error.
+ }
+ }
+ default:
+ self.state = oldState
+ // TODO: Show error.
+ }
+ }
+}
diff --git a/Taler/Views/SettingsView.swift b/Taler/Views/SettingsView.swift
index 63c1f3d..bb25587 100644
--- a/Taler/Views/SettingsView.swift
+++ b/Taler/Views/SettingsView.swift
@@ -65,46 +65,80 @@ extension View {
}
}
-struct PromptWithdrawView: View {
- let exchange: ExchangeItem
- let amount: Amount
-
- var body: some View {
- VStack {
- Text("Fees or something")
- }
- .navigationTitle("Withdraw Digital Cash")
- }
-}
-
struct WithdrawView: View {
- let exchange: ExchangeItem
+ @ObservedObject var model: WithdrawModel
@State var amount: String = ""
var body: some View {
- VStack {
- Button {
-
- } label: {
- Text("Scan Taler QR Code")
+ switch model.state {
+ case .begin:
+ VStack {
+ 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")
+ }
}
- Text("Or transfer manually:")
- HStack {
- TextField(exchange.currency, text: $amount)
+ .navigationTitle("Withdraw")
+ case .loading:
+ ProgressView()
+ .navigationTitle("Withdraw")
+ case .prompt(let rawAmount, let effectiveAmount):
+ VStack {
+ Text("Withdraw")
+ Text(effectiveAmount.readableDescription)
+ Text("Chosen Amount")
+ Text(rawAmount.readableDescription)
+ Text("Fee")
+ Text("- \((try! rawAmount - effectiveAmount).readableDescription)")
+ Text("Exchange")
+ Text(model.exchange.name)
+ Button {
+ // TODO
+ } label: {
+ Text("Confirm Withdraw")
+ }
}
- NavigationLink {
- // TODO: Handle when the user inputs a non-valid amount
- /*do {
- let am = try Amount.init(fromString: exchange.currency + ":" + amount)
- PromptWithdrawView(exchange: exchange, amount: am)
- } catch {
-
- }*/
- } label: {
- Text("Check Fees")
+ .navigationTitle("Withdraw")
+ case .promptTOS(let rawAmount, let effectiveAmount, let tos, let etag):
+ VStack {
+ Text("Withdraw")
+ Text(effectiveAmount.readableDescription)
+ Text("Chosen Amount")
+ Text(rawAmount.readableDescription)
+ Text("Fee")
+ Text("- \((try! rawAmount - effectiveAmount).readableDescription)")
+ Text("Exchange")
+ Text(model.exchange.name)
+ NavigationLink {
+ VStack {
+ ScrollView {
+ Text(tos)
+ }
+ Button {
+ model.acceptTos()
+ } label: {
+ Text("Accept Terms of Service")
+ }
+
+ }
+ .navigationTitle("Review Terms of Service")
+ } label: {
+ Text("Review Terms")
+ }
}
+ .navigationTitle("Withdraw")
}
- .navigationTitle("Withdraw")
}
}
@@ -149,7 +183,7 @@ struct ExchangeListView: View {
Text("Currency: " + exchange.currency)
.frame(maxWidth: .infinity)
NavigationLink {
- WithdrawView(exchange: exchange)
+ WithdrawView(model: exchangeManager.withdraw(exchange: exchange))
} label: {
Text("Withdraw")
}
diff --git a/Taler/WalletBackend.swift b/Taler/WalletBackend.swift
index a3f52e6..e440173 100644
--- a/Taler/WalletBackend.swift
+++ b/Taler/WalletBackend.swift
@@ -300,6 +300,11 @@ struct WalletBackendListExchanges: WalletBackendFormattedRequest {
var exchangeBaseUrl: String
var currency: String
var paytoUris: [String]
+
+ var name: String {
+ let url = URL(string: exchangeBaseUrl)!
+ return url.host!
+ }
}
struct Response: Decodable {
@@ -366,9 +371,9 @@ struct WalletBackendGetExchangeTermsOfService: WalletBackendFormattedRequest {
}
struct Response: Decodable {
- var tos: String
+ var content: String
var currentEtag: String
- var acceptedEtag: String
+ var acceptedEtag: String?
}
func operation() -> String {
@@ -896,7 +901,6 @@ 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]
diff --git a/taler-swift/Sources/taler-swift/Amount.swift b/taler-swift/Sources/taler-swift/Amount.swift
index e480575..62e1d7b 100644
--- a/taler-swift/Sources/taler-swift/Amount.swift
+++ b/taler-swift/Sources/taler-swift/Amount.swift
@@ -71,6 +71,21 @@ public class Amount: Codable, CustomStringConvertible {
}
}
+ /// The string representation of the amount, formatted as "`value`.`fraction` `currency`".
+ public var readableDescription: String {
+ if fraction == 0 {
+ return "\(value) \(currency)"
+ } else {
+ var frac = fraction
+ var fracStr = ""
+ while (frac > 0) {
+ fracStr += "\(frac / (Amount.fractionalBase / 10))"
+ frac = (frac * 10) % Amount.fractionalBase
+ }
+ return "\(value).\(fracStr) \(currency)"
+ }
+ }
+
/// Whether the value is valid. An amount is valid if and only if the currency is not empty and the value is less than the maximum allowed value.
var valid: Bool {
if currency.range(of: Amount.currencyRegex, options: .regularExpression) == nil {
@@ -188,7 +203,7 @@ public class Amount: Codable, CustomStringConvertible {
/// - Throws:
/// - `AmountError.incompatibleCurrency` if `left` and `right` do not share the same currency.
/// - Returns: The sum of `left` and `right`, normalized.
- static func + (left: Amount, right: Amount) throws -> Amount {
+ public static func + (left: Amount, right: Amount) throws -> Amount {
if left.currency != right.currency {
throw AmountError.incompatibleCurrency
}
@@ -208,7 +223,7 @@ public class Amount: Codable, CustomStringConvertible {
/// - Throws:
/// - `AmountError.incompatibleCurrency` if `left` and `right` do not share the same currency.
/// - Returns: The difference of `left` and `right`, normalized.
- static func - (left: Amount, right: Amount) throws -> Amount {
+ public static func - (left: Amount, right: Amount) throws -> Amount {
if left.currency != right.currency {
throw AmountError.incompatibleCurrency
}
@@ -232,7 +247,7 @@ public class Amount: Codable, CustomStringConvertible {
/// - dividend: The amount to divide.
/// - divisor: The scalar dividing `dividend`.
/// - Returns: The quotient of `dividend` and `divisor`, normalized.
- static func / (dividend: Amount, divisor: UInt32) throws -> Amount {
+ public static func / (dividend: Amount, divisor: UInt32) throws -> Amount {
guard divisor != 0 else { throw AmountError.divideByZero }
let result = try dividend.normalizedCopy()
if (divisor == 1) {
@@ -251,7 +266,7 @@ public class Amount: Codable, CustomStringConvertible {
/// - amount: The amount to multiply.
/// - factor: The scalar multiplying `amount`.
/// - Returns: The product of `amount` and `factor`, normalized.
- static func * (amount: Amount, factor: UInt32) throws -> Amount {
+ public static func * (amount: Amount, factor: UInt32) throws -> Amount {
let result = try amount.normalizedCopy()
result.value = result.value * UInt64(factor)
let fraction_tmp = UInt64(result.fraction) * UInt64(factor)
@@ -333,7 +348,7 @@ public class Amount: Codable, CustomStringConvertible {
/// - Parameters:
/// - currency: The currency to use.
/// - Returns: The zero amount for `currency`.
- static func zero(currency: String) -> Amount {
+ public static func zero(currency: String) -> Amount {
return Amount(currency: currency, value: 0, fraction: 0)
}
}