ThreeAmountsSection.swift (12712B)
1 /* 2 * This file is part of GNU Taler, ©2022-25 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 ThreeAmountsSheet: View { // should be in a separate file 12 let stack: CallStack 13 let scope: ScopeInfo? 14 var common: TransactionCommon 15 var topAbbrev: String 16 var topTitle: String 17 var bottomTitle: String? 18 var bottomAbbrev: String? 19 let baseURL: String? 20 let noFees: Bool? // true if exchange charges no fees at all 21 var feeIsNegative: Bool? // show fee with minus (or plus) sign, or no sign if nil 22 let large: Bool // set to false for QR or IBAN 23 let summary: String? 24 25 #if DEBUG 26 @AppStorage("developerMode") var developerMode: Bool = true 27 #else 28 @AppStorage("developerMode") var developerMode: Bool = false 29 #endif 30 31 var body: some View { 32 let incoming = common.isIncoming 33 let pending = common.isPending || common.isFinalizing 34 let dialog = common.isDialog 35 let isDone = common.isDone 36 let incomplete = !(isDone || pending || dialog) 37 let raw = common.amountRaw 38 let effective: Amount? = incomplete ? nil : common.amountEffective 39 let fee: Amount? = incomplete ? nil : common.fee() 40 41 let defaultBottomTitle = incoming ? (pending ? String(localized: "Pending amount to obtain:") 42 : String(localized: "Obtained amount:") ) 43 : (pending ? String(localized: "Amount to pay:") 44 : String(localized: "Paid amount:") ) 45 let defaultBottomAbbrev = incoming ? (pending ? String(localized: "Pending:", comment: "mini") 46 : String(localized: "Obtained:", comment: "mini") ) 47 : (pending ? String(localized: "Pay:", comment: "mini") 48 : String(localized: "Paid:", comment: "mini") ) 49 let majorLcl = common.txState.major.localizedState 50 let txStateLcl = developerMode && pending ? (common.txState.minor?.localizedState ?? majorLcl) 51 : majorLcl 52 ThreeAmountsSection(stack: stack.push(), 53 scope: scope, 54 topTitle: topTitle, 55 topAbbrev: topAbbrev, 56 topAmount: raw, 57 noFees: noFees, 58 fee: fee, 59 feeIsNegative: feeIsNegative, 60 bottomTitle: bottomTitle ?? defaultBottomTitle, 61 bottomAbbrev: bottomAbbrev ?? defaultBottomAbbrev, 62 bottomAmount: effective, 63 large: large, 64 pendingDialog: pending || dialog, 65 isDone: isDone, 66 incoming: incoming, 67 baseURL: baseURL, 68 txStateLcl: txStateLcl, 69 summary: summary, 70 products: nil) 71 } 72 } 73 74 struct ProductImage: Codable, Hashable { 75 var imageBase64: String 76 var description: String 77 var price: Amount? 78 79 init(_ image: String, _ desc: String, _ price: Amount?) { 80 self.imageBase64 = image 81 self.description = desc 82 self.price = price 83 } 84 85 var image: Image? { 86 Image(imageBase64) 87 } 88 } 89 90 // MARK: - 91 struct ThreeAmountsSection: View { 92 let stack: CallStack 93 let scope: ScopeInfo? 94 var topTitle: String 95 var topAbbrev: String 96 var topAmount: Amount 97 let noFees: Bool? // true if exchange charges no fees at all 98 var fee: Amount? // nil = don't show fee line, zero = no fee for this tx 99 var feeIsNegative: Bool? // show fee with minus (or plus) sign, or no sign if nil 100 var bottomTitle: String 101 var bottomAbbrev: String 102 var bottomAmount: Amount? // nil = incomplete (aborted, timed out) 103 let large: Bool 104 let pendingDialog: Bool 105 let isDone: Bool 106 let incoming: Bool 107 let baseURL: String? 108 let txStateLcl: String? // localizedState 109 let summary: String? 110 let products: [Product]? 111 112 @EnvironmentObject private var controller: Controller 113 @Environment(\.colorScheme) private var colorScheme 114 @Environment(\.colorSchemeContrast) private var colorSchemeContrast 115 @AppStorage("minimalistic") var minimalistic: Bool = false 116 117 @State private var productImages: [ProductImage] = [] 118 @State private var currencyInfo: CurrencyInfo = CurrencyInfo.zero(UNKNOWN) 119 120 @MainActor 121 private func viewDidLoad() async { 122 var temp: [ProductImage] = [] 123 if let products { 124 for product in products { 125 if let imageBase64 = product.image { 126 let productImage = ProductImage(imageBase64, product.description, product.price) 127 temp.append(productImage) 128 } 129 } 130 } 131 productImages = temp 132 if let scope { 133 currencyInfo = controller.info(for: scope) ?? CurrencyInfo.zero(UNKNOWN) 134 } 135 } 136 137 @ViewBuilder 138 var productImageSections: some View { 139 ForEach(productImages, id: \.self) { productImage in 140 if let image = productImage.image { 141 Section { 142 HStack { 143 image.resizable() 144 .scaledToFill() 145 .frame(width: 64, height: 64) 146 .accessibilityHidden(true) 147 Text(productImage.description) 148 // if let product_id = product.product_id { 149 // Text(product_id) 150 // } 151 if let price = productImage.price { 152 Spacer() 153 AmountV(scope, price, isNegative: nil) 154 } 155 }.talerFont(.body) 156 .accessibilityElement(children: .combine) 157 } 158 } 159 } 160 } 161 162 var body: some View { 163 let currency = currencyInfo.currency 164 let labelColor = WalletColors().labelColor 165 let bottomColor = pendingDialog ? WalletColors().pendingColor(incoming) 166 : WalletColors().transactionColor(incoming) 167 let hasNoFees = noFees ?? false 168 productImageSections 169 Section { 170 if let summary { 171 if productImages.isEmpty { // otherwise we already have rendered the images 172 Text(summary) // and thus don't need a summary 173 .talerFont(.title3) 174 .padding(.bottom) 175 } 176 } 177 178 if currency != "UNKNOWN" { 179 if pendingDialog || isDone { 180 Text(pendingDialog ? "Payment will be made in \(currency)" 181 : "Payment was made in \(currency)") 182 .talerFont(.callout) 183 .padding(.top, 4) 184 } 185 AmountRowV(stack: stack.push(), 186 title: minimalistic ? topAbbrev : topTitle, 187 amount: topAmount, 188 scope: scope, 189 isNegative: nil, 190 color: labelColor, 191 large: false) 192 .padding(.bottom, 4) 193 if hasNoFees == false { // otherwise raw==effective 194 if let fee { 195 let title = minimalistic ? String(localized: "Exchange fee (short):", defaultValue: "Fee:", comment: "short version") 196 : String(localized: "Exchange fee (long):", defaultValue: "Fee:", comment: "long version") 197 AmountRowV(stack: stack.push(), 198 title: title, 199 amount: fee, 200 scope: scope, 201 isNegative: fee.isZero ? nil : feeIsNegative, 202 color: labelColor, 203 large: false) 204 .padding(.bottom, 4) 205 } 206 if let bottomAmount { 207 AmountRowV(stack: stack.push(), 208 title: minimalistic ? bottomAbbrev : bottomTitle, 209 amount: bottomAmount, 210 scope: scope, 211 isNegative: nil, 212 color: bottomColor, 213 large: large) 214 } 215 } 216 let serviceURL = scope?.url ?? baseURL 217 if let serviceURL { 218 VStack(alignment: .leading) { 219 // TODO: "Issued by" for withdrawals 220 Text(minimalistic ? "Payment service:" : "Using payment service:") 221 .multilineTextAlignment(.leading) 222 .talerFont(.body) 223 Text(serviceURL.trimURL) 224 .frame(maxWidth: .infinity, alignment: .trailing) 225 .multilineTextAlignment(.center) 226 .talerFont(large ? .title3 : .body) 227 // .fontWeight(large ? .medium : .regular) // @available(iOS 16.0, *) 228 .foregroundColor(labelColor) 229 } 230 .padding(.top, 4) 231 .frame(maxWidth: .infinity, alignment: .leading) 232 .listRowSeparator(.hidden) 233 .accessibilityElement(children: .combine) 234 } 235 } 236 } header: { 237 let header = scope?.url?.trimURL ?? scope?.currency ?? summary != nil ? "Summary" : nil 238 if let header { 239 Text(header) 240 .talerFont(.title3) 241 .foregroundColor(WalletColors().secondary(colorScheme, colorSchemeContrast)) 242 } 243 } 244 .task { await viewDidLoad() } 245 .onChange(of: scope) { newVal in 246 if let newVal { 247 currencyInfo = controller.info(for: newVal) ?? CurrencyInfo.zero(UNKNOWN) 248 } 249 } 250 } 251 } 252 // MARK: - 253 #if DEBUG 254 struct ThreeAmounts_Previews: PreviewProvider { 255 @MainActor 256 struct StateContainer: View { 257 // @State private var previewD: CurrencyInfo = CurrencyInfo.zero(DEMOCURRENCY) 258 // @State private var previewT: CurrencyInfo = CurrencyInfo.zero(TESTCURRENCY) 259 260 var body: some View { 261 let scope = ScopeInfo.zero(LONGCURRENCY) 262 let common = TransactionCommon(type: .withdrawal, 263 transactionId: "someTxID", 264 timestamp: Timestamp(from: 1_666_666_000_000), 265 scopes: [scope], 266 txState: TransactionState(major: .done), 267 txActions: [], 268 amountRaw: Amount(currency: LONGCURRENCY, cent: 20), 269 amountEffective: Amount(currency: LONGCURRENCY, cent: 10)) 270 // let test = Amount(currency: TESTCURRENCY, cent: 123) 271 // let demo = Amount(currency: DEMOCURRENCY, cent: 123456) 272 List { 273 ThreeAmountsSheet(stack: CallStack("Preview"), 274 scope: scope, 275 common: common, 276 topAbbrev: "Withdrawal", 277 topTitle: "Withdrawal", 278 baseURL: DEMOEXCHANGE, 279 noFees: false, 280 large: 1==0, summary: nil) 281 .safeAreaInset(edge: .bottom) { 282 Button(String("Preview")) {} 283 .buttonStyle(TalerButtonStyle(type: .prominent)) 284 .padding(.horizontal) 285 .disabled(true) 286 } 287 } 288 } 289 } 290 291 static var previews: some View { 292 StateContainer() 293 // .environment(\.sizeCategory, .extraExtraLarge) Canvas Device Settings 294 } 295 } 296 #endif