taler-ios

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

TransactionTypeDetail.swift (15716B)


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