taler-ios

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

ManualDetailsV.swift (10551B)


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