taler-ios

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

TransactionTypeDetail.swift (15398B)


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