taler-ios

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

P2PSubjectV.swift (9695B)


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