P2pReceiveURIView.swift (9199B)
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 // Will be called either by the user scanning a QR code or tapping the provided link, 13 // from another user's Send. We show the P2P details - but first the ToS must be accepted. 14 struct P2pReceiveURIView: View { 15 private let symLog = SymLogV(0) 16 let stack: CallStack 17 let url: URL // the scanned URL 18 let oimEuro: Bool 19 20 @EnvironmentObject private var model: WalletModel 21 @EnvironmentObject private var controller: Controller 22 @Environment(\.colorScheme) private var colorScheme 23 @Environment(\.colorSchemeContrast) private var colorSchemeContrast 24 @AppStorage("minimalistic") var minimalistic: Bool = false 25 @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic 26 27 @StateObject private var cash: OIMcash 28 @State private var currencyInfo: CurrencyInfo = CurrencyInfo.zero(UNKNOWN) 29 @State private var peerPushCreditResponse: PreparePeerPushCreditResponse? = nil 30 @State private var accept: Bool = false 31 @State private var exchange: Exchange? = nil 32 @Namespace var namespace 33 34 let navTitle = String(localized: "Receive", comment: "Nav Title") 35 36 // Since there is no selectedBalance, we cannot know which currency we'll need to render at this time 37 init(stack: CallStack, url: URL, oimEuro: Bool) { 38 self.stack = stack 39 self.url = url 40 self.oimEuro = oimEuro 41 // let oimCurrency = oimCurrency(selectedBalance.wrappedValue?.scopeInfo) // might be nil ==> OIMeuros 42 let oimCurrency = oimCurrency(nil, oimEuro: oimEuro) // nil ==> OIMeuros 43 let oimCash = OIMcash(oimCurrency) 44 self._cash = StateObject(wrappedValue: { oimCash }()) 45 } 46 47 @MainActor 48 private func viewDidLoad() async { 49 symLog.log(".task") 50 if let ppResponse = try? await model.preparePeerPushCredit(url.trimmedString) { 51 let baseUrl = ppResponse.exchangeBaseUrl 52 exchange = try? await model.getExchangeByUrl(url: baseUrl) 53 await controller.checkCurrencyInfo(for: baseUrl, model: model) 54 let oimCurrency = oimCurrency(ppResponse.scopeInfo, oimEuro: oimEuro) 55 cash.setCurrency(oimCurrency) 56 controller.removeURL(url) // tx is now saved by wallet-core 57 peerPushCreditResponse = ppResponse 58 } else { 59 peerPushCreditResponse = nil 60 } 61 } 62 63 var body: some View { 64 #if PRINT_CHANGES 65 let _ = Self._printChanges() 66 let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear 67 #endif 68 VStack { 69 if let peerPushCreditResponse { 70 let scope = peerPushCreditResponse.scopeInfo 71 let balance = controller.balance(for: scope) 72 let tosAccepted = exchange?.tosStatus == .accepted 73 || exchange?.tosStatus == .missingTos // no ToS to accept 74 if !tosAccepted { 75 ToSButtonView(stack: stack.push(), 76 exchangeBaseUrl: peerPushCreditResponse.exchangeBaseUrl, 77 viewID: SHEET_RCV_P2P_TOS, 78 p2p: true, 79 acceptAction: nil) 80 } 81 List { 82 PeerPushCreditView(stack: stack.push(), 83 raw: peerPushCreditResponse.amountRaw, 84 effective: peerPushCreditResponse.amountEffective, 85 isDone: false, 86 scope: peerPushCreditResponse.scopeInfo, 87 summary: peerPushCreditResponse.contractTerms.summary) 88 ExpiresView(expiration: peerPushCreditResponse.contractTerms.purse_expiration) 89 } 90 .listStyle(myListStyle.style).anyView 91 .navigationTitle(navTitle) 92 .task(id: controller.currencyTicker) { 93 let currency = peerPushCreditResponse.amountRaw.currencyStr 94 currencyInfo = controller.info2(for: currency, controller.currencyTicker) 95 } 96 .safeAreaInset(edge: .bottom) { 97 if peerPushCreditResponse.txState.isConfirmed { 98 Button("Done") { dismissTop(stack.push()) } 99 .buttonStyle(TalerButtonStyle(type: .prominent)) 100 .padding(.horizontal) 101 } else { 102 if tosAccepted { 103 PeerPushCreditAccept(stack: stack.push(), url: url, 104 transactionId: peerPushCreditResponse.transactionId, 105 accept: $accept) 106 } 107 } 108 } 109 #if OIM && DEBUG 110 .overlay { if #available(iOS 16.4, *) { 111 if controller.oimSheetActive { 112 OIMp2pReceiveView(stack: stack.push(), 113 cash: cash, 114 available: balance?.available, 115 peerPushCreditResponse: $peerPushCreditResponse, 116 fwdButtonTapped: $accept) 117 .environmentObject(NamespaceWrapper(namespace)) // keep OIMviews apart 118 } 119 } } 120 #endif 121 } else { 122 #if DEBUG 123 let message = url.host 124 #else 125 let message: String? = nil 126 #endif 127 LoadingView(stack: stack.push(), scopeInfo: nil, message: message) 128 } 129 } 130 // must be here and not at LoadingView(), because this needs to run a 2nd time after ToS was accepted 131 .task { await viewDidLoad() } 132 .onAppear() { 133 symLog.log("onAppear") 134 DebugViewC.shared.setSheetID(SHEET_RCV_P2P, stack: stack.push()) 135 } 136 } 137 } 138 // MARK: - 139 struct PeerPushCreditAccept: View { 140 let stack: CallStack 141 let url: URL? 142 let transactionId: String 143 @Binding var accept: Bool // triggers 'destination' when set true 144 145 var body: some View { 146 let destination = P2pAcceptDone(stack: stack.push(), 147 url: url, 148 transactionId: transactionId, 149 incoming: true) 150 let actions = Group { 151 NavLink($accept) { destination } 152 } 153 Button("Accept and receive") { // SHEET_RCV_P2P_ACCEPT 154 accept = true 155 } 156 .background(actions) 157 .buttonStyle(TalerButtonStyle(type: .prominent)) 158 .padding(.horizontal) 159 } 160 } 161 // MARK: - 162 struct PeerPushCreditView: View { 163 let stack: CallStack 164 let raw: Amount 165 let effective: Amount 166 let isDone: Bool 167 let scope: ScopeInfo? 168 let summary: String? 169 170 var body: some View { 171 let fee = try! Amount.diff(raw, effective) 172 ThreeAmountsSection(stack: stack.push(), 173 scope: scope, 174 topTitle: String(localized: "Gross Amount to receive:"), 175 topAbbrev: String(localized: "Receive gross:", comment: "mini"), 176 topAmount: raw, 177 noFees: nil, // TODO: check baseURL for fees 178 fee: fee, 179 bottomTitle: String(localized: "Net Amount to receive:"), 180 bottomAbbrev: String(localized: "Receive net:", comment: "mini"), 181 bottomAmount: effective, 182 large: false, 183 pendingDialog: false, 184 isDone: isDone, 185 incoming: true, 186 baseURL: nil, 187 txStateLcl: nil, 188 summary: summary, 189 products: nil) 190 } 191 } 192 // MARK: - 193 struct ExpiresView: View { 194 let expiration: Timestamp? 195 196 @Environment(\.colorScheme) private var colorScheme 197 @Environment(\.colorSchemeContrast) private var colorSchemeContrast 198 @AppStorage("minimalistic") var minimalistic: Bool = false 199 var body: some View { 200 if let expiration { 201 let (dateString, date) = TalerDater.dateString(expiration, minimalistic) 202 let a11yDate = TalerDater.accessibilityDate(date) ?? dateString 203 let a11yLabel = String(localized: "Expires: \(a11yDate)", comment: "a11y") 204 Text("Expires: \(dateString)") 205 .talerFont(.body) 206 .accessibilityLabel(a11yLabel) 207 .foregroundColor(WalletColors().secondary(colorScheme, colorSchemeContrast)) 208 } else { // should never happen 209 Text("Unknown Expiration") 210 .talerFont(.body) 211 .foregroundColor(WalletColors().secondary(colorScheme, colorSchemeContrast)) 212 } 213 } 214 }