taler-ios

iOS apps for GNU Taler (wallet)
Log | Files | Refs | README | LICENSE

TransactionSummaryList.swift (29744B)


      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 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 MerchantHeader: View {
     29     let terms: MerchantContractTerms?
     30 
     31     func summary(_ terms: MerchantContractTerms) -> String {
     32         if let i18nDict = terms.summaryI18n {
     33             if !i18nDict.isEmpty {
     34                 for code in Locale.preferredLanguageCodes {
     35                     if let descI18n = i18nDict[code] {
     36                         return descI18n
     37                     }
     38                 }
     39             }
     40         }
     41         return terms.summary
     42     }
     43 
     44     var body: some View {
     45         if let terms {
     46             Section {
     47                 Text(summary(terms))
     48                     .talerFont(.title3)
     49             } header: {
     50                 HStack {
     51                     Spacer()
     52                     VStack(alignment: .center) {
     53                         if let imageBase64 = terms.merchant.logo {
     54                             if let url = NSURL(string: imageBase64) {
     55                                 if let data = NSData(contentsOf: url as URL) {
     56                                     if let uiImage = UIImage(data: data as Data) {
     57                                         Image(uiImage: uiImage)
     58                                             .resizable()
     59                                             .aspectRatio(contentMode: .fit)
     60                                             .frame(maxHeight: 60)
     61                                     }
     62                                 }
     63                             }
     64                         } else {
     65 #if TALER_NIGHTLY
     66                             let imageName = if #available(iOS 17.0, *) { MERCHANT17 } else { MERCHANT14 }
     67                             Image(systemName: imageName)
     68                                 .resizable()
     69                                 .frame(width: 44, height: 44)
     70 #endif
     71                         }
     72                         let merchant = terms.merchant.name
     73                         Text(merchant)
     74                             .talerFont(.title3)
     75                     }.foregroundStyle(Color(.primary))
     76                     Spacer()
     77                 }
     78             }
     79         }
     80     }
     81 }
     82 // MARK: -
     83 struct PaymentTransactionView: View {
     84     private let symLog = SymLogV(0)
     85     let stack: CallStack
     86     let common: TransactionCommon
     87     let paymentTransaction: PaymentTransaction
     88     @Binding var scope: ScopeInfo?
     89     @Binding var effective: Amount?
     90     @Binding var payNow: Bool
     91     @Binding var selectedChoice: Int?
     92 
     93     @EnvironmentObject private var controller: Controller
     94     @EnvironmentObject private var model: WalletModel
     95 
     96     func summary(_ info: OrderShortInfo?) -> String? {
     97         if let i18nDict = info?.summary_i18n {
     98             if !i18nDict.isEmpty {
     99                 for code in Locale.preferredLanguageCodes {
    100                     if let i18n = i18nDict[code] {
    101                         return i18n
    102                     }
    103                 }
    104             }
    105         }
    106         if let summary = info?.summary {
    107             return summary
    108         }
    109         return String(localized: "No summary", comment: "OrderShortInfo.summary")
    110     }
    111 
    112     func selectIndex(_ index: Int?, of choices: [ChoiceTriple]) -> Int? {
    113         let index1 = index ?? 0
    114         let select = index1 < choices.count ? index1 : 0
    115         let choice = choices[select]
    116         let selectionDetail = choice.0
    117         if selectionDetail.status == .paymentPossible {
    118             selectedChoice = select
    119             effective = selectionDetail.amountEffective
    120             return select
    121         }
    122         return nil
    123     }
    124 
    125     func automaticOrDefault(_ automaticIndex: Int?,_ defaultChoiceIndex: Int?, of choices: [ChoiceTriple]) {
    126         if let automaticIndex {
    127             // Pay Automatically - usually with a subscription token
    128             if let selectedAutomatic = selectIndex(automaticIndex, of: choices) {
    129                 if selectedAutomatic == automaticIndex {
    130                     // TODO: check if automatic choice does NOT have an amount
    131                     // we definitely don't want to pay MONEY automatically, only tokens
    132                     payNow = true
    133                     return
    134                 } else if let defaultChoiceIndex {
    135                     if selectedAutomatic == defaultChoiceIndex { return }
    136                     // else fall thru and select defaultChoiceIndex
    137                 } else {
    138                     return      // there is no default - keep selectedAutomatic
    139                 }
    140             }
    141         }
    142         let _ = selectIndex(defaultChoiceIndex, of: choices)
    143     }
    144 
    145     var body: some View {
    146 #if PRINT_CHANGES
    147         let _ = Self._printChanges()
    148         let _ = symLog.vlog()
    149 #endif
    150         let details = paymentTransaction.details
    151         if common.isDialog {        // show payment confirmation dialog
    152             MerchantHeader(terms: details.contractTerms)
    153 
    154             if let choicesForPayment = controller.choicesForPayment {
    155                 if let (choices, showHeader) = choicesForPayment.choiceTriple() {
    156                     let hasAutomatic = choicesForPayment.automaticExecution ?? false
    157                     let automaticIndex = hasAutomatic ? choicesForPayment.automaticExecutableIndex : nil
    158                     ChoicesView(stack: stack.push(),
    159                          choiceTriple: choices,
    160                            showHeader: showHeader,
    161                        automaticIndex: automaticIndex,
    162                        selectedChoice: $selectedChoice)
    163                     .task {
    164                         automaticOrDefault(automaticIndex, choicesForPayment.defaultChoiceIndex, of: choices)
    165                     }
    166                     .onChange(of: selectedChoice) { newValue in
    167                         if let newValue, newValue < choices.count {
    168                             let newChoice = choices[newValue]
    169                             effective = newChoice.0.amountEffective
    170                             scope = newChoice.0.scopeInfo
    171                         } else {
    172                             effective = nil
    173                             scope = nil
    174                         }
    175                     }
    176 
    177                     if let selectedChoice {
    178                         let choice = choices[selectedChoice]
    179                         let selectionDetail: ChoiceSelectionDetail = choice.0
    180 //                        let contractChoice: ContractChoice = choice.1
    181 
    182                         PaymentView2(stack: stack.push(),           // TODO: details.info.merchant.name
    183                                       paid: false,
    184                                        raw: selectionDetail.amountRaw,
    185                                  effective: effective,
    186                                 firstScope: scope,
    187                                    baseURL: nil,
    188                                    summary: summary(details.info),
    189                                   products: details.info?.products ?? [],
    190                             balanceDetails: selectionDetail.balanceDetails)
    191                     }
    192                 }
    193             }
    194         } else { // show finished payment
    195             TransactionPayDetailV(paymentTx: paymentTransaction)    // TODO: details.info.merchant.name
    196             ThreeAmountsSheet(stack: stack.push(),
    197                               scope: scope,
    198                              common: common,
    199                           topAbbrev: String(localized: "Price:", comment: "mini"),
    200                            topTitle: String(localized: "Price (net):"),
    201                             baseURL: nil,               // TODO: baseURL
    202                              noFees: nil,               // TODO: noFees
    203                       feeIsNegative: false,
    204                               large: true,
    205                             summary: details.info?.summary ?? EMPTYSTRING)
    206         } // show finished payment
    207     } // body
    208 } // PaymentTransactionView
    209 // MARK: -
    210 struct TransactionSummaryList: View {
    211     private let symLog = SymLogV(0)
    212     let stack: CallStack
    213     let transactionId: String
    214     @Binding var talerTX: TalerTransaction
    215     let navTitle: String?
    216     let hasDone: Bool
    217     let showDone: TalerButtonStyleType?
    218     let url: URL?           // the scanned talerURL from PaymentView
    219     let withActions: Bool
    220 
    221     @EnvironmentObject private var controller: Controller
    222     @EnvironmentObject private var model: WalletModel
    223     @Environment(\.colorScheme) private var colorScheme
    224     @Environment(\.colorSchemeContrast) private var colorSchemeContrast
    225     @Environment(\.dismiss) var dismiss     // call dismiss() to pop back
    226     @AppStorage("minimalistic") var minimalistic: Bool = false
    227     @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic
    228     @AppStorage("preferredColorScheme") var preferredColorScheme: Int = 0
    229 #if DEBUG
    230     @AppStorage("developerMode") var developerMode: Bool = true
    231 #else
    232     @AppStorage("developerMode") var developerMode: Bool = false
    233 #endif
    234 
    235     @State private var currencyInfo: CurrencyInfo = CurrencyInfo.zero(UNKNOWN)
    236     @State private var isCopied: Bool = false
    237     @State private var ignoreThis: Bool = false
    238     @State private var didDelete: Bool = false
    239     @State var jsonTransaction: String = EMPTYSTRING
    240     @State var viewId = UUID()
    241     @State private var selectedChoice: Int? = nil
    242     @State private var effective: Amount? = nil
    243     @State private var scope: ScopeInfo? = nil
    244     @State private var payNow: Bool = false
    245     @Namespace var topID
    246 
    247     func loadTransaction() async {
    248         if let reloadedTransaction = try? await model.getTransactionById(transactionId,
    249                                                     includeContractTerms: true, viewHandles: false) {
    250             symLog.log("reloaded \(reloadedTransaction.localizedType): \(reloadedTransaction.common.txState.major)")
    251             withAnimation {
    252                 talerTX = reloadedTransaction;
    253                 scope = reloadedTransaction.common.scopes.first
    254                 viewId = UUID()      // redraw
    255             }
    256             if developerMode {
    257                 if let json = try? await model.jsonTransactionById(transactionId,
    258                                               includeContractTerms: true, viewHandles: false) {
    259                     jsonTransaction = json
    260                 } else {
    261                     jsonTransaction = EMPTYSTRING
    262                 }
    263             }
    264         } else {
    265             withAnimation{ talerTX = TalerTransaction(dummyCurrency: DEMOCURRENCY); viewId = UUID() }
    266             jsonTransaction = EMPTYSTRING
    267         }
    268     }
    269 
    270     private func payTransaction() async {
    271         if let confirmPayResult = try? await model.confirmPay(transactionId,
    272                                                               choiceIndex: selectedChoice) {
    273 //          symLog.log(confirmPayResult as Any)
    274             if confirmPayResult.type == "done" {
    275                 if let url {
    276                     controller.removeURL(url)
    277                 }
    278             } else {
    279                 if let url {
    280                     controller.removeURL(url)    // TODO: pending might fail - in which case we might want to try again
    281                 }
    282             }
    283         }
    284     }
    285 
    286     @MainActor
    287     @discardableResult
    288     func checkDismiss(_ notification: Notification, _ logStr: String = EMPTYSTRING) -> Bool {
    289         if hasDone {
    290             if let transition = notification.userInfo?[TRANSACTIONTRANSITION] as? TransactionTransition {
    291                 if transition.transactionId == talerTX.common.transactionId {       // is the transition for THIS transaction?
    292                     symLog.log(logStr)
    293                     if talerTX.common.type.isPayment {
    294                         checkReload(notification, logStr)
    295                     } else {
    296                         dismissTop(stack.push())        // if this view is in a sheet then dissmiss the sheet
    297                         return true
    298                     }
    299                 }
    300             }
    301         } else { // no sheet but the details view -> reload
    302             checkReload(notification, logStr)
    303         }
    304         return false
    305     }
    306 
    307     @MainActor
    308     private func dismiss(_ stack: CallStack) {
    309         if hasDone {        // if this view is in a sheet then dissmiss the whole sheet
    310             dismissTop(stack.push())
    311         } else {            // on a NavigationStack just pop
    312             dismiss()
    313         }
    314     }
    315 
    316     func checkReload(_ notification: Notification, _ logStr: String = EMPTYSTRING) {
    317         if let transition = notification.userInfo?[TRANSACTIONTRANSITION] as? TransactionTransition {
    318             if transition.transactionId == transactionId {       // is the transition for THIS transaction?
    319                 let newMajor = transition.newTxState.major
    320                 Task { // runs on MainActor
    321                        // flush the screen first, then reload
    322                     withAnimation { talerTX = TalerTransaction(dummyCurrency: DEMOCURRENCY); viewId = UUID() }
    323                     symLog.log("newState: \(newMajor), reloading transaction")
    324                     if newMajor != .none {              // don't reload after delete
    325                         await loadTransaction()
    326                     }
    327                 }
    328             }
    329         } else { // Yikes - should never happen
    330 // TODO:      logger.warning("Can't get notification.userInfo as TransactionTransition")
    331             symLog.log(notification.userInfo as Any)
    332         }
    333     }
    334 
    335     func localizedState(_ txState: TransactionState) -> String {
    336         let major = txState.major
    337         if major != .failed {
    338             if let minorState = txState.minor {
    339                 if developerMode { return minorState.localizedDbgState }
    340 //                if talerTX.isPayment {
    341 //                  return String(localized: "Payment", comment: "TxMajorState heading")
    342 //                }
    343                 return minorState.localizedState ?? major.localizedState
    344             }
    345         }
    346         return major.localizedState
    347     }
    348 
    349     @ViewBuilder
    350     func dateAndStatus(_ common: TransactionCommon) -> some View {
    351         let (dateString, date) = TalerDater.dateString(common.timestamp, minimalistic)
    352         let a11yDate = TalerDater.accessibilityDate(date) ?? dateString
    353         Text(dateString)
    354             .talerFont(.body)
    355             .accessibilityLabel(a11yDate)
    356             .foregroundColor(WalletColors().secondary(colorScheme, colorSchemeContrast))
    357             .id(topID)
    358         let state = localizedState(common.txState)
    359         let statusT = Text(state)
    360             .multilineTextAlignment(.trailing)
    361         let imageT = Text(common.type.icon())
    362             .accessibilityHidden(true)
    363         HStack(alignment: .center, spacing: HSPACING) {
    364             imageT
    365             Spacer(minLength: 0)
    366             statusT
    367         }   // TODO: a11y for tx icon
    368         if developerMode {
    369             if !jsonTransaction.isEmpty {
    370                 CopyButton(textToCopy: jsonTransaction, isCopied: $isCopied, title: "Copy JSON")
    371             }
    372         }
    373     }
    374 
    375     @ViewBuilder
    376     func suspendResume(_ common: TransactionCommon) -> some View {
    377         if talerTX.isSuspendable {
    378             TransactionButton(transactionId: common.transactionId,
    379                                     command: .suspend,
    380                                     warning: nil,
    381                                  didExecute: $ignoreThis,
    382                                      action: model.suspendTransaction)
    383             .listRowSeparator(.hidden)
    384         }
    385         if talerTX.isResumable {
    386             TransactionButton(transactionId: common.transactionId,
    387                                     command: .resume,
    388                                     warning: nil,
    389                                  didExecute: $ignoreThis,
    390                                      action: model.resumeTransaction)
    391             .listRowSeparator(.hidden)
    392         }
    393     }
    394 
    395     @ViewBuilder
    396     func abortFailDelete(_ common: TransactionCommon) -> some View {
    397         if talerTX.isAbortable {
    398             let warning = String(localized: "Are you sure you want to abort this transaction?")
    399             TransactionButton(transactionId: common.transactionId,
    400                                     command: .abort,
    401                                     warning: warning,
    402                                  didExecute: $ignoreThis,
    403                                      action: model.abortTransaction)
    404         } // Abort button
    405         if talerTX.isFailable {
    406             let warning = String(localized: "Are you sure you want to abandon this transaction?")
    407             TransactionButton(transactionId: common.transactionId,
    408                                     command: .fail,
    409                                     warning: warning,
    410                                  didExecute: $ignoreThis,
    411                                      action: model.failTransaction)
    412         } // Fail button
    413         if talerTX.isDeleteable {
    414             let warning = String(localized: "Are you sure you want to delete this transaction?")
    415             TransactionButton(transactionId: common.transactionId,
    416                                     command: .delete,
    417                                     warning: warning,
    418                                  didExecute: $didDelete,
    419                                      action: model.deleteTransaction)
    420             .onChange(of: didDelete) { wasDeleted in
    421                 if wasDeleted {
    422                     symLog.log("wasDeleted -> dismiss view")
    423                     dismiss(stack)
    424                 }
    425             }
    426         } // Delete button
    427     }
    428 
    429     var body: some View {
    430 #if PRINT_CHANGES
    431         let _ = Self._printChanges()
    432         let _ = symLog.vlog()       // just to get the # to compare it with .onAppear & onDisappear
    433 #endif
    434         let common = talerTX.common
    435 //        let scope = common.scopes.first                                     // might be nil if scopes == []
    436         let locale = TalerDater.shared.locale
    437         let isPaying = talerTX.isPayment && talerTX.isDialog
    438         let navTitle2 = talerTX.isDone ? talerTX.localizedTypePast
    439                             : isPaying ? String(localized: "Confirm Payment", comment:"pay merchant navTitle")
    440                                        : talerTX.localizedType
    441         Group {
    442           if common.type != .dummy && transactionId == common.transactionId {
    443             let list = List {
    444                 if developerMode && withActions { suspendResume(common) }
    445                 if !isPaying {
    446                     Section {
    447                         VStack(alignment: .leading) {
    448                             dateAndStatus(common)
    449                                 .listRowSeparator(.hidden)
    450                                 .talerFont(.title)
    451                         }.overlay {
    452                             if common.isWorking {
    453                                 RotatingTaler(size: 80, progress: true, once: false,
    454                                    rotationEnabled: Binding.constant(true))
    455                             }
    456                         }
    457                     }
    458                 }
    459                 TransactionTypeDetail(stack: stack.push(),
    460                                 transaction: $talerTX,
    461                                      payNow: $payNow,
    462                              selectedChoice: $selectedChoice,
    463                                       scope: $scope,
    464                                   effective: $effective,
    465                                     hasDone: hasDone)
    466 
    467                 // TODO: Retry Countdown, Retry Now button
    468 //                if talerTX.isRetryable, let retryAction {
    469 //                    TransactionButton(transactionId: common.transactionId, command: .retry,
    470 //                                      warning: nil, action: abortAction)
    471 //                } // Retry button
    472                 if withActions { abortFailDelete(common) }
    473             }.id(viewId)    // change viewId to enforce a draw update
    474             .listStyle(myListStyle.style).anyView
    475             .background(FullBackground())
    476             .navigationBarBackButtonHidden(hasDone)
    477             .interactiveDismissDisabled(hasDone)           // can only use "Done" button to dismiss
    478             .safeAreaInset(edge: .bottom) {
    479                 if isPaying, case .payment(let paymentTransaction) = talerTX {
    480                     let details = paymentTransaction.details
    481                     if let effective, let url, let terms = details.contractTerms {
    482                         let formatted = effective.formatted(currencyInfo)
    483                         PaySafeArea(symLog: symLog,
    484                                      stack: stack.push(),
    485                                      terms: terms,
    486                               amountString: formatted.0,
    487                                 amountA11y: formatted.1,
    488                                     payNow: $payNow)
    489                         .onChange(of: payNow) { payNow2 in
    490                             if payNow2 {
    491                                 Task {
    492                                     payNow = false
    493                                     await payTransaction()
    494                                 }
    495                             }
    496                         }
    497                     } else {
    498                         Button("Cancel") { dismissTop(stack.push()) }
    499                             .buttonStyle(TalerButtonStyle(type: .bordered))
    500                             .padding(.horizontal)
    501                     } // Cancel
    502                 } else if let showDone {
    503                     Button("Done") { dismissTop(stack.push()) }
    504                         .buttonStyle(TalerButtonStyle(type: showDone))
    505                         .padding(.horizontal)
    506                 }
    507             }
    508             .onNotification(.TransactionExpired) { notification in
    509                 // TODO: Alert user that this tx just expired
    510                 if checkDismiss(notification, "newTxState.major == expired  => dismiss sheet") {
    511         // TODO:                  logger.info("newTxState.major == expired  => dismiss sheet")
    512                 }
    513             }
    514             .onNotification(.TransactionDone) { notification in
    515                 checkDismiss(notification, "newTxState.major == done  => dismiss sheet")
    516             }
    517             .onNotification(.DismissSheet) { notification in
    518                 checkDismiss(notification, "exchangeWaitReserve or withdrawCoins  => dismiss sheet")
    519             }
    520             .onNotification(.PendingReady) { notification in
    521                 checkReload(notification, "pending ready ==> reload for talerURI")
    522             }
    523             .onNotification(.TransactionStateTransition) { notification in
    524                 if !didDelete {
    525                     checkReload(notification, "some transition ==> reload")
    526                 }
    527             }
    528             .navigationTitle(navTitle ?? navTitle2)
    529 
    530             if #available(iOS 17.0, *) {
    531                 list
    532                     .scrollContentBackground(preferredColorScheme == 3 ? .hidden : .visible)
    533                     .toolbarTitleDisplayMode(.inlineLarge)
    534             } else {
    535                 list
    536             }
    537           } else {
    538             Color.clear
    539                 .frame(maxWidth: .infinity, maxHeight: .infinity)
    540                 .task {
    541                     symLog.log("task - load transaction")
    542                     await loadTransaction()
    543                 }
    544           } // else
    545         } // Group
    546         .onChange(of: scope) { newVal in
    547             if let newVal {
    548                 currencyInfo = controller.info(for: newVal) ?? CurrencyInfo.zero(UNKNOWN)
    549             }
    550         }
    551         .onAppear {
    552             symLog.log("onAppear")
    553             DebugViewC.shared.setViewID(VIEW_TRANSACTIONSUMMARY, stack: stack.push())
    554         }
    555         .onDisappear {
    556             symLog.log("onDisappear")
    557         }
    558     }
    559 } // TransactionSummaryList
    560     // MARK: -
    561     struct KYCbutton: View {
    562         let kycUrl: String?
    563 
    564         var body: some View {
    565             if let kycUrl {
    566                 if let destination = URL(string: kycUrl) {
    567                     LinkButton(destination: destination,
    568                                hintTitle: String(localized: "You need to pass a legitimization procedure.", comment: "KYC"),
    569                                buttonTitle: String(localized: "Open legitimization website", comment: "KYC"),
    570                                   a11yHint: String(localized: "Will go to legitimization website to permit this withdrawal.", comment: "a11y"),
    571                                      badge: NEEDS_KYC)
    572                 }
    573             }
    574         }
    575     }
    576     // MARK: -
    577     struct PendingWithdrawalDetails: View {
    578         let stack: CallStack
    579         @Binding var transaction: TalerTransaction
    580         let details: WithdrawalTransactionDetails
    581 
    582         var body: some View {
    583             let common = transaction.common
    584             if transaction.isPendingKYC {
    585                 if let kycUrl = common.kycUrl {
    586                     KYCbutton(kycUrl: common.kycUrl)
    587                 } else {
    588                     Text("Legitimization procedure required", comment: "KYC")
    589                 }
    590             }
    591             let withdrawalDetails = details.withdrawalDetails
    592             switch withdrawalDetails.type {
    593                 case .manual:               // "Make a wire transfer of \(amount) to"
    594                     ManualDetailsV(stack: stack.push(), common: common, details: withdrawalDetails)
    595 
    596                 case .bankIntegrated:       // "Authorize now" (with bank)
    597                     if !transaction.isPendingKYC {              // cannot authorize if KYC is needed first
    598                         let confirmed = withdrawalDetails.confirmed ?? false
    599                         if !confirmed {
    600                             if let confirmationUrl = withdrawalDetails.bankConfirmationUrl {
    601                                 if let destination = URL(string: confirmationUrl) {
    602                                     LinkButton(destination: destination,
    603                                                  hintTitle: String(localized: "The bank is waiting for your authorization."),
    604                                                buttonTitle: String(localized: "Authorize now"),
    605                                                   a11yHint: String(localized: "Will go to bank website to authorize this withdrawal.", comment: "a11y"),
    606                                                      badge: CONFIRM_BANK)
    607                     }   }   }   }
    608                 @unknown default:
    609                     ErrorView(stack.push(),
    610                               title: "Unknown withdrawal type",        // should not happen, so no L10N
    611                             message: withdrawalDetails.type.rawValue,
    612                            copyable: true) {
    613                         dismissTop(stack.push())
    614                     }
    615             } // switch
    616         }
    617     }
    618 // MARK: -
    619     struct QRCodeDetails: View {
    620         var transaction : TalerTransaction
    621         var body: some View {
    622             let details = transaction.detailsToShow()
    623             let keys = details.keys
    624             if keys.contains(TALERURI) {
    625                 if let talerURI = details[TALERURI] {
    626                     if talerURI.count > 10 {
    627                         QRCodeDetailView(talerURI: talerURI,
    628                                    talerCopyShare: talerURI,
    629                                          incoming: transaction.isP2pIncoming,
    630                                            amount: transaction.common.amountRaw,
    631                                             scope: transaction.common.scopes.first)
    632                                             // scopes shouldn't (- but might) be nil!
    633                     }
    634                 }
    635             } else if keys.contains(EXCHANGEBASEURL) {
    636                 if let baseURL = details[EXCHANGEBASEURL] {
    637                     Text("from \(baseURL.trimURL)", comment: "baseURL") 
    638                         .talerFont(.title2)
    639                         .padding(.bottom)
    640                 }
    641             }
    642         }
    643     }
    644 // MARK: -
    645 #if DEBUG
    646 //struct TransactionSummary_Previews: PreviewProvider {
    647 //    static func deleteTransactionDummy(transactionId: String) async throws {}
    648 //    static func doneActionDummy() {}
    649 //    static var withdrawal = TalerTransaction(incoming: true,
    650 //                                         pending: true,
    651 //                                              id: "some withdrawal ID",
    652 //                                            time: Timestamp(from: 1_666_000_000_000))
    653 //    static var payment = TalerTransaction(incoming: false,
    654 //                                      pending: false,
    655 //                                           id: "some payment ID",
    656 //                                         time: Timestamp(from: 1_666_666_000_000))
    657 //    static func reloadActionDummy(transactionId: String) async -> TalerTransaction { return withdrawal }
    658 //    static var previews: some View {
    659 //        Group {
    660 //            TransactionSummaryV(transaction: withdrawal, reloadAction: reloadActionDummy, doneAction: doneActionDummy)
    661 //            TransactionSummaryV(transaction: payment, reloadAction: reloadActionDummy)
    662 //        }
    663 //    }
    664 //}
    665 #endif