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