taler-ios

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

SendAmountView.swift (9191B)


      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 // Called by SendAmountV when tapping [􁉇Send]
     13 struct SendAmountView: View {
     14     private let symLog = SymLogV(0)
     15     let stack: CallStack
     16     let cash: OIMcash
     17     let balance: Balance
     18     @Binding var buttonSelected: Bool
     19     @Binding var amountLastUsed: Amount
     20     @Binding var amountToTransfer: Amount
     21     @Binding var amountAvailable: Amount
     22     @Binding var summary: String
     23     @Binding var iconID: String?
     24 
     25     @EnvironmentObject private var controller: Controller
     26     @EnvironmentObject private var model: WalletModel
     27     @AppStorage("minimalistic") var minimalistic: Bool = false
     28 
     29     @State var peerPushCheck: CheckPeerPushDebitResponse? = nil
     30     @State private var expireDays = SEVENDAYS
     31     @State private var insufficient = false
     32 //    @State private var feeAmount: Amount? = nil
     33     @State private var feeString = (EMPTYSTRING, EMPTYSTRING)
     34     @State private var shortcutSelected = false
     35     @State private var amountShortcut = Amount.zero(currency: EMPTYSTRING)      // Update currency when used
     36     @State private var exchange: Exchange? = nil                                // wg. noFees
     37 
     38     private func shortcutAction(_ shortcut: Amount) {
     39         amountShortcut = shortcut
     40         shortcutSelected = true
     41     }
     42     private func buttonAction() { buttonSelected = true }
     43 
     44     public static func navTitle(_ currency: String, _ condition: Bool = false) -> String {
     45         condition ? String(localized: "NavTitle_Send_Currency",
     46                         defaultValue: "Send \(currency)",
     47                              comment: "NavTitle: Send 'currency'")
     48                   : String(localized: "NavTitle_Send",
     49                         defaultValue: "Send",
     50                              comment: "NavTitle: Send")
     51     }
     52 
     53     private func feeLabel(_ feeStr: String) -> String {
     54         feeStr.isEmpty ? EMPTYSTRING : String(localized: "+ \(feeStr) fee")
     55     }
     56 
     57     private func fee(raw: Amount, effective: Amount?) -> Amount? {
     58         do {     // Outgoing: fee = effective - raw
     59             if let effective {
     60                 let fee = try effective - raw
     61                 return fee
     62             }
     63         } catch {}
     64         return nil
     65     }
     66 
     67     private func feeIsNotZero() -> Bool? {
     68         if let hasNoFees = exchange?.noFees {
     69             if hasNoFees {
     70                 return nil      // this exchange never has fees
     71             }
     72         }
     73         return peerPushCheck == nil ? false
     74                                     : true // TODO: !(feeAmount?.isZero ?? false)
     75     }
     76 
     77     @MainActor
     78     private func computeFee(_ amount: Amount) async -> ComputeFeeResult? {
     79         if amount.isZero {
     80             return ComputeFeeResult.zero()
     81         }
     82         let insufficient = (try? amount > amountAvailable) ?? true
     83         if insufficient {
     84             return ComputeFeeResult.insufficient()
     85         }
     86             do {
     87                 let ppCheck = try await model.checkPeerPushDebit(amount, scope: balance.scopeInfo, viewHandles: true)
     88                 let raw = ppCheck.amountRaw
     89                 if let effective = ppCheck.amountEffective {
     90                     if let fee = fee(raw: raw, effective: effective) {
     91                         feeString = fee.formatted(balance.scopeInfo, isNegative: false)
     92                         symLog.log("Fee = \(feeString.0)")
     93                         let insufficient = (try? effective > amountAvailable) ?? true
     94 
     95                         peerPushCheck = ppCheck
     96                         let feeLabel = (feeLabel(feeString.0), feeLabel(feeString.1))
     97 //                      announce("\(amountVoiceOver), \(feeLabel)")
     98                         return ComputeFeeResult(insufficient: insufficient,
     99                                                    feeAmount: fee,
    100                                                       feeStr: feeLabel,
    101                                                     numCoins: nil)
    102                     } else {
    103                         peerPushCheck = nil
    104                     }
    105                 }
    106             } catch {
    107                 // handle cancel, errors
    108                 symLog.log("❗️ \(error), \(error.localizedDescription)")
    109                 switch error {
    110                     case let walletError as WalletBackendError:
    111                         switch walletError {
    112                             case .walletCoreError(let wError):
    113                                 if wError?.code == 7027 {
    114                                     if let details = wError?.insufficientBalanceDetails {
    115                                         symLog.log(details.causeHint?.localizedCause(amount.currencyStr))
    116                                     }
    117                                     // TODO: [10.2] AmountInputV ignores the insufficient flag,
    118                                     // so neither this verdict nor its localized cause is shown
    119                                     return ComputeFeeResult.insufficient()
    120                                 }
    121                             default: break
    122                         }
    123                     default: break
    124                 }
    125             }
    126         return nil
    127     }
    128 
    129     @MainActor
    130     private func newBalance() async {
    131         let scope = balance.scopeInfo
    132         symLog.log("❗️ task \(scope.currency)")
    133 
    134         if let available = try? await model.getMaxPeerPushDebitAmount(scope, viewHandles: true) {
    135             amountAvailable = available
    136         } else {
    137             amountAvailable = balance.available
    138         }
    139     }
    140 
    141     var body: some View {
    142 #if PRINT_CHANGES
    143         let _ = Self._printChanges()
    144         let _ = symLog.vlog()       // just to get the # to compare it with .onAppear & onDisappear
    145 #endif
    146         Group {
    147 //                let availableStr = amountAvailable.formatted(balance.scopeInfo, isNegative: false)
    148 //              let availableA11y = amountAvailable.formatted(currencyInfo, isNegative: false, useISO: true, a11y: ".")
    149 //              let amountVoiceOver = amountToTransfer.formatted(currencyInfo, isNegative: false)
    150 //              let insufficientLabel2 = String(localized: "but you only have \(availableStr) to send.")
    151 
    152                 let inputDestination = P2PSubjectV(stack: stack.push(),
    153                                                     cash: cash,
    154                                                    scope: balance.scopeInfo,
    155                                                available: balance.available,
    156                                                 feeLabel: (feeLabel(feeString.0), feeLabel(feeString.1)),
    157                                             feeIsNotZero: feeIsNotZero(),
    158                                                 outgoing: true,
    159                                         amountToTransfer: $amountToTransfer,    // from the textedit
    160                                                  summary: $summary,
    161                                                   iconID: $iconID,
    162                                               expireDays: $expireDays)
    163                 let shortcutDestination = P2PSubjectV(stack: stack.push(),
    164                                                        cash: cash,
    165                                                       scope: balance.scopeInfo,
    166                                                   available: balance.available,
    167                                                    feeLabel: nil,
    168                                                feeIsNotZero: feeIsNotZero(),
    169                                                    outgoing: true,
    170                                            amountToTransfer: $amountShortcut,   // from the tapped shortcut button
    171                                                     summary: $summary,
    172                                                      iconID: $iconID,
    173                                                  expireDays: $expireDays)
    174                 let actions = Group {
    175                     NavLink($buttonSelected) { inputDestination }
    176                     NavLink($shortcutSelected) { shortcutDestination }
    177                 }
    178 //                let a11yLabel = String(localized: "Amount to send:", comment: "a11y, no abbreviations")
    179                 AmountInputV(stack: stack.push(),
    180                              scope: balance.scopeInfo,
    181                    amountAvailable: $amountAvailable,
    182                        amountLabel: nil,            // will use "Available for transfer: xxx", trailing
    183                          a11yLabel: EMPTYSTRING,    // a11yLabel,
    184                   amountToTransfer: $amountToTransfer,
    185                     amountLastUsed: amountLastUsed,
    186                            wireFee: nil,
    187                            summary: $summary,
    188                     shortcutAction: shortcutAction,
    189                       buttonAction: buttonAction,
    190                         isIncoming: false,
    191                         computeFee: computeFee)
    192                 .background(actions)
    193         } // Group
    194         .task(id: balance) { await newBalance() }
    195         .onAppear {
    196             DebugViewC.shared.setViewID(VIEW_P2P_SEND, stack: stack.push())
    197             symLog.log("❗️ onAppear")
    198         }
    199     } // body
    200 }