taler-ios

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

TransactionTypeDetail.swift (16739B)


      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     var body: some View {
     60 #if PRINT_CHANGES
     61         let _ = Self._printChanges()
     62         let _ = symLog.vlog()
     63 #endif
     64         let common = transaction.common
     65         let pending = transaction.isPending
     66         let isDialog = transaction.isDialog
     67         Group {
     68             switch transaction {
     69                 case .dummy(_): Group {
     70                     let title = EMPTYSTRING
     71                     Text(title)
     72                         .talerFont(.body)
     73                     RotatingTaler(size: 100, progress: true, once: false, rotationEnabled: $rotationEnabled)
     74                         .frame(maxWidth: .infinity, alignment: .center)
     75                         // has its own accessibilityLabel
     76                 }
     77                 case .withdrawal(let withdrawalTransaction): Group {
     78                     let details = withdrawalTransaction.details
     79                     if common.isAborted && details.withdrawalDetails.type == .manual {
     80                         if let days = abortedHint(details.withdrawalDetails.reserveClosingDelay) {
     81                             let wireBack = days > 0 ? String(localized: "If you have already sent money to the payment service, it will wire it back in \(days) days.")
     82                                                     : String(localized: "If you have already sent money to the payment service, it will wire it back in a few days.")
     83                             Text("The withdrawal was aborted.\n\n\(wireBack)")
     84                                 .talerFont(.callout)
     85                         }
     86                     }
     87                     if pending {
     88                         PendingWithdrawalDetails(stack: stack.push(),
     89                                            transaction: $transaction,
     90                                                details: details)
     91                     } // ManualDetails or Confirm now (with bank)
     92                     ThreeAmountsSheet(stack: stack.push(),
     93                                       scope: scope,
     94                                      common: common,
     95                                   topAbbrev: String(localized: "Chosen:", comment: "mini"),
     96                                    topTitle: String(localized: "Chosen amount to withdraw:"),
     97                                     baseURL: details.exchangeBaseUrl,
     98                                      noFees: nil,               // TODO: noFees
     99                               feeIsNegative: true,
    100                                       large: false,
    101                                     summary: nil)
    102                     if developerMode {
    103                         let withdrawalDetails = details.withdrawalDetails
    104                         if let validDetails = withdrawalDetails.exchangeCreditAccountDetails?.validDetails {
    105                             let validCount = validDetails.count
    106                             if validCount > 0 {
    107                                 ForEach(validDetails, id: \.self) { detail in
    108                                     let label = detail.bankLabel ?? EMPTYSTRING
    109                                     if let options = detail.transferOptions, !options.isEmpty {
    110                                         ForEach(options, id: \.self) { option in
    111                                             if let uri = option.uri {
    112                                                 CopyButton(uri, title: "Copy \(label) uri")
    113                                             } else if let payto = option.paytoUri {
    114                                                 CopyButton(payto, title: "Copy \(label) payto")
    115                                             }
    116                     }   }   }   }   }   }
    117                 }
    118                 case .deposit(let depositTransaction): Group {
    119                     if transaction.common.isPendingKYCauth {
    120                         KYCauth(stack: stack.push(), common: common)
    121                     } else if transaction.isPendingKYC {
    122                         KYCbutton(kycUrl: common.kycUrl)
    123                     }
    124                     ThreeAmountsSheet(stack: stack.push(),
    125                                       scope: scope,
    126                                      common: common,
    127                                   topAbbrev: String(localized: "Deposit:", comment: "mini"),
    128                                    topTitle: String(localized: "Amount to deposit:"),
    129                                     baseURL: nil,               // TODO: baseURL
    130                                      noFees: nil,               // TODO: noFees
    131                               feeIsNegative: false,
    132                                       large: true,
    133                                     summary: nil)
    134                 }
    135 
    136                 case .payment(let paymentTransaction):
    137                     PaymentTransactionView(stack: stack.push(),
    138                                           common: common,
    139                               paymentTransaction: paymentTransaction,
    140                                            scope: $scope,
    141                                        effective: $effective,
    142                                           payNow: $payNow,
    143                                   selectedChoice: $selectedChoice)
    144                     .task(id: common.txState.hashValue) {
    145                         let state = common.txState
    146                         if common.isDialog {
    147                             let choicesTuple = controller.choicesTuple
    148                             let choicesForPayment = choicesTuple.1
    149                             if choicesTuple.0 != common.transactionId || choicesForPayment == nil {
    150                                 symLog.log("❗️❗️ task: \(common.txState)")
    151                                 await controller.loadChoicesForPayment(stack.push(),model,
    152                                                                        txId: common.transactionId)
    153                             } else {
    154                                 symLog.log("❗️❗️ choicesForPayment exists, no need for task: \(state)")
    155                             }
    156                         } else {
    157                             symLog.log("❗️❗️ \(common.type.rawValue), no need for task: \(state)")
    158                         }
    159                     }
    160                 case .refund(let refundTransaction): Group {
    161                     let details = refundTransaction.details                 // TODO: more details, details.info?.merchant.name
    162                     ThreeAmountsSheet(stack: stack.push(),
    163                                       scope: scope,
    164                                      common: common,
    165                                   topAbbrev: String(localized: "Refunded:", comment: "mini"),
    166                                    topTitle: String(localized: "Refunded amount:"),
    167                                     baseURL: nil,               // TODO: baseURL
    168                                      noFees: nil,               // TODO: noFees
    169                               feeIsNegative: true,
    170                                       large: true,
    171                                     summary: details.paymentInfo?.summary)
    172                 }
    173                 case .refresh(let refreshTransaction): Group {
    174                     let labelColor = WalletColors().labelColor
    175                     let errorColor = WalletColors().errorColor
    176                     let details = refreshTransaction.details
    177                     Section {
    178                         Text(details.refreshReason.localizedRefreshReason)
    179                             .talerFont(.title)
    180                         let input = details.refreshInputAmount
    181                         AmountRowV(stack: stack.push(),
    182                                    title: minimalistic ? "Refreshed:" : "Refreshed amount:",
    183                                   amount: input,
    184                                    scope: scope,
    185                               isNegative: nil,
    186                                    color: labelColor,
    187                                    large: true)
    188                         if let fee = refreshFee(input: input, output: details.refreshOutputAmount) {
    189                             AmountRowV(stack: stack.push(),
    190                                        title: minimalistic ? "Fee:" : "Refreshed fee:",
    191                                       amount: fee,
    192                                        scope: scope,
    193                                   isNegative: fee.isZero ? nil : true,
    194                                        color: labelColor,
    195                                        large: true)
    196                         }
    197                         if let error = details.error {
    198                             HStack {
    199                                 VStack(alignment: .leading) {
    200                                     Text(error.hint)
    201                                         .talerFont(.headline)
    202                                         .foregroundColor(errorColor)
    203                                         .listRowSeparator(.hidden)
    204                                     if let stack = error.stack {
    205                                         Text(stack)
    206                                             .talerFont(.body)
    207                                             .foregroundColor(errorColor)
    208                                             .listRowSeparator(.hidden)
    209                                     }
    210                                 }
    211                                 let stackStr = error.stack ?? EMPTYSTRING
    212                                 let errorStr = error.hint + "\n" + stackStr
    213                                 CopyButton(errorStr, vertical: true)
    214                                     .accessibilityLabel(Text("Copy the error", comment: "a11y"))
    215                                     .disabled(false)
    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 }