taler-ios

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

SendAmountView.swift (8627B)


      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                                     return ComputeFeeResult.insufficient()
    112                                 }
    113                             default: break
    114                         }
    115                     default: break
    116                 }
    117             }
    118         return nil
    119     }
    120 
    121     @MainActor
    122     private func newBalance() async {
    123         let scope = balance.scopeInfo
    124         symLog.log("❗️ task \(scope.currency)")
    125 
    126         if let available = try? await model.getMaxPeerPushDebitAmount(scope, viewHandles: true) {
    127             amountAvailable = available
    128         } else {
    129             amountAvailable = Amount.zero(currency: scope.currency)
    130         }
    131     }
    132 
    133     var body: some View {
    134 #if PRINT_CHANGES
    135         let _ = Self._printChanges()
    136         let _ = symLog.vlog()       // just to get the # to compare it with .onAppear & onDisappear
    137 #endif
    138         Group {
    139                 let availableStr = amountAvailable.formatted(balance.scopeInfo, isNegative: false)
    140 //              let availableA11y = amountAvailable.formatted(currencyInfo, isNegative: false, useISO: true, a11y: ".")
    141 //              let amountVoiceOver = amountToTransfer.formatted(currencyInfo, isNegative: false)
    142 //              let insufficientLabel2 = String(localized: "but you only have \(availableStr) to send.")
    143 
    144                 let inputDestination = P2PSubjectV(stack: stack.push(),
    145                                                     cash: cash,
    146                                                    scope: balance.scopeInfo,
    147                                                available: balance.available,
    148                                                 feeLabel: (feeLabel(feeString.0), feeLabel(feeString.1)),
    149                                             feeIsNotZero: feeIsNotZero(),
    150                                                 outgoing: true,
    151                                         amountToTransfer: $amountToTransfer,    // from the textedit
    152                                                  summary: $summary,
    153                                                   iconID: $iconID,
    154                                               expireDays: $expireDays)
    155                 let shortcutDestination = P2PSubjectV(stack: stack.push(),
    156                                                        cash: cash,
    157                                                       scope: balance.scopeInfo,
    158                                                   available: balance.available,
    159                                                    feeLabel: nil,
    160                                                feeIsNotZero: feeIsNotZero(),
    161                                                    outgoing: true,
    162                                            amountToTransfer: $amountShortcut,   // from the tapped shortcut button
    163                                                     summary: $summary,
    164                                                      iconID: $iconID,
    165                                                  expireDays: $expireDays)
    166                 let actions = Group {
    167                     NavLink($buttonSelected) { inputDestination }
    168                     NavLink($shortcutSelected) { shortcutDestination }
    169                 }
    170                 let a11yLabel = String(localized: "Amount to send:", comment: "a11y, no abbreviations")
    171                 AmountInputV(stack: stack.push(),
    172                              scope: balance.scopeInfo,
    173                    amountAvailable: $amountAvailable,
    174                        amountLabel: nil,        // will use "Available for transfer: xxx", trailing
    175                          a11yLabel: a11yLabel,
    176                   amountToTransfer: $amountToTransfer,
    177                     amountLastUsed: amountLastUsed,
    178                            wireFee: nil,
    179                            summary: $summary,
    180                     shortcutAction: shortcutAction,
    181                       buttonAction: buttonAction,
    182                         isIncoming: false,
    183                         computeFee: computeFee)
    184                 .background(actions)
    185         } // Group
    186         .task(id: balance) { await newBalance() }
    187         .onAppear {
    188             DebugViewC.shared.setViewID(VIEW_P2P_SEND, stack: stack.push())
    189             symLog.log("❗️ onAppear")
    190         }
    191     } // body
    192 }