taler-ios

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

commit 1f0c84ae3a5d01e8127f84a8156d5b2fb2e982cf
parent e522d4385f5f88747a813ab0138395a0769b6fa7
Author: Marc Stibane <marc@taler.net>
Date:   Sat, 25 May 2024 21:51:21 +0200

checkPayForTemplate

Diffstat:
MTalerWallet1/Model/Model+Payment.swift | 45+++++++++++++++++++++------------------------
MTalerWallet1/Views/Banking/DepositAmountV.swift | 23++++++++++++++---------
MTalerWallet1/Views/HelperViews/AmountInputV.swift | 125++++++++++++++++++++++++++++++++++++++++---------------------------------------
MTalerWallet1/Views/HelperViews/SubjectInputV.swift | 70+++++++++++++++++++++++++++++++++-------------------------------------
MTalerWallet1/Views/Peer2peer/SendAmount.swift | 37++++++++++++++++++++++---------------
MTalerWallet1/Views/Sheets/Payment/PayTemplateV.swift | 186+++++++++++++++++++++++++++++++++++++++----------------------------------------
MTalerWallet1/Views/Sheets/Payment/PaymentView.swift | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
MTalerWallet1/Views/Sheets/URLSheet.swift | 8++++++--
8 files changed, 319 insertions(+), 249 deletions(-)

