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