taler-ios

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

commit d2a363c17370d1af852683cf4ca5f0877fdca6a9
parent b8635ef50af639db230a8323712a2d2022ccda13
Author: Marc Stibane <marc@taler.net>
Date:   Tue, 20 Feb 2024 19:15:34 +0100

Pay-Template

Diffstat:
MTalerWallet.xcodeproj/project.pbxproj | 24++++++++++++++++++------
MTalerWallet1/Controllers/DebugViewC.swift | 4+++-
ATalerWallet1/Views/HelperViews/AmountInputV.swift | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ATalerWallet1/Views/HelperViews/SubjectInputV.swift | 122+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ATalerWallet1/Views/Sheets/Payment/PayTemplateV.swift | 217+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
DTalerWallet1/Views/Sheets/Payment/PayTemplateView.swift | 140-------------------------------------------------------------------------------
MTalerWallet1/Views/Sheets/Payment/PaymentView.swift | 23++++++++++++++++++++---
MTalerWallet1/Views/Sheets/URLSheet.swift | 9+++++----
8 files changed, 499 insertions(+), 154 deletions(-)

diff --git a/TalerWallet.xcodeproj/project.pbxproj b/TalerWallet.xcodeproj/project.pbxproj @@ -65,7 +65,6 @@ 4E3EAE452A990778009F1BE8 /* P2PReadyV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB3136029FEE79B007D68BC /* P2PReadyV.swift */; }; 4E3EAE462A990778009F1BE8 /* TextFieldAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095482989CBFE0043A8A1 /* TextFieldAlert.swift */; }; 4E3EAE472A990778009F1BE8 /* QuiteSomeCoins.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EBA82AC2A3F580500E5F39A /* QuiteSomeCoins.swift */; }; - 4E3EAE482A990778009F1BE8 /* PayTemplateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EBA56402A7FF5200084948B /* PayTemplateView.swift */; }; 4E3EAE492A990778009F1BE8 /* ManualWithdrawDone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB431662A1E55C700C5690E /* ManualWithdrawDone.swift */; }; 4E3EAE4B2A990778009F1BE8 /* ShareSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E753A072A0B6A5F002D9328 /* ShareSheet.swift */; }; 4E3EAE4C2A990778009F1BE8 /* AmountRowV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095492989CBFE0043A8A1 /* AmountRowV.swift */; }; @@ -227,7 +226,6 @@ 4EB3136129FEE79B007D68BC /* P2PReadyV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB3136029FEE79B007D68BC /* P2PReadyV.swift */; }; 4EB431672A1E55C700C5690E /* ManualWithdrawDone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB431662A1E55C700C5690E /* ManualWithdrawDone.swift */; }; 4EBA563F2A7FD9390084948B /* SuperScriptDigits.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EBA563E2A7FD9390084948B /* SuperScriptDigits.swift */; }; - 4EBA56412A7FF5200084948B /* PayTemplateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EBA56402A7FF5200084948B /* PayTemplateView.swift */; }; 4EBA82AB2A3EB2CA00E5F39A /* TransactionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EBA82AA2A3EB2CA00E5F39A /* TransactionButton.swift */; }; 4EBA82AD2A3F580500E5F39A /* QuiteSomeCoins.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EBA82AC2A3F580500E5F39A /* QuiteSomeCoins.swift */; }; 4EBC0F012B7B3CD600C0CB19 /* DepositIbanV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EBC0F002B7B3CD600C0CB19 /* DepositIbanV.swift */; }; @@ -247,6 +245,12 @@ 4EE171882B49635800BF9FF5 /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = 4EE171872B49635800BF9FF5 /* MarkdownUI */; }; 4EE171902B49FE2B00BF9FF5 /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = 4EE1718F2B49FE2B00BF9FF5 /* OrderedCollections */; }; 4EE171922B49FE4E00BF9FF5 /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = 4EE171912B49FE4E00BF9FF5 /* OrderedCollections */; }; + 4EEC118D2B83DE4800146CFF /* AmountInputV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EEC118C2B83DE4700146CFF /* AmountInputV.swift */; }; + 4EEC118E2B83DE4800146CFF /* AmountInputV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EEC118C2B83DE4700146CFF /* AmountInputV.swift */; }; + 4EEC11932B83FB7A00146CFF /* SubjectInputV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EEC11922B83FB7A00146CFF /* SubjectInputV.swift */; }; + 4EEC11942B83FB7A00146CFF /* SubjectInputV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EEC11922B83FB7A00146CFF /* SubjectInputV.swift */; }; + 4EEC11962B840F1100146CFF /* PayTemplateV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EEC11952B840F1100146CFF /* PayTemplateV.swift */; }; + 4EEC11972B840F1100146CFF /* PayTemplateV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EEC11952B840F1100146CFF /* PayTemplateV.swift */; }; 4EEC157329F8242800D46A03 /* QRGeneratorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EEC157229F8242800D46A03 /* QRGeneratorView.swift */; }; 4EEC157629F8ECBF00D46A03 /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = 4EEC157529F8ECBF00D46A03 /* CodeScanner */; }; 4EEC157829F9032900D46A03 /* Sheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EEC157729F9032900D46A03 /* Sheet.swift */; }; @@ -411,7 +415,6 @@ 4EB3136029FEE79B007D68BC /* P2PReadyV.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = P2PReadyV.swift; sourceTree = "<group>"; }; 4EB431662A1E55C700C5690E /* ManualWithdrawDone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManualWithdrawDone.swift; sourceTree = "<group>"; }; 4EBA563E2A7FD9390084948B /* SuperScriptDigits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuperScriptDigits.swift; sourceTree = "<group>"; }; - 4EBA56402A7FF5200084948B /* PayTemplateView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PayTemplateView.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>"; }; 4EBC0F002B7B3CD600C0CB19 /* DepositIbanV.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DepositIbanV.swift; sourceTree = "<group>"; }; @@ -423,6 +426,9 @@ 4ECB62812A0BB01D004ABBB7 /* SelectDays.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectDays.swift; sourceTree = "<group>"; }; 4ED2F94A2A278F5100453B40 /* ThreeAmountsV.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThreeAmountsV.swift; sourceTree = "<group>"; }; 4EDBDCD82AB787CB00925C02 /* CallStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallStack.swift; sourceTree = "<group>"; }; + 4EEC118C2B83DE4700146CFF /* AmountInputV.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AmountInputV.swift; sourceTree = "<group>"; }; + 4EEC11922B83FB7A00146CFF /* SubjectInputV.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubjectInputV.swift; sourceTree = "<group>"; }; + 4EEC11952B840F1100146CFF /* PayTemplateV.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PayTemplateV.swift; sourceTree = "<group>"; }; 4EEC157229F8242800D46A03 /* QRGeneratorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRGeneratorView.swift; sourceTree = "<group>"; }; 4EEC157729F9032900D46A03 /* Sheet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Sheet.swift; sourceTree = "<group>"; }; 4EEC157929F9427F00D46A03 /* QRSheet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRSheet.swift; sourceTree = "<group>"; }; @@ -698,7 +704,7 @@ children = ( 4EB0952D2989CBFE0043A8A1 /* PaymentView.swift */, 4E6EF56A2B65A33300AF252A /* PaymentDone.swift */, - 4EBA56402A7FF5200084948B /* PayTemplateView.swift */, + 4EEC11952B840F1100146CFF /* PayTemplateV.swift */, ); path = Payment; sourceTree = "<group>"; @@ -758,6 +764,8 @@ 4EF840A62A0B85F400EE0D47 /* CopyShare.swift */, 4ECB62812A0BB01D004ABBB7 /* SelectDays.swift */, 4EA551242A2C923600FEC9A8 /* CurrencyInputView.swift */, + 4EEC118C2B83DE4700146CFF /* AmountInputV.swift */, + 4EEC11922B83FB7A00146CFF /* SubjectInputV.swift */, 4E2D8DD42B45822A00234039 /* AmountV.swift */, 4E53A33629F50B7B00830EC2 /* CurrencyField.swift */, 4EEC157229F8242800D46A03 /* QRGeneratorView.swift */, @@ -1068,6 +1076,7 @@ 4E3EAE202A990778009F1BE8 /* MainView.swift in Sources */, 4E3EAE212A990778009F1BE8 /* Buttons.swift in Sources */, 4E3EAE222A990778009F1BE8 /* TransactionButton.swift in Sources */, + 4EEC118D2B83DE4800146CFF /* AmountInputV.swift in Sources */, 4E3EAE232A990778009F1BE8 /* BalancesSectionView.swift in Sources */, 4E3EAE242A990778009F1BE8 /* QRGeneratorView.swift in Sources */, 4E3EAE252A990778009F1BE8 /* WithdrawAcceptDone.swift in Sources */, @@ -1090,11 +1099,13 @@ 4E605DAF2AADDD13002FB9A7 /* UIScreen+screenSize.swift in Sources */, 4E3EAE312A990778009F1BE8 /* SendAmount.swift in Sources */, 4E3EAE332A990778009F1BE8 /* EqualIconWidthDomain.swift in Sources */, + 4EEC11932B83FB7A00146CFF /* SubjectInputV.swift in Sources */, 4E3EAE342A990778009F1BE8 /* SuperScriptDigits.swift in Sources */, 4E3EAE352A990778009F1BE8 /* P2pPayURIView.swift in Sources */, 4E3EAE362A990778009F1BE8 /* Model+Payment.swift in Sources */, 4E3EAE372A990778009F1BE8 /* SettingsView.swift in Sources */, 4E3EAE382A990778009F1BE8 /* PaymentView.swift in Sources */, + 4EEC11962B840F1100146CFF /* PayTemplateV.swift in Sources */, 4E3EAE392A990778009F1BE8 /* WithdrawURIView.swift in Sources */, 4E3EAE3A2A990778009F1BE8 /* CopyShare.swift in Sources */, 4E3EAE3B2A990778009F1BE8 /* TalerWallet1App.swift in Sources */, @@ -1111,7 +1122,6 @@ 4E3EAE462A990778009F1BE8 /* TextFieldAlert.swift in Sources */, 4E3EAE472A990778009F1BE8 /* QuiteSomeCoins.swift in Sources */, 4E2D8DD52B45822A00234039 /* AmountV.swift in Sources */, - 4E3EAE482A990778009F1BE8 /* PayTemplateView.swift in Sources */, 4E3EAE492A990778009F1BE8 /* ManualWithdrawDone.swift in Sources */, 4E3EAE4B2A990778009F1BE8 /* ShareSheet.swift in Sources */, 4EC4008F2AE8019700DF72C7 /* ExchangeRowView.swift in Sources */, @@ -1179,6 +1189,7 @@ 4EB095682989CBFE0043A8A1 /* MainView.swift in Sources */, 4EB0956A2989CBFE0043A8A1 /* Buttons.swift in Sources */, 4EBA82AB2A3EB2CA00E5F39A /* TransactionButton.swift in Sources */, + 4EEC118E2B83DE4800146CFF /* AmountInputV.swift in Sources */, 4EB095602989CBFE0043A8A1 /* BalancesSectionView.swift in Sources */, 4EEC157329F8242800D46A03 /* QRGeneratorView.swift in Sources */, 4E5A88F72A3B9E5B00072618 /* WithdrawAcceptDone.swift in Sources */, @@ -1201,11 +1212,13 @@ 4E605DB02AADDD13002FB9A7 /* UIScreen+screenSize.swift in Sources */, 4E40E0BE29F25ABB00B85369 /* SendAmount.swift in Sources */, 4E8E25332A1CD39700A27BFA /* EqualIconWidthDomain.swift in Sources */, + 4EEC11942B83FB7A00146CFF /* SubjectInputV.swift in Sources */, 4EBA563F2A7FD9390084948B /* SuperScriptDigits.swift in Sources */, 4E578E942A4822D500F21F1C /* P2pPayURIView.swift in Sources */, 4EB095542989CBFE0043A8A1 /* Model+Payment.swift in Sources */, 4EB0954F2989CBFE0043A8A1 /* SettingsView.swift in Sources */, 4EB095552989CBFE0043A8A1 /* PaymentView.swift in Sources */, + 4EEC11972B840F1100146CFF /* PayTemplateV.swift in Sources */, 4EB095612989CBFE0043A8A1 /* WithdrawURIView.swift in Sources */, 4EF840A72A0B85F400EE0D47 /* CopyShare.swift in Sources */, 4EB094ED298979620043A8A1 /* TalerWallet1App.swift in Sources */, @@ -1222,7 +1235,6 @@ 4EB0956B2989CBFE0043A8A1 /* TextFieldAlert.swift in Sources */, 4EBA82AD2A3F580500E5F39A /* QuiteSomeCoins.swift in Sources */, 4E2D8DD62B45822A00234039 /* AmountV.swift in Sources */, - 4EBA56412A7FF5200084948B /* PayTemplateView.swift in Sources */, 4EB431672A1E55C700C5690E /* ManualWithdrawDone.swift in Sources */, 4E753A082A0B6A5F002D9328 /* ShareSheet.swift in Sources */, 4EC400902AE8019700DF72C7 /* ExchangeRowView.swift in Sources */, diff --git a/TalerWallet1/Controllers/DebugViewC.swift b/TalerWallet1/Controllers/DebugViewC.swift @@ -81,7 +81,9 @@ public let SHEET_WITHDRAW_CONFIRM = SHEET_WITHDRAW_ACCEPT + 1 // 133 waiti // openURL (Link, NFC or scan QR) ==> pays merchant public let SHEET_PAYMENT = SHEET_WITHDRAWAL + 10 // 140 Pay Merchant public let SHEET_PAY_TEMPLATE = SHEET_PAYMENT + 1 // 141 Pay Merchant Template -public let SHEET_PAY_ACCEPT = SHEET_PAY_TEMPLATE + 1 // 142 Pay Accept +public let SHEET_PAY_TEMPL_AMOUNT = SHEET_PAY_TEMPLATE + 1 // 142 Pay Template Amount +public let SHEET_PAY_TEMPL_SUBJECT = SHEET_PAY_TEMPL_AMOUNT + 1 // 143 Pay Template Subject +public let SHEET_PAY_ACCEPT = SHEET_PAY_TEMPL_SUBJECT + 1 // 144 Pay Accept // MARK: P2P Pay Invoice // p2p pull debit - openURL (Link or scan QR) diff --git a/TalerWallet1/Views/HelperViews/AmountInputV.swift b/TalerWallet1/Views/HelperViews/AmountInputV.swift @@ -0,0 +1,114 @@ +/* + * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. + * See LICENSE.md + */ +import SwiftUI +import taler_swift +import SymLog + +struct AmountInputV: View { + private let symLog = SymLogV(0) + let stack: CallStack + // the scanned URL + let url: URL? + let amountAvailable: Amount? // TODO: GetMaxPeerPushAmount + @Binding var amountToTransfer: Amount + let amountLabel: String + let wantsSummary: Bool // if true we call SubjectInputV next + @Binding var summary: String + @Binding var insufficient: Bool + @Binding var feeAmount: Amount? + let shortcutAction: ((_ amount: Amount) -> Void)? + let buttonAction: () -> Void + + @EnvironmentObject private var controller: Controller + @EnvironmentObject private var model: WalletModel + @Environment(\.colorScheme) private var colorScheme + @Environment(\.colorSchemeContrast) private var colorSchemeContrast + + @State private var preparePayResult: PreparePayResult? = nil + @State private var feeStr: String = EMPTYSTRING + + var feeLabel: String { feeStr.count > 0 ? String(localized: "+ \(feeStr) fee") : EMPTYSTRING } + + func preparePayForTemplate() async { + if let url { + do { + let ppCheck = try await model.preparePayForTemplateM(url.absoluteString, amount: amountToTransfer, summary: summary) + let amount = ppCheck.amountRaw + let currency = amount.currencyStr + let currencyInfo = controller.info(for: currency, controller.currencyTicker) + insufficient = ppCheck.status == .insufficientBalance + feeAmount = templateFee(ppCheck: ppCheck) + if let feeAmount { + feeStr = feeAmount.string(currencyInfo) + } else { feeStr = EMPTYSTRING } + let amountVoiceOver = amount.string(currencyInfo) + announce(this: "\(amountVoiceOver), \(feeLabel)") + preparePayResult = ppCheck + } catch { // TODO: error + symLog.log(error.localizedDescription) + } + } + } + + var body: some View { + let currency = amountToTransfer.currencyStr + let currencyInfo = controller.info(for: currency, controller.currencyTicker) + let insufficientLabel = String(localized: "You don't have enough \(currency).") + let available = amountAvailable?.string(currencyInfo) ?? nil + let disabled = insufficient || amountToTransfer.isZero + VStack(alignment: .trailing) { + if let available { + Text("Available:\t\(available)") + .talerFont(.title3) + .padding(.bottom, 2) + // .accessibility(sortPriority: 3) + } + CurrencyInputView(amount: $amountToTransfer, + available: amountAvailable, + title: amountLabel, + shortcutAction: shortcutAction) +// .accessibility(sortPriority: 2) + Text(insufficient ? insufficientLabel + : feeLabel) + .talerFont(.body) + .foregroundColor(insufficient ? .red + : (feeAmount?.isZero ?? true) ? WalletColors().secondary(colorScheme, colorSchemeContrast) + : .red) +// .accessibility(sortPriority: 1) + .padding(4) + Group { + if let url { + let destination = LazyView { + PaymentView(stack: stack.push(), + url: url, + template: true, + amountToTransfer: $amountToTransfer, + summary: $summary) + } + NavigationLink(destination: destination) { + Text("Next") + } + .buttonStyle(TalerButtonStyle(type: .prominent)) + .disabled(disabled) + } else { + Button("Next") { + buttonAction() + } + .buttonStyle(TalerButtonStyle(type: .prominent)) + .disabled(disabled) + } + } +// .accessibility(sortPriority: 0) + .task(id: amountToTransfer.value) { + symLog.log(".task") + await preparePayForTemplate() + } + }.padding(.horizontal) + .onAppear() { + // symLog.log("onAppear") + DebugViewC.shared.setSheetID(SHEET_PAY_TEMPL_AMOUNT) + } + } +} diff --git a/TalerWallet1/Views/HelperViews/SubjectInputV.swift b/TalerWallet1/Views/HelperViews/SubjectInputV.swift @@ -0,0 +1,122 @@ +/* + * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. + * See LICENSE.md + */ +import SwiftUI +import taler_swift +import SymLog + +struct SubjectInputV<TargetView: View>: View { + private let symLog = SymLogV(0) + let stack: CallStack + // the scanned URL + let url: URL? + let amountAvailable: Amount? // TODO: GetMaxPeerPushAmount + @Binding var amountToTransfer: Amount + let amountLabel: String + @Binding var summary: String + @Binding var insufficient: Bool + @Binding var feeAmount: Amount? + let feeIsNotZero: Bool? // nil = no fees at all, false = no fee for this tx + let currencyInfo: CurrencyInfo + + var targetView: TargetView + +// let destination: Destination + + @EnvironmentObject private var controller: Controller + @EnvironmentObject private var model: WalletModel + @Environment(\.colorScheme) private var colorScheme + @Environment(\.colorSchemeContrast) private var colorSchemeContrast + @AppStorage("minimalistic") var minimalistic: Bool = false + + @State private var preparePayResult: PreparePayResult? = nil + @State private var feeStr: String = EMPTYSTRING + @FocusState private var isFocused: Bool + + var feeLabel: String { feeStr.count > 0 ? String(localized: "+ \(feeStr) fee") : EMPTYSTRING } + + func preparePayForTemplate() async { + if let url { + do { + let ppCheck = try await model.preparePayForTemplateM(url.absoluteString, amount: amountToTransfer, summary: summary) + let amount = ppCheck.amountRaw + let currency = amount.currencyStr + let currencyInfo = controller.info(for: currency, controller.currencyTicker) + insufficient = ppCheck.status == .insufficientBalance + feeAmount = templateFee(ppCheck: ppCheck) + if let feeAmount { + feeStr = feeAmount.string(currencyInfo) + } else { feeStr = EMPTYSTRING } + let amountVoiceOver = amount.string(currencyInfo) + announce(this: "\(amountVoiceOver), \(feeLabel)") + preparePayResult = ppCheck + } catch { // TODO: error + symLog.log(error.localizedDescription) + } + } + } + + var body: some View { + let currency = amountToTransfer.currencyStr + let currencyInfo = controller.info(for: currency, controller.currencyTicker) + let insufficientLabel = String(localized: "You don't have enough \(currency).") + let available = amountAvailable?.string(currencyInfo) ?? nil + let disabled = insufficient || summary.count == 0 + VStack(alignment: .leading) { + if let available { + Text("Available:\t\(available)") + .talerFont(.title3) + .padding(.bottom, 2) +// .accessibility(sortPriority: 3) + } + + if !minimalistic { + Text("Enter subject:") // Purpose + .talerFont(.title3) + .accessibilityAddTraits(.isHeader) + .accessibilityRemoveTraits(.isStaticText) + .padding(.top) + } + Group { if #available(iOS 16.0, *) { + TextField(minimalistic ? "Subject" : EMPTYSTRING, text: $summary, axis: .vertical) + .focused($isFocused) + .lineLimit(2...) + } else { + TextField("Subject", text: $summary) + .focused($isFocused) + // .lineLimit(2...5) // lineLimit' is only available in iOS 16.0 or newer + } } // Group for iOS16+ & iOS15 + .talerFont(.title2) + .foregroundColor(WalletColors().fieldForeground) // text color + .background(WalletColors().fieldBackground) + .textFieldStyle(.roundedBorder) + .onAppear { + symLog.log("dispatching kbd...") + DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) { + isFocused = true // make first responder - raise keybord + symLog.log("...kbd isFocused") + } + } + + let subLabel = insufficient ? insufficientLabel + : feeLabel + Text(subLabel) + .talerFont(.body) + .foregroundColor(insufficient ? .red + : (feeAmount?.isZero ?? true) ? WalletColors().secondary(colorScheme, colorSchemeContrast) + : .red) + // .accessibility(sortPriority: 1) + .padding(4) + + NavigationLink("Next", destination: targetView) + .buttonStyle(TalerButtonStyle(type: .prominent)) + .disabled(disabled) +// .accessibility(sortPriority: 0) + }.padding(.horizontal) + .onAppear() { +// symLog.log("onAppear") + DebugViewC.shared.setSheetID(SHEET_PAY_TEMPL_SUBJECT) + } + } +} diff --git a/TalerWallet1/Views/Sheets/Payment/PayTemplateV.swift b/TalerWallet1/Views/Sheets/Payment/PayTemplateV.swift @@ -0,0 +1,217 @@ +/* + * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. + * See LICENSE.md + */ +import SwiftUI +import taler_swift +import SymLog + +func templateFee(ppCheck: PreparePayResult?) -> Amount? { + do { + if let ppCheck { + // Outgoing: fee = effective - raw + if let effective = ppCheck.amountEffective { + let fee = try effective - ppCheck.amountRaw + return fee + } + } + } catch {} + return nil +} + +// Will be called either by the user scanning a QR code or tapping the provided link, +// both from the shop's website. We show the payment details +struct PayTemplateV: View { + private let symLog = SymLogV(0) + let stack: CallStack + + // the scanned URL + let url: URL + + @EnvironmentObject private var controller: Controller + @EnvironmentObject private var model: WalletModel + @AppStorage("minimalistic") var minimalistic: Bool = false + @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic + + let navTitle = String(localized: "Customize Order", comment:"pay merchant") + + @State private var insufficient = false + @State private var preparePayResult: PreparePayResult? = nil + @State private var currencyName: String? = nil + @State private var amountToTransfer = Amount.zero(currency: EMPTYSTRING) // Update currency when used + @State private var amountShortcut = Amount.zero(currency: EMPTYSTRING) // Update currency when used + @State private var shortcutSelected = false + @State private var buttonSelected1 = false + @State private var buttonSelected2 = false + @State private var summary: String = EMPTYSTRING // templateParam + @State private var wantsSummary: Bool = false + @State private var feeAmount: Amount? = nil + @State private var feeStr: String = EMPTYSTRING + + var feeLabel: String { feeStr.count > 0 ? String(localized: "+ \(feeStr) fee") : EMPTYSTRING } + + private func shortcutAction(_ shortcut: Amount) { + amountShortcut = shortcut + shortcutSelected = true + } + private func buttonAction1() { + buttonSelected1 = true + } + private func buttonAction2() { + buttonSelected2 = true + } + func acceptAction(preparePayResult: PreparePayResult) { + Task { // runs on MainActor + do { + let confirmPayResult = try await model.confirmPayM(preparePayResult.transactionId) +// symLog.log(confirmPayResult as Any) + if confirmPayResult.type != "done" { + controller.playSound(0) + // TODO: show error + } + } catch { + controller.playSound(0) + // TODO: error + symLog.log(error.localizedDescription) + } + dismissTop() + } + } + + func queryURL() -> Bool { + if let queryParameters = url.queryParameters { + if let amountStr = queryParameters["amount"] { + currencyName = amountStr + amountToTransfer = Amount.zero(currency: amountStr) + } + if let summaryStr = queryParameters["summary"] { + summary = summaryStr + wantsSummary = true + } + return true + } + return false + } + + func preparePayForTemplate() async { + do { + let ppCheck = try await model.preparePayForTemplateM(url.absoluteString, amount: amountToTransfer, summary: summary) + let amount = ppCheck.amountRaw + let currency = amount.currencyStr + let currencyInfo = controller.info(for: currency, controller.currencyTicker) + insufficient = ppCheck.status == .insufficientBalance + feeAmount = templateFee(ppCheck: ppCheck) + if let feeAmount { + feeStr = feeAmount.string(currencyInfo) + } else { feeStr = EMPTYSTRING } + let amountVoiceOver = amount.string(currencyInfo) + announce(this: "\(amountVoiceOver), \(feeLabel)") + preparePayResult = ppCheck + } catch { // TODO: error + symLog.log(error.localizedDescription) + } + } + + var body: some View { + if let preparePayResult { + let effective = preparePayResult.amountEffective + let baseURL = preparePayResult.contractTerms.exchanges.first?.url + let raw = preparePayResult.amountRaw + let currency = raw.currencyStr + let currencyInfo = controller.info(for: currency, controller.currencyTicker) + let amountLabel = minimalistic ? String(localized: "Amount:") + : String(localized: "Amount to pay:") + let finalDestinationI = LazyView { + PaymentView(stack: stack.push(), + url: url, + template: true, + amountToTransfer: $amountToTransfer, + summary: $summary) + } + let inputDestination = LazyView { + SubjectInputV(stack: stack.push(), url: url, + amountAvailable: nil, + amountToTransfer: $amountToTransfer, + amountLabel: amountLabel, + summary: $summary, + insufficient: $insufficient, + feeAmount: $feeAmount, + feeIsNotZero: true, // feeIsNotZero(), + currencyInfo: currencyInfo, + targetView: finalDestinationI) + } + let finalDestinationS = LazyView { + PaymentView(stack: stack.push(), + url: url, + template: true, + amountToTransfer: $amountShortcut, + summary: $summary) + } + let shortcutDestination = LazyView { + SubjectInputV(stack: stack.push(), url: url, + amountAvailable: nil, + amountToTransfer: $amountShortcut, + amountLabel: amountLabel, + summary: $summary, + insufficient: $insufficient, + feeAmount: $feeAmount, + feeIsNotZero: true, // feeIsNotZero(), + currencyInfo: currencyInfo, + targetView: finalDestinationS) + } + Group { + if let currencyName { // template included a currency name => let the user input an amount + let amountInput = AmountInputV(stack: stack.push(), url: url, + amountAvailable: nil, + amountToTransfer: $amountToTransfer, + amountLabel: amountLabel, + wantsSummary: wantsSummary, + summary: $summary, + insufficient: $insufficient, + feeAmount: $feeAmount, + shortcutAction: shortcutAction, + buttonAction: buttonAction1) + ScrollView { + if wantsSummary { + amountInput + .background(NavigationLink(destination: shortcutDestination, isActive: $shortcutSelected) + { EmptyView() }.frame(width: 0).opacity(0).hidden()) + .background( NavigationLink(destination: inputDestination, isActive: $buttonSelected1) + { EmptyView() }.frame(width: 0).opacity(0).hidden()) + + } else { + amountInput + .background(NavigationLink(destination: finalDestinationS, isActive: $shortcutSelected) + { EmptyView() }.frame(width: 0).opacity(0).hidden()) + .background(NavigationLink(destination: finalDestinationI, isActive: $buttonSelected1) + { EmptyView() }.frame(width: 0).opacity(0).hidden()) + } + } // ScrollVStack + } else if wantsSummary { // check summary + ScrollView { + inputDestination + .background(NavigationLink(destination: finalDestinationI, isActive: $buttonSelected2) + { EmptyView() }.frame(width: 0).opacity(0).hidden() + ) + } + } else { + PaymentView(stack: stack.push(), + url: url, + template: true, + amountToTransfer: $amountToTransfer, + summary: $summary) + } + }.onAppear() { + symLog.log("onAppear") + DebugViewC.shared.setSheetID(SHEET_PAY_TEMPLATE) + }.navigationTitle(navTitle) + } else { + LoadingView(url: url, message: nil) + .task { + symLog.log(".task") + let hasParams = queryURL() + await preparePayForTemplate() + } + } + } +} diff --git a/TalerWallet1/Views/Sheets/Payment/PayTemplateView.swift b/TalerWallet1/Views/Sheets/Payment/PayTemplateView.swift @@ -1,140 +0,0 @@ -/* - * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. - * 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 shop's website. We show the payment details -struct PayTemplateView: View { - private let symLog = SymLogV(0) - let stack: CallStack - let navTitle = String(localized: "Confirm Payment", comment:"pay merchant") - - // the scanned URL - let url: URL - - @EnvironmentObject private var controller: Controller - @EnvironmentObject private var model: WalletModel - @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic - - @State private var preparePayResult: PreparePayResult? = nil - @State private var amount: Amount? = nil // templateParam - @State private var summary: String? = nil // templateParam - - func acceptAction(preparePayResult: PreparePayResult) { - Task { // runs on MainActor - do { - let confirmPayResult = try await model.confirmPayM(preparePayResult.transactionId) -// symLog.log(confirmPayResult as Any) - if confirmPayResult.type != "done" { - controller.playSound(0) - // TODO: show error - } - } catch { - controller.playSound(0) - // TODO: error - symLog.log(error.localizedDescription) - } - dismissTop() - } - } - - func queryURL() -> Bool { - if let queryParameters = url.queryParameters { - if let amountStr = queryParameters["amount"] { - amount = Amount.zero(currency: amountStr) - } - if let summaryStr = queryParameters["summary"] { - summary = summaryStr - } - return true - } - return false - } - - func preparePayForTemplate() async { - do { - let result = try await model.preparePayForTemplateM(url.absoluteString, amount: amount, summary: summary) - preparePayResult = result - } catch { // TODO: error - symLog.log(error.localizedDescription) - } - } - - var body: some View { - Group { - if let preparePayResult { - let effective = preparePayResult.amountEffective - List { - let baseURL = preparePayResult.contractTerms.exchanges.first?.url - let raw = preparePayResult.amountRaw - let currency = raw.currencyStr - let topTitle = String(localized: "Amount to pay:") - let topAbbrev = String(localized: "Pay:", comment: "mini") - if let effective { - // TODO: already paid - let fee = try! Amount.diff(raw, effective) // TODO: different currencies - ThreeAmountsV(stack: stack.push(), - topTitle: topTitle, - topAbbrev: topAbbrev, - topAmount: raw, fee: fee, - bottomTitle: String(localized: "Amount to spend:"), - bottomAbbrev: String(localized: "Effective:", comment: "mini"), - bottomAmount: effective, - large: false, pending: false, incoming: false, - baseURL: baseURL, - status: nil, - summary: nil, - merchant: nil) - // TODO: payment: popup with all possible exchanges, check fees - } else if let balanceDetails = preparePayResult.balanceDetails { // Insufficient - Text("You don't have enough \(currency)") - .talerFont(.body) - ThreeAmountsV(stack: stack.push(), - topTitle: topTitle, - topAbbrev: topAbbrev, - topAmount: raw, fee: nil, - bottomTitle: String(localized: "Amount available:"), - bottomAbbrev: String(localized: "Available:", comment: "mini"), - bottomAmount: balanceDetails.balanceAvailable, - large: false, pending: false, incoming: false, - baseURL: baseURL, - status: nil, - summary: nil, - merchant: nil) - } else { - // TODO: Error - neither effective nor balanceDetails - Text("Error") - .talerFont(.body) - } - } - .listStyle(myListStyle.style).anyView - .safeAreaInset(edge: .bottom) { - if let effective { - Button(navTitle, action: { acceptAction(preparePayResult: preparePayResult) }) - .buttonStyle(TalerButtonStyle(type: .prominent)) - .padding(.horizontal) - } else { - Button("Cancel", action: { dismissTop() }) - .buttonStyle(TalerButtonStyle(type: .bordered)) - .padding(.horizontal) - } - } - .navigationTitle(navTitle) - } else { - LoadingView(url: url, message: nil) - .task { - symLog.log(".task") - let hasParams = queryURL() - await preparePayForTemplate() - } - } - }.onAppear() { - symLog.log("onAppear") - DebugViewC.shared.setSheetID(SHEET_PAY_TEMPLATE) - } - } -} diff --git a/TalerWallet1/Views/Sheets/Payment/PaymentView.swift b/TalerWallet1/Views/Sheets/Payment/PaymentView.swift @@ -15,6 +15,9 @@ struct PaymentView: View { // the scanned URL let url: URL + let template: Bool + @Binding var amountToTransfer: Amount + @Binding var summary: String @EnvironmentObject private var model: WalletModel @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic @@ -93,8 +96,15 @@ struct PaymentView: View { .task { // this runs only once do { // TODO: cancelled symLog.log(".task") - let result = try await model.preparePayForUriM(url.absoluteString) - preparePayResult = result + if template { + let result = try await model.preparePayForTemplateM(url.absoluteString, + amount: amountToTransfer, + summary: summary) + preparePayResult = result + } else { + let result = try await model.preparePayForUriM(url.absoluteString) + preparePayResult = result + } } catch { // TODO: error symLog.log(error.localizedDescription) } @@ -107,6 +117,7 @@ struct PaymentView: View { } } // MARK: - +#if false struct PaymentURIView_Previews: PreviewProvider { static var previews: some View { let merchant = Merchant(name: "Merchant") @@ -153,6 +164,12 @@ struct PaymentURIView_Previews: PreviewProvider { ) let url = URL(string: "taler://pay/some_amount")! - PaymentView(stack: CallStack("Preview"), url: url, preparePayResult: details) +// @State private var amount: Amount? = nil // templateParam +// @State private var summary: String? = nil // templateParam + + PaymentView(stack: CallStack("Preview"), + url: url, template: false, amountToTransfer: nil, summary: nil, + preparePayResult: details) } } +#endif diff --git a/TalerWallet1/Views/Sheets/URLSheet.swift b/TalerWallet1/Views/Sheets/URLSheet.swift @@ -13,8 +13,8 @@ struct URLSheet: View { var urlToOpen: URL @EnvironmentObject private var controller: Controller - @State private var amountToTransfer = Amount.zero(currency: "") - + @State private var amountToTransfer = Amount.zero(currency: EMPTYSTRING) + @State private var summary = EMPTYSTRING var body: some View { #if PRINT_CHANGES let _ = Self._printChanges() @@ -29,13 +29,14 @@ struct URLSheet: View { case .withdrawExchange: WithdrawExchangeV(stack: stack.push(), url: urlToOpen) case .pay: - PaymentView(stack: stack.push(), url: urlToOpen) + PaymentView(stack: stack.push(), url: urlToOpen, + template: false, amountToTransfer: $amountToTransfer, summary: $summary) case .payPull: P2pPayURIView(stack: stack.push(), url: urlToOpen) case .payPush: P2pReceiveURIView(stack: stack.push(), url: urlToOpen) case .payTemplate: - PayTemplateView(stack: stack.push(), url: urlToOpen) + PayTemplateV(stack: stack.push(), url: urlToOpen) case .refund: RefundURIView(stack: stack.push(), url: urlToOpen) default: // Error view