diff --git a/TalerWallet1/Model/Model+Payment.swift b/TalerWallet1/Model/Model+Payment.swift @@ -1,7 +1,10 @@ /* - * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. + * This file is part of GNU Taler, ©2022-24 Taler Systems S.A. * See LICENSE.md */ +/** + * @author Marc Stibane + */ import Foundation import taler_swift import AnyCodable @@ -163,7 +166,7 @@ struct TemplateParams: Codable { let summary: String? // Human-readable short summary of the contract } /// A request to get an exchange's payment contract terms. -fileprivate struct PreparePayForTemplate: WalletBackendFormattedRequest { +fileprivate struct PreparePayForTemplateRequest: WalletBackendFormattedRequest { typealias Response = PreparePayResult func operation() -> String { "preparePayForTemplate" } func args() -> Args { Args(talerPayTemplateUri: talerPayTemplateUri, templateParams: templateParams) } @@ -177,45 +180,39 @@ fileprivate struct PreparePayForTemplate: WalletBackendFormattedRequest { } // MARK: - struct TemplateContractDetails: Codable { - let summary: String? // Human-readable short summary of the contract + let summary: String? // Human-readable short summary of the contract. Fixed if this field exists, editable if nil let currency: String? - let amount: Amount? // Total amount payable + let amount: Amount? // Total amount payable. Fixed if this field exists, editable if nil let minimumAge: Int - let payDuration: String? // RelativeTime + let payDuration: RelativeTime enum CodingKeys: String, CodingKey { - case summary - case currency - case amount + case summary, currency, amount case minimumAge = "minimum_age" case payDuration = "pay_duration" } } struct TemplateContractDetailsDefaults: Codable { - let summary: String? // Human-readable short summary of the contract + let summary: String? // Default 'Human-readable short summary' if editable, or empty if nil. let currency: String? - let amount: Amount? // Total amount payable - let minimumAge: Int? - - enum CodingKeys: String, CodingKey { - case summary - case currency - case amount - case minimumAge = "minimum_age" - } + let amount: Amount? // Default amount if editable, or unspecified if nil. } -/// The result from checkPayForTemplate -struct WalletTemplateDetails: Codable { +struct TalerMerchantTemplateDetails: Codable { let templateContract: TemplateContractDetails let editableDefaults: TemplateContractDetailsDefaults? - let requiredCurrency: String? - +// let requiredCurrency: String? enum CodingKeys: String, CodingKey { case templateContract = "template_contract" case editableDefaults = "editable_defaults" - case requiredCurrency = "required_currency" +// case requiredCurrency = "required_currency" } } + +/// The result from checkPayForTemplate +struct WalletTemplateDetails: Codable { + let templateDetails: TalerMerchantTemplateDetails + let supportedCurrencies: [String] +} /// A request to get an exchange's payment contract terms. fileprivate struct CheckPayForTemplate: WalletBackendFormattedRequest { typealias Response = WalletTemplateDetails @@ -266,7 +263,7 @@ extension WalletModel { func preparePayForTemplateM(_ talerPayTemplateUri: String, amount: Amount?, summary: String?, viewHandles: Bool = false) // M for MainActor async throws -> PreparePayResult { let templateParams = TemplateParams(amount: amount, summary: summary) - let request = PreparePayForTemplate(talerPayTemplateUri: talerPayTemplateUri, templateParams: templateParams) + let request = PreparePayForTemplateRequest(talerPayTemplateUri: talerPayTemplateUri, templateParams: templateParams) let response = try await sendRequest(request, ASYNCDELAY, viewHandles: viewHandles) return response } diff --git a/TalerWallet1/Views/Banking/DepositAmountV.swift b/TalerWallet1/Views/Banking/DepositAmountV.swift @@ -1,7 +1,10 @@ /* - * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. + * This file is part of GNU Taler, ©2022-24 Taler Systems S.A. * See LICENSE.md */ +/** + * @author Marc Stibane + */ import SwiftUI import taler_swift import SymLog @@ -44,8 +47,6 @@ struct DepositAmountV: View { feeAmount = nil return feeAmount } - - var feeLabel: String { feeStr.count > 0 ? String(localized: "+ \(feeStr) fee") : EMPTYSTRING } private func feeIsNotZero() -> Bool? { if let hasNoFees = exchange?.noFees { @@ -109,9 +110,9 @@ struct DepositAmountV: View { title: minimalistic ? String(localized: "Amount:") : String(localized: "Amount to deposit:"), shortcutAction: nil) -// .padding(.top) + Text(insufficient ? insufficientLabel - : feeLabel) + : feeLabel(feeStr)) .talerFont(.body) .foregroundColor(insufficient ? .red : (feeAmount?.isZero ?? true) ? WalletColors().secondary(colorScheme, colorSchemeContrast) @@ -167,11 +168,15 @@ struct DepositAmountV: View { prepareDepositResult = nil } else if let paytoUri { if let ppCheck = try? await model.prepareDepositM(paytoUri, amount: amountToTransfer) { - prepareDepositResult = ppCheck - if let feeAmount = fee(ppCheck: prepareDepositResult) { + if let feeAmount = fee(ppCheck: ppCheck) { feeStr = feeAmount.string(currencyInfo) - } else { feeStr = EMPTYSTRING } - announce("\(amountVoiceOver), \(feeLabel)") + let feeLabel = feeLabel(feeStr) + announce("\(amountVoiceOver), \(feeLabel)") + } else { + feeStr = EMPTYSTRING + announce(amountVoiceOver) + } + prepareDepositResult = ppCheck } else { prepareDepositResult = nil } diff --git a/TalerWallet1/Views/HelperViews/AmountInputV.swift b/TalerWallet1/Views/HelperViews/AmountInputV.swift @@ -1,7 +1,10 @@ /* - * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. + * This file is part of GNU Taler, ©2022-24 Taler Systems S.A. * See LICENSE.md */ +/** + * @author Marc Stibane + */ import SwiftUI import taler_swift import SymLog @@ -14,10 +17,10 @@ struct AmountInputV: View { let amountAvailable: Amount? // TODO: GetMaxPeerPushAmount @Binding var amountToTransfer: Amount let amountLabel: String - let wantsSummary: Bool // if true we call SubjectInputV next + let summaryIsEditable: Bool // if true we call SubjectInputV next @Binding var summary: String - @Binding var insufficient: Bool - @Binding var feeAmount: Amount? +// @Binding var insufficient: Bool +// @Binding var feeAmount: Amount? let shortcutAction: ((_ amount: Amount) -> Void)? let buttonAction: () -> Void @@ -29,24 +32,22 @@ struct AmountInputV: View { @State private var preparePayResult: PreparePayResult? = nil @State private var feeStr: String = EMPTYSTRING - var feeLabel: String { feeStr.count > 0 ? String(localized: "+ \(feeStr) fee") : EMPTYSTRING } + struct Flags { + let insufficient: Bool + let disabled: Bool + } - func preparePayForTemplate() async { - if let url { - if 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 + func checkAvailable(amount: Amount) -> Flags { + if let amountAvailable { + do { + let insufficient = try amountAvailable > amount + let disabled = insufficient || amount.isZero + return Flags(insufficient: insufficient, disabled: disabled) + } catch { + // TODO: error Amounts don't match } } + return Flags(insufficient: false, disabled: amount.isZero) } var body: some View { @@ -54,58 +55,60 @@ struct AmountInputV: View { 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) { + let flags = checkAvailable(amount: amountToTransfer) + ScrollView { VStack(alignment: .trailing) { if let available { Text("Available:\t\(available)") .talerFont(.title3) .padding(.bottom, 2) - // .accessibility(sortPriority: 3) +// .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)) - .disabled(disabled) - } else { - Button("Next") { - buttonAction() - } - .buttonStyle(TalerButtonStyle(type: .prominent, disabled: disabled)) - .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) + if flags.insufficient { + Text(insufficientLabel) + .talerFont(.body) + .foregroundColor(.red) + .padding(4) +// } else { +// Text(feeLabel(feeStr)) +// .talerFont(.body) +// .foregroundColor((feeAmount?.isZero ?? true) ? WalletColors().secondary(colorScheme, colorSchemeContrast) +// : .red) +// .padding(4) +// .accessibility(sortPriority: 1) } + Button("Next") { buttonAction() } + .buttonStyle(TalerButtonStyle(type: .prominent, disabled: flags.disabled)) + .disabled(flags.disabled) + }.padding(.horizontal) } // ScrollVStack + .frame(maxWidth: .infinity, alignment: .leading) + .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all)) + .onAppear() { +// symLog.log("onAppear") + DebugViewC.shared.setSheetID(SHEET_PAY_TEMPL_AMOUNT) + } +// .task(id: amountToTransfer.value) { +// if let url { +// symLog.log(".task preparePayForTemplate") +// if let result = await preparePayForTemplate(model: model, +// url: url, +// amount: amountToTransfer, +// summary: summaryIsEditable ? summary ?? " " +// : nil, +// announce: announce) +// { symLog.log("preparePayForTemplate finished") +// insufficient = result.insufficient +// feeAmount = result.feeAmount +// feeStr = result.feeStr +// preparePayResult = result.ppCheck +// } else { +// symLog.log("preparePayForTemplateM failed") +// } +// } +// } } } diff --git a/TalerWallet1/Views/HelperViews/SubjectInputV.swift b/TalerWallet1/Views/HelperViews/SubjectInputV.swift @@ -1,7 +1,10 @@ /* - * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. + * This file is part of GNU Taler, ©2022-24 Taler Systems S.A. * See LICENSE.md */ +/** + * @author Marc Stibane + */ import SwiftUI import taler_swift import SymLog @@ -15,8 +18,8 @@ struct SubjectInputV<TargetView: View>: View { @Binding var amountToTransfer: Amount let amountLabel: String @Binding var summary: String - @Binding var insufficient: Bool - @Binding var feeAmount: Amount? +// @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 @@ -30,37 +33,21 @@ struct SubjectInputV<TargetView: View>: View { @Environment(\.colorSchemeContrast) private var colorSchemeContrast @AppStorage("minimalistic") var minimalistic: Bool = false + let navTitle = String(localized: "Custom Summary", comment:"pay merchant") + @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 { - if 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("\(amountVoiceOver), \(feeLabel)") - preparePayResult = ppCheck - } - } - } - 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 feeStr = feeAmount?.string(currencyInfo) ?? EMPTYSTRING +// let insufficientLabel = String(localized: "You don't have enough \(currency).") +// let feeLabel = insufficient ? insufficientLabel +// : feeLabel(feeStr) let available = amountAvailable?.string(currencyInfo) ?? nil - let disabled = insufficient || summary.count == 0 - VStack(alignment: .leading) { +// let disabled = insufficient || summary.count == 0 + let disabled = summary.count == 0 + ScrollView { VStack(alignment: .leading) { if let available { Text("Available:\t\(available)") .talerFont(.title3) @@ -97,22 +84,31 @@ struct SubjectInputV<TargetView: View>: View { } } } - - let subLabel = insufficient ? insufficientLabel - : feeLabel - Text(subLabel) + HStack { + Text(amountToTransfer.string(currencyInfo)) + // TODO: hasFees? +// Text(feeLabel) + } .talerFont(.body) - .foregroundColor(insufficient ? .red - : (feeAmount?.isZero ?? true) ? WalletColors().secondary(colorScheme, colorSchemeContrast) - : .red) - // .accessibility(sortPriority: 1) +// .foregroundColor(insufficient ? .red : WalletColors().secondary(colorScheme, colorSchemeContrast)) + .foregroundColor(WalletColors().secondary(colorScheme, colorSchemeContrast)) +// .accessibility(sortPriority: 1) .padding(4) +// if insufficient { +// Text(insufficientLabel) +// .talerFont(.body) +// .foregroundColor(.red) +// .padding(4) +// } NavigationLink("Next", destination: targetView) .buttonStyle(TalerButtonStyle(type: .prominent, disabled: disabled)) .disabled(disabled) // .accessibility(sortPriority: 0) - }.padding(.horizontal) + }.padding(.horizontal) } // ScrollVStack + .navigationTitle(navTitle) + .frame(maxWidth: .infinity, alignment: .leading) + .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all)) .onAppear() { // symLog.log("onAppear") DebugViewC.shared.setSheetID(SHEET_PAY_TEMPL_SUBJECT) diff --git a/TalerWallet1/Views/Peer2peer/SendAmount.swift b/TalerWallet1/Views/Peer2peer/SendAmount.swift @@ -1,7 +1,10 @@ /* - * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. + * This file is part of GNU Taler, ©2022-24 Taler Systems S.A. * See LICENSE.md */ +/** + * @author Marc Stibane + */ import SwiftUI import taler_swift import SymLog @@ -43,8 +46,6 @@ struct SendAmount: View { feeAmount = nil return feeAmount } - - var feeLabel: String { feeStr.count > 0 ? String(localized: "+ \(feeStr) fee") : EMPTYSTRING } private func shortcutAction(_ shortcut: Amount) { amountShortcut = shortcut @@ -82,11 +83,11 @@ struct SendAmount: View { let inputDestination = LazyView { P2PSubjectV(stack: stack.push(), - feeLabel: feeLabel, + feeLabel: feeLabel(feeStr), feeIsNotZero: feeIsNotZero(), currencyInfo: currencyInfo, amountToSend: true, - amountToTransfer: $amountToTransfer, + amountToTransfer: $amountToTransfer, // from the textedit summary: $summary, expireDays: $expireDays) } @@ -96,20 +97,22 @@ struct SendAmount: View { feeIsNotZero: feeIsNotZero(), currencyInfo: currencyInfo, amountToSend: true, - amountToTransfer: $amountShortcut, + amountToTransfer: $amountShortcut, // from the tapped shortcut button summary: $summary, expireDays: $expireDays) } ScrollView { + let amountLabel = minimalistic ? String(localized: "Amount:") + : String(localized: "Amount to send:") + AmountInputV(stack: stack.push(), url: nil, amountAvailable: amountAvailable, amountToTransfer: $amountToTransfer, - amountLabel: minimalistic ? String(localized: "Amount:") - : String(localized: "Amount to send:"), - wantsSummary: true, + amountLabel: amountLabel, + summaryIsEditable: true, summary: $summary, - insufficient: $insufficient, - feeAmount: $feeAmount, +// insufficient: $insufficient, +// feeAmount: $feeAmount, shortcutAction: shortcutAction, buttonAction: buttonAction) .background(NavigationLink(destination: shortcutDestination, isActive: $shortcutSelected) @@ -149,13 +152,17 @@ struct SendAmount: View { feeStr = EMPTYSTRING } else { if let ppCheck = try? await model.checkPeerPushDebitM(amountToTransfer) { - peerPushCheck = ppCheck // TODO: set from exchange // agePicker.setAges(ages: peerPushCheck?.ageRestrictionOptions) - if let feeAmount = fee(ppCheck: peerPushCheck) { + if let feeAmount = fee(ppCheck: ppCheck) { feeStr = feeAmount.string(currencyInfo) - } else { feeStr = EMPTYSTRING } - announce("\(amountVoiceOver), \(feeLabel)") + let feeLabel = feeLabel(feeStr) + announce("\(amountVoiceOver), \(feeLabel)") + } else { + feeStr = EMPTYSTRING + announce(amountVoiceOver) + } + peerPushCheck = ppCheck } else { peerPushCheck = nil } diff --git a/TalerWallet1/Views/Sheets/Payment/PayTemplateV.swift b/TalerWallet1/Views/Sheets/Payment/PayTemplateV.swift @@ -1,26 +1,17 @@ /* - * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. + * This file is part of GNU Taler, ©2022-24 Taler Systems S.A. * See LICENSE.md */ +/** + * @author Marc Stibane + */ 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 +// Will be called either by the user scanning a pay-template QR code or tapping the provided link, +// both from the shop's website or even from a printed QR code. +// We check whether amount and/or summary is editable, and finally go to PaymentView struct PayTemplateV: View { private let symLog = SymLogV(0) let stack: CallStack @@ -33,22 +24,23 @@ struct PayTemplateV: View { @AppStorage("minimalistic") var minimalistic: Bool = false @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic - let navTitle = String(localized: "Customize Order", comment:"pay merchant") + let navTitle = String(localized: "Custom Amount", comment:"pay merchant") - @State private var insufficient = false - @State private var preparePayResult: PreparePayResult? = nil - @State private var currencyName: String? = nil +// @State private var insufficient = false +// @State private var preparePayResult: PreparePayResult? = nil + @State private var templateContract: TemplateContractDetails? = nil + @State private var currencyName = EMPTYSTRING + @State private var amountIsEditable = false @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 summaryIsEditable = 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 } +// @State private var feeAmount: Amount? = nil +// @State private var feeStr: String = EMPTYSTRING private func shortcutAction(_ shortcut: Amount) { amountShortcut = shortcut @@ -73,46 +65,13 @@ struct PayTemplateV: View { } } - 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 { - if 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 - } else { - symLog.log("preparePayForTemplateM failed") - } - } - 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) + if let templateContract { // 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: currencyName, controller.currencyTicker) let amountLabel = minimalistic ? String(localized: "Amount:") : String(localized: "Amount to pay:") let finalDestinationI = LazyView { @@ -120,53 +79,58 @@ struct PayTemplateV: View { url: url, template: true, amountToTransfer: $amountToTransfer, - summary: $summary) - } + summary: $summary, + amountIsEditable: amountIsEditable, + summaryIsEditable: summaryIsEditable) + } // final destination with amountToTransfer, after user input of amount + let finalDestinationS = LazyView { + PaymentView(stack: stack.push(), + url: url, + template: true, + amountToTransfer: $amountShortcut, + summary: $summary, + amountIsEditable: amountIsEditable, + summaryIsEditable: summaryIsEditable) + } // final destination with amountShortcut, when user tapped a shortcut + let inputDestination = LazyView { SubjectInputV(stack: stack.push(), url: url, amountAvailable: nil, amountToTransfer: $amountToTransfer, amountLabel: amountLabel, summary: $summary, - insufficient: $insufficient, - feeAmount: $feeAmount, - feeIsNotZero: true, // feeIsNotZero(), +// insufficient: $insufficient, +// feeAmount: $feeAmount, + feeIsNotZero: true, // TODO: feeIsNotZero() currencyInfo: currencyInfo, targetView: finalDestinationI) - } - let finalDestinationS = LazyView { - PaymentView(stack: stack.push(), - url: url, - template: true, - amountToTransfer: $amountShortcut, - summary: $summary) - } + } // destination to subject input let shortcutDestination = LazyView { SubjectInputV(stack: stack.push(), url: url, amountAvailable: nil, amountToTransfer: $amountShortcut, amountLabel: amountLabel, summary: $summary, - insufficient: $insufficient, - feeAmount: $feeAmount, - feeIsNotZero: true, // feeIsNotZero(), +// insufficient: $insufficient, +// feeAmount: $feeAmount, + feeIsNotZero: true, // TODO: feeIsNotZero() currencyInfo: currencyInfo, targetView: finalDestinationS) - } + }// destination to subject input, when user tapped an amount shortcut Group { - if let currencyName { // template included a currency name => let the user input an amount + if amountIsEditable { // template contract amount is not fixed => let the user input an amount first let amountInput = AmountInputV(stack: stack.push(), url: url, amountAvailable: nil, amountToTransfer: $amountToTransfer, amountLabel: amountLabel, - wantsSummary: wantsSummary, + summaryIsEditable: summaryIsEditable, summary: $summary, - insufficient: $insufficient, - feeAmount: $feeAmount, +// insufficient: $insufficient, +// feeAmount: $feeAmount, shortcutAction: shortcutAction, buttonAction: buttonAction1) ScrollView { - if wantsSummary { + if summaryIsEditable { // after amount input, amountInput .background(NavigationLink(destination: shortcutDestination, isActive: $shortcutSelected) { EmptyView() }.frame(width: 0).opacity(0).hidden()) @@ -180,32 +144,64 @@ struct PayTemplateV: View { .background(NavigationLink(destination: finalDestinationI, isActive: $buttonSelected1) { EmptyView() }.frame(width: 0).opacity(0).hidden()) } - } // ScrollVStack - } else if wantsSummary { // check summary + } + } else if summaryIsEditable { // template contract summary is not fixed => let the user input a summary ScrollView { inputDestination .background(NavigationLink(destination: finalDestinationI, isActive: $buttonSelected2) { EmptyView() }.frame(width: 0).opacity(0).hidden() ) } - } else { + } else { // both template contract amount and summary are fixed => directly show the payment PaymentView(stack: stack.push(), url: url, template: true, amountToTransfer: $amountToTransfer, - summary: $summary) + summary: $summary, + amountIsEditable: amountIsEditable, + summaryIsEditable: summaryIsEditable) } - }.onAppear() { + }.navigationTitle(navTitle) + .frame(maxWidth: .infinity, alignment: .leading) + .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all)) + .onAppear() { symLog.log("onAppear") DebugViewC.shared.setSheetID(SHEET_PAY_TEMPLATE) - }.navigationTitle(navTitle) + } } else { LoadingView(scopeInfo: nil, message: url.host) .task { - symLog.log("LoadingView.task preparePayForTemplate") - let hasParams = queryURL() - await preparePayForTemplate() - symLog.log("preparePayForTemplate finished") + if let details = try? await model.checkPayForTemplateM(url.absoluteString) { + let supportedCurrency0 = details.supportedCurrencies[0] + let contract = details.templateDetails.templateContract // specifies fixed amount/summary + amountIsEditable = contract.amount == nil + summaryIsEditable = contract.summary == nil + let defaults = details.templateDetails.editableDefaults // might be nil, or its fields might be nil + let prepCurrency = contract.currency ?? defaults?.currency ?? supportedCurrency0 + let zeroAmount = Amount(currency: prepCurrency, cent: 0) + let prepAmount = contract.amount ?? defaults?.amount // might be nil + let prepSummary = contract.summary ?? defaults?.summary // might be nil +// symLog.log("LoadingView.task preparePayForTemplate") +// if let result = await preparePayForTemplate(model: model, +// url: url, +// amount: amountIsEditable ? prepAmount ?? zeroAmount +// : nil, +// summary: summaryIsEditable ? prepSummary ?? " " +// : nil, +// announce: announce) +// { symLog.log("preparePayForTemplate finished") + amountToTransfer = prepAmount ?? zeroAmount + currencyName = prepCurrency + summary = prepSummary ?? EMPTYSTRING + templateContract = contract +// insufficient = result.insufficient +// feeAmount = result.feeAmount +// feeStr = result.feeStr +// preparePayResult = result.ppCheck +// } else { +// symLog.log("preparePayForTemplateM failed") +// } + } } } } diff --git a/TalerWallet1/Views/Sheets/Payment/PaymentView.swift b/TalerWallet1/Views/Sheets/Payment/PaymentView.swift @@ -1,11 +1,69 @@ /* - * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. + * This file is part of GNU Taler, ©2022-24 Taler Systems S.A. * See LICENSE.md */ +/** + * @author Marc Stibane + */ import SwiftUI import taler_swift import SymLog +typealias Announce = (_ this: String) -> () + +func feeLabel(_ feeString: String) -> String { + feeString.count > 0 ? String(localized: "+ \(feeString) fee") + : EMPTYSTRING +} + +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 +} + +struct PayForTemplateResult { + let ppCheck: PreparePayResult + let insufficient: Bool + let feeAmount: Amount? + let feeStr: String +} + +func preparePayForTemplate(model: WalletModel, + url: URL, + amount: Amount?, + summary: String?, + announce: Announce) + async -> PayForTemplateResult? { + if let ppCheck = try? await model.preparePayForTemplateM(url.absoluteString, amount: amount, summary: summary) { + let controller = Controller.shared + let amountRaw = ppCheck.amountRaw + let currency = amountRaw.currencyStr + let currencyInfo = controller.info(for: currency, controller.currencyTicker) + let amountVoiceOver = amountRaw.string(currencyInfo) + let insufficient = ppCheck.status == .insufficientBalance + if let feeAmount = templateFee(ppCheck: ppCheck) { + let feeStr = feeAmount.string(currencyInfo) + let feeLabel = feeLabel(feeStr) + announce("\(amountVoiceOver), \(feeLabel)") + return PayForTemplateResult(ppCheck: ppCheck, insufficient: insufficient, + feeAmount: feeAmount, feeStr: feeStr) + } + announce(amountVoiceOver) + return PayForTemplateResult(ppCheck: ppCheck, insufficient: insufficient, + feeAmount: nil, feeStr: EMPTYSTRING) + } + return nil +} + +// MARK: - // 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 in a sheet. struct PaymentView: View { @@ -18,6 +76,8 @@ struct PaymentView: View { let template: Bool @Binding var amountToTransfer: Amount @Binding var summary: String + let amountIsEditable: Bool // + let summaryIsEditable: Bool // @EnvironmentObject private var model: WalletModel @EnvironmentObject private var controller: Controller @@ -34,6 +94,7 @@ struct PaymentView: View { // TODO: show balanceDetails.balanceAvailable let baseURL = preparePayResult.contractTerms.exchanges.first?.url let raw = preparePayResult.amountRaw + let status = preparePayResult.status let currency = raw.currencyStr let topTitle = String(localized: "Amount to pay:") let topAbbrev = String(localized: "Pay:", comment: "mini") @@ -102,8 +163,8 @@ struct PaymentView: View { symLog.log(".task") if template { if let result = try? await model.preparePayForTemplateM(url.absoluteString, - amount: amountToTransfer, - summary: summary) { + amount: amountIsEditable ? amountToTransfer : nil, + summary: summaryIsEditable ? summary : nil) { preparePayResult = result } } else { @@ -170,9 +231,10 @@ struct PaymentURIView_Previews: PreviewProvider { // @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) + PaymentView(stack: CallStack("Preview"), url: url, + template: false, amountToTransfer: nil, summary: nil, + amountIsEditable: false, summaryIsEditable: false, + preparePayResult: details) } } #endif diff --git a/TalerWallet1/Views/Sheets/URLSheet.swift b/TalerWallet1/Views/Sheets/URLSheet.swift @@ -1,7 +1,10 @@ /* - * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. + * This file is part of GNU Taler, ©2022-24 Taler Systems S.A. * See LICENSE.md */ +/** + * @author Marc Stibane + */ import SwiftUI import taler_swift import SymLog @@ -32,7 +35,8 @@ struct URLSheet: View { WithdrawExchangeV(stack: stack.push(), url: urlToOpen) // TODO: just check the ToS case .pay: PaymentView(stack: stack.push(), url: urlToOpen, - template: false, amountToTransfer: $amountToTransfer, summary: $summary) + template: false, amountToTransfer: $amountToTransfer, summary: $summary, + amountIsEditable: false, summaryIsEditable: false) case .payPull: P2pPayURIView(stack: stack.push(), url: urlToOpen) case .payPush: