commit a41e3ea751bf572995a11e81e5aa7065fae2160b parent 8abb8bda005475d2567ba8af2d1b9b1130d1ed80 Author: Marc Stibane <marc@taler.net> Date: Tue, 20 Jun 2023 09:23:54 +0200 Overhaul withdraw + p2p Diffstat:
21 files changed, 607 insertions(+), 373 deletions(-)
diff --git a/TalerWallet.xcodeproj/project.pbxproj b/TalerWallet.xcodeproj/project.pbxproj @@ -74,7 +74,6 @@ 4EB095602989CBFE0043A8A1 /* BalancesSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB0953A2989CBFE0043A8A1 /* BalancesSectionView.swift */; }; 4EB095612989CBFE0043A8A1 /* WithdrawURIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB0953C2989CBFE0043A8A1 /* WithdrawURIView.swift */; }; 4EB095622989CBFE0043A8A1 /* WithdrawModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB0953D2989CBFE0043A8A1 /* WithdrawModel.swift */; }; - 4EB095632989CBFE0043A8A1 /* WithdrawAcceptView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB0953E2989CBFE0043A8A1 /* WithdrawAcceptView.swift */; }; 4EB095642989CBFE0043A8A1 /* WithdrawProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB0953F2989CBFE0043A8A1 /* WithdrawProgressView.swift */; }; 4EB095652989CBFE0043A8A1 /* WithdrawTOSView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095402989CBFE0043A8A1 /* WithdrawTOSView.swift */; }; 4EB095662989CBFE0043A8A1 /* SideBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095422989CBFE0043A8A1 /* SideBarView.swift */; }; @@ -90,6 +89,8 @@ 4EB095702989CBFE0043A8A1 /* PendingOpsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB0954E2989CBFE0043A8A1 /* PendingOpsListView.swift */; }; 4EB3136129FEE79B007D68BC /* SendNow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB3136029FEE79B007D68BC /* SendNow.swift */; }; 4EB431672A1E55C700C5690E /* ManualWithdrawDone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB431662A1E55C700C5690E /* ManualWithdrawDone.swift */; }; + 4EBA82AB2A3EB2CA00E5F39A /* TransactionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EBA82AA2A3EB2CA00E5F39A /* TransactionButton.swift */; }; + 4EBA82AD2A3F580500E5F39A /* QuiteSomeCoins.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EBA82AC2A3F580500E5F39A /* QuiteSomeCoins.swift */; }; 4EC90C782A1B528B0071DC58 /* ExchangeSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EC90C772A1B528B0071DC58 /* ExchangeSectionView.swift */; }; 4ECB62802A0BA6DF004ABBB7 /* Peer2peerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ECB627F2A0BA6DF004ABBB7 /* Peer2peerModel.swift */; }; 4ECB62822A0BB01D004ABBB7 /* SelectDays.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ECB62812A0BB01D004ABBB7 /* SelectDays.swift */; }; @@ -202,7 +203,6 @@ 4EB0953A2989CBFE0043A8A1 /* BalancesSectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BalancesSectionView.swift; sourceTree = "<group>"; }; 4EB0953C2989CBFE0043A8A1 /* WithdrawURIView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WithdrawURIView.swift; sourceTree = "<group>"; }; 4EB0953D2989CBFE0043A8A1 /* WithdrawModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WithdrawModel.swift; sourceTree = "<group>"; }; - 4EB0953E2989CBFE0043A8A1 /* WithdrawAcceptView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WithdrawAcceptView.swift; sourceTree = "<group>"; }; 4EB0953F2989CBFE0043A8A1 /* WithdrawProgressView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WithdrawProgressView.swift; sourceTree = "<group>"; }; 4EB095402989CBFE0043A8A1 /* WithdrawTOSView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WithdrawTOSView.swift; sourceTree = "<group>"; }; 4EB095422989CBFE0043A8A1 /* SideBarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SideBarView.swift; sourceTree = "<group>"; }; @@ -218,6 +218,8 @@ 4EB0954E2989CBFE0043A8A1 /* PendingOpsListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PendingOpsListView.swift; sourceTree = "<group>"; }; 4EB3136029FEE79B007D68BC /* SendNow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendNow.swift; sourceTree = "<group>"; }; 4EB431662A1E55C700C5690E /* ManualWithdrawDone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManualWithdrawDone.swift; sourceTree = "<group>"; }; + 4EBA82AA2A3EB2CA00E5F39A /* TransactionButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionButton.swift; sourceTree = "<group>"; }; + 4EBA82AC2A3F580500E5F39A /* QuiteSomeCoins.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuiteSomeCoins.swift; sourceTree = "<group>"; }; 4EC90C772A1B528B0071DC58 /* ExchangeSectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExchangeSectionView.swift; sourceTree = "<group>"; }; 4ECB627F2A0BA6DF004ABBB7 /* Peer2peerModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Peer2peerModel.swift; sourceTree = "<group>"; }; 4ECB62812A0BB01D004ABBB7 /* SelectDays.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectDays.swift; sourceTree = "<group>"; }; @@ -393,6 +395,7 @@ 4EB095292989CBFE0043A8A1 /* ExchangeListView.swift */, 4EC90C772A1B528B0071DC58 /* ExchangeSectionView.swift */, 4E50B34F2A1BEE8000F9F01C /* ManualWithdraw.swift */, + 4EBA82AC2A3F580500E5F39A /* QuiteSomeCoins.swift */, 4EB431662A1E55C700C5690E /* ManualWithdrawDone.swift */, ); path = Exchange; @@ -436,7 +439,6 @@ isa = PBXGroup; children = ( 4EB0953C2989CBFE0043A8A1 /* WithdrawURIView.swift */, - 4EB0953E2989CBFE0043A8A1 /* WithdrawAcceptView.swift */, 4E5A88F62A3B9E5B00072618 /* WithdrawAcceptDone.swift */, 4EB0953F2989CBFE0043A8A1 /* WithdrawProgressView.swift */, 4EB095402989CBFE0043A8A1 /* WithdrawTOSView.swift */, @@ -467,6 +469,7 @@ 4E5A88F42A38A4FD00072618 /* QRCodeDetailView.swift */, 4E6EDD862A363D8D0031D520 /* ListStyle.swift */, 4EB095482989CBFE0043A8A1 /* TextFieldAlert.swift */, + 4EBA82AA2A3EB2CA00E5F39A /* TransactionButton.swift */, 4EB095492989CBFE0043A8A1 /* AmountView.swift */, 4EB0954A2989CBFE0043A8A1 /* LoadingView.swift */, 4EB095432989CBFE0043A8A1 /* LaunchAnimationView.swift */, @@ -701,6 +704,7 @@ 4EB095032989C9BC0043A8A1 /* Controller.swift in Sources */, 4EB095682989CBFE0043A8A1 /* MainView.swift in Sources */, 4EB0956A2989CBFE0043A8A1 /* Buttons.swift in Sources */, + 4EBA82AB2A3EB2CA00E5F39A /* TransactionButton.swift in Sources */, 4EB095602989CBFE0043A8A1 /* BalancesSectionView.swift in Sources */, 4EEC157329F8242800D46A03 /* QRGeneratorView.swift in Sources */, 4E5A88F72A3B9E5B00072618 /* WithdrawAcceptDone.swift in Sources */, @@ -711,7 +715,6 @@ 4EB095212989CBCB0043A8A1 /* WalletBackendError.swift in Sources */, 4EB0955E2989CBFE0043A8A1 /* PendingRowView.swift in Sources */, 4EB0955B2989CBFE0043A8A1 /* BalancesModel.swift in Sources */, - 4EB095632989CBFE0043A8A1 /* WithdrawAcceptView.swift in Sources */, 4EB0956D2989CBFE0043A8A1 /* LoadingView.swift in Sources */, 4E50B3502A1BEE8000F9F01C /* ManualWithdraw.swift in Sources */, 4E5A88F52A38A4FD00072618 /* QRCodeDetailView.swift in Sources */, @@ -736,6 +739,7 @@ 4EA1ABBE29A3833A008821EA /* PublicConstants.swift in Sources */, 4EB3136129FEE79B007D68BC /* SendNow.swift in Sources */, 4EB0956B2989CBFE0043A8A1 /* TextFieldAlert.swift in Sources */, + 4EBA82AD2A3F580500E5F39A /* QuiteSomeCoins.swift in Sources */, 4EB431672A1E55C700C5690E /* ManualWithdrawDone.swift in Sources */, 4E9320472A164BC700A87B0E /* ReceivePurpose.swift in Sources */, 4E753A082A0B6A5F002D9328 /* ShareSheet.swift in Sources */, diff --git a/TalerWallet1/Controllers/PublicConstants.swift b/TalerWallet1/Controllers/PublicConstants.swift @@ -8,7 +8,8 @@ public let LAUNCHDURATION: Double = 1.60 public let SLIDEDURATION: Double = 0.45 public let HTTPS = "https://" -public let DEMOBANK = HTTPS + "bAnK.dEmO.tAlEr.nEt" // should be weird to read, but still work +//public let DEMOBANK = HTTPS + "bAnK.dEmO.tAlEr.nEt" // should be weird to read, but still work +public let DEMOBANK = HTTPS + "bank.demo.taler.net" public let DEMOSHOP = HTTPS + "shop.demo.taler.net" public let DEMOBACKEND = HTTPS + "backend.demo.taler.net" //public let DEMOEXCHANGE = HTTPS + "eXcHaNgE.dEmO.tAlEr.nEt" diff --git a/TalerWallet1/Model/Peer2peerModel.swift b/TalerWallet1/Model/Peer2peerModel.swift @@ -22,7 +22,7 @@ struct PeerContractTerms: Codable { // MARK: - /// The result from CheckPeerPushDebit struct CheckPeerPushDebitResponse: Codable { - let exchangeBaseUrl: String + let exchangeBaseUrl: String? let amountRaw: Amount let amountEffective: Amount let maxExpirationDate: Timestamp? // TODO: limit expiration (30 days or 7 days) @@ -43,8 +43,9 @@ fileprivate struct CheckPeerPushDebit: WalletBackendFormattedRequest { struct CheckPeerPullCreditResponse: Codable { let scopeInfo: ScopeInfo? let exchangeBaseUrl: String? - let amountEffective: Amount let amountRaw: Amount + let amountEffective: Amount + var numCoins: Int? // Number of coins this amountEffective will create } /// A request to check fees before invoicing another wallet. fileprivate struct CheckPeerPullCredit: WalletBackendFormattedRequest { @@ -105,7 +106,7 @@ extension Peer2peerModel { } /// generate peer-push. Networking involved @MainActor - func initiatePeerPushDebitM(_ baseURL: String, terms: PeerContractTerms) // M for MainActor + func initiatePeerPushDebitM(_ baseURL: String?, terms: PeerContractTerms) // M for MainActor async throws -> PeerPushResponse { let request = InitiatePeerPushDebit(exchangeBaseUrl: baseURL, partialContractTerms: terms) @@ -116,15 +117,17 @@ extension Peer2peerModel { // MARK: - extension Peer2peerModel { - private static var models: [Peer2peerModel] = [] // a list of models even though I currently need only one + private static var exchanges: [String] = [] // names of exchanges + private static var models: [Peer2peerModel] = [] // one model per exchange - static func model() -> Peer2peerModel { - if Peer2peerModel.models.count > 0 { - let model = Peer2peerModel.models[0] + static func model(baseURL: String) -> Peer2peerModel { + if let index = Peer2peerModel.exchanges.firstIndex(of:baseURL) { + let model = Peer2peerModel.models[index] return model } else { // new model let model = Peer2peerModel() Peer2peerModel.models.append(model) + Peer2peerModel.exchanges.append(baseURL) return model } } diff --git a/TalerWallet1/Model/WithdrawModel.swift b/TalerWallet1/Model/WithdrawModel.swift @@ -52,12 +52,12 @@ fileprivate struct GetWithdrawalDetailsForURI: WalletBackendFormattedRequest { // MARK: - /// The result from getWithdrawalDetailsForAmount struct ManualWithdrawalDetails: Decodable { - var amountRaw: Amount - var amountEffective: Amount - var paytoUris: [String] - var tosAccepted: Bool - var ageRestrictionOptions: [Int]? - var numCoins: Int? + var tosAccepted: Bool // Did the user accept the current version of the exchange's terms of service? + var amountRaw: Amount // Amount that the user will transfer to the exchange + var amountEffective: Amount // Amount that will be added to the user's wallet balance + var paytoUris: [String] // Ways to pay the exchange + var ageRestrictionOptions: [Int]? // Array of ages + var numCoins: Int? // Number of coins this amountEffective will create } /// A request to get an exchange's withdrawal details. fileprivate struct GetWithdrawalDetailsForAmount: WalletBackendFormattedRequest { @@ -180,10 +180,10 @@ extension WithdrawModel { } @MainActor func sendAcceptIntWithdrawalM(_ exchangeBaseUrl: String, withdrawURL: String) // M for MainActor - async throws -> String? { + async throws -> AcceptWithdrawalResponse? { let request = AcceptBankIntegratedWithdrawal(talerWithdrawUri: withdrawURL, exchangeBaseUrl: exchangeBaseUrl) let response = try await sendRequest(request, ASYNCDELAY) - return response.confirmTransferUrl + return response } @MainActor func sendAcceptManualWithdrawalM(_ exchangeBaseUrl: String, amount: Amount, restrictAge: Int?) // M for MainActor @@ -196,7 +196,7 @@ extension WithdrawModel { // MARK: - extension WithdrawModel { - private static var exchanges: [String] = [] // names of exchanges + private static var exchanges: [String] = [] // names of exchanges private static var models: [WithdrawModel] = [] // one model per exchange static func model(baseURL: String) -> WithdrawModel { diff --git a/TalerWallet1/Views/Balances/BalancesListView.swift b/TalerWallet1/Views/Balances/BalancesListView.swift @@ -18,6 +18,7 @@ struct BalancesListView: View { let hamburgerAction: () -> Void @State private var centsToTransfer: UInt64 = 0 + @State private var purpose: String = "" @State private var showQRScanner: Bool = false @State private var showCameraAlert: Bool = false @@ -73,7 +74,8 @@ struct BalancesListView: View { let _ = Self._printChanges() let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear #endif - Content(symLog: symLog, balances: $balances, centsToTransfer: $centsToTransfer, + Content(symLog: symLog, balances: $balances, + centsToTransfer: $centsToTransfer, purpose: $purpose, reloadAction: reloadAction) .navigationTitle(navTitle) .navigationBarTitleDisplayMode(.automatic) @@ -109,6 +111,7 @@ extension BalancesListView { @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic @Binding var balances: [Balance] @Binding var centsToTransfer: UInt64 + @Binding var purpose: String var reloadAction: () async -> Void var body: some View { @@ -119,7 +122,10 @@ extension BalancesListView { Group { // necessary for .backslide transition (bug in SwiftUI) List(balances, id: \.self) { balance in let model = TransactionsModel.model(currency: balance.available.currencyStr) - BalancesSectionView(balance: balance, centsToTransfer: $centsToTransfer, model: model) + BalancesSectionView(balance: balance, + centsToTransfer: $centsToTransfer, + purpose: $purpose, + model: model) } .refreshable { symLog?.log("refreshing") diff --git a/TalerWallet1/Views/Balances/BalancesSectionView.swift b/TalerWallet1/Views/Balances/BalancesSectionView.swift @@ -18,6 +18,7 @@ struct BalancesSectionView: View { private let symLog = SymLogV() var balance:Balance @Binding var centsToTransfer: UInt64 + @Binding var purpose: String var model: TransactionsModel? @State private var isShowingDetailView = false @@ -29,7 +30,19 @@ struct BalancesSectionView: View { @State private var uncompletedTransactions: [Transaction] = [] func dummyTransaction (_ transactionId: String) async throws {} + func reloadOneAction(_ transactionId: String) async throws -> Transaction { + if let model { + return try await model.getTransactionByIdT(transactionId) + } else { + throw WalletBackendError.walletCoreError + } + } + var body: some View { +#if DEBUG + let _ = Self._printChanges() + let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear +#endif let currency = balance.available.currencyStr let reloadCompleted = { if let model { @@ -52,6 +65,7 @@ struct BalancesSectionView: View { let deleteAction = model?.deleteTransactionT ?? dummyTransaction let abortAction = model?.abortTransactionT ?? dummyTransaction + let p2pModel = Peer2peerModel.model(baseURL: currency) Section { // if "KUDOS" == currency && !balance.available.isZero { @@ -60,27 +74,33 @@ struct BalancesSectionView: View { // } HStack(spacing: 0) { NavigationLink(destination: LazyView { - SendAmount(amountAvailable: balance.available) + SendAmount(model: p2pModel, + amountAvailable: balance.available, + centsToTransfer: $centsToTransfer, + purpose: $purpose) }, tag: 1, selection: $buttonSelected ) { EmptyView() }.frame(width: 0).opacity(0).hidden() NavigationLink(destination: LazyView { - RequestPayment(scopeInfo: balance.scopeInfo, - centsToTransfer: $centsToTransfer) + RequestPayment(model: p2pModel, + scopeInfo: balance.scopeInfo, + centsToTransfer: $centsToTransfer, + purpose: $purpose) }, tag: 2, selection: $buttonSelected ) { EmptyView() }.frame(width: 0).opacity(0).hidden() NavigationLink(destination: LazyView { TransactionsListView(navTitle: String(localized: "Transactions"), currency: currency, transactions: completedTransactions, - reloadAction: reloadCompleted, + reloadAllAction: reloadCompleted, + reloadOneAction: reloadOneAction, deleteAction: deleteAction, abortAction: abortAction) }, tag: 3, selection: $buttonSelected ) { EmptyView() }.frame(width: 0).opacity(0).hidden() BalanceRowView(amount: balance.available, sendAction: { -print("button: Send Coins: \(currency)") +print("button: Send \(currency)") buttonSelected = 1 // will trigger SendAmount NavigationLink }, recvAction: { print("button: Request Payment: \(currency)") @@ -99,7 +119,8 @@ let _ = print("button: Pending Transactions: \(currency)") LazyView { TransactionsListView(navTitle: String(localized: "Pending"), currency: currency, transactions: pendingTransactions, - reloadAction: reloadPending, + reloadAllAction: reloadPending, + reloadOneAction: reloadOneAction, deleteAction: deleteAction, abortAction: abortAction) } @@ -121,9 +142,10 @@ let _ = print("button: Uncompleted Transactions: \(currency)") LazyView { TransactionsListView(navTitle: String(localized: "Uncompleted"), currency: currency, transactions: uncompletedTransactions, - reloadAction: reloadUncompleted, + reloadAllAction: reloadUncompleted, + reloadOneAction: reloadOneAction, deleteAction: deleteAction, - abortAction: abortAction) + abortAction: abortAction) } } label: { UncompletedRowView(uncompletedTransactions: uncompletedTransactions) @@ -146,6 +168,7 @@ let _ = print("button: Uncompleted Transactions: \(currency)") #if DEBUG fileprivate struct BindingViewContainer : View { @State var centsToTransfer: UInt64 = 333 + @State private var purpose: String = "bla-bla" var body: some View { let scopeInfo = ScopeInfo(type: ScopeInfo.ScopeInfoType.exchange, exchangeBaseUrl: DEMOEXCHANGE, currency: LONGCURRENCY) @@ -156,7 +179,10 @@ fileprivate struct BindingViewContainer : View { requiresUserInput: false, scopeInfo: scopeInfo) List { - BalancesSectionView(balance: balance, centsToTransfer: $centsToTransfer, model: nil) + BalancesSectionView(balance: balance, + centsToTransfer: $centsToTransfer, + purpose: $purpose, + model: nil) } } } diff --git a/TalerWallet1/Views/Exchange/ExchangeSectionView.swift b/TalerWallet1/Views/Exchange/ExchangeSectionView.swift @@ -7,6 +7,7 @@ import taler_swift struct ExchangeRowView: View { let exchange: Exchange + let currency: String @Binding var centsToTransfer: UInt64 @State private var buttonSelected: Int? = nil @@ -30,15 +31,13 @@ struct ExchangeRowView: View { }.listRowSeparator(.hidden) HStack { // buttons just set "buttonSelected" so the NavigationLink will trigger - Button("Deposit\nCoins") { buttonSelected = 1 } + Button("Deposit\n\(currency)") { buttonSelected = 1 } .multilineTextAlignment(.center) - .lineLimit(2) .buttonStyle(TalerButtonStyle(type: .bordered)) .disabled(true) // TODO: after implementing Deposit check available - Button("Withdraw\nCoins") { buttonSelected = 2 } + Button("Withdraw\n\(currency)") { buttonSelected = 2 } .multilineTextAlignment(.center) - .lineLimit(2) .buttonStyle(TalerButtonStyle(type: .bordered)) }.listRowSeparator(.visible) // .listRowSeparatorTint(.red) @@ -61,7 +60,7 @@ struct ExchangeSectionView: View { #endif Section { ForEach(exchanges) { exchange in - ExchangeRowView(exchange: exchange, centsToTransfer: $centsToTransfer) + ExchangeRowView(exchange: exchange, currency: currency, centsToTransfer: $centsToTransfer) } .accessibilityElement(children: .combine) } header: { diff --git a/TalerWallet1/Views/Exchange/ManualWithdraw.swift b/TalerWallet1/Views/Exchange/ManualWithdraw.swift @@ -6,33 +6,18 @@ import SwiftUI import taler_swift import SymLog +// Will be called by the user tapping "Withdraw Coins" in the exchange list struct ManualWithdraw: View { private let symLog = SymLogV() - let navTitle = String(localized: "Withdraw Coins") - var exchange: Exchange - var model: WithdrawModel? + let exchange: Exchange + let model: WithdrawModel? @Binding var centsToTransfer: UInt64 @State var manualWithdrawalDetails: ManualWithdrawalDetails? = nil -// @State var numCoins: Int = 0 - - // returns numCoins, 0 if invalid, -1 if unknown - // either fees or empty string - private func numAndFee(detailsForAmount: ManualWithdrawalDetails?) -> (Int, String) { - do { - if let details = detailsForAmount { - let fee = try details.amountRaw - details.amountEffective - return (details.numCoins ?? -1, // either the number of coins, or unknown - fee.isZero ? "" : fee.readableDescription) - } - } catch {} - symLog.log("invalid") - return (0, "") // invalid - } - @State var ageMenuList: [Int] = [] - @State var selectedAge = 0 +// @State var ageMenuList: [Int] = [] +// @State var selectedAge = 0 var body: some View { #if DEBUG @@ -42,64 +27,32 @@ struct ManualWithdraw: View { let currency = exchange.currency! let navTitle = String(localized: "Withdraw \(currency)") let currencyField = CurrencyField(value: $centsToTransfer, currency: currency) // becomeFirstResponder - let agePicker = AgePicker(ageMenuList: $ageMenuList, selectedAge: $selectedAge) +// let agePicker = AgePicker(ageMenuList: $ageMenuList, selectedAge: $selectedAge) ScrollView { Text("from \(exchange.exchangeBaseUrl.trimURL())") .font(.title3) - CurrencyInputView(currencyField: currencyField, title: String(localized: "Amount to withdraw:")) - - let (numCoins, fee) = numAndFee(detailsForAmount: manualWithdrawalDetails) - let unknown = (numCoins < 0) - let invalid = (numCoins == 0) - let manyCoins = (numCoins > 99) - let quiteSome = (numCoins > 199) - let tooMany = (numCoins > 999) - let hasFee = (fee.count > 0) - let shownFee = hasFee ? String(localized: "- \(fee)") : String(localized: "No") - Text(invalid ? "invalid amount" - : tooMany ? "too many coins for a single withdrawal" - : "\(shownFee) withdrawal fee") - .foregroundColor((invalid || tooMany || hasFee) ? .red : .primary) - .padding() + CurrencyInputView(currencyField: currencyField, + title: String(localized: "Amount to withdraw:")) - if !invalid { - HStack { - Text(unknown ? "Some" : "\(numCoins)") - .foregroundColor(quiteSome ? .red : .primary) - Text(tooMany ? "coins" : "coins to obtain:") - .foregroundColor(tooMany ? .red : .primary) + let someCoins = SomeCoins(details: manualWithdrawalDetails) + QuiteSomeCoins(someCoins: someCoins, shouldShowFee: true, + currency: currency, amountEffective: manualWithdrawalDetails?.amountEffective) - Spacer() - if !tooMany { - let effective = manualWithdrawalDetails?.amountEffective ?? Amount(currency: currency, value: 0) - Text(effective.readableDescription) - } - } // xx coins to obtain: YYY currency - .padding(.top) -// .font(.title3) - - if !tooMany { - if manyCoins { - Text(quiteSome ? "Warning: It will take quite some time\nto generate this many coins!" - : "Warning: It will take some time\nto generate this many coins.") - .multilineTextAlignment(.leading) - .padding(.top, 6) - .foregroundColor(quiteSome ? .red : .primary) - } // warnings + if !someCoins.invalid { + if !someCoins.tooMany { +// agePicker - agePicker - - if let tosAcc = manualWithdrawalDetails?.tosAccepted { - if tosAcc { - let restrictAge: Int? = (selectedAge == 0) ? nil - : selectedAge -let _ = print(selectedAge, restrictAge) + if let tosAccepted = manualWithdrawalDetails?.tosAccepted { + if tosAccepted { +// let restrictAge: Int? = (selectedAge == 0) ? nil +// : selectedAge +//let _ = print(selectedAge, restrictAge) NavigationLink(destination: LazyView { ManualWithdrawDone(exchange: exchange, model: model, - centsToTransfer: centsToTransfer, - restrictAge: restrictAge) + centsToTransfer: centsToTransfer) +// restrictAge: restrictAge) }) { Text("Confirm Withdrawal") // VIEW_WITHDRAW_ACCEPT }.buttonStyle(TalerButtonStyle(type: .prominent)) @@ -144,15 +97,13 @@ let _ = print(selectedAge, restrictAge) #if DEBUG struct ManualWithdraw_Container : View { @State private var centsToTransfer: UInt64 = 510 - @State private var manualWithdrawalDetails = ManualWithdrawalDetails(amountRaw: try! Amount(fromString: LONGCURRENCY + ":5.1"), - amountEffective: try! Amount(fromString: LONGCURRENCY + ":5.0"), - paytoUris: [], - tosAccepted: false, - ageRestrictionOptions: [], - numCoins: 6) - + @State private var details = ManualWithdrawalDetails(tosAccepted: false, + amountRaw: try! Amount(fromString: LONGCURRENCY + ":5.1"), + amountEffective: try! Amount(fromString: LONGCURRENCY + ":5.0"), + paytoUris: [], + ageRestrictionOptions: [], + numCoins: 6) var body: some View { - let model = WithdrawModel.model(baseURL: DEMOEXCHANGE) let exchange = Exchange(exchangeBaseUrl: DEMOEXCHANGE, currency: LONGCURRENCY, paytoUris: [], @@ -161,9 +112,9 @@ struct ManualWithdraw_Container : View { ageRestrictionOptions: [], permanent: false) ManualWithdraw(exchange: exchange, - model: model, + model: nil, centsToTransfer: $centsToTransfer, - manualWithdrawalDetails: manualWithdrawalDetails) + manualWithdrawalDetails: details) } } diff --git a/TalerWallet1/Views/Exchange/ManualWithdrawDone.swift b/TalerWallet1/Views/Exchange/ManualWithdrawDone.swift @@ -10,13 +10,22 @@ struct ManualWithdrawDone: View { private let symLog = SymLogV() let navTitle = String(localized: "Wire Transfer") - var exchange: Exchange - var model: WithdrawModel? - var centsToTransfer: UInt64 - var restrictAge: Int? + let exchange: Exchange + let model: WithdrawModel? + let centsToTransfer: UInt64 +// let restrictAge: Int? + @State var acceptManualWithdrawalResult: AcceptManualWithdrawalResult? @State var withdrawalTransaction: Transaction? + func reloadOneAction(_ transactionId: String) async throws -> Transaction { + if let model { + return try await model.getTransactionByIdT(transactionId) + } else { + throw WalletBackendError.walletCoreError + } + } + var body: some View { #if DEBUG let _ = Self._printChanges() @@ -25,7 +34,8 @@ struct ManualWithdrawDone: View { VStack { if let transaction = withdrawalTransaction { TransactionDetailView(transaction: transaction, - doneAction: {ViewState.shared.popToRootView()}) + reloadAction: reloadOneAction, + doneAction: ViewState.shared.popToRootView) .navigationBarBackButtonHidden(true) // exit only by Done-Button .navigationTitle(navTitle) } else { @@ -40,11 +50,10 @@ struct ManualWithdrawDone: View { if let model { let amount = Amount.amountFromCents(exchange.currency!, centsToTransfer) let result = try await model.sendAcceptManualWithdrawalM(exchange.exchangeBaseUrl, - amount: amount, restrictAge: restrictAge) + amount: amount, restrictAge: 0) print(result as Any) let transaction = try await model.getTransactionByIdT(result!.transactionId) withdrawalTransaction = transaction -// acceptManualWithdrawalResult = result } } catch { // TODO: error symLog.log(error.localizedDescription) diff --git a/TalerWallet1/Views/Exchange/QuiteSomeCoins.swift b/TalerWallet1/Views/Exchange/QuiteSomeCoins.swift @@ -0,0 +1,102 @@ +/* + * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. + * See LICENSE.md + */ +import SwiftUI +import taler_swift +import SymLog + +struct SomeCoins { + let numCoins: Int // 0 == invalid, -1 == unknown + var unknown: Bool { numCoins < 0 } + var invalid: Bool { numCoins == 0 } + var manyCoins: Bool { numCoins > 99 } + var quiteSome: Bool { numCoins > 199 } + var tooMany: Bool { numCoins > 999 } + + let fee: String + var hasFee: Bool { fee.count > 0 } +} + +extension SomeCoins { + init(details: ManualWithdrawalDetails?) { + do { + if let details { + // Incoming: fee = raw - effective + let fee = try details.amountRaw - details.amountEffective + self.init(numCoins: details.numCoins ?? -1, // either the number of coins, or unknown + fee: fee.isZero ? "" : fee.readableDescription) + return + } + } catch {} + self.init(numCoins: 0, fee:"") // invalid + } + + init(details: CheckPeerPullCreditResponse?) { + do { + if let details { + // Incoming: fee = raw - effective + let fee = try details.amountRaw - details.amountEffective + self.init(numCoins: details.numCoins ?? -1, // either the number of coins, or unknown + fee: fee.isZero ? "" : fee.readableDescription) + return + } + } catch {} + self.init(numCoins: 0, fee:"") // invalid + } +} +// MARK: - +struct QuiteSomeCoins: View { + private let symLog = SymLogV() + let someCoins: SomeCoins + let shouldShowFee: Bool + let currency: String + let amountEffective: Amount? + + var body: some View { + if shouldShowFee { + let shownFee = someCoins.hasFee ? String(localized: "- \(someCoins.fee)") + : String(localized: "No") + Text(someCoins.invalid ? "invalid amount" + : someCoins.tooMany ? "too many coins for a single withdrawal" + : "\(shownFee) withdrawal fee") + .foregroundColor((someCoins.invalid || someCoins.tooMany || someCoins.hasFee) ? .red : .primary) + .padding(4) + } + if !someCoins.invalid { + HStack { + Text(someCoins.unknown ? "Some" : "\(someCoins.numCoins)") + .foregroundColor(someCoins.quiteSome ? .red : .primary) + Text(someCoins.tooMany ? "coins" : "coins to obtain:") + .foregroundColor(someCoins.tooMany ? .red : .primary) + + Spacer() + if !someCoins.tooMany { + let effective = amountEffective ?? Amount(currency: currency, value: 0) + Text(effective.readableDescription) + } + } // xx coins to obtain: YYY currency +// .font(.title3) + .padding(.top) + + if !someCoins.tooMany { + if someCoins.manyCoins { + Text(someCoins.quiteSome ? "Warning: It will take quite some time\nto generate this many coins!" + : "Warning: It will take some time\nto generate this many coins.") + .multilineTextAlignment(.leading) + .padding(.top, 6) + .foregroundColor(someCoins.quiteSome ? .red : .primary) + } // warnings + } + } + } +} +// MARK: - +struct QuiteSomeCoins_Previews: PreviewProvider { + static var previews: some View { + QuiteSomeCoins(someCoins: SomeCoins(numCoins: 4, fee: "20 " + LONGCURRENCY), + shouldShowFee: true, + currency: LONGCURRENCY, + amountEffective: nil) + } +} diff --git a/TalerWallet1/Views/HelperViews/TransactionButton.swift b/TalerWallet1/Views/HelperViews/TransactionButton.swift @@ -0,0 +1,88 @@ +/* + * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. + * See LICENSE.md + */ +import SwiftUI +import taler_swift +import AVFoundation + +struct TransactionButton: View { + let transactionId : String + let command : TxAction + let action: (_ transactionId: String) async throws -> Void + + @State var disabled: Bool = false + @State var executed: Bool = false + var body: some View { + let role: ButtonRole? = (command == .abort) ? .cancel + : (command == .delete) ? .destructive + : nil + Button(role: role, action: { + Task { + disabled = true // don't try this more than once + do { + try await action(transactionId) +// symLog.log("\(executed) \(transactionId)") + executed = true + } catch { // TODO: error +// symLog.log(error.localizedDescription) + } + } + }, label: { + HStack { + if executed { + switch command { + case .delete: + Text("Deleted from list") + case .abort: + Text("Abort pending...") + case .fail: + Text("Failing...") + case .suspend: + Text("Suspending...") + case .resume: + Text("Resuming...") + } + } else { + let spaces = " " + switch command { + case .delete: + Text("Delete from list" + spaces) + Image(systemName: "trash") // + case .abort: + Text("Abort" + spaces) + Image(systemName: "x.circle") // + case .fail: + Text("Fail" + spaces) + Image(systemName: "fanblades.slash") // + case .suspend: + Text("Suspend" + spaces) + Image(systemName: "clock.badge.xmark") // + case .resume: + Text("Resume" + spaces) + Image(systemName: "clock.arrow.circlepath") // + } + } + } + .font(.title) + .frame(maxWidth: .infinity) + }) + .buttonStyle(.bordered) + .controlSize(.large) + .padding(.horizontal) + .disabled(disabled) + } + +} + + +#if DEBUG +struct TransactionButton_Previews: PreviewProvider { + + static var previews: some View { + List { + TransactionButton(transactionId: "String", command: .abort, action: {transactionId in }) + } + } +} +#endif diff --git a/TalerWallet1/Views/Peer2peer/ReceivePurpose.swift b/TalerWallet1/Views/Peer2peer/ReceivePurpose.swift @@ -9,7 +9,6 @@ import SymLog struct ReceivePurpose: View { private let symLog = SymLogV() @FocusState private var isFocused: Bool - let model = Peer2peerModel.model() @State var peerPullCheck: CheckPeerPullCreditResponse? var scopeInfo: ScopeInfo @@ -38,6 +37,7 @@ struct ReceivePurpose: View { var body: some View { let amount = Amount.amountFromCents(scopeInfo.currency, centsToTransfer) + let model = Peer2peerModel.model(baseURL: scopeInfo.currency) let fee = pullFee(ppCheck: peerPullCheck) VStack (spacing: 6) { diff --git a/TalerWallet1/Views/Peer2peer/RequestPayment.swift b/TalerWallet1/Views/Peer2peer/RequestPayment.swift @@ -9,11 +9,12 @@ import SymLog struct RequestPayment: View { private let symLog = SymLogV() + let model: Peer2peerModel? var scopeInfo: ScopeInfo @Binding var centsToTransfer: UInt64 + @Binding var purpose: String @State private var peerPullCheck: CheckPeerPullCreditResponse? = nil - @State private var purpose: String = "" @State private var expireDays: UInt = 0 var body: some View { @@ -29,6 +30,10 @@ struct RequestPayment: View { CurrencyInputView(currencyField: currencyField, title: String(localized: "Amount to receive:")) + let someCoins = SomeCoins(details: peerPullCheck) + QuiteSomeCoins(someCoins: someCoins, shouldShowFee: true, + currency: currency, amountEffective: peerPullCheck?.amountEffective) + HStack { let disabled = centsToTransfer == 0 @@ -59,10 +64,24 @@ struct RequestPayment: View { .navigationTitle(navTitle) .onAppear { // make CurrencyField show the keyboard DebugViewC.shared.setViewID(VIEW_INVOICE_P2P) - print("❗️Yikes \(navTitle) onAppear") + symLog.log("❗️Yikes \(navTitle) onAppear") } .onDisappear { - print("❗️Yikes \(navTitle) onDisappear") + symLog.log("❗️Yikes \(navTitle) onDisappear") + } + .task(id: centsToTransfer) { + let amount = Amount.amountFromCents(currency, centsToTransfer) + do { + if let model { + let ppCheck = try await model.checkPeerPullCreditM(amount, exchangeBaseUrl: nil) + peerPullCheck = ppCheck + // TODO: set from exchange +// agePicker.setAges(ages: peerPushCheck?.ageRestrictionOptions) + } + } catch { // TODO: error + symLog.log(error.localizedDescription) + peerPullCheck = nil + } } } } diff --git a/TalerWallet1/Views/Peer2peer/SendAmount.swift b/TalerWallet1/Views/Peer2peer/SendAmount.swift @@ -6,32 +6,29 @@ import SwiftUI import taler_swift import SymLog +// Will be called by the user tapping "Send Coins" in the balances list struct SendAmount: View { private let symLog = SymLogV() - let navTitle = String(localized: "Send Coins") -// @ObservedObject private var keyboardResponder = KeyboardResponder() -// @FocusState private var isFocused: Bool - let model = Peer2peerModel.model() - @State var peerPushCheck: CheckPeerPushDebitResponse? + let model: Peer2peerModel? let amountAvailable: Amount - let buttonFont: Font = .title2 + @Binding var centsToTransfer: UInt64 + @Binding var purpose: String - @State private var centsToTransfer: UInt64 = 0 - @State private var purpose: String = "" + @State var peerPushCheck: CheckPeerPushDebitResponse? @State private var expireDays: UInt = 0 private func fee(ppCheck: CheckPeerPushDebitResponse?) -> String { do { - if let p2pcheck = ppCheck { - let fee = try p2pcheck.amountEffective - p2pcheck.amountRaw + if let ppCheck { + // Outgoing: fee = effective - raw + let fee = try ppCheck.amountEffective - ppCheck.amountRaw return fee.readableDescription } } catch {} return "" } - var body: some View { #if DEBUG let _ = Self._printChanges() @@ -51,15 +48,11 @@ struct SendAmount: View { Text("+ \(fee) payment fee") .foregroundColor(.red) - Text("Choose where to send to:") - .padding(.top) - .font(.title3) + .padding(4) + HStack { - let kbdShown: Bool = false // keyboardResponder.keyboardHeight > 0 - let title2 = kbdShown ? "To wallet" : "To another\nwallet" let disabled = centsToTransfer == 0 // TODO: check amountAvailable - NavigationLink(destination: LazyView { SendPurpose(model: model, amountAvailable: amountAvailable, @@ -82,19 +75,21 @@ struct SendAmount: View { .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all)) .navigationTitle(navTitle) .onAppear { // make CurrencyField show the keyboard - symLog.log("onAppear") DebugViewC.shared.setViewID(VIEW_SEND_P2P) - print("❗️Yikes SendAmount onAppear") + symLog.log("❗️Yikes SendAmount onAppear") } .onDisappear { - print("❗️Yikes SendAmount onDisappear") + symLog.log("❗️Yikes SendAmount onDisappear") } .task(id: centsToTransfer) { let amount = Amount.amountFromCents(currency, centsToTransfer) do { - peerPushCheck = try await model.checkPeerPushDebitM(amount) - // TODO: set from exchange -// agePicker.setAges(ages: peerPushCheck?.ageRestrictionOptions) + if let model { + let ppCheck = try await model.checkPeerPushDebitM(amount) + peerPushCheck = ppCheck + // TODO: set from exchange + // agePicker.setAges(ages: peerPushCheck?.ageRestrictionOptions) + } } catch { // TODO: error symLog.log(error.localizedDescription) peerPushCheck = nil @@ -104,10 +99,22 @@ struct SendAmount: View { } // MARK: - #if DEBUG +struct SendAmount_Container : View { + @State private var centsToTransfer: UInt64 = 510 + @State private var purpose: String = "" + + var body: some View { + let amount = Amount(currency: LONGCURRENCY, integer: 10, fraction: 0) + SendAmount(model: nil, + amountAvailable: amount, + centsToTransfer: $centsToTransfer, + purpose: $purpose) + } +} + struct SendAmount_Previews: PreviewProvider { static var previews: some View { - let amount = Amount(currency: "TaLeR", integer: 10, fraction: 0) - SendAmount(amountAvailable: amount) + SendAmount_Container() } } #endif diff --git a/TalerWallet1/Views/Peer2peer/SendNow.swift b/TalerWallet1/Views/Peer2peer/SendNow.swift @@ -53,8 +53,8 @@ struct SendNow: View { if let model { let timestamp = Timestamp.inSomeDays(expireDays) let terms = PeerContractTerms(amount: amountToSend, summary: purpose, purse_expiration: timestamp) - let baseURL = DEMOEXCHANGE // TODO: use correct baseURL - peerPushResponse = try await model.initiatePeerPushDebitM(baseURL, terms: terms) + // TODO: user might choose baseURL + peerPushResponse = try await model.initiatePeerPushDebitM(nil, terms: terms) talerURI = peerPushResponse?.talerUri } } catch { // TODO: error diff --git a/TalerWallet1/Views/Transactions/TransactionDetailView.swift b/TalerWallet1/Views/Transactions/TransactionDetailView.swift @@ -9,11 +9,15 @@ import SymLog struct TransactionDetailView: View { private let symLog = SymLogV() @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic + @AppStorage("developerMode") var developerMode: Bool = false - var transaction: Transaction + @State var transaction: Transaction + var reloadAction: ((_ transactionId: String) async throws -> Transaction) var deleteAction: ((_ transactionId: String) async throws -> Void)? var abortAction: ((_ transactionId: String) async throws -> Void)? var doneAction: (() -> Void)? + var suspendAction: ((_ transactionId: String) async throws -> Void)? + var resumeAction: ((_ transactionId: String) async throws -> Void)? var body: some View { #if DEBUG @@ -30,35 +34,49 @@ struct TransactionDetailView: View { Text("\(dateString)") .font(.title2) // .listRowSeparator(.hidden) - SwitchCase(transaction: transaction, common: common) + SwitchCase(transaction: $transaction) if transaction.isAbortable { if let abortAction { - AbortButton(common: common, abortAction: abortAction) + TransactionButton(transactionId: common.transactionId, + command: .abort, action: abortAction) } } if transaction.isDeleteable { if let deleteAction { - DeleteButton(common: common, deleteAction: deleteAction) + TransactionButton(transactionId: common.transactionId, + command: .delete, action: deleteAction) } } if let doneAction { DoneButton(doneAction: doneAction) - .onNotification(.TransactionStateTransition) { notification in -print(notification.userInfo) - if let transition = notification.userInfo?[TRANSACTIONTRANSITION] as? TransactionTransition { -print(transition.newTxState.major) - if transition.transactionId == common.transactionId { - doneAction() - } - } - } } -// if transaction.isSuspendable { if let suspendAction { -// SuspendButton(common: common, suspendAction: suspendAction) -// } } -// if transaction.isResumable { if let resumeAction { -// ResumeButton(common: common, resumeAction: resumeAction) -// } } + if developerMode { + if transaction.isSuspendable { if let suspendAction { + TransactionButton(transactionId: common.transactionId, + command: .suspend, action: suspendAction) + } } + if transaction.isResumable { if let resumeAction { + TransactionButton(transactionId: common.transactionId, + command: .resume, action: resumeAction) + } } + } + }.listStyle(myListStyle.style).anyView + }.onNotification(.TransactionStateTransition) { notification in + if let transition = notification.userInfo?[TRANSACTIONTRANSITION] as? TransactionTransition { + if transition.transactionId == common.transactionId { + let newState = transition.newTxState.major + if newState == .done { if let doneAction { + symLog.log("newTxState.major == done") + doneAction() + }} else { Task { do { + symLog.log("newState: \(newState), reloading transaction") + let reloadedTransaction = try await reloadAction(common.transactionId) + transaction = reloadedTransaction // redraw + } catch { + symLog.log(error.localizedDescription) + }}} + } + } else { + symLog.log(notification.userInfo as Any) } - .listStyle(myListStyle.style).anyView } .navigationTitle(navTitle) .onAppear { @@ -73,21 +91,34 @@ print(transition.newTxState.major) // //extension TransactionDetail { struct SwitchCase: View { - let transaction: Transaction - let common: TransactionCommon + @Binding var transaction: Transaction var body: some View { - let pending = (common.txState.major == TransactionMajorState.pending) + let common = transaction.common + let pending = transaction.isPending switch transaction { case .withdrawal(let withdrawalTransaction): let details = withdrawalTransaction.details if pending { - switch details.withdrawalDetails.type { - case .manual: - ManualDetails(common: common, details: details.withdrawalDetails) + let withdrawalDetails = details.withdrawalDetails + switch withdrawalDetails.type { + case .manual: // "Make a wire transfer of \(amount) to" + ManualDetails(common: common, details: withdrawalDetails) + + case .bankIntegrated: // "Confirm with bank" + VStack { + if let confirmationUrl = withdrawalDetails.bankConfirmationUrl { + if let destination = URL(string: confirmationUrl) { + // Show Hint that User should Confirm on bank website + Text("Waiting for bank confirmation") + .listRowSeparator(.hidden) + Link("Confirm with bank", destination: destination) + .buttonStyle(TalerButtonStyle(type: .prominent, narrow: false, aligned: .center)) + .padding(.horizontal) - case .bankIntegrated: - QRCodeDetails(transaction: transaction) + } + } + } } } ThreeAmounts(common: common, topTitle: String(localized: "Chosen amount to withdraw:"), @@ -148,78 +179,6 @@ print(transition.newTxState.major) } } } - struct DeleteButton: View { - var common : TransactionCommon - var deleteAction: (_ transactionId: String) async throws -> Void - - @State var disabled: Bool = false - @State var deleted: Bool = false - var body: some View { - Button(role: .destructive, action: { - Task { // delete from wallet-core - disabled = true // don't try this more than once - do { - try await deleteAction(common.transactionId) -// symLog.log("deleted \(common.transactionId)") - deleted = true - } catch { // TODO: error -// symLog.log(error.localizedDescription) - } - } - }, label: { - HStack { - if deleted { - Text("Deleted from list") - } else { - Text("Delete from list" + " ") - Image(systemName: "trash") - } - } - .font(.title) - .frame(maxWidth: .infinity) - }) - .buttonStyle(.bordered) - .controlSize(.large) - .padding(.horizontal) - .disabled(disabled) - } - } - struct AbortButton: View { - var common : TransactionCommon - var abortAction: (_ transactionId: String) async throws -> Void - - @State var disabled: Bool = false - @State var aborting: Bool = false - var body: some View { - Button(role: .cancel, action: { - Task { // delete from wallet-core - disabled = true // don't try this more than once - do { - try await abortAction(common.transactionId) -// symLog.log("aborted \(common.transactionId)") - aborting = true - } catch { // TODO: error -// symLog.log(error.localizedDescription) - } - } - }, label: { - HStack { - if aborting { - Text("Abort pending...") - } else { - Text("Abort" + " ") - Image(systemName: "x.circle") - } - } - .font(.title) - .frame(maxWidth: .infinity) - }) - .buttonStyle(.bordered) - .controlSize(.large) - .padding(.horizontal) - .disabled(disabled) - } - } struct DoneButton: View { var doneAction: () -> Void @@ -245,10 +204,11 @@ struct TransactionDetail_Previews: PreviewProvider { pending: false, id: "some payment ID", time: Timestamp(from: 1_666_666_000_000)) + static func reloadActionDummy(transactionId: String) async -> Transaction { return withdrawal } static var previews: some View { Group { - TransactionDetailView(transaction: withdrawal, doneAction: doneActionDummy) - TransactionDetailView(transaction: payment, deleteAction: deleteTransactionDummy) + TransactionDetailView(transaction: withdrawal, reloadAction: reloadActionDummy, doneAction: doneActionDummy) + TransactionDetailView(transaction: payment, reloadAction: reloadActionDummy, deleteAction: deleteTransactionDummy) } } } diff --git a/TalerWallet1/Views/Transactions/TransactionsListView.swift b/TalerWallet1/Views/Transactions/TransactionsListView.swift @@ -12,7 +12,8 @@ struct TransactionsListView: View { let currency: String let transactions: [Transaction] - var reloadAction: () async -> () + let reloadAllAction: () async -> () + let reloadOneAction: ((_ transactionId: String) async throws -> Transaction) let deleteAction: (_ transactionId: String) async throws -> () let abortAction: (_ transactionId: String) async throws -> () @@ -26,7 +27,8 @@ struct TransactionsListView: View { // let title = AttributedString(localized: "^[\(count) Ticket](inflect: true)") let title: String = "\(count) \(navTitle)" Content(symLog: symLog, currency: currency, transactions: transactions, myListStyle: $myListStyle, - reloadAction: reloadAction, deleteAction: deleteAction, abortAction: abortAction) + reloadAllAction: reloadAllAction, reloadOneAction: reloadOneAction, + deleteAction: deleteAction, abortAction: abortAction) .navigationTitle(title) .navigationBarTitleDisplayMode(.large) // .inline .onAppear { @@ -34,7 +36,7 @@ struct TransactionsListView: View { } .task { symLog.log(".task ") - await reloadAction() + await reloadAllAction() } } } @@ -45,7 +47,8 @@ extension TransactionsListView { let currency: String let transactions: [Transaction] @Binding var myListStyle: MyListStyle - let reloadAction: () async -> () + let reloadAllAction: () async -> () + let reloadOneAction: ((_ transactionId: String) async throws -> Transaction) let deleteAction: (_ transactionId: String) async throws -> () let abortAction: (_ transactionId: String) async throws -> () @@ -87,6 +90,7 @@ extension TransactionsListView { NavigationLink { LazyView { // whole row like in a tableView // pending may not be deleted, but only aborted TransactionDetailView(transaction: transaction, + reloadAction: reloadOneAction, deleteAction: deleteAction, abortAction: abortAction) }} label: { @@ -97,7 +101,7 @@ extension TransactionsListView { } .refreshable { symLog?.log("refreshing") - await reloadAction() + await reloadAllAction() } .listStyle(myListStyle.style).anyView .onAppear { diff --git a/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawAcceptDone.swift b/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawAcceptDone.swift @@ -8,54 +8,61 @@ import SymLog struct WithdrawAcceptDone: View { private let symLog = SymLogV() - @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic + let navTitle = String(localized: "Confirm with Bank") - let confirmTransferUrl: String? + let exchangeBaseUrl: String + let model: WithdrawModel? + let url: URL - let navTitle = String(localized: "Confirm with Bank") + @State private var confirmTransferUrl: String? = nil + @State private var transaction: Transaction? = nil + + func reloadOneAction(_ transactionId: String) async throws -> Transaction { + if let model { + return try await model.getTransactionByIdT(transactionId) + } else { + throw WalletBackendError.walletCoreError + } + } var body: some View { +#if DEBUG + let _ = Self._printChanges() + let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear +#endif VStack { - Text(navTitle) - .font(.title) - .padding() - List { - if let confirmTransferUrl { - let destination = URL(string: confirmTransferUrl)! - // Show Hint that User should Confirm on bank website - Text("Waiting for bank confirmation") - .listRowSeparator(.hidden) - - - Link("Confirm with bank", destination: destination) - .buttonStyle(TalerButtonStyle(type: .prominent, narrow: false, aligned: .center)) - // balances will be updated by TransactionStateTransition - } + if let transaction { + TransactionDetailView(transaction: transaction, + reloadAction: reloadOneAction, + doneAction: { dismissTop() }) + .navigationBarBackButtonHidden(true) + .interactiveDismissDisabled() // can only use "Done" button to dismiss + .navigationTitle(navTitle) + } else { + WithdrawProgressView(message: "Bank Confirmation") + .navigationTitle("Loading...") } - .listStyle(myListStyle.style).anyView - - } - .interactiveDismissDisabled() // can only use "Done" button to dismiss - .navigationBarHidden(true) // no back button, no title - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) - .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all)) - - .safeAreaInset(edge: .bottom) { - Button("Done", action: { dismissTop() }) - .lineLimit(2) - .disabled(false) - .buttonStyle(TalerButtonStyle(type: .bordered, narrow: false, aligned: .center)) - .padding() - } - .navigationTitle(navTitle) - .onAppear() { + }.onAppear() { + symLog.log("onAppear") DebugViewC.shared.setSheetID(SHEET_WITHDRAW_CONFIRM) + }.task { + do { + if let model { + let result = try await model.sendAcceptIntWithdrawalM(exchangeBaseUrl, withdrawURL: url.absoluteString) + confirmTransferUrl = result!.confirmTransferUrl + transaction = try await model.getTransactionByIdT(result!.transactionId) + } + } catch { // TODO: error + symLog.log(error.localizedDescription) + } } } } // MARK: - struct WithdrawAcceptDone_Previews: PreviewProvider { static var previews: some View { - WithdrawAcceptDone(confirmTransferUrl: DEMOBANK) + WithdrawAcceptDone(exchangeBaseUrl: DEMOEXCHANGE, + model: nil, + url: URL(string: DEMOSHOP)!) } } diff --git a/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawAcceptView.swift b/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawAcceptView.swift @@ -8,23 +8,26 @@ import SymLog struct WithdrawAcceptView: View { private let symLog = SymLogV() - let url: URL - var model: WithdrawModel? - let navTitle = String(localized: "Accept Withdrawal") - let detailsForAmount: ManualWithdrawalDetails - let baseURL: String + + let exchangeBaseUrl: String + let model: WithdrawModel? + let amount: Amount? + let url: URL @State private var buttonSelected: Int? = nil @State private var confirmTransferUrl: String? = nil + @State private var transactionId: String? = nil + @State var manualWithdrawalDetails: ManualWithdrawalDetails? func acceptAction() -> () { Task { do { if let model { - if let transferUrl = try await model.sendAcceptIntWithdrawalM(baseURL, withdrawURL: url.absoluteString) { - symLog.log(transferUrl) - confirmTransferUrl = transferUrl + if let acceptWithdrawalResponse = try await model.sendAcceptIntWithdrawalM(exchangeBaseUrl, withdrawURL: url.absoluteString) { + confirmTransferUrl = acceptWithdrawalResponse.confirmTransferUrl + transactionId = acceptWithdrawalResponse.transactionId + symLog.log(confirmTransferUrl ?? "❗️Yikes: No confirmTransferUrl") buttonSelected = 1 // trigger NavigationLink } else { // TODO: error sendAcceptIntWithdrawal failed @@ -37,35 +40,37 @@ struct WithdrawAcceptView: View { } var body: some View { - List { - let raw = detailsForAmount.amountRaw - let effective = detailsForAmount.amountEffective - let fee = try! Amount.diff(raw, effective) // TODO: different currencies - let outColor = WalletColors().transactionColor(false) - let inColor = WalletColors().transactionColor(true) + VStack { + if let manualWithdrawalDetails { + List { - HStack(spacing: 0) { - NavigationLink(destination: LazyView { - WithdrawAcceptDone(confirmTransferUrl: confirmTransferUrl) - }, tag: 1, selection: $buttonSelected - ) { EmptyView() }.frame(width: 0).opacity(0).hidden() - - ThreeAmountsView(topTitle: String(localized: "Chosen amount to withdraw:"), - topAmount: raw, fee: fee, - bottomTitle: String(localized: "Coins to be withdrawn:"), - bottomAmount: effective, - large: false, pending: false, incoming: true, - baseURL: baseURL) + HStack(spacing: 0) { + NavigationLink(destination: LazyView { + WithdrawAcceptDone(model: model, confirmTransferUrl: confirmTransferUrl, transactionId: transactionId) + }, tag: 1, selection: $buttonSelected + ) { EmptyView() }.frame(width: 0).opacity(0).hidden() + + ThreeAmountsView(topTitle: String(localized: "Chosen amount to withdraw:"), + topAmount: raw, fee: fee, + bottomTitle: String(localized: "Coins to be withdrawn:"), + bottomAmount: effective, + large: false, pending: false, incoming: true, + baseURL: exchangeBaseUrl) + } + } + .safeAreaInset(edge: .bottom) { + Button("Confirm Withdrawal", action: acceptAction) + .lineLimit(2) + .disabled(false) + .buttonStyle(TalerButtonStyle(type: .prominent, narrow: false, aligned: .center)) + .padding() + } + .navigationTitle(navTitle) + } else { + WithdrawProgressView(message: exchangeBaseUrl.trimURL()) + .navigationTitle("Found Exchange") } } - .safeAreaInset(edge: .bottom) { - Button("Confirm Withdrawal", action: acceptAction) - .lineLimit(2) - .disabled(false) - .buttonStyle(TalerButtonStyle(type: .prominent, narrow: false, aligned: .center)) - .padding() - } - .navigationTitle(navTitle) // .overlay { // VStack { // ErrorView(errortext: "unknown state") // TODO: Error @@ -75,18 +80,32 @@ struct WithdrawAcceptView: View { .onAppear() { DebugViewC.shared.setSheetID(SHEET_WITHDRAW_ACCEPT) } + .task { if let amount, let model { + do { // TODO: cancelled + symLog.log(".task") + if exchangeBaseUrl.hasPrefix(HTTPS) { + symLog.log("amount: \(amount), baseURL: \(String(describing: exchangeBaseUrl))") + // TODO: let user choose exchange from list + manualWithdrawalDetails = try await model.loadWithdrawalDetailsForAmountM(exchangeBaseUrl, amount: amount) + symLog.log("raw: \(manualWithdrawalDetails!.amountRaw), effective: \(manualWithdrawalDetails!.amountEffective)") + } else { + // TODO: error no exchange! + } + } catch { // TODO: error + symLog.log(error.localizedDescription) + } + } else { + // TODO: error no amount! + } } } } // MARK: - struct WithdrawAcceptView_Previews: PreviewProvider { static var previews: some View { - let details = ManualWithdrawalDetails(amountRaw: try! Amount(fromString: LONGCURRENCY + ":2.4"), - amountEffective: try! Amount(fromString: LONGCURRENCY + ":2.2"), - paytoUris: [], - tosAccepted: true) - WithdrawAcceptView(url: URL(string: DEMOSHOP)!, - model: nil, - detailsForAmount: details, - baseURL: DEMOEXCHANGE) + let amount = try! Amount(fromString: LONGCURRENCY + ":2.4") + WithdrawAcceptView(exchangeBaseUrl: DEMOEXCHANGE, + model: nil, + amount: amount, + url: URL(string: DEMOSHOP)!) } } diff --git a/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawProgressView.swift b/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawProgressView.swift @@ -8,12 +8,16 @@ struct WithdrawProgressView: View { let message: String var body: some View { - Form { + VStack { Spacer() ProgressView() Spacer() - Text(message) - .font(.title) + HStack { + Spacer() + Text(message) + .font(.title) + Spacer() + } Spacer() Spacer() } diff --git a/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawURIView.swift b/TalerWallet1/Views/WithdrawBankIntegrated/WithdrawURIView.swift @@ -3,6 +3,7 @@ * See LICENSE.md */ import SwiftUI +import taler_swift import SymLog // Will be called either by the user scanning a QR code or tapping the provided link, both from the bank's website @@ -10,41 +11,67 @@ import SymLog // after the user confirmed the withdrawal, we remind them to return to the bank website to confirm there, too struct WithdrawURIView: View { private let symLog = SymLogV() + let navTitle = String(localized: "Accept Withdrawal") + // the URL from the bank website - var url: URL + let url: URL let model = WithdrawModel.model(baseURL: "global") // TODO: get baseURL from URL // the exchange used for this withdrawal. - @State var exchangeBaseUrl: String = "" - @State var manualWithdrawalDetails: ManualWithdrawalDetails? - @State var didAcceptTOS: Bool = false + @State private var exchangeBaseUrl: String? = nil + @State private var manualWithdrawalDetails: ManualWithdrawalDetails? var body: some View { let badURL = "Error in URL: \(url)" VStack { - if !didAcceptTOS { // user must accept ToS first - WithdrawTOSView(exchangeBaseUrl: exchangeBaseUrl, - model: model, - viewID: SHEET_WITHDRAW_TOS) { - didAcceptTOS = true + if let manualWithdrawalDetails, let exchangeBaseUrl { + List { + let raw = manualWithdrawalDetails.amountRaw + let effective = manualWithdrawalDetails.amountEffective + let currency = raw.currencyStr + let fee = try! Amount.diff(raw, effective) + let outColor = WalletColors().transactionColor(false) + let inColor = WalletColors().transactionColor(true) + + ThreeAmountsView(topTitle: String(localized: "Chosen amount to withdraw:"), + topAmount: raw, fee: fee, + bottomTitle: String(localized: "\(currency) to be withdrawn:"), + bottomAmount: effective, + large: false, pending: false, incoming: true, + baseURL: exchangeBaseUrl) + let someCoins = SomeCoins(details: manualWithdrawalDetails) + QuiteSomeCoins(someCoins: someCoins, shouldShowFee: false, + currency: raw.currencyStr, amountEffective: effective) } - } else { // show Amount details and let user accept - WithdrawAcceptView(url: url, model: model, - detailsForAmount: manualWithdrawalDetails!, - baseURL: exchangeBaseUrl) - } - } - .overlay { - if !exchangeBaseUrl.hasPrefix(HTTPS) { - WithdrawProgressView(message: url.host ?? badURL) - .navigationTitle("Contacting Exchange") - } else if manualWithdrawalDetails == nil { - WithdrawProgressView(message: exchangeBaseUrl.trimURL()) - .navigationTitle("Found Exchange") + .navigationTitle(navTitle) + let tosAccepted = manualWithdrawalDetails.tosAccepted + if tosAccepted { + NavigationLink(destination: LazyView { + WithdrawAcceptDone(exchangeBaseUrl: exchangeBaseUrl, model: model, url: url) + }) { + Text("Confirm Withdrawal") // SHEET_WITHDRAW_ACCEPT + }.buttonStyle(TalerButtonStyle(type: .prominent)) + .padding() + } else { + NavigationLink(destination: LazyView { + WithdrawTOSView(exchangeBaseUrl: exchangeBaseUrl, + model: model, + viewID: SHEET_WITHDRAW_TOS, + acceptAction: nil) // pop back to here + }) { + Text("Check Terms of Service") // VIEW_WITHDRAW_TOS + }.buttonStyle(TalerButtonStyle(type: .prominent)) + .padding() + } + } else { + // Yikes no details or no baseURL +// WithdrawProgressView(message: url.host ?? badURL) +// .navigationTitle("Contacting Exchange") } } .onAppear() { + symLog.log("onAppear") DebugViewC.shared.setSheetID(SHEET_WITHDRAWAL) } .task { @@ -57,19 +84,17 @@ struct WithdrawURIView: View { } else if let first = withdrawUriInfo.possibleExchanges.first { exchangeBaseUrl = first.exchangeBaseUrl } - if exchangeBaseUrl.hasPrefix(HTTPS) { - symLog.log("amount: \(amount), baseURL: \(String(describing: exchangeBaseUrl))") - // TODO: let user choose exchange from list - manualWithdrawalDetails = try await model.loadWithdrawalDetailsForAmountM(exchangeBaseUrl, amount: amount) - symLog.log("raw: \(manualWithdrawalDetails!.amountRaw), effective: \(manualWithdrawalDetails!.amountEffective)") - if manualWithdrawalDetails!.tosAccepted { - didAcceptTOS = true - } - } else { - // TODO: error no exchange! + if let exchangeBaseUrl { + let details = try await model.loadWithdrawalDetailsForAmountM(exchangeBaseUrl, amount: amount) + manualWithdrawalDetails = details +// agePicker.setAges(ages: details?.ageRestrictionOptions) + } else { // TODO: error + symLog.log("no exchangeBaseUrl") + manualWithdrawalDetails = nil } } catch { // TODO: error symLog.log(error.localizedDescription) + manualWithdrawalDetails = nil } } }