taler-ios

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

TransactionSummaryList.swift (30133B)


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