TransactionTypeDetail.swift (15398B)
1 /* 2 * This file is part of GNU Taler, ©2022-26 Taler Systems S.A. 3 * See LICENSE.md 4 */ 5 /** 6 * @author Marc Stibane 7 */ 8 import SwiftUI 9 import taler_swift 10 import SymLog 11 12 13 struct TransactionTypeDetail: View { 14 private let symLog = SymLogV(0) 15 let stack: CallStack 16 @Binding var transaction: TalerTransaction 17 @Binding var payNow: Bool 18 @Binding var selectedChoice: Int? 19 @Binding var scope: ScopeInfo? 20 @Binding var effective: Amount? 21 let hasDone: Bool 22 23 @EnvironmentObject private var controller: Controller 24 @EnvironmentObject private var model: WalletModel 25 @Environment(\.colorScheme) private var colorScheme 26 @Environment(\.colorSchemeContrast) private var colorSchemeContrast 27 @AppStorage("minimalistic") var minimalistic: Bool = false 28 @State private var rotationEnabled = true 29 @State private var ignoreAccept: Bool = false // accept could be set by OIM to trigger accept 30 @State private var isCopied: Bool = false 31 32 func refreshFee(input: Amount, output: Amount) -> Amount? { 33 do { 34 let fee = try input - output 35 return fee 36 } catch { 37 38 } 39 return nil 40 } 41 42 func abortedHint(_ delay: Duration?) -> UInt? { 43 if let delay { 44 if let microseconds = try? delay.microseconds() { 45 let days = microseconds / (24 * 3600 * 1000 * 1000) 46 if days > 0 { 47 return UInt(days) 48 } 49 } 50 return 0 51 } 52 return nil 53 } 54 55 var body: some View { 56 #if PRINT_CHANGES 57 let _ = Self._printChanges() 58 let _ = symLog.vlog() 59 #endif 60 let common = transaction.common 61 let pending = transaction.isPending 62 let isDialog = transaction.isDialog 63 Group { 64 switch transaction { 65 case .dummy(_): Group { 66 let title = EMPTYSTRING 67 Text(title) 68 .talerFont(.body) 69 RotatingTaler(size: 100, progress: true, once: false, rotationEnabled: $rotationEnabled) 70 .frame(maxWidth: .infinity, alignment: .center) 71 // has its own accessibilityLabel 72 } 73 case .withdrawal(let withdrawalTransaction): Group { 74 let details = withdrawalTransaction.details 75 if common.isAborted && details.withdrawalDetails.type == .manual { 76 if let days = abortedHint(details.withdrawalDetails.reserveClosingDelay) { 77 let wireBack = days > 0 ? String(localized: "If you have already sent money to the payment service, it will wire it back in \(days) days.") 78 : String(localized: "If you have already sent money to the payment service, it will wire it back in a few days.") 79 Text("The withdrawal was aborted.\n\n\(wireBack)") 80 .talerFont(.callout) 81 } 82 } 83 if pending { 84 PendingWithdrawalDetails(stack: stack.push(), 85 transaction: $transaction, 86 details: details) 87 } // ManualDetails or Confirm now (with bank) 88 ThreeAmountsSheet(stack: stack.push(), 89 scope: scope, 90 common: common, 91 topAbbrev: String(localized: "Chosen:", comment: "mini"), 92 topTitle: String(localized: "Chosen amount to withdraw:"), 93 baseURL: details.exchangeBaseUrl, 94 noFees: nil, // TODO: noFees 95 feeIsNegative: true, 96 large: false, 97 summary: nil) 98 } 99 case .deposit(let depositTransaction): Group { 100 if transaction.common.isPendingKYCauth { 101 KYCauth(stack: stack.push(), common: common) 102 } else if transaction.isPendingKYC { 103 KYCbutton(kycUrl: common.kycUrl) 104 } 105 ThreeAmountsSheet(stack: stack.push(), 106 scope: scope, 107 common: common, 108 topAbbrev: String(localized: "Deposit:", comment: "mini"), 109 topTitle: String(localized: "Amount to deposit:"), 110 baseURL: nil, // TODO: baseURL 111 noFees: nil, // TODO: noFees 112 feeIsNegative: false, 113 large: true, 114 summary: nil) 115 } 116 117 case .payment(let paymentTransaction): 118 PaymentTransactionView(stack: stack.push(), 119 common: common, 120 paymentTransaction: paymentTransaction, 121 scope: $scope, 122 effective: $effective, 123 payNow: $payNow, 124 selectedChoice: $selectedChoice) 125 .task(id: common.txState.hashValue) { 126 let state = common.txState 127 if common.isDialog { 128 if controller.choicesForPayment == nil { 129 symLog.log("❗️❗️ task: \(common.txState)") 130 await controller.loadChoicesForPayment(stack.push(),model, 131 txId: common.transactionId) 132 } else { 133 symLog.log("❗️❗️ choicesForPayment exists, no need for task: \(state)") 134 } 135 } else { 136 symLog.log("❗️❗️ \(common.type.rawValue), no need for task: \(state)") 137 } 138 } 139 case .refund(let refundTransaction): Group { 140 let details = refundTransaction.details // TODO: more details, details.info?.merchant.name 141 ThreeAmountsSheet(stack: stack.push(), 142 scope: scope, 143 common: common, 144 topAbbrev: String(localized: "Refunded:", comment: "mini"), 145 topTitle: String(localized: "Refunded amount:"), 146 baseURL: nil, // TODO: baseURL 147 noFees: nil, // TODO: noFees 148 feeIsNegative: true, 149 large: true, 150 summary: details.paymentInfo?.summary) 151 } 152 case .refresh(let refreshTransaction): Group { 153 let labelColor = WalletColors().labelColor 154 let errorColor = WalletColors().errorColor 155 let details = refreshTransaction.details 156 Section { 157 Text(details.refreshReason.localizedRefreshReason) 158 .talerFont(.title) 159 let input = details.refreshInputAmount 160 AmountRowV(stack: stack.push(), 161 title: minimalistic ? "Refreshed:" : "Refreshed amount:", 162 amount: input, 163 scope: scope, 164 isNegative: nil, 165 color: labelColor, 166 large: true) 167 if let fee = refreshFee(input: input, output: details.refreshOutputAmount) { 168 AmountRowV(stack: stack.push(), 169 title: minimalistic ? "Fee:" : "Refreshed fee:", 170 amount: fee, 171 scope: scope, 172 isNegative: fee.isZero ? nil : true, 173 color: labelColor, 174 large: true) 175 } 176 if let error = details.error { 177 HStack { 178 VStack(alignment: .leading) { 179 Text(error.hint) 180 .talerFont(.headline) 181 .foregroundColor(errorColor) 182 .listRowSeparator(.hidden) 183 if let stack = error.stack { 184 Text(stack) 185 .talerFont(.body) 186 .foregroundColor(errorColor) 187 .listRowSeparator(.hidden) 188 } 189 } 190 let stackStr = error.stack ?? EMPTYSTRING 191 let errorStr = error.hint + "\n" + stackStr 192 CopyButton(textToCopy: errorStr, isCopied: $isCopied, vertical: true) 193 .accessibilityLabel(Text("Copy the error", comment: "a11y")) 194 .disabled(false) 195 } 196 } 197 } 198 } 199 200 case .peer2peer(let p2pTransaction): Group { 201 let details = p2pTransaction.details 202 if transaction.isPendingKYC { 203 KYCbutton(kycUrl: common.kycUrl) 204 } 205 if !transaction.isDone { 206 ExpiresView(expiration: details.info.expiration) 207 } 208 if transaction.isRcvCoins && common.isDialog { 209 PeerPushCreditView(stack: stack.push(), 210 raw: common.amountRaw, 211 effective: common.amountEffective, 212 isDone: common.isDone, 213 scope: scope, 214 summary: details.info.summary) 215 PeerPushCreditAccept(stack: stack.push(), url: nil, 216 transactionId: common.transactionId, 217 accept: $ignoreAccept) 218 } else if transaction.isPayInvoice && common.isDialog { 219 PeerPullDebitView(stack: stack.push(), 220 raw: common.amountRaw, 221 effective: common.amountEffective, 222 isDone: common.isDone, 223 scope: scope, 224 summary: details.info.summary) 225 PeerPullDebitConfirm(stack: stack.push(), url: nil, 226 transactionId: common.transactionId) 227 } else { 228 // TODO: isSendCoins should show QR only while not yet expired - either set timer or wallet-core should do so and send a state-changed notification 229 // TODO: details.info.summary 230 if pending { 231 if transaction.isPendingReady { 232 QRCodeDetails(transaction: transaction) 233 if hasDone { 234 Text("QR code and link can also be scanned or copied / shared from Transactions later.") 235 .multilineTextAlignment(.leading) 236 .talerFont(.subheadline) 237 // .padding(.top) 238 } 239 } else { 240 Text("This transaction is not yet ready...") 241 .multilineTextAlignment(.leading) 242 .talerFont(.subheadline) 243 } 244 } 245 let colon = ":" 246 let localizedType = transaction.isDone ? transaction.localizedTypePast 247 : transaction.localizedType 248 ThreeAmountsSheet(stack: stack.push(), 249 scope: scope, 250 common: common, 251 topAbbrev: localizedType + colon, 252 topTitle: localizedType + colon, 253 baseURL: details.exchangeBaseUrl, 254 noFees: nil, // TODO: noFees 255 feeIsNegative: true, 256 large: false, 257 summary: details.info.summary) 258 } // else 259 } // p2p 260 261 case .recoup(let recoupTransaction): Group { 262 let details = recoupTransaction.details // TODO: details.recoupReason 263 ThreeAmountsSheet(stack: stack.push(), 264 scope: scope, 265 common: common, 266 topAbbrev: String(localized: "Recoup:", comment: "mini"), 267 topTitle: String(localized: "Recoup:"), 268 baseURL: nil, // TODO: baseURL, noFees 269 noFees: nil, 270 feeIsNegative: nil, 271 large: true, 272 summary: details.recoupReason) 273 } 274 case .denomLoss(let denomLossTransaction): Group { 275 let details = denomLossTransaction.details // TODO: more details, details.lossEventType.rawValue 276 ThreeAmountsSheet(stack: stack.push(), 277 scope: scope, 278 common: common, 279 topAbbrev: String(localized: "Lost:", comment: "mini"), 280 topTitle: String(localized: "Money lost:"), 281 baseURL: details.exchangeBaseUrl, 282 noFees: nil, // TODO: noFees 283 feeIsNegative: nil, 284 large: true, 285 summary: details.lossEventType.rawValue) 286 } 287 } // switch 288 } // Group 289 } 290 }