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