taler-ios

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

ManualDetailsV.swift (10209B)


      1 /*
      2  * This file is part of GNU Taler, ©2022-26 Taler Systems S.A.
      3  * See LICENSE.md
      4  */
      5 /**
      6  * @author Marc Stibane
      7  */
      8 import SwiftUI
      9 import OrderedCollections
     10 import taler_swift
     11 
     12 struct ManualDetailsV: View {
     13     let stack: CallStack
     14     let common: TransactionCommon
     15     let details: WithdrawalDetails
     16 
     17     var body: some View {
     18         if let accountDetails = details.exchangeCreditAccountDetails,
     19            let validDetails = accountDetails.validDetails {
     20             if let scope: ScopeInfo = common.scopes.first {
     21                 let obtainStr = common.amountEffective.formatted(scope , isNegative: false)
     22                 let title = String(localized: "The payment service is waiting for your wire-transfer.", comment: "")
     23                 WireTransferView(stack: stack.push(),
     24                                  title: title,
     25                              titleA11y: title,
     26                             reservePub: details.reservePub,
     27                              obtainStr: obtainStr,              // only for withdrawal
     28                                kycData: nil,                    // only for deposit auth
     29                           validDetails: validDetails)
     30             } else { ErrorView(stack.push(), title: "Payment Error", message: "No scope") }
     31         } else { ErrorView(stack.push(), title: "Payment Error", message: "No valid details") }
     32     } // body
     33 }
     34 // MARK: -
     35 // called by ManualDetailsV (amount to transfer is chosen by user),
     36 // and by KYCauth (user chose the amount to deposit - but first they must transfer kycAmount to the Exchange to prove having control over the account)
     37 struct WireTransferView: View {
     38     let stack: CallStack
     39     let title: String
     40     let titleA11y: String
     41     let reservePub: String
     42     let obtainStr: (String, String)?        // only for withdrawal
     43     let kycData: KycData?                   // only for deposit auth
     44     let validDetails: AccountDetailsArray
     45 
     46     @EnvironmentObject private var model: WalletModel
     47 #if DEBUG
     48     @AppStorage("developerMode") var developerMode: Bool = true
     49 #else
     50     @AppStorage("developerMode") var developerMode: Bool = false
     51 #endif
     52     @AppStorage("minimalistic") var minimalistic: Bool = false
     53     @State private var validIndex = 0
     54     @State private var listID = UUID()
     55 
     56     func redraw(_ newIndex: Int) -> Void {
     57         if newIndex != validIndex {
     58             validIndex = newIndex
     59             withAnimation { listID = UUID() }
     60         }
     61     }
     62 
     63     @ViewBuilder func qrCodesRow(_ transferOptions: [TransferOption]) -> some View {
     64         let qrCodesForPayto = QRcodesForPayto(stack: stack.push(),
     65                                     transferOptions: transferOptions)
     66         NavigationLink(destination: qrCodesForPayto) {
     67             Text(minimalistic ? "QR"
     68                               : "Wire transfer QR codes")
     69             .talerFont(.title3)
     70         }
     71     }
     72 
     73     var body: some View {
     74         let validCount = validDetails.count         // guaranteed to be > 0
     75         // validDetails can shrink (e.g. when a conversion rate lookup starts failing)
     76         // while validIndex still points to an account which is gone by now
     77         let account = validDetails[min(validIndex, validCount - 1)]
     78         let json = account.toJSON()
     79         // transferAmount is the amount the user must wire - for an account which needs
     80         // currency conversion that is in the external currency, and thus different from amountRaw
     81         if let transferAmount = account.transferAmount {
     82             if let transferOptions = account.transferOptions,
     83                // an option of type "uri" has neither paytoUri nor qrCodes - use one which has
     84                let firstOption = transferOptions.first(where: { $0.paytoUri != nil }) ?? transferOptions.first {
     85                 let specs = account.currencySpecification
     86                 let amountStr = transferAmount.formatted(specs: specs, isNegative: false)
     87                 let amountValue = transferAmount.valueStr
     88 //            let _ = print(amountStr, " | ", obtainStr)
     89                 if !minimalistic {
     90                     Text(title)
     91                         .bold()
     92                         .multilineTextAlignment(.leading)
     93                         .listRowSeparator(.hidden)
     94                         .accessibilityLabel(titleA11y)
     95                 }
     96                 if validCount > 1 {
     97                     if validCount > 3 { // too many for SegmentControl
     98                         AccountPicker(title: String(localized: "Bank"),
     99                                       value: $validIndex,
    100                              accountDetails: validDetails,
    101                                      action: redraw)
    102                         .listRowSeparator(.hidden)
    103                         .pickerStyle(.menu)
    104                     } else {
    105                         SegmentControl(value: $validIndex, accountDetails: validDetails, action: redraw)
    106                             .listRowSeparator(.hidden)
    107                     }
    108                 } else if let bankName = account.bankLabel {
    109                     Text(bankName + ":   " + amountStr.0)
    110                         .accessibilityLabel(bankName + ":   " + amountStr.1)
    111                 } else {
    112                     Text(amountStr.0)
    113                         .accessibilityLabel(amountStr.1)
    114                 }
    115 
    116                 let countA = transferOptions.count
    117                 let countB = firstOption.qrCodes?.count ?? 0
    118 
    119                 if let paytoUri = firstOption.paytoUri {
    120                     let payto = PayTo(paytoUri)
    121                     if let receiverStr = payto.receiver {
    122                         let wireDetails = ManualDetailsWireV(stack: stack.push(),
    123                                                         reservePub: reservePub,
    124                                                              payto: payto,
    125                                                       restrictions: account.creditRestrictions,
    126                                                        amountValue: amountValue,
    127                                                          amountStr: amountStr,
    128                                                          obtainStr: obtainStr,              // only for withdrawal
    129                                                          debitIBAN: kycData?.debitIBAN      // only for deposit auth
    130                                             )
    131                         NavigationLink(destination: wireDetails) {
    132                             Text(minimalistic ? "Instructions"
    133                                               : "Wire transfer instructions")
    134                             .talerFont(.title3)
    135                         }
    136 
    137                         Group {
    138                             if countA > 1 || countB > 1 {
    139                                 qrCodesRow(transferOptions)
    140                             } else if countB > 0 {   // show the single QR directly
    141                                 Text("If your banking software runs on another device, you can scan this QR code:")
    142                                     .listRowSeparator(.hidden)
    143                                 QRcodeCopyShare(option: firstOption)
    144                                     .listRowSeparator(.automatic)
    145                             }
    146                         }.id(listID)
    147                             .talerFont(.body)
    148 #if DEBUG
    149                         if developerMode {
    150                             if let iban = payto.iban {
    151                                 Text(minimalistic ? "**Alternative:** Use this PayTo-Link:"
    152                                                   : "**Alternative:** If your bank already supports PayTo, you can use this PayTo-Link instead:")
    153                                     .multilineTextAlignment(.leading)
    154                                     .padding(.top)
    155                                     .listRowSeparator(.hidden)
    156                                 let title = String(localized: "Share the PayTo URL", comment: "a11y")
    157                                 let minTitle = String(localized: "Share PayTo", comment: "mini")
    158                                 // TODO: chQRr
    159                                 let textToShare = String("\(payto)\n\nIBAN: \(iban)\nReceiver: \(receiverStr)\nAmount: \(amountStr.1)\nSubject: \(reservePub)")
    160             let _ = print(textToShare)
    161                                 ShareButton(textToShare, title: minimalistic ? minTitle : title)
    162                                     .frame(maxWidth: .infinity, alignment: .center)
    163                                     .accessibilityLabel(Text(title))
    164                                     .disabled(false)
    165                                     .listRowSeparator(.hidden)
    166                             }
    167                         }
    168 #endif
    169                     } else { ErrorView(stack.push(), title: "No receiver", message: json, copyable: true) }
    170                 } else { ErrorView(stack.push(), title: "No payto URL", message: json, copyable: true) }
    171             } else { ErrorView(stack.push(), title: "No transfer options", message: json, copyable: true) }
    172         } else { ErrorView(stack.push(), title:  "No amount", message: json, copyable: true) }
    173     }
    174 }
    175 // MARK: -
    176 #if DEBUG
    177 struct ManualDetails_Previews: PreviewProvider {
    178     static var previews: some View {
    179         let common = TransactionCommon(type: .withdrawal,
    180                               transactionId: "someTxID",
    181                                   timestamp: Timestamp(from: 1_666_666_000_000),
    182                                      scopes: [],
    183                                     txState: TransactionState(major: .done),
    184                                   txActions: [],
    185                                   amountRaw: Amount(currency: LONGCURRENCY, cent: 220),
    186                             amountEffective: Amount(currency: LONGCURRENCY, cent: 110))
    187 //        let payto = "payto://iban/SANDBOXX/DE159593?receiver-name=Exchange+Company"
    188         let details = WithdrawalDetails(type: .manual, 
    189                                   reservePub: "ReSeRvEpUbLiC_KeY_FoR_WiThDrAwAl",
    190                               reserveIsReady: false,
    191                                    confirmed: false)
    192         List {
    193             ManualDetailsV(stack: CallStack("Preview"), common: common, details: details)
    194         }
    195     }
    196 }
    197 #endif