diff options
-rw-r--r-- | Taler.xcodeproj/project.pbxproj | 44 | ||||
-rw-r--r-- | Taler/Model/ExchangeManager.swift | 5 | ||||
-rw-r--r-- | Taler/Model/WithdrawModel.swift | 107 | ||||
-rw-r--r-- | Taler/Views/SettingsView.swift | 100 | ||||
-rw-r--r-- | Taler/WalletBackend.swift | 10 | ||||
-rw-r--r-- | taler-swift/Sources/taler-swift/Amount.swift | 25 |
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) } } |