taler-ios

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

ChoicesView.swift (9030B)


      1 /*
      2  * This file is part of GNU Taler, ©2022-26 Taler Systems S.A.
      3  * See LICENSE.md
      4  */
      5 /**
      6  * @author Marc Stibane
      7  */
      8 import SwiftUI
      9 import taler_swift
     10 
     11 struct SelectedBackground: View, Sendable {
     12     let useColor: Bool?
     13 
     14     var body: some View {
     15         let is26: Bool = if #available(iOS 26.0, *) { true } else { false }
     16         let rect = RoundedRectangle(cornerRadius: is26 ? 26 : 10)
     17         let line = useColor == nil ? 0.5 : 2.4
     18         let color = useColor == true ? WalletColors().primaryAccent
     19                   : useColor == false ? .secondary : WalletColors().gray4
     20         if #available(iOS 17.0, *) {
     21             rect
     22                 .fill(.background)
     23                 .strokeBorder(color, lineWidth: line)
     24         } else {
     25             rect
     26                 .strokeBorder(color, lineWidth: line)
     27                 .background(.background)
     28                 .clipped()
     29                 .clipShape(rect)
     30         }
     31     }
     32 }
     33 // MARK: -
     34 struct TokenView: View, Sendable {
     35     let count: Int?
     36     let slug: String
     37     let tokenFamilies: [String: ContractTokenFamily]   // token_family_slug: String
     38 
     39     var body: some View {
     40         if let family = tokenFamilies[slug] {
     41             let accent = WalletColors().primaryAccent
     42             let onPrimary = WalletColors().onPrimary
     43             let isPass = family.details.clazz == "subscription" // else "discount"
     44             HStack {
     45                 TokenLabel(title: family.name, isActive: true, isPass: isPass)
     46                 Spacer()
     47                 if let count {
     48                     Text("x\(count)")
     49                         .foregroundColor(onPrimary)
     50                         .padding(.horizontal, 8)
     51                         .padding(.vertical, 4)
     52                         .background(Capsule().fill(accent.opacity(0.3)))
     53                 }
     54             }
     55             .talerFont(.headline)
     56             .padding(4)
     57             .background(RoundedRectangle(cornerRadius: 8).fill(accent.opacity(0.1)))
     58         }
     59 
     60     }
     61 }
     62 struct OutputTokenView: View, Sendable {
     63     let outputs: [ContractOutputAny]
     64     let tokenFamilies: [String: ContractTokenFamily]   // token_family_slug: String
     65 
     66     var body: some View {
     67         ForEach(outputs, id: \.self) { output in
     68             switch output {
     69                 case .token(let token):
     70                     if let slug = token.tokenFamilySlug {
     71                         let count = token.count ?? 1
     72                         TokenView(count: count > 1 ? count : nil,
     73                                    slug: slug,
     74                           tokenFamilies: tokenFamilies)
     75                     }
     76                 case .taxReceipt(let taxReceipt):
     77                     let amount = taxReceipt.amount
     78                     // TODO: taxReceipt
     79                 case .unknown(let string):
     80                     let _ = print("❗️❗️ChoicesView: Unknown output type: \(string)")
     81             }
     82         }
     83     }
     84 }
     85 struct InputTokenView: View, Sendable {
     86     let inputs: [ContractInput]
     87     let tokenFamilies: [String: ContractTokenFamily]   // token_family_slug: String
     88 
     89     var body: some View {
     90         ForEach(inputs, id: \.self) { input in
     91             if let slug = input.tokenFamilySlug {
     92                 let count = input.count ?? 1
     93                 TokenView(count: count, slug: slug, tokenFamilies: tokenFamilies)
     94             }
     95         }
     96     }
     97 }
     98 // MARK: -
     99 struct ChoicesView: View, Sendable {
    100     let stack: CallStack
    101     let choiceTriples: [ChoiceTriple]
    102     let tokenFamilies: [String: ContractTokenFamily]?   // token_family_slug: String
    103     let showHeader: Bool
    104     let automaticIndex: Int?
    105     @Binding var selectedChoice: Int?
    106 
    107     @Environment(\.colorScheme) private var colorScheme
    108     @Environment(\.colorSchemeContrast) private var colorSchemeContrast
    109 
    110     var body: some View {
    111         Section {
    112             ForEach(choiceTriples, id: \.2) { triple in
    113                 let selectionDetail: ChoiceSelectionDetail = triple.0
    114                 let contractChoice: ContractChoice = triple.1
    115                 let index: Int = triple.2
    116                 let isPaymentPossible = selectionDetail.status == .paymentPossible
    117                 let payAutomatic = automaticIndex == index
    118                 let scopeInfo = selectionDetail.scopeInfo
    119                 let amount = selectionDetail.amountRaw
    120                 /// 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.
    121                 /// 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.
    122                 /// Tokens only, paymentPossible. If automaticIndex, then also hide it (and auto-pay it). Otherwise show it, so the user can select it.
    123                 if (!amount.isZero || isPaymentPossible) && !payAutomatic {
    124                     VStack(alignment: .leading) {
    125                         if showHeader {     // V1
    126                             if let description = contractChoice.locDescription {
    127                                 Text(description)
    128                                     .talerFont(.body)
    129                             }   }
    130 
    131                         HStack {
    132                             Spacer()
    133                             AmountV(stack: stack.push(),
    134                                     scope: scopeInfo,
    135                                    amount: contractChoice.amount)
    136                             .foregroundColor(isPaymentPossible ? .primary : .secondary)
    137                         }
    138                         if let tokenFamilies {
    139                             // Spend Input tokens
    140                             let inputs = contractChoice.inputs
    141                             if !inputs.isEmpty {
    142                                 Text("Spend now:")
    143                                     .talerFont(.body)
    144                                     .padding(.bottom, -4)
    145                                 InputTokenView(inputs: inputs, tokenFamilies: tokenFamilies)
    146                             }
    147                             // Receive output tokens
    148                             let outputs = contractChoice.outputs
    149                             if !outputs.isEmpty {
    150                                 Text("Get with your purchase:")
    151                                     .talerFont(.body)
    152                                     .padding(.bottom, -4)
    153                                 OutputTokenView(outputs: outputs, tokenFamilies: tokenFamilies)
    154                             }
    155                         }
    156                     }.overlay {
    157                         Color.primary.opacity(INVISIBLE)        // Color.clear doesn't accept taps!
    158                             .contentShape(.rect)
    159                             .onTapGesture {
    160                                 if isPaymentPossible {
    161                                     selectedChoice = index
    162                                 }
    163                             }
    164                     }
    165                     .listRowBackground(
    166                         SelectedBackground(useColor: index == selectedChoice ? isPaymentPossible : nil)
    167                     )
    168                 }
    169             }.listRowSeparator(.hidden)
    170         } header: {
    171             Text("Payment Options")
    172                 .talerFont(.title3)
    173                 .foregroundColor(WalletColors().secondary2(colorScheme, colorSchemeContrast))
    174         }
    175     }
    176 }
    177 // MARK: -
    178 struct ProductsView: View, Sendable {
    179     let stack: CallStack
    180     let scope: ScopeInfo?
    181     let products: [ProductSold]
    182 
    183     @Environment(\.colorScheme) private var colorScheme
    184     @Environment(\.colorSchemeContrast) private var colorSchemeContrast
    185 
    186     var body: some View {
    187         if !products.isEmpty {
    188             Section {
    189                 ForEach(products, id: \.self) { product in
    190                     HStack {
    191                         VStack(alignment: .leading) {
    192                             if let name = product.productName {
    193                                 Text(name)
    194                                     .bold()
    195                                     .talerFont(.body)
    196                             }
    197                             Text(product.locDescription)
    198                                 .talerFont(.subheadline)
    199 
    200                             HStack {
    201                                 if let quantity = product.unitQuantity {
    202                                     Text(quantity)
    203                                 }
    204                                 if let unit = product.unit {
    205                                     // TODO: MerchantUnit
    206                                     Text(unit)
    207                                 }
    208                             }.talerFont(.subheadline)
    209                         }
    210                         Spacer()
    211                         if let price = product.prices?.first ?? product.price {
    212                             AmountV(scope, price, isNegative: nil)
    213                         }
    214                     }
    215                     .listRowSeparator(.hidden)
    216                 }
    217             }
    218         }
    219     }
    220 }