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