taler-ios

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

commit 006c8593605c12996d90057074274c8322e2a75f
parent aaf5850aece1529a12d355b8876cd842f69ce3f1
Author: Marc Stibane <marc@taler.net>
Date:   Wed, 24 Jun 2026 08:36:27 +0200

DiscountsPasses (wip)

Diffstat:
MTalerWallet1/Views/Balances/DiscountPasses.swift | 177+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
MTalerWallet1/Views/HelperViews/SegmentControl.swift | 4++--
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 }