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