ThreeAmountsSection.swift (10856B)
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 } 71 } 72 73 struct ProductImage: Codable, Hashable { 74 var imageBase64: String 75 76 var image: Image? { 77 Image(imageBase64) 78 } 79 } 80 81 // MARK: - 82 struct ThreeAmountsSection: View { 83 let stack: CallStack 84 let scope: ScopeInfo? 85 var topTitle: String 86 var topAbbrev: String 87 var topAmount: Amount 88 let noFees: Bool? // true if exchange charges no fees at all 89 var fee: Amount? // nil = don't show fee line, zero = no fee for this tx 90 var feeIsNegative: Bool? // show fee with minus (or plus) sign, or no sign if nil 91 var bottomTitle: String 92 var bottomAbbrev: String 93 var bottomAmount: Amount? // nil = incomplete (aborted, timed out) 94 let large: Bool 95 let pendingDialog: Bool 96 let isDone: Bool 97 let incoming: Bool 98 let baseURL: String? 99 let txStateLcl: String? // localizedState 100 let summary: String? 101 102 @EnvironmentObject private var controller: Controller 103 @Environment(\.colorScheme) private var colorScheme 104 @Environment(\.colorSchemeContrast) private var colorSchemeContrast 105 @AppStorage("minimalistic") var minimalistic: Bool = false 106 107 @State private var currencyInfo: CurrencyInfo = CurrencyInfo.zero(UNKNOWN) 108 109 @MainActor 110 private func viewDidLoad() async { 111 if let scope { 112 currencyInfo = controller.info(for: scope) ?? CurrencyInfo.zero(UNKNOWN) 113 } 114 } 115 116 117 var body: some View { 118 let currency = currencyInfo.currency 119 let labelColor = WalletColors().labelColor 120 let bottomColor = pendingDialog ? WalletColors().pendingColor(incoming) 121 : WalletColors().transactionColor(incoming) 122 let hasNoFees = noFees ?? false 123 Section { 124 // if let summary { 125 // Text(summary) 126 // .talerFont(.title3) 127 // .padding(.bottom) 128 // } 129 130 if currency != "UNKNOWN" { 131 if pendingDialog || isDone { 132 Text(pendingDialog ? "Payment will be made in \(currency)" 133 : "Payment was made in \(currency)") 134 .talerFont(.callout) 135 .padding(.top, 4) 136 } 137 AmountRowV(stack: stack.push(), 138 title: minimalistic ? topAbbrev : topTitle, 139 amount: topAmount, 140 scope: scope, 141 isNegative: nil, 142 color: labelColor, 143 large: false) 144 .padding(.bottom, 4) 145 if hasNoFees == false { // otherwise raw==effective 146 if let fee { 147 let title = minimalistic ? String(localized: "Exchange fee (short):", defaultValue: "Fee:", comment: "short version") 148 : String(localized: "Exchange fee (long):", defaultValue: "Fee:", comment: "long version") 149 AmountRowV(stack: stack.push(), 150 title: title, 151 amount: fee, 152 scope: scope, 153 isNegative: fee.isZero ? nil : feeIsNegative, 154 color: labelColor, 155 large: false) 156 .padding(.bottom, 4) 157 } 158 if let bottomAmount { 159 AmountRowV(stack: stack.push(), 160 title: minimalistic ? bottomAbbrev : bottomTitle, 161 amount: bottomAmount, 162 scope: scope, 163 isNegative: nil, 164 color: bottomColor, 165 large: large) 166 } 167 } 168 let serviceURL = scope?.url ?? baseURL 169 if let serviceURL { 170 VStack(alignment: .leading) { 171 // TODO: "Issued by" for withdrawals 172 Text(minimalistic ? "Payment service:" : "Using payment service:") 173 .multilineTextAlignment(.leading) 174 .talerFont(.body) 175 Text(serviceURL.trimURL) 176 .frame(maxWidth: .infinity, alignment: .trailing) 177 .multilineTextAlignment(.center) 178 .talerFont(large ? .title3 : .body) 179 // .fontWeight(large ? .medium : .regular) // @available(iOS 16.0, *) 180 .foregroundColor(labelColor) 181 } 182 .padding(.top, 4) 183 .frame(maxWidth: .infinity, alignment: .leading) 184 .listRowSeparator(.hidden) 185 .accessibilityElement(children: .combine) 186 } 187 } 188 } header: { 189 let header = scope?.url?.trimURL ?? scope?.currency ?? (summary != nil ? "Summary" : nil) 190 if let header { 191 Text(header) 192 .talerFont(.title3) 193 .foregroundColor(WalletColors().secondary2(colorScheme, colorSchemeContrast)) 194 } 195 } 196 .task { await viewDidLoad() } 197 .onChange(of: scope) { newVal in 198 if let newVal { 199 currencyInfo = controller.info(for: newVal) ?? CurrencyInfo.zero(UNKNOWN) 200 } 201 } 202 } 203 } 204 // MARK: - 205 #if DEBUG 206 struct ThreeAmounts_Previews: PreviewProvider { 207 @MainActor 208 struct StateContainer: View { 209 // @State private var previewD: CurrencyInfo = CurrencyInfo.zero(DEMOCURRENCY) 210 // @State private var previewT: CurrencyInfo = CurrencyInfo.zero(TESTCURRENCY) 211 212 var body: some View { 213 let scope = ScopeInfo.zero(LONGCURRENCY) 214 let common = TransactionCommon(type: .withdrawal, 215 transactionId: "someTxID", 216 timestamp: Timestamp(from: 1_666_666_000_000), 217 scopes: [scope], 218 txState: TransactionState(major: .done), 219 txActions: [], 220 amountRaw: Amount(currency: LONGCURRENCY, cent: 20), 221 amountEffective: Amount(currency: LONGCURRENCY, cent: 10)) 222 // let test = Amount(currency: TESTCURRENCY, cent: 123) 223 // let demo = Amount(currency: DEMOCURRENCY, cent: 123456) 224 List { 225 ThreeAmountsSheet(stack: CallStack("Preview"), 226 scope: scope, 227 common: common, 228 topAbbrev: "Withdrawal", 229 topTitle: "Withdrawal", 230 baseURL: DEMOEXCHANGE, 231 noFees: false, 232 large: 1==0, summary: nil) 233 .safeAreaInset(edge: .bottom) { 234 Button(String("Preview")) {} 235 .buttonStyle(TalerButtonStyle(type: .prominent)) 236 .padding(.horizontal) 237 .disabled(true) 238 } 239 } 240 } 241 } 242 243 static var previews: some View { 244 StateContainer() 245 // .environment(\.sizeCategory, .extraExtraLarge) Canvas Device Settings 246 } 247 } 248 #endif