commit d3b950b0bb25f209eba0b72b152a639ad542e413
parent 5090aad727eb51154142d403eaf15bbfbae94860
Author: Marc Stibane <marc@taler.net>
Date: Wed, 7 Oct 2026 16:24:00 +0200
Complete DD94 Discounts & Passes
Diffstat:
3 files changed, 189 insertions(+), 62 deletions(-)
diff --git a/TalerWallet1/Views/Sheets/Payment/ChoicesView.swift b/TalerWallet1/Views/Sheets/Payment/ChoicesView.swift
@@ -98,32 +98,62 @@ struct InputTokenView: View, Sendable {
// MARK: -
struct ChoicesView: View, Sendable {
let stack: CallStack
- let choiceTriple: [ChoiceTriple]
+ let choiceTriples: [ChoiceTriple]
+ let tokenFamilies: [String: ContractTokenFamily]? // token_family_slug: String
let showHeader: Bool
let automaticIndex: Int?
@Binding var selectedChoice: Int?
+ @Environment(\.colorScheme) private var colorScheme
+ @Environment(\.colorSchemeContrast) private var colorSchemeContrast
+
var body: some View {
- ForEach(choiceTriple, id: \.1) { choice in
- let selectionDetail: ChoiceSelectionDetail = choice.0
- let contractChoice: ContractChoice = choice.1
- let index: Int = choice.2
- let isPaymentPossible = selectionDetail.status == .paymentPossible
- let payAutomatic = automaticIndex == index
- let scopeInfo = selectionDetail.scopeInfo
- let amount = selectionDetail.amountRaw
- /// If it contains money, always show it. Disable but don’t hide it if insufficient - that serves as advertisement what you could buy if you had more money / tokens.
- /// If it only contains tokens, and insufficient, then hide it. Most probably there is a money payment option to acquire those tokens, which is advertisement enough.
- /// Tokens only, paymentPossible. If automaticIndex, then also hide it (and auto-pay it). Otherwise show it, so the user can select it.
- if (!amount.isZero || isPaymentPossible) && !payAutomatic {
- Section {
- let amountV = HStack {
- Spacer()
- AmountV(stack: stack.push(),
- scope: scopeInfo,
- amount: contractChoice.amount)
- .foregroundColor(isPaymentPossible ? .primary : .secondary )
- } .overlay {
+ Section {
+ ForEach(choiceTriples, id: \.2) { triple in
+ let selectionDetail: ChoiceSelectionDetail = triple.0
+ let contractChoice: ContractChoice = triple.1
+ let index: Int = triple.2
+ let isPaymentPossible = selectionDetail.status == .paymentPossible
+ let payAutomatic = automaticIndex == index
+ let scopeInfo = selectionDetail.scopeInfo
+ let amount = selectionDetail.amountRaw
+ /// If it contains money, always show it. Disable but don’t hide it if insufficient - that serves as advertisement what you could buy if you had more money / tokens.
+ /// If it only contains tokens, and insufficient, then hide it. Most probably there is a money payment option to acquire those tokens, which is advertisement enough.
+ /// Tokens only, paymentPossible. If automaticIndex, then also hide it (and auto-pay it). Otherwise show it, so the user can select it.
+ if (!amount.isZero || isPaymentPossible) && !payAutomatic {
+ VStack(alignment: .leading) {
+ if showHeader { // V1
+ if let description = contractChoice.locDescription {
+ Text(description)
+ .talerFont(.body)
+ } }
+
+ HStack {
+ Spacer()
+ AmountV(stack: stack.push(),
+ scope: scopeInfo,
+ amount: contractChoice.amount)
+ .foregroundColor(isPaymentPossible ? .primary : .secondary)
+ }
+ if let tokenFamilies {
+ // Spend Input tokens
+ let inputs = contractChoice.inputs
+ if !inputs.isEmpty {
+ Text("Spend now:")
+ .talerFont(.body)
+ .padding(.bottom, -4)
+ InputTokenView(inputs: inputs, tokenFamilies: tokenFamilies)
+ }
+ // Receive output tokens
+ let outputs = contractChoice.outputs
+ if !outputs.isEmpty {
+ Text("Get with your purchase:")
+ .talerFont(.body)
+ .padding(.bottom, -4)
+ OutputTokenView(outputs: outputs, tokenFamilies: tokenFamilies)
+ }
+ }
+ }.overlay {
Color.primary.opacity(INVISIBLE) // Color.clear doesn't accept taps!
.contentShape(.rect)
.onTapGesture {
@@ -132,20 +162,58 @@ struct ChoicesView: View, Sendable {
}
}
}
- if index == selectedChoice {
- amountV.listRowBackground (
- SelectedBackground(useColor: isPaymentPossible)
-// .padding(.top, n == 1 ? 0 : -15)
-// .padding(.bottom, n == 10 ? 0 : -15)
- )
- } else {
- amountV
+ .listRowBackground(
+ SelectedBackground(useColor: index == selectedChoice ? isPaymentPossible : nil)
+ )
+ }
+ }.listRowSeparator(.hidden)
+ } header: {
+ Text("Payment Options")
+ .talerFont(.title3)
+ .foregroundColor(WalletColors().secondary2(colorScheme, colorSchemeContrast))
+ }
+ }
+}
+// MARK: -
+struct ProductsView: View, Sendable {
+ let stack: CallStack
+ let scope: ScopeInfo?
+ let products: [ProductSold]
+
+ @Environment(\.colorScheme) private var colorScheme
+ @Environment(\.colorSchemeContrast) private var colorSchemeContrast
+
+ var body: some View {
+ if !products.isEmpty {
+ Section {
+ ForEach(products, id: \.self) { product in
+ HStack {
+ VStack(alignment: .leading) {
+ if let name = product.productName {
+ Text(name)
+ .bold()
+ .talerFont(.body)
+ }
+ Text(product.locDescription)
+ .talerFont(.subheadline)
+
+ HStack {
+ if let quantity = product.unitQuantity {
+ Text(quantity)
+ }
+ if let unit = product.unit {
+ // TODO: MerchantUnit
+ Text(unit)
+ }
+ }.talerFont(.subheadline)
+ }
+ Spacer()
+ if let price = product.prices?.first ?? product.price {
+ AmountV(scope, price, isNegative: nil)
+ }
}
- } header: {
- if showHeader {
- if let description = contractChoice.locDescription {
- Text(description)
- } } }
+ .listRowSeparator(.hidden)
+ }
}
}
}
diff --git a/TalerWallet1/Views/Transactions/PaidTransactionDetailV.swift b/TalerWallet1/Views/Transactions/PaidTransactionDetailV.swift
@@ -44,12 +44,40 @@ struct PaidTransactionDetailV: View {
}
if let info {
// Text(info.merchant.name) TODO: show name & logo here?
- Text("Order-ID:")
- .talerFont(.title3)
- .listRowSeparator(.hidden)
- Text(info.orderId)
- .talerFont(.body)
-
+ if common.isDone {
+ VStack(alignment: .leading) {
+ Text("Order-ID:")
+ .talerFont(.title3)
+ HStack {
+ Spacer()
+ Text(info.orderId)
+ .talerFont(.body)
+ }
+ }
+ }
+ }
+ VStack(alignment: .leading) {
+ if let info {
+ Text(info.summary)
+ .talerFont(.title3)
+ }
+ if let choiceIndex = details.choiceIndex {
+ if let contractTerms = details.contractTerms {
+ if let choices = contractTerms.choices,
+ choices.indices.contains(choiceIndex) {
+ let choice = choices[choiceIndex]
+ HStack {
+ if let info {
+ Spacer()
+ }
+ Text(choice.locDescription ?? "Index: \(choiceIndex)") // fallback should never happen
+ .talerFont(.body)
+ }
+ }
+ }
+ }
+ }
+ if let info {
if let fulfillmentUrl = info.fulfillmentUrl, common.isDone {
// per api-common.rst's WebURL definition, fulfillment_url must be http(s) -
// reject any other scheme (tel:, sms:, a third-party app's custom scheme, ...)
@@ -84,16 +112,6 @@ struct PaidTransactionDetailV: View {
}
}
}
- if let choiceIndex = details.choiceIndex {
- if let contractTerms = details.contractTerms {
- if let choices = contractTerms.choices,
- choices.indices.contains(choiceIndex) {
- let choice = choices[choiceIndex]
- Text(choice.locDescription ?? "Index: \(choiceIndex)") // fallback should never happen
- .talerFont(.body)
- }
- }
- }
}
if let products = terms?.products {
if !products.isEmpty {
diff --git a/TalerWallet1/Views/Transactions/PaymentTransactionView.swift b/TalerWallet1/Views/Transactions/PaymentTransactionView.swift
@@ -13,23 +13,49 @@ import SymLog
struct MerchantHeader: View {
let terms: MerchantContractTerms?
+ @Environment(\.colorScheme) private var colorScheme
+ @Environment(\.colorSchemeContrast) private var colorSchemeContrast
+
@State private var image: Image? = nil
+ func compactAddress(_ address: Location?) -> String? {
+ if let address {
+ var result: String = EMPTYSTRING
+ if let street = address.street {
+ result.append(street)
+ }
+ if let town = address.town {
+ if !result.isEmpty {
+ result.append(", ")
+ }
+ result.append(town)
+ }
+ if let country = address.country {
+ if !result.isEmpty {
+ result.append(", ")
+ }
+ result.append(country)
+ }
+ return result.isEmpty ? nil : result
+ }
+ return nil
+ }
+
var body: some View {
if let terms {
Section {
Text(terms.locSummary)
.talerFont(.title3)
} header: {
- HStack {
- Spacer()
- VStack(alignment: .center) {
+ VStack(alignment: .leading) {
+ HStack {
if terms.merchant.logo != nil {
if let image {
image
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxHeight: 50)
+ .padding(.trailing, 8)
}
} else {
#if TALER_NIGHTLY
@@ -39,16 +65,25 @@ struct MerchantHeader: View {
.frame(width: 44, height: 44)
#endif
}
- let merchant = terms.merchant.name
- Text(merchant)
- .talerFont(.title3)
+ VStack(alignment: .leading) {
+ let merchant = terms.merchant
+ Text(merchant.name)
+ .talerFont(.title3)
+ if let address = compactAddress(merchant.address) {
+ Text(address)
+ .talerFont(.body)
+ }
+ }
} .foregroundStyle(Color.primary)
+ .padding(.bottom)
.task {
if let imageBase64 = terms.merchant.logo {
image = Image(imageBase64: imageBase64)
}
}
- Spacer()
+ Text("Summary")
+ .talerFont(.title3)
+ .foregroundColor(WalletColors().secondary2(colorScheme, colorSchemeContrast))
}
}
}
@@ -147,22 +182,28 @@ struct PaymentTransactionView: View {
} else {
symLog.log("❗️❗️ choicesForPayment exists, no need for task: \(state)")
}
- }
+ } // loadChoicesForPayment
let choicesTuple = controller.choicesTuple
if let txId = choicesTuple.0, let choicesForPayment = choicesTuple.1 {
- let terms = choicesForPayment.contractTerms
if txId == common.transactionId {
+ let terms = choicesForPayment.contractTerms
+ if let products = terms.products {
+ ProductsView(stack: stack.push(),
+ scope: scope,
+ products: products)
+ }
if let (choices, showHeader) = choicesForPayment.choiceTriple() {
let hasAutomatic = choicesForPayment.automaticExecution ?? false
let automaticIndex = hasAutomatic ? choicesForPayment.automaticExecutableIndex : nil
ChoicesView(stack: stack.push(),
- choiceTriple: choices,
+ choiceTriples: choices,
+ tokenFamilies: terms.tokenFamilies,
showHeader: showHeader,
automaticIndex: automaticIndex,
selectedChoice: $selectedChoice)
.task {
- automaticOrDefault(automaticIndex, choicesForPayment.defaultChoiceIndex, of: choices)
+ automaticOrDefault(automaticIndex, choicesForPayment.defaultChoiceIndex, of: choices)
}
.onChange(of: selectedChoice) { newValue in
if let newValue, newValue < choices.count {
@@ -185,7 +226,7 @@ struct PaymentTransactionView: View {
PaymentView2(stack: stack.push(), // TODO: details.info.merchant.name
paid: false,
raw: selectionDetail.amountRaw,
- effective: effective,
+ effective: effective, // V0?
firstScope: scope,
baseURL: nil,
summary: summary,