TransactionSummaryV.swift (39882B)
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 import SymLog 11 12 extension TalerTransaction { // for Dummys 13 init(dummyCurrency: String) { 14 let amount = Amount.zero(currency: dummyCurrency) 15 let now = Timestamp.now() 16 let common = TransactionCommon(type: .dummy, 17 transactionId: EMPTYSTRING, 18 timestamp: now, 19 scopes: [], 20 txState: TransactionState(major: .pending), 21 txActions: [], 22 amountRaw: amount, 23 amountEffective: amount) 24 self = .dummy(DummyTransaction(common: common)) 25 } 26 } 27 // MARK: - 28 struct TransactionSummaryV: View { 29 private let symLog = SymLogV(0) 30 let stack: CallStack 31 // let scope: ScopeInfo? 32 let transactionId: String 33 @Binding var talerTX: TalerTransaction 34 let navTitle: String? 35 let hasDone: Bool 36 let abortAction: ((_ transactionId: String, _ viewHandles: Bool) async throws -> Void)? 37 let deleteAction: ((_ transactionId: String, _ viewHandles: Bool) async throws -> Void)? 38 let failAction: ((_ transactionId: String, _ viewHandles: Bool) async throws -> Void)? 39 let suspendAction: ((_ transactionId: String, _ viewHandles: Bool) async throws -> Void)? 40 let resumeAction: ((_ transactionId: String, _ viewHandles: Bool) async throws -> Void)? 41 42 @EnvironmentObject private var controller: Controller 43 @EnvironmentObject private var model: WalletModel 44 @Environment(\.colorScheme) private var colorScheme 45 @Environment(\.colorSchemeContrast) private var colorSchemeContrast 46 @Environment(\.dismiss) var dismiss // call dismiss() to pop back 47 @AppStorage("minimalistic") var minimalistic: Bool = false 48 @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic 49 #if DEBUG 50 @AppStorage("developerMode") var developerMode: Bool = true 51 #else 52 @AppStorage("developerMode") var developerMode: Bool = false 53 #endif 54 55 @State private var ignoreThis: Bool = false 56 @State private var didDelete: Bool = false 57 @State var jsonTransaction: String = "" 58 @State var viewId = UUID() 59 @Namespace var topID 60 61 func loadTransaction() async { 62 if let reloadedTransaction = try? await model.getTransactionById(transactionId, 63 includeContractTerms: true, viewHandles: false) { 64 symLog.log("reloaded \(reloadedTransaction.localizedType): \(reloadedTransaction.common.txState.major)") 65 withAnimation { talerTX = reloadedTransaction; viewId = UUID() } // redraw 66 if developerMode { 67 if let json = try? await model.jsonTransactionById(transactionId, 68 includeContractTerms: true, viewHandles: false) { 69 jsonTransaction = json 70 } else { 71 jsonTransaction = "" 72 } 73 } 74 } else { 75 withAnimation{ talerTX = TalerTransaction(dummyCurrency: DEMOCURRENCY); viewId = UUID() } 76 jsonTransaction = "" 77 } 78 } 79 80 @MainActor 81 @discardableResult 82 func checkDismiss(_ notification: Notification, _ logStr: String = EMPTYSTRING) -> Bool { 83 if hasDone { 84 if let transition = notification.userInfo?[TRANSACTIONTRANSITION] as? TransactionTransition { 85 if transition.transactionId == talerTX.common.transactionId { // is the transition for THIS transaction? 86 symLog.log(logStr) 87 dismissTop(stack.push()) // if this view is in a sheet then dissmiss the sheet 88 return true 89 } 90 } 91 } else { // no sheet but the details view -> reload 92 checkReload(notification, logStr) 93 } 94 return false 95 } 96 97 @MainActor 98 private func dismiss(_ stack: CallStack) { 99 if hasDone { // if this view is in a sheet then dissmiss the whole sheet 100 dismissTop(stack.push()) 101 } else { // on a NavigationStack just pop 102 dismiss() 103 } 104 } 105 106 func checkReload(_ notification: Notification, _ logStr: String = EMPTYSTRING) { 107 if let transition = notification.userInfo?[TRANSACTIONTRANSITION] as? TransactionTransition { 108 if transition.transactionId == transactionId { // is the transition for THIS transaction? 109 let newMajor = transition.newTxState.major 110 Task { // runs on MainActor 111 // flush the screen first, then reload 112 withAnimation { talerTX = TalerTransaction(dummyCurrency: DEMOCURRENCY); viewId = UUID() } 113 symLog.log("newState: \(newMajor), reloading transaction") 114 if newMajor != .none { // don't reload after delete 115 await loadTransaction() 116 } 117 } 118 } 119 } else { // Yikes - should never happen 120 // TODO: logger.warning("Can't get notification.userInfo as TransactionTransition") 121 symLog.log(notification.userInfo as Any) 122 } 123 } 124 125 var body: some View { 126 #if PRINT_CHANGES 127 let _ = Self._printChanges() 128 let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear 129 #endif 130 let common = talerTX.common 131 if common.type != .dummy && transactionId == common.transactionId { 132 let scope = common.scopes.first // might be nil if scopes == [] 133 // let pending = transaction.isPending 134 let locale = TalerDater.shared.locale 135 let (dateString, date) = TalerDater.dateString(common.timestamp, minimalistic) 136 let a11yDate = TalerDater.accessibilityDate(date) ?? dateString 137 let navTitle2 = talerTX.isDone ? talerTX.localizedTypePast 138 : talerTX.localizedType 139 List { 140 if developerMode { 141 if talerTX.isSuspendable { if let suspendAction { 142 TransactionButton(transactionId: common.transactionId, 143 command: .suspend, 144 warning: nil, 145 didExecute: $ignoreThis, 146 action: suspendAction) 147 .listRowSeparator(.hidden) 148 } } 149 if talerTX.isResumable { if let resumeAction { 150 TransactionButton(transactionId: common.transactionId, 151 command: .resume, 152 warning: nil, 153 didExecute: $ignoreThis, 154 action: resumeAction) 155 .listRowSeparator(.hidden) 156 } } 157 } // Suspend + Resume buttons 158 Group { 159 Text(dateString) 160 .talerFont(.body) 161 .accessibilityLabel(a11yDate) 162 .foregroundColor(WalletColors().secondary(colorScheme, colorSchemeContrast)) 163 .id(topID) 164 let majorState = common.txState.major.localizedState 165 let minorState = common.txState.minor?.localizedState ?? nil 166 let state = developerMode ? talerTX.isPending ? minorState ?? majorState 167 : majorState 168 : majorState 169 let statusT = Text(state) 170 .multilineTextAlignment(.trailing) 171 let imageT = Text(common.type.icon()) 172 .accessibilityHidden(true) 173 let prefixT = Text("Status:") 174 let vLayout = VStack(alignment: .leading, spacing: 0) { 175 HStack { 176 imageT 177 prefixT 178 } // Icon + State 179 statusT 180 .frame(maxWidth: .infinity, alignment: .trailing) 181 } 182 if #available(iOS 16.4, *) { 183 ViewThatFits(in: .horizontal) { 184 HStack(spacing: HSPACING) { 185 imageT 186 Spacer() 187 prefixT 188 statusT 189 } 190 vLayout 191 } 192 } else { vLayout } // view for iOS 15 193 if developerMode { 194 if !jsonTransaction.isEmpty { 195 CopyButton(textToCopy: jsonTransaction, title: "Copy JSON") 196 } 197 } 198 } .listRowSeparator(.hidden) 199 .talerFont(.title) 200 .onAppear { // doesn't work - view still jumps 201 // scrollView.scrollTo(topID) 202 // withAnimation { scrollView.scrollTo(topID) } 203 } 204 205 TypeDetail(stack: stack.push(), 206 scope: scope, 207 transaction: $talerTX, 208 hasDone: hasDone) 209 210 // TODO: Retry Countdown, Retry Now button 211 // if talerTX.isRetryable, let retryAction { 212 // TransactionButton(transactionId: common.transactionId, command: .retry, 213 // warning: nil, action: abortAction) 214 // } // Retry button 215 if talerTX.isAbortable, let abortAction { 216 TransactionButton(transactionId: common.transactionId, 217 command: .abort, 218 warning: String(localized: "Are you sure you want to abort this transaction?"), 219 didExecute: $ignoreThis, 220 action: abortAction) 221 } // Abort button 222 if talerTX.isFailable, let failAction { 223 TransactionButton(transactionId: common.transactionId, 224 command: .fail, 225 warning: String(localized: "Are you sure you want to abandon this transaction?"), 226 didExecute: $ignoreThis, 227 action: failAction) 228 } // Fail button 229 if talerTX.isDeleteable, let deleteAction { 230 TransactionButton(transactionId: common.transactionId, 231 command: .delete, 232 warning: String(localized: "Are you sure you want to delete this transaction?"), 233 didExecute: $didDelete, 234 action: deleteAction) 235 .onChange(of: didDelete) { wasDeleted in 236 if wasDeleted { 237 symLog.log("wasDeleted -> dismiss view") 238 dismiss(stack) 239 } 240 } 241 } // Delete button 242 }.id(viewId) // change viewId to enforce a draw update 243 .listStyle(myListStyle.style).anyView 244 .onNotification(.TransactionExpired) { notification in 245 // TODO: Alert user that this tx just expired 246 if checkDismiss(notification, "newTxState.major == expired => dismiss sheet") { 247 // TODO: logger.info("newTxState.major == expired => dismiss sheet") 248 } 249 } 250 .onNotification(.TransactionDone) { notification in 251 checkDismiss(notification, "newTxState.major == done => dismiss sheet") 252 } 253 .onNotification(.DismissSheet) { notification in 254 checkDismiss(notification, "exchangeWaitReserve or withdrawCoins => dismiss sheet") 255 } 256 .onNotification(.PendingReady) { notification in 257 checkReload(notification, "pending ready ==> reload for talerURI") 258 } 259 .onNotification(.TransactionStateTransition) { notification in 260 if !didDelete { 261 checkReload(notification, "some transition ==> reload") 262 } 263 } 264 .navigationTitle(navTitle ?? navTitle2) 265 .onAppear { 266 symLog.log("onAppear") 267 DebugViewC.shared.setViewID(VIEW_TRANSACTIONSUMMARY, stack: stack.push()) 268 } 269 .onDisappear { 270 symLog.log("onDisappear") 271 } 272 } else { 273 Color.clear 274 .frame(maxWidth: .infinity, maxHeight: .infinity) 275 .task { 276 symLog.log("task - load transaction") 277 await loadTransaction() 278 } 279 } 280 } 281 // MARK: - 282 struct KYCbutton: View { 283 let kycUrl: String? 284 285 var body: some View { 286 if let kycUrl { 287 if let destination = URL(string: kycUrl) { 288 LinkButton(destination: destination, 289 hintTitle: String(localized: "You need to pass a legitimization procedure.", comment: "KYC"), 290 buttonTitle: String(localized: "Open legitimization website", comment: "KYC"), 291 a11yHint: String(localized: "Will go to legitimization website to permit this withdrawal.", comment: "a11y"), 292 badge: NEEDS_KYC) 293 } 294 } 295 } 296 } 297 // MARK: - 298 struct KYCauth: View { 299 let stack: CallStack 300 let common: TransactionCommon 301 302 @AppStorage("minimalistic") var minimalistic: Bool = false 303 @State private var accountID = 0 304 @State private var listID = UUID() 305 306 func redraw(_ newAccount: Int) -> Void { 307 if newAccount != accountID { 308 accountID = newAccount 309 withAnimation { listID = UUID() } 310 } 311 } 312 313 func validDetails(_ paytoUris: [String]) -> [ExchangeAccountDetails] { 314 var details: [ExchangeAccountDetails] = [] 315 for paytoUri in paytoUris { 316 let payTo = PayTo(paytoUri) 317 let amount = common.kycAuthTransferInfo?.amount 318 let detail = ExchangeAccountDetails(status: "ok", 319 paytoUri: paytoUri, 320 transferAmount: amount, 321 scope: common.scopes[0]) 322 details.append(detail) 323 } 324 return details 325 } 326 327 var body: some View { 328 if let info = common.kycAuthTransferInfo { 329 let debitPayTo = PayTo(info.debitPaytoUri) 330 let amount = info.amount 331 let amountStr = amount.formatted(specs: nil, 332 isNegative: false, 333 scope: common.scopes[0]) 334 let amountValue = amount.valueStr 335 let creditPaytoUris = info.creditPaytoUris 336 let validDetails = validDetails(creditPaytoUris) 337 if validDetails.count > 0 { 338 let countPaytos = creditPaytoUris.count 339 let account = validDetails[accountID] 340 341 Text("You need to verify having control over the bank account for the deposit.") 342 .bold() 343 .fixedSize(horizontal: false, vertical: true) // wrap in scrollview 344 .multilineTextAlignment(.leading) // otherwise 345 .listRowSeparator(.hidden) 346 347 if countPaytos > 1 { 348 if countPaytos > 3 { // too many for SegmentControl 349 AccountPicker(title: String(localized: "Bank"), 350 value: $accountID, 351 accountDetails: validDetails, 352 action: redraw) 353 .listRowSeparator(.hidden) 354 .pickerStyle(.menu) 355 } else { 356 SegmentControl(value: $accountID, 357 accountDetails: validDetails, 358 action: redraw) 359 .listRowSeparator(.hidden) 360 } 361 } else if let creditPaytoUri = creditPaytoUris.first { 362 if let bankName = account.bankLabel { 363 Text(bankName + ": " + amountStr.0) 364 .accessibilityLabel(bankName + ": " + amountStr.1) 365 // } else { 366 // Text(amountStr) 367 } 368 } 369 let payto = PayTo(account.paytoUri) 370 if let receiverStr = payto.receiver { 371 let wireDetails = ManualDetailsWireV(stack: stack.push(), 372 reservePub: info.accountPub, 373 receiverStr: receiverStr, 374 receiverZip: payto.postalCode, 375 receiverTown: payto.town, 376 iban: payto.iban, 377 xTaler: payto.xTaler ?? EMPTYSTRING, 378 amountValue: amountValue, 379 amountStr: amountStr, 380 obtainStr: nil, // only for withdrawal 381 debitIBAN: debitPayTo.iban, 382 account: account) 383 NavigationLink(destination: wireDetails) { 384 Text(minimalistic ? "Instructions" 385 : "Wire transfer instructions") 386 .talerFont(.title3) 387 } 388 } 389 } 390 } 391 } // body 392 } 393 // MARK: - 394 struct PendingWithdrawalDetails: View { 395 let stack: CallStack 396 @Binding var transaction: TalerTransaction 397 let details: WithdrawalTransactionDetails 398 399 var body: some View { 400 let common = transaction.common 401 if transaction.isPendingKYC { 402 if let kycUrl = common.kycUrl { 403 KYCbutton(kycUrl: common.kycUrl) 404 } else { 405 Text("Legitimization procedure required", comment: "KYC") 406 } 407 } 408 let withdrawalDetails = details.withdrawalDetails 409 switch withdrawalDetails.type { 410 case .manual: // "Make a wire transfer of \(amount) to" 411 ManualDetailsV(stack: stack.push(), common: common, details: withdrawalDetails) 412 413 case .bankIntegrated: // "Authorize now" (with bank) 414 if !transaction.isPendingKYC { // cannot authorize if KYC is needed first 415 let confirmed = withdrawalDetails.confirmed ?? false 416 if !confirmed { 417 if let confirmationUrl = withdrawalDetails.bankConfirmationUrl { 418 if let destination = URL(string: confirmationUrl) { 419 LinkButton(destination: destination, 420 hintTitle: String(localized: "The bank is waiting for your authorization."), 421 buttonTitle: String(localized: "Authorize now"), 422 a11yHint: String(localized: "Will go to bank website to authorize this withdrawal.", comment: "a11y"), 423 badge: CONFIRM_BANK) 424 } } } } 425 @unknown default: 426 ErrorView(title: "Unknown withdrawal type", // should not happen, so no L10N 427 message: withdrawalDetails.type.rawValue, 428 copyable: true) { 429 dismissTop(stack.push()) 430 } 431 } // switch 432 } 433 } 434 // MARK: - 435 struct TypeDetail: View { 436 let stack: CallStack 437 let scope: ScopeInfo? 438 @Binding var transaction: TalerTransaction 439 let hasDone: Bool 440 @Environment(\.colorScheme) private var colorScheme 441 @Environment(\.colorSchemeContrast) private var colorSchemeContrast 442 @AppStorage("minimalistic") var minimalistic: Bool = false 443 @State private var rotationEnabled = true 444 @State private var ignoreAccept: Bool = false // accept could be set by OIM to trigger accept 445 446 func refreshFee(input: Amount, output: Amount) -> Amount? { 447 do { 448 let fee = try input - output 449 return fee 450 } catch { 451 452 } 453 return nil 454 } 455 456 func abortedHint(_ delay: RelativeTime?) -> String? { 457 if let delay { 458 if let microseconds = try? delay.microseconds() { 459 let days = microseconds / (24 * 3600 * 1000 * 1000) 460 if days > 0 { 461 return String(days) 462 } 463 } 464 return "a few" 465 } 466 return nil 467 } 468 469 var body: some View { 470 let common = transaction.common 471 let pending = transaction.isPending 472 let dialog = transaction.isDialog 473 Group { 474 switch transaction { 475 case .dummy(_): 476 let title = EMPTYSTRING 477 Text(title) 478 .talerFont(.body) 479 RotatingTaler(size: 100, progress: true, rotationEnabled: $rotationEnabled) 480 .frame(maxWidth: .infinity, alignment: .center) 481 // has its own accessibilityLabel 482 case .withdrawal(let withdrawalTransaction): Group { 483 let details = withdrawalTransaction.details 484 if common.isAborted && details.withdrawalDetails.type == .manual { 485 if let dayStr = abortedHint(details.withdrawalDetails.reserveClosingDelay) { 486 Text("The withdrawal was aborted.\nIf you have already sent money to the payment service, it will wire it back in \(dayStr) days.") 487 .talerFont(.callout) 488 } 489 } 490 if pending { 491 PendingWithdrawalDetails(stack: stack.push(), 492 transaction: $transaction, 493 details: details) 494 } // ManualDetails or Confirm now (with bank) 495 ThreeAmountsSheet(stack: stack.push(), 496 scope: scope, 497 common: common, 498 topAbbrev: String(localized: "Chosen:", comment: "mini"), 499 topTitle: String(localized: "Chosen amount to withdraw:"), 500 baseURL: details.exchangeBaseUrl, 501 noFees: nil, // TODO: noFees 502 feeIsNegative: true, 503 large: false, 504 summary: nil, 505 merchant: nil) 506 } 507 case .deposit(let depositTransaction): Group { 508 if transaction.common.isPendingKYCauth { 509 KYCauth(stack: stack.push(), common: transaction.common) 510 } else if transaction.isPendingKYC { 511 KYCbutton(kycUrl: common.kycUrl) 512 } 513 ThreeAmountsSheet(stack: stack.push(), 514 scope: scope, 515 common: common, 516 topAbbrev: String(localized: "Deposit:", comment: "mini"), 517 topTitle: String(localized: "Amount to deposit:"), 518 baseURL: nil, // TODO: baseURL 519 noFees: nil, // TODO: noFees 520 feeIsNegative: false, 521 large: true, 522 summary: nil, 523 merchant: nil) 524 } 525 case .payment(let paymentTransaction): Group { 526 let details = paymentTransaction.details 527 if common.isDialog { 528 let firstScope = common.scopes.first 529 PaymentView2(stack: stack.push(), 530 paid: false, 531 raw: common.amountRaw, 532 effective: common.amountEffective, 533 firstScope: firstScope, 534 baseURL: nil, 535 summary: details.info.summary, 536 merchant: details.info.merchant.name, 537 products: details.info.products, 538 balanceDetails: nil) 539 540 } else { 541 TransactionPayDetailV(paymentTx: paymentTransaction) 542 ThreeAmountsSheet(stack: stack.push(), 543 scope: scope, 544 common: common, 545 topAbbrev: String(localized: "Price:", comment: "mini"), 546 topTitle: String(localized: "Price (net):"), 547 baseURL: nil, // TODO: baseURL 548 noFees: nil, // TODO: noFees 549 feeIsNegative: false, 550 large: true, 551 summary: details.info.summary, 552 merchant: details.info.merchant.name) 553 } 554 } 555 case .refund(let refundTransaction): Group { 556 let details = refundTransaction.details // TODO: more details 557 ThreeAmountsSheet(stack: stack.push(), 558 scope: scope, 559 common: common, 560 topAbbrev: String(localized: "Refunded:", comment: "mini"), 561 topTitle: String(localized: "Refunded amount:"), 562 baseURL: nil, // TODO: baseURL 563 noFees: nil, // TODO: noFees 564 feeIsNegative: true, 565 large: true, 566 summary: details.info?.summary, 567 merchant: details.info?.merchant.name) 568 } 569 case .refresh(let refreshTransaction): Group { 570 let labelColor = WalletColors().labelColor 571 let errorColor = WalletColors().errorColor 572 let details = refreshTransaction.details 573 Section { 574 Text(details.refreshReason.localizedRefreshReason) 575 .talerFont(.title) 576 let input = details.refreshInputAmount 577 AmountRowV(stack: stack.push(), 578 title: minimalistic ? "Refreshed:" : "Refreshed amount:", 579 amount: input, 580 scope: scope, 581 isNegative: nil, 582 color: labelColor, 583 large: true) 584 if let fee = refreshFee(input: input, output: details.refreshOutputAmount) { 585 AmountRowV(stack: stack.push(), 586 title: minimalistic ? "Fee:" : "Refreshed fee:", 587 amount: fee, 588 scope: scope, 589 isNegative: fee.isZero ? nil : true, 590 color: labelColor, 591 large: true) 592 } 593 if let error = details.error { 594 HStack { 595 VStack(alignment: .leading) { 596 Text(error.hint) 597 .talerFont(.headline) 598 .foregroundColor(errorColor) 599 .listRowSeparator(.hidden) 600 if let stack = error.stack { 601 Text(stack) 602 .talerFont(.body) 603 .foregroundColor(errorColor) 604 .listRowSeparator(.hidden) 605 } 606 } 607 let stackStr = error.stack ?? EMPTYSTRING 608 let errorStr = error.hint + "\n" + stackStr 609 CopyButton(textToCopy: errorStr, vertical: true) 610 .accessibilityLabel(Text("Copy the error", comment: "a11y")) 611 .disabled(false) 612 } 613 } 614 } 615 } 616 617 case .peer2peer(let p2pTransaction): Group { 618 let details = p2pTransaction.details 619 if transaction.isPendingKYC { 620 KYCbutton(kycUrl: common.kycUrl) 621 } 622 if !transaction.isDone { 623 ExpiresView(expiration: details.info.expiration) 624 } 625 if transaction.isRcvCoins && common.isDialog { 626 PeerPushCreditView(stack: stack.push(), 627 raw: common.amountRaw, 628 effective: common.amountEffective, 629 scope: scope, 630 summary: details.info.summary) 631 PeerPushCreditAccept(stack: stack.push(), url: nil, 632 transactionId: common.transactionId, 633 accept: $ignoreAccept) 634 } else if transaction.isPayInvoice && common.isDialog { 635 PeerPullDebitView(stack: stack.push(), 636 raw: common.amountRaw, 637 effective: common.amountEffective, 638 scope: scope, 639 summary: details.info.summary) 640 PeerPullDebitConfirm(stack: stack.push(), url: nil, 641 transactionId: common.transactionId) 642 } else { 643 // 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 644 if pending { 645 if transaction.isPendingReady { 646 QRCodeDetails(transaction: transaction) 647 if hasDone { 648 Text("QR code and link can also be scanned or copied / shared from Transactions later.") 649 .multilineTextAlignment(.leading) 650 .talerFont(.subheadline) 651 .padding(.top) 652 } 653 } else { 654 Text("This transaction is not yet ready...") 655 .multilineTextAlignment(.leading) 656 .talerFont(.subheadline) 657 } 658 } 659 let colon = ":" 660 let localizedType = transaction.isDone ? transaction.localizedTypePast 661 : transaction.localizedType 662 ThreeAmountsSheet(stack: stack.push(), 663 scope: scope, 664 common: common, 665 topAbbrev: localizedType + colon, 666 topTitle: localizedType + colon, 667 baseURL: details.exchangeBaseUrl, 668 noFees: nil, // TODO: noFees 669 feeIsNegative: true, 670 large: false, 671 summary: details.info.summary, 672 merchant: nil) 673 } // else 674 } // p2p 675 676 case .recoup(let recoupTransaction): Group { 677 let details = recoupTransaction.details // TODO: more details 678 ThreeAmountsSheet(stack: stack.push(), 679 scope: scope, 680 common: common, 681 topAbbrev: String(localized: "Recoup:", comment: "mini"), 682 topTitle: String(localized: "Recoup:"), 683 baseURL: nil, 684 noFees: nil, 685 feeIsNegative: nil, 686 large: true, // TODO: baseURL, noFees 687 summary: nil, 688 merchant: nil) 689 } 690 case .denomLoss(let denomLossTransaction): Group { 691 let details = denomLossTransaction.details // TODO: more details 692 ThreeAmountsSheet(stack: stack.push(), 693 scope: scope, 694 common: common, 695 topAbbrev: String(localized: "Lost:", comment: "mini"), 696 topTitle: String(localized: "Money lost:"), 697 baseURL: details.exchangeBaseUrl, 698 noFees: nil, 699 feeIsNegative: nil, 700 large: true, // TODO: baseURL, noFees 701 summary: details.lossEventType.rawValue, 702 merchant: nil) 703 } 704 } // switch 705 } // Group 706 } 707 } 708 // MARK: - 709 struct QRCodeDetails: View { 710 var transaction : TalerTransaction 711 var body: some View { 712 let details = transaction.detailsToShow() 713 let keys = details.keys 714 if keys.contains(TALERURI) { 715 if let talerURI = details[TALERURI] { 716 if talerURI.count > 10 { 717 QRCodeDetailView(talerURI: talerURI, 718 talerCopyShare: talerURI, 719 incoming: transaction.isP2pIncoming, 720 amount: transaction.common.amountRaw, 721 scope: transaction.common.scopes.first) 722 // scopes shouldn't (- but might) be nil! 723 } 724 } 725 } else if keys.contains(EXCHANGEBASEURL) { 726 if let baseURL = details[EXCHANGEBASEURL] { 727 Text("from \(baseURL.trimURL)", comment: "baseURL") 728 .talerFont(.title2) 729 .padding(.bottom) 730 } 731 } 732 } 733 } 734 } // TransactionSummaryV 735 // MARK: - 736 #if DEBUG 737 //struct TransactionSummary_Previews: PreviewProvider { 738 // static func deleteTransactionDummy(transactionId: String) async throws {} 739 // static func doneActionDummy() {} 740 // static var withdrawal = TalerTransaction(incoming: true, 741 // pending: true, 742 // id: "some withdrawal ID", 743 // time: Timestamp(from: 1_666_000_000_000)) 744 // static var payment = TalerTransaction(incoming: false, 745 // pending: false, 746 // id: "some payment ID", 747 // time: Timestamp(from: 1_666_666_000_000)) 748 // static func reloadActionDummy(transactionId: String) async -> TalerTransaction { return withdrawal } 749 // static var previews: some View { 750 // Group { 751 // TransactionSummaryV(transaction: withdrawal, reloadAction: reloadActionDummy, doneAction: doneActionDummy) 752 // TransactionSummaryV(transaction: payment, reloadAction: reloadActionDummy, deleteAction: deleteTransactionDummy) 753 // } 754 // } 755 //} 756 #endif