PaymentTransactionView.swift (11003B)
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 import SymLog 11 12 // MARK: - 13 struct MerchantHeader: View { 14 let terms: MerchantContractTerms? 15 16 @Environment(\.colorScheme) private var colorScheme 17 @Environment(\.colorSchemeContrast) private var colorSchemeContrast 18 19 @State private var image: Image? = nil 20 21 func compactAddress(_ address: Location?) -> String? { 22 if let address { 23 var result: String = EMPTYSTRING 24 if let street = address.street { 25 result.append(street) 26 } 27 if let town = address.town { 28 if !result.isEmpty { 29 result.append(", ") 30 } 31 result.append(town) 32 } 33 if let country = address.country { 34 if !result.isEmpty { 35 result.append(", ") 36 } 37 result.append(country) 38 } 39 return result.isEmpty ? nil : result 40 } 41 return nil 42 } 43 44 var body: some View { 45 if let terms { 46 Section { 47 Text(terms.locSummary) 48 .talerFont(.title3) 49 } header: { 50 VStack(alignment: .leading) { 51 HStack { 52 if terms.merchant.logo != nil { 53 if let image { 54 image 55 .resizable() 56 .aspectRatio(contentMode: .fit) 57 .frame(maxHeight: 50) 58 .padding(.trailing, 8) 59 } 60 } else { 61 #if TALER_NIGHTLY 62 let imageName = if #available(iOS 17.0, *) { MERCHANT17 } else { MERCHANT14 } 63 Image(systemName: imageName) 64 .resizable() 65 .frame(width: 44, height: 44) 66 #endif 67 } 68 VStack(alignment: .leading) { 69 let merchant = terms.merchant 70 Text(merchant.name) 71 .talerFont(.title3) 72 if let address = compactAddress(merchant.address) { 73 Text(address) 74 .talerFont(.body) 75 } 76 } 77 } .foregroundStyle(Color.primary) 78 .padding(.bottom) 79 .task { 80 if let imageBase64 = terms.merchant.logo { 81 image = Image(imageBase64: imageBase64) 82 } 83 } 84 Text("Summary") 85 .talerFont(.title3) 86 .foregroundColor(WalletColors().secondary2(colorScheme, colorSchemeContrast)) 87 } 88 } 89 } 90 } 91 } 92 // MARK: - 93 struct PaymentTransactionView: View { 94 private let symLog = SymLogV(0) 95 let stack: CallStack 96 let common: TransactionCommon 97 let paymentTransaction: PaymentTransaction 98 @Binding var scope: ScopeInfo? 99 @Binding var effective: Amount? 100 @Binding var possible: Bool? 101 @Binding var payNow: Bool 102 @Binding var selectedChoice: Int? 103 104 @EnvironmentObject private var controller: Controller 105 @EnvironmentObject private var model: WalletModel 106 107 func selectIndex(_ index: Int?, of choices: [ChoiceTriple]) -> Int? { 108 let index1 = index ?? 0 109 let select = index1 < choices.count ? index1 : 0 110 let choice = choices[select] 111 let selectionDetail = choice.0 112 if selectionDetail.status == .paymentPossible { 113 selectedChoice = select 114 effective = selectionDetail.amountEffective 115 possible = true 116 return select 117 } else { 118 possible = false 119 } 120 return nil 121 } 122 123 func automaticOrDefault(_ automaticIndex: Int?,_ defaultChoiceIndex: Int?, of choices: [ChoiceTriple]) { 124 if let automaticIndex { 125 // Pay Automatically - usually with a subscription token 126 if let selectedAutomatic = selectIndex(automaticIndex, of: choices) { 127 if selectedAutomatic == automaticIndex { 128 // check if automatic choice does NOT have an amount 129 let choiceTriple = choices[selectedAutomatic] 130 let selectionDetail = choiceTriple.0 131 if selectionDetail.status == .paymentPossible { 132 // we currently don't want to pay MONEY automatically, only tokens 133 // TODO: make the maximum value a user default 134 if selectionDetail.amountRaw.isZero { 135 payNow = true 136 } 137 } 138 return 139 } else if let defaultChoiceIndex { 140 if selectedAutomatic == defaultChoiceIndex { return } 141 // else fall thru and select defaultChoiceIndex 142 } else { // there is no default - keep selectedAutomatic 143 return 144 } 145 } 146 } 147 _ = selectIndex(defaultChoiceIndex, of: choices) 148 } 149 150 var isNotClaiming: Bool { 151 !common.isClaiming 152 } 153 154 func selectedOrFirst(_ choices: [ChoiceTriple]) -> ChoiceTriple? { 155 if let selectedChoice { 156 if selectedChoice < choices.count { 157 return choices[selectedChoice] 158 } 159 } 160 if let choice = choices.first { 161 return choice 162 } 163 return nil 164 } 165 166 var body: some View { 167 #if PRINT_CHANGES 168 let _ = Self._printChanges() 169 let _ = symLog.vlog() 170 #endif 171 let details = paymentTransaction.details 172 if common.isDialog { // show payment confirmation dialog 173 let state = common.txState 174 MerchantHeader(terms: details.contractTerms) 175 .task(id: state.hashValue) { 176 let choicesTuple = controller.choicesTuple 177 let choicesForPayment = choicesTuple.1 178 if choicesTuple.0 != common.transactionId || choicesForPayment == nil { 179 symLog.log("❗️❗️ task: \(state)") 180 await controller.loadChoicesForPayment(stack.push(), 181 txId: common.transactionId) 182 } else { 183 symLog.log("❗️❗️ choicesForPayment exists, no need for task: \(state)") 184 } 185 } // loadChoicesForPayment 186 187 let choicesTuple = controller.choicesTuple 188 if let txId = choicesTuple.0, let choicesForPayment = choicesTuple.1 { 189 if txId == common.transactionId { 190 let terms = choicesForPayment.contractTerms 191 if let products = terms.products { 192 ProductsView(stack: stack.push(), 193 scope: scope, 194 products: products) 195 } 196 if let (choices, showHeader) = choicesForPayment.choiceTriple() { 197 let hasAutomatic = choicesForPayment.automaticExecution ?? false 198 let automaticIndex = hasAutomatic ? choicesForPayment.automaticExecutableIndex : nil 199 ChoicesView(stack: stack.push(), 200 choiceTriples: choices, 201 tokenFamilies: terms.tokenFamilies, 202 showHeader: showHeader, 203 automaticIndex: automaticIndex, 204 selectedChoice: $selectedChoice) 205 .task { 206 automaticOrDefault(automaticIndex, choicesForPayment.defaultChoiceIndex, of: choices) 207 } 208 .onChange(of: selectedChoice) { newValue in 209 if let newValue, newValue < choices.count { 210 let newChoice = choices[newValue] 211 effective = newChoice.0.amountEffective 212 possible = newChoice.0.status == .paymentPossible 213 scope = newChoice.0.scopeInfo 214 } else { 215 effective = nil 216 possible = nil 217 scope = nil 218 } 219 } 220 221 if let choice = selectedOrFirst(choices) { 222 let summary = details.info?.locSummary ?? 223 String(localized: "No summary", comment: "OrderShortInfo.summary") 224 let selectionDetail: ChoiceSelectionDetail = choice.0 225 // let contractChoice: ContractChoice = choice.1 226 PaymentView2(stack: stack.push(), // TODO: details.info.merchant.name 227 paid: false, 228 raw: selectionDetail.amountRaw, 229 effective: effective, // V0? 230 firstScope: scope, 231 baseURL: nil, 232 summary: summary, 233 balanceDetails: selectionDetail.balanceDetails) 234 } 235 } else { // should never happen... 236 // TODO: show some error 237 } 238 } else { // cached data outdated 239 // TODO: purge and reload 240 } 241 } 242 } else if isNotClaiming { // show finished payment 243 PaidTransactionDetailV(stack: stack.push(), 244 paymentTx: paymentTransaction) // TODO: details.info.merchant.name 245 ThreeAmountsSheet(stack: stack.push(), 246 scope: scope, 247 common: common, 248 topAbbrev: String(localized: "Price:", comment: "mini"), 249 topTitle: String(localized: "Price (net):"), 250 baseURL: nil, // TODO: baseURL 251 noFees: nil, // TODO: noFees 252 feeIsNegative: false, 253 large: true, 254 summary: details.info?.locSummary ?? EMPTYSTRING) 255 } // show finished payment 256 } // body 257 } // PaymentTransactionView