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 }