taler-ios

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

TransactionTypeDetail.swift (14608B)


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