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