taler-ios

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

P2PSubjectV.swift (9204B)


      1 /*
      2  * This file is part of GNU Taler, ©2022-25 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 func p2pFee(ppCheck: CheckPeerPushDebitResponse) -> Amount? {
     13     do {
     14         // Outgoing: fee = effective - raw
     15         let fee = try ppCheck.amountEffective - ppCheck.amountRaw
     16         return fee
     17     } catch {}
     18     return nil
     19 }
     20 
     21 struct P2PSubjectV: View {
     22     private let symLog = SymLogV(0)
     23     let stack: CallStack
     24     let cash: OIMcash
     25     let scope: ScopeInfo
     26     let available: Amount
     27     let feeLabel: (String, String)?
     28     let feeIsNotZero: Bool?             // nil = no fees at all, false = no fee for this tx
     29     let outgoing: Bool
     30     @Binding var amountToTransfer: Amount
     31     @Binding var summary: String
     32     @Binding var iconID: String?
     33     @Binding var expireDays: UInt
     34 
     35     @EnvironmentObject private var controller: Controller
     36     @EnvironmentObject private var model: WalletModel
     37     @Environment(\.colorScheme) private var colorScheme
     38     @Environment(\.colorSchemeContrast) private var colorSchemeContrast
     39     @AppStorage("minimalistic") var minimalistic: Bool = false
     40 
     41     @State private var myFeeLabel: (String, String) = (EMPTYSTRING, EMPTYSTRING)
     42     @State private var transactionStarted: Bool = false
     43     @State private var sendOrRequest: Bool = false
     44     @FocusState private var isFocused: Bool
     45     @Namespace var namespace
     46 
     47     private func sendTitle(_ amountWithCurrency: String) -> String {
     48         String(localized: "Send \(amountWithCurrency) now",
     49                  comment: "amount with currency")
     50     }
     51     private func requTitle(_ amountWithCurrency: String) -> String {
     52         String(localized: "Request \(amountWithCurrency)",
     53                  comment: "amount with currency")
     54     }
     55 
     56     private func buttonTitle(_ amount: Amount) -> (String, String) {
     57         let amountWithCurrency = amount.formatted(scope, isNegative: false, useISO: true)
     58         return outgoing ? (sendTitle(amountWithCurrency.0), sendTitle(amountWithCurrency.1))
     59                         : (requTitle(amountWithCurrency.0), requTitle(amountWithCurrency.1))
     60     }
     61 
     62     private var placeHolder: String {
     63         return outgoing ? String(localized: "Sent with GNU TALER")
     64                         : String(localized: "Requested with GNU TALER")
     65     }
     66 
     67     private func subjectTitle(_ amount: Amount) -> String {
     68         let amountStr = amount.formatted(scope, isNegative: false)
     69         return outgoing ? String(localized: "NavTitle_Send_AmountStr",
     70                               defaultValue: "Send \(amountStr.0)",
     71                                    comment: "NavTitle: Send 'amountStr'")
     72                         : String(localized: "NavTitle_Request_AmountStr",
     73                               defaultValue: "Request \(amountStr.0)",
     74                                    comment: "NavTitle: Request 'amountStr'")
     75     }
     76 
     77     @MainActor
     78     private func checkPeerPushDebit() async {
     79         if outgoing && feeLabel == nil {
     80             if let ppCheck = try? await model.checkPeerPushDebit(amountToTransfer, scope: scope) {
     81                 if let feeAmount = p2pFee(ppCheck: ppCheck) {
     82                     let feeStr = feeAmount.formatted(scope, isNegative: false)
     83                     myFeeLabel = (String(localized: "+ \(feeStr.0) fee"),
     84                                   String(localized: "+ \(feeStr.1) fee"))
     85                 } else { myFeeLabel = (EMPTYSTRING, EMPTYSTRING) }
     86             } else {
     87                 print("❗️ checkPeerPushDebitM failed")
     88 
     89             }
     90         }
     91     }
     92 
     93     var body: some View {
     94 #if PRINT_CHANGES
     95         let _ = Self._printChanges()
     96         let _ = symLog.vlog(amountToTransfer.readableDescription)       // just to get the # to compare it with .onAppear & onDisappear
     97 #endif
     98         let scrollView = ScrollView { VStack (alignment: .leading, spacing: 6) {
     99             if let feeIsNotZero {       // don't show fee if nil
    100                 let label = feeLabel ?? myFeeLabel
    101                 if label.0.count > 0 {
    102                     Text(label.0)
    103                         .accessibilityLabel(label.1)
    104                         .frame(maxWidth: .infinity, alignment: .trailing)
    105                         .foregroundColor(WalletColors().secondary(colorScheme, colorSchemeContrast))
    106                         .talerFont(.body)
    107                 }
    108             }
    109             let enterSubject = String(localized: "Enter subject", comment: "Purpose, a11y")
    110             let enterColon = String("\(enterSubject):")
    111             if !minimalistic {
    112                 Text(enterSubject)    // Purpose
    113                     .talerFont(.title3)
    114                     .accessibilityHidden(true)
    115                     .padding(.top)
    116             }
    117             Group { if #available(iOS 16.4, *) {
    118                 TextField(placeHolder, text: $summary, axis: .vertical)
    119             } else {
    120                 TextField(placeHolder, text: $summary)
    121             } }
    122                 .talerFont(.title2)
    123                 .accessibilityLabel(enterColon)
    124                 .submitLabel(.next)
    125                 .focused($isFocused)
    126                 .onChange(of: summary) { newValue in
    127                     guard isFocused else { return }
    128                     guard newValue.contains("\n") else { return }
    129                     isFocused = false
    130                     summary = newValue.replacingOccurrences(of: LINEFEED, with: EMPTYSTRING)
    131                 }
    132                 .foregroundColor(WalletColors().fieldForeground)     // text color
    133                 .background(WalletColors().fieldBackground)
    134                 .textFieldStyle(.roundedBorder)
    135             Text(verbatim: "\(summary.count)/100")                          // maximum 100 characters
    136                 .frame(maxWidth: .infinity, alignment: .trailing)
    137                 .talerFont(.body)
    138                 .accessibilityLabel(EMPTYSTRING)
    139                 .accessibilityValue(String(localized: "\(summary.count) characters of 100"))
    140 
    141                 // TODO: compute max Expiration day from peerPushCheck to disable 30 (and even 7)
    142                 SelectDays(selected: $expireDays, maxExpiration: THIRTYDAYS, outgoing: outgoing)
    143                     .disabled(false)
    144                     .padding(.bottom)
    145 
    146                 let disabled = (expireDays == 0)    // || (summary.count < 1)    // TODO: check amountAvailable
    147             let destination = P2PReadyV(stack: stack.push(),
    148                                         scope: scope,
    149                                       summary: summary.count > 0 ? summary : placeHolder,
    150                                        iconID: iconID,
    151                                    expireDays: expireDays,
    152                                      outgoing: outgoing,
    153                              amountToTransfer: amountToTransfer,
    154                            transactionStarted: $transactionStarted)
    155             let actions = Group {
    156                 NavLink($sendOrRequest) { destination }
    157             }
    158 
    159             let buttonTitle = buttonTitle(amountToTransfer)
    160             Button(buttonTitle.0) {
    161                 sendOrRequest = true
    162             }
    163                 .background(actions)
    164                 .accessibilityLabel(buttonTitle.1)
    165                 .buttonStyle(TalerButtonStyle(type: .prominent, disabled: disabled))
    166                 .disabled(disabled)
    167                 .accessibilityHint(disabled ? String(localized: "enabled when subject and expiration are set", comment: "a11y")
    168                                             : EMPTYSTRING)
    169         }.padding(.horizontal) } // ScrollVStack
    170 //        .scrollBounceBehavior(.basedOnSize)  needs iOS 16.4
    171         .navigationTitle(subjectTitle(amountToTransfer))
    172         .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all))
    173         .task(id: amountToTransfer.value) { await checkPeerPushDebit() }
    174         .onAppear {
    175             DebugViewC.shared.setViewID(VIEW_P2P_SUBJECT, stack: stack.push())
    176 //            print("❗️ P2PSubjectV onAppear")
    177         }
    178         .onDisappear {
    179 //            print("❗️ P2PSubjectV onDisappear")
    180         }
    181 
    182         scrollView
    183 #if OIM
    184             .overlay { if #available(iOS 16.4, *) {
    185                 if controller.oimModeActive {
    186                     OIMSubjectView(stack: stack.push(),
    187                                     cash: cash,
    188                                available: available,
    189                         amountToTransfer: $amountToTransfer,
    190                             selectedGoal: $iconID,
    191                          fwdButtonTapped: $sendOrRequest)
    192                     .environmentObject(NamespaceWrapper(namespace))             // keep OIMviews apart
    193                 }
    194             } }
    195 #endif
    196     }
    197 }
    198 // MARK: -
    199 #if DEBUG
    200 //struct SendPurpose_Previews: PreviewProvider {
    201 //    static var previews: some View {
    202 //        @State var summary: String = EMPTYSTRING
    203 //        @State var expireDays: UInt = 0
    204 //        let amount = Amount(currency: LONGCURRENCY, integer: 10, fraction: 0)
    205 //        SendPurpose(amountAvailable: amount,
    206 //                   amountToTransfer: 543,
    207 //                                fee: "0,43",
    208 //                            summary: $summary,
    209 //                         expireDays: $expireDays)
    210 //    }
    211 //}
    212 #endif