ThreeAmountsSection.swift (12678B)
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 if let url = NSURL(string: imageBase64) { 87 if let data = NSData(contentsOf: url as URL) { 88 if let uiImage = UIImage(data: data as Data) { 89 return Image(uiImage: uiImage) 90 } 91 } 92 } 93 return nil 94 } 95 } 96 97 // MARK: - 98 struct ThreeAmountsSection: View { 99 let stack: CallStack 100 let scope: ScopeInfo? 101 var topTitle: String 102 var topAbbrev: String 103 var topAmount: Amount 104 let noFees: Bool? // true if exchange charges no fees at all 105 var fee: Amount? // nil = don't show fee line, zero = no fee for this tx 106 var feeIsNegative: Bool? // show fee with minus (or plus) sign, or no sign if nil 107 var bottomTitle: String 108 var bottomAbbrev: String 109 var bottomAmount: Amount? // nil = incomplete (aborted, timed out) 110 let large: Bool 111 let pendingDialog: Bool 112 let isDone: Bool 113 let incoming: Bool 114 let baseURL: String? 115 let txStateLcl: String? // localizedState 116 let summary: String? 117 let products: [Product]? 118 119 @EnvironmentObject private var controller: Controller 120 @Environment(\.colorScheme) private var colorScheme 121 @Environment(\.colorSchemeContrast) private var colorSchemeContrast 122 @AppStorage("minimalistic") var minimalistic: Bool = false 123 124 @State private var productImages: [ProductImage] = [] 125 @State private var currencyInfo: CurrencyInfo = CurrencyInfo.zero(UNKNOWN) 126 127 @MainActor 128 private func viewDidLoad() async { 129 var temp: [ProductImage] = [] 130 if let products { 131 for product in products { 132 if let imageBase64 = product.image { 133 let productImage = ProductImage(imageBase64, product.description, product.price) 134 temp.append(productImage) 135 } 136 } 137 } 138 productImages = temp 139 if let scope { 140 currencyInfo = controller.info(for: scope) ?? CurrencyInfo.zero(UNKNOWN) 141 } 142 } 143 144 @ViewBuilder 145 var productImageSections: some View { 146 ForEach(productImages, id: \.self) { productImage in 147 if let image = productImage.image { 148 Section { 149 HStack { 150 image.resizable() 151 .scaledToFill() 152 .frame(width: 64, height: 64) 153 .accessibilityHidden(true) 154 Text(productImage.description) 155 // if let product_id = product.product_id { 156 // Text(product_id) 157 // } 158 if let price = productImage.price { 159 Spacer() 160 AmountV(scope, price, isNegative: nil) 161 } 162 }.talerFont(.body) 163 .accessibilityElement(children: .combine) 164 } 165 } 166 } 167 } 168 169 var body: some View { 170 let currency = currencyInfo.currency 171 let labelColor = WalletColors().labelColor 172 let foreColor = pendingDialog ? WalletColors().pendingColor(incoming) 173 : WalletColors().transactionColor(incoming) 174 let hasNoFees = noFees ?? false 175 productImageSections 176 Section { 177 if let summary { 178 if productImages.isEmpty { // otherwise we already have rendered the images 179 Text(summary) // and thus don't need a summary 180 .talerFont(.title3) 181 .padding(.bottom) 182 } 183 } 184 185 if pendingDialog || isDone { 186 Text(pendingDialog ? "Payment will be made in \(currency)" 187 : "Payment was made in \(currency)") 188 .talerFont(.callout) 189 .padding(.top, 4) 190 } 191 AmountRowV(stack: stack.push(), 192 title: minimalistic ? topAbbrev : topTitle, 193 amount: topAmount, 194 scope: scope, 195 isNegative: nil, 196 color: labelColor, 197 large: false) 198 .padding(.bottom, 4) 199 if hasNoFees == false { // otherwise raw==effective 200 if let fee { 201 let title = minimalistic ? String(localized: "Exchange fee (short):", defaultValue: "Fee:", comment: "short version") 202 : String(localized: "Exchange fee (long):", defaultValue: "Fee:", comment: "long version") 203 AmountRowV(stack: stack.push(), 204 title: title, 205 amount: fee, 206 scope: scope, 207 isNegative: fee.isZero ? nil : feeIsNegative, 208 color: labelColor, 209 large: false) 210 .padding(.bottom, 4) 211 } 212 if let bottomAmount { 213 AmountRowV(stack: stack.push(), 214 title: minimalistic ? bottomAbbrev : bottomTitle, 215 amount: bottomAmount, 216 scope: scope, 217 isNegative: nil, 218 color: foreColor, 219 large: large) 220 } 221 } 222 let serviceURL = scope?.url ?? baseURL 223 if let serviceURL { 224 VStack(alignment: .leading) { 225 // TODO: "Issued by" for withdrawals 226 Text(minimalistic ? "Payment service:" : "Using payment service:") 227 .multilineTextAlignment(.leading) 228 .talerFont(.body) 229 Text(serviceURL.trimURL) 230 .frame(maxWidth: .infinity, alignment: .trailing) 231 .multilineTextAlignment(.center) 232 .talerFont(large ? .title3 : .body) 233 // .fontWeight(large ? .medium : .regular) // @available(iOS 16.0, *) 234 .foregroundColor(labelColor) 235 } 236 .padding(.top, 4) 237 .frame(maxWidth: .infinity, alignment: .leading) 238 .listRowSeparator(.hidden) 239 .accessibilityElement(children: .combine) 240 } 241 } header: { 242 let header = scope?.url?.trimURL ?? scope?.currency ?? summary != nil ? "Summary" : nil 243 if let header { 244 Text(header) 245 .talerFont(.title3) 246 .foregroundColor(WalletColors().secondary(colorScheme, colorSchemeContrast)) 247 } 248 } 249 .task { await viewDidLoad() } 250 .onChange(of: scope) { newVal in 251 if let newVal { 252 currencyInfo = controller.info(for: newVal) ?? CurrencyInfo.zero(UNKNOWN) 253 } 254 } 255 } 256 } 257 // MARK: - 258 #if DEBUG 259 struct ThreeAmounts_Previews: PreviewProvider { 260 @MainActor 261 struct StateContainer: View { 262 // @State private var previewD: CurrencyInfo = CurrencyInfo.zero(DEMOCURRENCY) 263 // @State private var previewT: CurrencyInfo = CurrencyInfo.zero(TESTCURRENCY) 264 265 var body: some View { 266 let scope = ScopeInfo.zero(LONGCURRENCY) 267 let common = TransactionCommon(type: .withdrawal, 268 transactionId: "someTxID", 269 timestamp: Timestamp(from: 1_666_666_000_000), 270 scopes: [scope], 271 txState: TransactionState(major: .done), 272 txActions: [], 273 amountRaw: Amount(currency: LONGCURRENCY, cent: 20), 274 amountEffective: Amount(currency: LONGCURRENCY, cent: 10)) 275 // let test = Amount(currency: TESTCURRENCY, cent: 123) 276 // let demo = Amount(currency: DEMOCURRENCY, cent: 123456) 277 List { 278 ThreeAmountsSheet(stack: CallStack("Preview"), 279 scope: scope, 280 common: common, 281 topAbbrev: "Withdrawal", 282 topTitle: "Withdrawal", 283 baseURL: DEMOEXCHANGE, 284 noFees: false, 285 large: 1==0, summary: nil) 286 .safeAreaInset(edge: .bottom) { 287 Button(String("Preview")) {} 288 .buttonStyle(TalerButtonStyle(type: .prominent)) 289 .padding(.horizontal) 290 .disabled(true) 291 } 292 } 293 } 294 } 295 296 static var previews: some View { 297 StateContainer() 298 // .environment(\.sizeCategory, .extraExtraLarge) Canvas Device Settings 299 } 300 } 301 #endif