taler-ios

iOS apps for GNU Taler (wallet)
Log | Files | Refs | README | LICENSE

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 }