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