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