taler-ios

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

P2PSubjectV.swift (9751B)


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