taler-ios

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

SendAmountView.swift (9033B)


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