ThreeAmountsSection.swift (11780B)
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 let merchant: String? 25 26 #if DEBUG 27 @AppStorage("developerMode") var developerMode: Bool = true 28 #else 29 @AppStorage("developerMode") var developerMode: Bool = false 30 #endif 31 32 var body: some View { 33 let raw = common.amountRaw 34 let effective = common.amountEffective 35 let fee = common.fee() 36 let incoming = common.isIncoming 37 let pending = common.isPending || common.isFinalizing 38 let isDone = common.isDone 39 let incomplete = !(isDone || pending) 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: incomplete ? nil : effective, 63 large: large, 64 pending: pending, 65 incoming: incoming, 66 baseURL: baseURL, 67 txStateLcl: txStateLcl, 68 summary: summary, 69 merchant: merchant, 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 pending: Bool 112 let incoming: Bool 113 let baseURL: String? 114 let txStateLcl: String? // localizedState 115 let summary: String? 116 let merchant: String? 117 let products: [Product]? 118 119 @Environment(\.colorScheme) private var colorScheme 120 @Environment(\.colorSchemeContrast) private var colorSchemeContrast 121 @AppStorage("minimalistic") var minimalistic: Bool = false 122 123 @State private var productImages: [ProductImage] = [] 124 125 @MainActor 126 private func viewDidLoad() async { 127 var temp: [ProductImage] = [] 128 if let products { 129 for product in products { 130 if let imageBase64 = product.image { 131 let productImage = ProductImage(imageBase64, product.description, product.price) 132 temp.append(productImage) 133 } 134 } 135 } 136 productImages = temp 137 } 138 139 var body: some View { 140 let labelColor = WalletColors().labelColor 141 let foreColor = pending ? WalletColors().pendingColor(incoming) 142 : WalletColors().transactionColor(incoming) 143 let hasNoFees = noFees ?? false 144 ForEach(productImages, id: \.self) { productImage in 145 if let image = productImage.image { 146 Section { 147 HStack { 148 image.resizable() 149 .scaledToFill() 150 .frame(width: 64, height: 64) 151 .accessibilityHidden(true) 152 Text(productImage.description) 153 // if let product_id = product.product_id { 154 // Text(product_id) 155 // } 156 if let price = productImage.price { 157 Spacer() 158 AmountV(scope, price, isNegative: nil) 159 } 160 }.talerFont(.body) 161 .accessibilityElement(children: .combine) 162 } 163 } 164 } 165 Section { 166 if let summary { 167 if productImages.count == 0 { 168 Text(summary) 169 .talerFont(.title3) 170 .lineLimit(4) 171 .padding(.bottom) 172 } 173 } 174 if let merchant { 175 Text(merchant) 176 .talerFont(.title3) 177 .lineLimit(4) 178 .padding(.bottom) 179 } 180 AmountRowV(stack: stack.push(), 181 title: minimalistic ? topAbbrev : topTitle, 182 amount: topAmount, 183 scope: scope, 184 isNegative: nil, 185 color: labelColor, 186 large: false) 187 .padding(.bottom, 4) 188 if hasNoFees == false { 189 if let fee { 190 let title = minimalistic ? String(localized: "Exchange fee (short):", defaultValue: "Fee:", comment: "short version") 191 : String(localized: "Exchange fee (long):", defaultValue: "Fee:", comment: "long version") 192 AmountRowV(stack: stack.push(), 193 title: title, 194 amount: fee, 195 scope: scope, 196 isNegative: fee.isZero ? nil : feeIsNegative, 197 color: labelColor, 198 large: false) 199 .padding(.bottom, 4) 200 } 201 if let bottomAmount { 202 AmountRowV(stack: stack.push(), 203 title: minimalistic ? bottomAbbrev : bottomTitle, 204 amount: bottomAmount, 205 scope: scope, 206 isNegative: nil, 207 color: foreColor, 208 large: large) 209 } 210 } 211 let serviceURL = scope?.url ?? baseURL 212 if let serviceURL { 213 VStack(alignment: .leading) { 214 // TODO: "Issued by" for withdrawals 215 Text(minimalistic ? "Payment service:" : "Using payment service:") 216 .multilineTextAlignment(.leading) 217 .talerFont(.body) 218 Text(serviceURL.trimURL) 219 .frame(maxWidth: .infinity, alignment: .trailing) 220 .multilineTextAlignment(.center) 221 .talerFont(large ? .title3 : .body) 222 // .fontWeight(large ? .medium : .regular) // @available(iOS 16.0, *) 223 .foregroundColor(labelColor) 224 } 225 .padding(.top, 4) 226 .frame(maxWidth: .infinity, alignment: .leading) 227 .listRowSeparator(.hidden) 228 .accessibilityElement(children: .combine) 229 } 230 } header: { 231 let header = scope?.url?.trimURL ?? scope?.currency ?? "Summary" 232 Text(header) 233 .talerFont(.title3) 234 .foregroundColor(WalletColors().secondary(colorScheme, colorSchemeContrast)) 235 } 236 .task { await viewDidLoad() } 237 } 238 } 239 // MARK: - 240 #if DEBUG 241 struct ThreeAmounts_Previews: PreviewProvider { 242 @MainActor 243 struct StateContainer: View { 244 // @State private var previewD: CurrencyInfo = CurrencyInfo.zero(DEMOCURRENCY) 245 // @State private var previewT: CurrencyInfo = CurrencyInfo.zero(TESTCURRENCY) 246 247 var body: some View { 248 let scope = ScopeInfo.zero(LONGCURRENCY) 249 let common = TransactionCommon(type: .withdrawal, 250 transactionId: "someTxID", 251 timestamp: Timestamp(from: 1_666_666_000_000), 252 scopes: [scope], 253 txState: TransactionState(major: .done), 254 txActions: [], 255 amountRaw: Amount(currency: LONGCURRENCY, cent: 20), 256 amountEffective: Amount(currency: LONGCURRENCY, cent: 10)) 257 // let test = Amount(currency: TESTCURRENCY, cent: 123) 258 // let demo = Amount(currency: DEMOCURRENCY, cent: 123456) 259 List { 260 ThreeAmountsSheet(stack: CallStack("Preview"), 261 scope: scope, 262 common: common, 263 topAbbrev: "Withdrawal", 264 topTitle: "Withdrawal", 265 baseURL: DEMOEXCHANGE, 266 noFees: false, 267 large: 1==0, summary: nil, merchant: nil) 268 .safeAreaInset(edge: .bottom) { 269 Button(String("Preview")) {} 270 .buttonStyle(TalerButtonStyle(type: .prominent)) 271 .padding(.horizontal) 272 .disabled(true) 273 } 274 } 275 } 276 } 277 278 static var previews: some View { 279 StateContainer() 280 // .environment(\.sizeCategory, .extraExtraLarge) Canvas Device Settings 281 } 282 } 283 #endif