taler-ios

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

TransactionTypeDetail.swift (16113B)


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