taler-ios

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

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 }