commit 006c8593605c12996d90057074274c8322e2a75f
parent aaf5850aece1529a12d355b8876cd842f69ce3f1
Author: Marc Stibane <marc@taler.net>
Date: Wed, 24 Jun 2026 08:36:27 +0200
DiscountsPasses (wip)
Diffstat:
2 files changed, 161 insertions(+), 20 deletions(-)
diff --git a/TalerWallet1/Views/Balances/DiscountPasses.swift b/TalerWallet1/Views/Balances/DiscountPasses.swift
@@ -11,10 +11,115 @@ import taler_swift
import SymLog
/// This view shows ...
+struct DiscountsPassesItem: View {
+ private let symLog = SymLogV(0)
+ let stack: CallStack
+ let token: TalerToken
+ let isPass: Bool // false = isDiscount
+ let activeExpired: Int
+ let index: Int
+ let deleteAction: (String) -> Void
+
+ @Environment(\.colorScheme) private var colorScheme
+ @Environment(\.colorSchemeContrast) private var colorSchemeContrast
+ @AppStorage("minimalistic") var minimalistic: Bool = false
+
+ func validDate(_ isValid: Bool, isExpired: Bool) -> (String, String) {
+ if isExpired {
+ let (endDateString, endDate) = TalerDater.dateString(token.validityEnd, minimalistic)
+ let endA11yDate = TalerDater.accessibilityDate(endDate) ?? endDateString
+
+ let (dateString, date) = TalerDater.dateString(token.validityStart, minimalistic)
+ let a11yDate = TalerDater.accessibilityDate(date) ?? dateString
+
+ let result0 = String(localized: "Validity period: \(dateString) - \(endDateString)")
+ let result1 = String(localized: "Validity period: \(a11yDate) to \(endA11yDate)")
+ return (result0, result1)
+ } else if isValid {
+ let (dateString, date) = TalerDater.dateString(token.validityEnd, minimalistic)
+ let a11yDate = TalerDater.accessibilityDate(date) ?? dateString
+ let result0 = String(localized: "Valid until \(dateString)")
+ let result1 = String(localized: "Valid until \(a11yDate)")
+ return (result0, result1)
+ } else {
+ let (dateString, date) = TalerDater.dateString(token.validityStart, minimalistic)
+ let a11yDate = TalerDater.accessibilityDate(date) ?? dateString
+ let result0 = String(localized: "Valid from \(dateString)")
+ let result1 = String(localized: "Valid from \(a11yDate)")
+ return (result0, result1)
+ }
+ }
+ var body: some View {
+ let isValid = token.validityStart < .now()
+ let isExpired = token.validityEnd < .now()
+ let primary = WalletColors().primary(colorSchemeContrast)
+ let secondary = WalletColors().secondary(colorScheme, colorSchemeContrast)
+ let talerColor = WalletColors().talerColor
+
+ let name = Text(token.name)
+ .talerFont(.title)
+ .foregroundColor(primary)
+ let desc = Text(token.description)
+ .talerFont(.body)
+ .foregroundColor(primary)
+
+ Section {
+ let swipe = Button {
+ symLog.log("delete \(token.id)")
+ deleteAction(token.id)
+ } label: {
+ Label("Delete", systemImage: "trash")
+ }.tint(WalletColors().negative)
+ let row = HStack(alignment: .top) {
+ name
+ Spacer()
+ if let available = token.tokensAvailable {
+ // Discount
+ Text("x\(available)")
+ .foregroundColor(talerColor)
+ } else {
+ // Pass
+ if isValid {
+ Text("active")
+ .foregroundColor(talerColor)
+ }
+ }
+ }
+ if isValid {
+ row
+ } else {
+ row.swipeActions(edge: .trailing) { swipe }
+ }
+
+ let merchant = token.merchantInfo?.name ?? "Unknown"
+ if isExpired {
+ Text(isPass ? "Provided by: \(merchant)"
+ : isValid ? "Was redeemable at: \(merchant)"
+ : "Redeemed at: \(merchant)")
+ .talerFont(.footnote)
+ .foregroundColor(secondary)
+
+ } else {
+ Text(isPass ? "Provided by: \(merchant)"
+ : "Redeemable at: \(merchant)")
+ .talerFont(.footnote)
+ .foregroundColor(secondary)
+ }
+ let dateStrings = validDate(isValid, isExpired: isExpired)
+ VStack {
+ Text(dateStrings.0)
+ .accessibilityLabel(dateStrings.1)
+ .talerFont(.body)
+ }.frame(maxWidth: .infinity)
+ .background(talerColor.opacity(isValid ? 0.1 : 0))
+ }
+ }
+}
+// MARK: -
struct DiscountsPassesList: View {
private let symLog = SymLogV(0)
let stack: CallStack
- let isDiscount: Bool
+ let isPass: Bool
let navTitle: String
@EnvironmentObject private var model: WalletModel
@@ -22,28 +127,63 @@ struct DiscountsPassesList: View {
@AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic
@State private var activeExpired: Int = 0
+ func delete(_ tokenID: String) async {
+ if isPass {
+ try? await model.deleteSubscription(tokenID)
+ } else {
+ try? await model.deleteDiscount(tokenID)
+ }
+ }
func refresh() async {
// symLog.log("refreshing balances")
- if isDiscount {
- await controller.loadDiscounts(stack.push("refreshing discounts"), model)
- } else {
+ if isPass {
await controller.loadSubscriptions(stack.push("refreshing passes"), model)
+ } else {
+ await controller.loadDiscounts(stack.push("refreshing discounts"), model)
}
}
+ func talerTokens() -> [TalerToken] {
+ let tokens = isPass ? controller.subscriptions
+ : controller.discounts
+ let active = activeExpired == 0
+
+ return tokens.filter {
+ let isValid = $0.validityEnd > .now()
+ return active ? isValid
+ : !isValid
+ }
+ }
var body: some View {
- let count = isDiscount ? controller.discounts.count : 0
+ let tokens = talerTokens()
+ let count = tokens.count
let list = List {
- let strings = [String(localized: "Active", comment: "segmented control for passes & discounts"),
- String(localized: "Expired", comment: "segmented control for passes & discounts")]
- SegmentControl2(value: $activeExpired, strings: strings) { index in
- //
- }
- .listRowSeparator(.hidden)
- .padding(.bottom, 10)
+ if count == 0 {
+ Text(isPass ? "No passes yet"
+ : "No discounts yet")
+ .talerFont(.headline)
+ } else {
+ let comment: StaticString = "segmented control for passes & discounts"
+ let strings = [String(localized: "Active", comment: comment),
+ String(localized: "Expired", comment: comment)]
+ SegmentControl2(value: $activeExpired, strings: strings)
+ .listRowSeparator(.hidden)
+ .padding(.bottom, 10)
- ForEach(0..<count, id: \.self) { index in
-// DiscountPassesItem(stack: stack, isDiscount: isDiscount, index: index)
+ ForEach(0..<count, id: \.self) { index in
+ let token = tokens[index]
+ DiscountsPassesItem(stack: stack.push(),
+ token: token,
+ isPass: isPass,
+ activeExpired: activeExpired,
+ index: index
+ ) { deletionID in
+ Task { // runs on MainActor
+ let _ = await delete(deletionID)
+ await refresh()
+ }
+ }
+ }
}
}
.listStyle(myListStyle.style).anyView
@@ -52,14 +192,13 @@ struct DiscountsPassesList: View {
controller.hapticNotification(.success)
await refresh()
}
- .navigationTitle(navTitle)
list
+ .navigationTitle(navTitle)
.task {
await refresh()
}
}
}
-// MARK: -
struct DiscountPassesSection: View {
let stack: CallStack
@EnvironmentObject private var controller: Controller
@@ -94,13 +233,15 @@ struct DiscountPassesSection: View {
}
let discountsDest = DiscountsPassesList(stack: stack.push(),
- isDiscount: true,
+ isPass: false,
navTitle: discText)
let passesDest = DiscountsPassesList(stack: stack.push(),
- isDiscount: false,
+ isPass: true,
navTitle: passText)
NavigationLink(destination: discountsDest) { discountsLabel }
+ // TODO: a11y for discCount > 0
NavigationLink(destination: passesDest) { passesLabel }
+ // TODO: a11y for passCount > 0
}
.listRowSeparator(.hidden)
.talerFont(.title2)
diff --git a/TalerWallet1/Views/HelperViews/SegmentControl.swift b/TalerWallet1/Views/HelperViews/SegmentControl.swift
@@ -87,7 +87,7 @@ struct SegmentControl: View {
struct SegmentControl2: View {
@Binding var value: Int
let strings: [String]
- let action: (Int) -> Void
+// let action: (Int) -> Void
@Environment(\.colorScheme) private var colorScheme
@Environment(\.colorSchemeContrast) private var colorSchemeContrast
@@ -119,7 +119,7 @@ struct SegmentControl2: View {
.accessibilityAddTraits(index == value ? .isSelected : [])
.frame(maxWidth: .infinity)
.onTapGesture {
- action(index)
+// action(index)
withAnimation(.easeInOut(duration: 0.150)) {
value = index
}