PaymentTransactionView.swift (8767B)
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 @State private var image: Image? = nil 17 18 func summary(_ terms: MerchantContractTerms) -> String { 19 if let i18nDict = terms.summaryI18n { 20 if !i18nDict.isEmpty { 21 for code in Locale.preferredLanguageCodes { 22 if let descI18n = i18nDict[code] { 23 return descI18n 24 } 25 } 26 } 27 } 28 return terms.summary 29 } 30 31 var body: some View { 32 if let terms { 33 Section { 34 Text(summary(terms)) 35 .talerFont(.title3) 36 } header: { 37 HStack { 38 Spacer() 39 VStack(alignment: .center) { 40 if terms.merchant.logo != nil { 41 if let image { 42 image 43 .resizable() 44 .aspectRatio(contentMode: .fit) 45 .frame(maxHeight: 50) 46 } 47 } else { 48 #if TALER_NIGHTLY 49 let imageName = if #available(iOS 17.0, *) { MERCHANT17 } else { MERCHANT14 } 50 Image(systemName: imageName) 51 .resizable() 52 .frame(width: 44, height: 44) 53 #endif 54 } 55 let merchant = terms.merchant.name 56 Text(merchant) 57 .talerFont(.title3) 58 } .foregroundStyle(Color(.primary)) 59 .task { 60 if let imageBase64 = terms.merchant.logo { 61 image = Image(imageBase64: imageBase64) 62 } 63 } 64 Spacer() 65 } 66 } 67 } 68 } 69 } 70 // MARK: - 71 struct PaymentTransactionView: View { 72 private let symLog = SymLogV(0) 73 let stack: CallStack 74 let common: TransactionCommon 75 let paymentTransaction: PaymentTransaction 76 @Binding var scope: ScopeInfo? 77 @Binding var effective: Amount? 78 @Binding var payNow: Bool 79 @Binding var selectedChoice: Int? 80 81 @EnvironmentObject private var controller: Controller 82 @EnvironmentObject private var model: WalletModel 83 84 func summary(_ info: OrderShortInfo?) -> String? { 85 if let i18nDict = info?.summary_i18n { 86 if !i18nDict.isEmpty { 87 for code in Locale.preferredLanguageCodes { 88 if let i18n = i18nDict[code] { 89 return i18n 90 } 91 } 92 } 93 } 94 if let summary = info?.summary { 95 return summary 96 } 97 return String(localized: "No summary", comment: "OrderShortInfo.summary") 98 } 99 100 func selectIndex(_ index: Int?, of choices: [ChoiceTriple]) -> Int? { 101 let index1 = index ?? 0 102 let select = index1 < choices.count ? index1 : 0 103 let choice = choices[select] 104 let selectionDetail = choice.0 105 if selectionDetail.status == .paymentPossible { 106 selectedChoice = select 107 effective = selectionDetail.amountEffective 108 return select 109 } 110 return nil 111 } 112 113 func automaticOrDefault(_ automaticIndex: Int?,_ defaultChoiceIndex: Int?, of choices: [ChoiceTriple]) { 114 if let automaticIndex { 115 // Pay Automatically - usually with a subscription token 116 if let selectedAutomatic = selectIndex(automaticIndex, of: choices) { 117 if selectedAutomatic == automaticIndex { 118 // check if automatic choice does NOT have an amount 119 let choiceTriple = choices[selectedAutomatic] 120 let selectionDetail = choiceTriple.0 121 if selectionDetail.status == .paymentPossible { 122 // we currently don't want to pay MONEY automatically, only tokens 123 // TODO: make the maximum value a user default 124 if selectionDetail.amountRaw.isZero { 125 payNow = true 126 } 127 } 128 return 129 } else if let defaultChoiceIndex { 130 if selectedAutomatic == defaultChoiceIndex { return } 131 // else fall thru and select defaultChoiceIndex 132 } else { // there is no default - keep selectedAutomatic 133 return 134 } 135 } 136 } 137 let _ = selectIndex(defaultChoiceIndex, of: choices) 138 } 139 140 var body: some View { 141 #if PRINT_CHANGES 142 let _ = Self._printChanges() 143 let _ = symLog.vlog() 144 #endif 145 let details = paymentTransaction.details 146 if common.isDialog { // show payment confirmation dialog 147 MerchantHeader(terms: details.contractTerms) 148 149 let choicesTuple = controller.choicesTuple 150 if let txId = choicesTuple.0, let choicesForPayment = choicesTuple.1 { 151 if txId == common.transactionId { 152 if let (choices, showHeader) = choicesForPayment.choiceTriple() { 153 let hasAutomatic = choicesForPayment.automaticExecution ?? false 154 let automaticIndex = hasAutomatic ? choicesForPayment.automaticExecutableIndex : nil 155 ChoicesView(stack: stack.push(), 156 choiceTriple: choices, 157 showHeader: showHeader, 158 automaticIndex: automaticIndex, 159 selectedChoice: $selectedChoice) 160 .task { 161 automaticOrDefault(automaticIndex, choicesForPayment.defaultChoiceIndex, of: choices) 162 } 163 .onChange(of: selectedChoice) { newValue in 164 if let newValue, newValue < choices.count { 165 let newChoice = choices[newValue] 166 effective = newChoice.0.amountEffective 167 scope = newChoice.0.scopeInfo 168 } else { 169 effective = nil 170 scope = nil 171 } 172 } 173 174 if let selectedChoice { 175 let choice = choices[selectedChoice] 176 let selectionDetail: ChoiceSelectionDetail = choice.0 177 // let contractChoice: ContractChoice = choice.1 178 179 PaymentView2(stack: stack.push(), // TODO: details.info.merchant.name 180 paid: false, 181 raw: selectionDetail.amountRaw, 182 effective: effective, 183 firstScope: scope, 184 baseURL: nil, 185 summary: summary(details.info), 186 products: details.info?.products ?? [], 187 balanceDetails: selectionDetail.balanceDetails) 188 } 189 } else { // should never happen... 190 // TODO: show some error 191 } 192 } else { // cached data outdated 193 // TODO: purge and reload 194 } 195 } 196 } else { // show finished payment 197 TransactionPayDetailV(stack: stack.push(), 198 paymentTx: paymentTransaction) // TODO: details.info.merchant.name 199 ThreeAmountsSheet(stack: stack.push(), 200 scope: scope, 201 common: common, 202 topAbbrev: String(localized: "Price:", comment: "mini"), 203 topTitle: String(localized: "Price (net):"), 204 baseURL: nil, // TODO: baseURL 205 noFees: nil, // TODO: noFees 206 feeIsNegative: false, 207 large: true, 208 summary: details.info?.summary ?? EMPTYSTRING) 209 } // show finished payment 210 } // body 211 } // PaymentTransactionView