P2pReceiveURIView.swift (8722B)
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.absoluteString) { 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 if !tosAccepted { 74 ToSButtonView(stack: stack.push(), 75 exchangeBaseUrl: peerPushCreditResponse.exchangeBaseUrl, 76 viewID: SHEET_RCV_P2P_TOS, 77 p2p: true, 78 acceptAction: nil) 79 } 80 List { 81 PeerPushCreditView(stack: stack.push(), 82 raw: peerPushCreditResponse.amountRaw, 83 effective: peerPushCreditResponse.amountEffective, 84 scope: peerPushCreditResponse.scopeInfo, 85 summary: peerPushCreditResponse.contractTerms.summary) 86 ExpiresView(expiration: peerPushCreditResponse.contractTerms.purse_expiration) 87 } 88 .listStyle(myListStyle.style).anyView 89 .navigationTitle(navTitle) 90 .task(id: controller.currencyTicker) { 91 let currency = peerPushCreditResponse.amountRaw.currencyStr 92 currencyInfo = controller.info2(for: currency, controller.currencyTicker) 93 } 94 .safeAreaInset(edge: .bottom) { 95 if peerPushCreditResponse.txState.isConfirmed { 96 Button("Done") { dismissTop(stack.push()) } 97 .buttonStyle(TalerButtonStyle(type: .prominent)) 98 .padding(.horizontal) 99 } else { 100 if tosAccepted { 101 PeerPushCreditAccept(stack: stack.push(), url: url, 102 transactionId: peerPushCreditResponse.transactionId, 103 accept: $accept) 104 } 105 } 106 } 107 #if OIM && DEBUG 108 .overlay { if #available(iOS 16.4, *) { 109 if controller.oimSheetActive { 110 OIMp2pReceiveView(stack: stack.push(), 111 cash: cash, 112 available: balance?.available, 113 peerPushCreditResponse: $peerPushCreditResponse, 114 fwdButtonTapped: $accept) 115 .environmentObject(NamespaceWrapper(namespace)) // keep OIMviews apart 116 } 117 } } 118 #endif 119 } else { 120 #if DEBUG 121 let message = url.host 122 #else 123 let message: String? = nil 124 #endif 125 LoadingView(stack: stack.push(), scopeInfo: nil, message: message) 126 } 127 } 128 // must be here and not at LoadingView(), because this needs to run a 2nd time after ToS was accepted 129 .task { await viewDidLoad() } 130 .onAppear() { 131 symLog.log("onAppear") 132 DebugViewC.shared.setSheetID(SHEET_RCV_P2P) 133 } 134 } 135 } 136 // MARK: - 137 struct PeerPushCreditAccept: View { 138 let stack: CallStack 139 let url: URL? 140 let transactionId: String 141 @Binding var accept: Bool // triggers 'destination' when set true 142 143 var body: some View { 144 let destination = P2pAcceptDone(stack: stack.push(), 145 url: url, 146 transactionId: transactionId, 147 incoming: true) 148 let actions = Group { 149 NavLink($accept) { destination } 150 } 151 Button("Accept and receive") { // SHEET_RCV_P2P_ACCEPT 152 accept = true 153 } 154 .background(actions) 155 .buttonStyle(TalerButtonStyle(type: .prominent)) 156 .padding(.horizontal) 157 } 158 } 159 // MARK: - 160 struct PeerPushCreditView: View { 161 let stack: CallStack 162 let raw: Amount 163 let effective: Amount 164 let scope: ScopeInfo? 165 let summary: String 166 167 var body: some View { 168 let currency = raw.currencyStr 169 let fee = try! Amount.diff(raw, effective) 170 ThreeAmountsSection(stack: stack.push(), 171 scope: scope, 172 topTitle: String(localized: "Gross Amount to receive:"), 173 topAbbrev: String(localized: "Receive gross:", comment: "mini"), 174 topAmount: raw, 175 noFees: nil, // TODO: check baseURL for fees 176 fee: fee, 177 bottomTitle: String(localized: "Net Amount to receive:"), 178 bottomAbbrev: String(localized: "Receive net:", comment: "mini"), 179 bottomAmount: effective, 180 large: false, pending: false, incoming: true, 181 baseURL: nil, 182 txStateLcl: nil, 183 summary: summary, 184 merchant: nil, 185 products: nil) 186 } 187 } 188 // MARK: - 189 struct ExpiresView: View { 190 let expiration: Timestamp 191 192 @Environment(\.colorScheme) private var colorScheme 193 @Environment(\.colorSchemeContrast) private var colorSchemeContrast 194 @AppStorage("minimalistic") var minimalistic: Bool = false 195 var body: some View { 196 let (dateString, date) = TalerDater.dateString(expiration, minimalistic) 197 let a11yDate = TalerDater.accessibilityDate(date) ?? dateString 198 let a11yLabel = String(localized: "Expires: \(a11yDate)", comment: "a11y") 199 Text("Expires: \(dateString)") 200 .talerFont(.body) 201 .accessibilityLabel(a11yLabel) 202 .foregroundColor(WalletColors().secondary(colorScheme, colorSchemeContrast)) 203 } 204 }