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