taler-ios

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

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 }