taler-ios

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

SendAmountView.swift (9580B)


      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 private 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                 if let raw = ppCheck.amountRaw,
     89                    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                         // wallet-core found coins for amount + fees, so it is affordable.
     94                         let insufficient = false
     95 //                        let insufficient = (try? effective > amountAvailable) ?? true
     96                         // Comparing effective with amountAvailable would be wrong:
     97                         // that is the *raw* maximum (getMaxPeerPushDebitAmount),
     98                         // which effective exceeds near the maximum.
     99 
    100                         peerPushCheck = ppCheck
    101                         let feeLabel = (feeLabel(feeString.0), feeLabel(feeString.1))
    102 //                      announce("\(amountVoiceOver), \(feeLabel)")
    103                         return ComputeFeeResult(insufficient: insufficient,
    104                                                    feeAmount: fee,
    105                                                       feeStr: feeLabel,
    106                                                     numCoins: nil)
    107                     } else {
    108                         peerPushCheck = nil
    109                     }
    110                 }
    111             } catch {
    112                 // handle cancel, errors
    113                 symLog.log("❗️ \(error), \(error.localizedDescription)")
    114                 switch error {
    115                     case let walletError as WalletBackendError:
    116                         switch walletError {
    117                             case .walletCoreError(let wError):
    118                                 if wError?.code == 7027 {
    119                                     if let details = wError?.insufficientBalanceDetails {
    120                                         symLog.log(details.causeHint?.localizedCause(amount.currencyStr))
    121                                     }
    122                                     // TODO: [10.2] AmountInputV ignores the insufficient flag,
    123                                     // so neither this verdict nor its localized cause is shown
    124                                     return ComputeFeeResult.insufficient()
    125                                 }
    126                             default: break
    127                         }
    128                     default: break
    129                 }
    130             }
    131         return nil
    132     }
    133 
    134     @MainActor
    135     private func newBalance() async {
    136         let scope = balance.scopeInfo
    137         symLog.log("❗️ task \(scope.currency)")
    138 
    139         if let available = try? await model.getMaxPeerPushDebitAmount(scope, viewHandles: true) {
    140             amountAvailable = available
    141         } else {
    142             amountAvailable = balance.available
    143         }
    144     }
    145 
    146     var body: some View {
    147 #if PRINT_CHANGES
    148         let _ = Self._printChanges()
    149         let _ = symLog.vlog()       // just to get the # to compare it with .onAppear & onDisappear
    150 #endif
    151         Group {
    152 //                let availableStr = amountAvailable.formatted(balance.scopeInfo, isNegative: false)
    153 //              let availableA11y = amountAvailable.formatted(currencyInfo, isNegative: false, useISO: true, a11y: ".")
    154 //              let amountVoiceOver = amountToTransfer.formatted(currencyInfo, isNegative: false)
    155 //              let insufficientLabel2 = String(localized: "but you only have \(availableStr) to send.")
    156 
    157                 let inputDestination = P2PSubjectV(stack: stack.push(),
    158                                                     cash: cash,
    159                                                    scope: balance.scopeInfo,
    160                                                available: balance.available,
    161                                                 feeLabel: (feeLabel(feeString.0), feeLabel(feeString.1)),
    162                                             feeIsNotZero: feeIsNotZero(),
    163                                                 outgoing: true,
    164                                         amountToTransfer: $amountToTransfer,    // from the textedit
    165                                                  summary: $summary,
    166                                                   iconID: $iconID,
    167                                               expireDays: $expireDays)
    168                 let shortcutDestination = P2PSubjectV(stack: stack.push(),
    169                                                        cash: cash,
    170                                                       scope: balance.scopeInfo,
    171                                                   available: balance.available,
    172                                                    feeLabel: nil,
    173                                                feeIsNotZero: feeIsNotZero(),
    174                                                    outgoing: true,
    175                                            amountToTransfer: $amountShortcut,   // from the tapped shortcut button
    176                                                     summary: $summary,
    177                                                      iconID: $iconID,
    178                                                  expireDays: $expireDays)
    179                 let actions = Group {
    180                     NavLink($buttonSelected) { inputDestination }
    181                     NavLink($shortcutSelected) { shortcutDestination }
    182                 }
    183 //                let a11yLabel = String(localized: "Amount to send:", comment: "a11y, no abbreviations")
    184                 AmountInputV(stack: stack.push(),
    185                              scope: balance.scopeInfo,
    186                    amountAvailable: $amountAvailable,
    187                        amountLabel: nil,            // will use "Available for transfer: xxx", trailing
    188                          a11yLabel: EMPTYSTRING,    // a11yLabel,
    189                   amountToTransfer: $amountToTransfer,
    190                     amountLastUsed: amountLastUsed,
    191                            wireFee: nil,
    192                            summary: $summary,
    193                     shortcutAction: shortcutAction,
    194                       buttonAction: buttonAction,
    195                         isIncoming: false,
    196                         computeFee: computeFee)
    197                 .background(actions)
    198         } // Group
    199         .task(id: balance) { await newBalance() }
    200         .onAppear {
    201             DebugViewC.shared.setViewID(VIEW_P2P_SEND, stack: stack.push())
    202             symLog.log("❗️ onAppear")
    203         }
    204     } // body
    205 }