P2PSubjectV.swift (9258B)
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 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.isEmpty { 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 let numChars = summary.count 136 Text(verbatim: "\(numChars)/100") // maximum 100 characters 137 .frame(maxWidth: .infinity, alignment: .trailing) 138 .talerFont(.body) 139 .accessibilityLabel(EMPTYSTRING) 140 .accessibilityValue(String(localized: "\(numChars) characters of 100")) 141 142 // TODO: compute max Expiration day from peerPushCheck to disable 30 (and even 7) 143 SelectDays(selected: $expireDays, maxExpiration: THIRTYDAYS, outgoing: outgoing) 144 .disabled(false) 145 .padding(.bottom) 146 147 let disabled = (expireDays == 0) // || (numChars < 1) // TODO: check amountAvailable 148 let destination = P2PReadyV(stack: stack.push(), 149 scope: scope, 150 summary: numChars > 0 ? summary : placeHolder, 151 iconID: iconID, 152 expireDays: expireDays, 153 outgoing: outgoing, 154 amountToTransfer: amountToTransfer, 155 transactionStarted: $transactionStarted) 156 let actions = Group { 157 NavLink($sendOrRequest) { destination } 158 } 159 160 let buttonTitle = buttonTitle(amountToTransfer) 161 Button(buttonTitle.0) { 162 sendOrRequest = true 163 } 164 .background(actions) 165 .accessibilityLabel(buttonTitle.1) 166 .buttonStyle(TalerButtonStyle(type: .prominent, disabled: disabled)) 167 .disabled(disabled) 168 .accessibilityHint(disabled ? String(localized: "enabled when subject and expiration are set", comment: "a11y") 169 : EMPTYSTRING) 170 }.padding(.horizontal) } // ScrollVStack 171 // .scrollBounceBehavior(.basedOnSize) needs iOS 16.4 172 // .ignoresSafeArea(.keyboard, edges: .bottom) 173 .navigationTitle(subjectTitle(amountToTransfer)) 174 .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all)) 175 .task(id: amountToTransfer.value) { await checkPeerPushDebit() } 176 .onAppear { 177 DebugViewC.shared.setViewID(VIEW_P2P_SUBJECT, stack: stack.push()) 178 // print("❗️ P2PSubjectV onAppear") 179 } 180 .onDisappear { 181 // print("❗️ P2PSubjectV onDisappear") 182 } 183 184 scrollView 185 #if OIM 186 .overlay { if #available(iOS 16.4, *) { 187 if controller.oimModeActive { 188 OIMSubjectView(stack: stack.push(), 189 cash: cash, 190 available: available, 191 amountToTransfer: $amountToTransfer, 192 selectedGoal: $iconID, 193 fwdButtonTapped: $sendOrRequest) 194 .environmentObject(NamespaceWrapper(namespace)) // keep OIMviews apart 195 } 196 } } 197 #endif 198 } 199 } 200 // MARK: - 201 #if DEBUG 202 //struct SendPurpose_Previews: PreviewProvider { 203 // static var previews: some View { 204 // @State var summary: String = EMPTYSTRING 205 // @State var expireDays: UInt = 0 206 // let amount = Amount(currency: LONGCURRENCY, integer: 10, fraction: 0) 207 // SendPurpose(amountAvailable: amount, 208 // amountToTransfer: 543, 209 // fee: "0,43", 210 // summary: $summary, 211 // expireDays: $expireDays) 212 // } 213 //} 214 #endif