ManualDetailsWireV.swift (19500B)
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 TransferRestrictionsV: View { 13 let amountStr: (String, String) 14 let obtainStr: (String, String)? // only for withdrawal 15 let debitIBAN: String? // != nil then transfer tinyAmount for deposit auth 16 let restrictions: [AccountRestriction]? 17 18 @AppStorage("minimalistic") var minimalistic: Bool = false 19 20 private func transferMini(_ amountS: String) -> String { 21 let amountNBS = amountS.nbs 22 return String(localized: "Transfer \(amountNBS) to the payment service.") 23 } 24 private func transferMaxi(_ amountS: String, _ obtainS: String) -> String { 25 let amountNBS = amountS.nbs 26 let obtainNBS = obtainS.nbs 27 return String(localized: "You need to transfer \(amountNBS) from your regular bank account to the payment service to receive \(obtainNBS) as digital cash in this wallet.") 28 } 29 30 private func authMini(_ amountS: String, _ debitS: String) -> String { 31 let amountNBS = amountS.nbs 32 return String(localized: "Transfer \(amountNBS) from account \(debitS) to verify having control over it.") 33 } 34 private func authMaxi(_ amountS: String, _ debitS: String) -> String { 35 let amountNBS = amountS.nbs 36 return String(localized: "You need to transfer \(amountNBS) to the payment service from your bank account \(debitS) to verify having control over it. Don't use a different bank account, or the verification will fail.") 37 } 38 39 var body: some View { 40 VStack(alignment: .leading) { 41 if let debitIBAN { // deposit auth 42 Text(minimalistic ? authMini(amountStr.0, debitIBAN) 43 : authMaxi(amountStr.0, debitIBAN)) 44 .accessibilityLabel(minimalistic ? authMini(amountStr.1, debitIBAN) 45 : authMaxi(amountStr.1, debitIBAN)) 46 .talerFont(.body) 47 .multilineTextAlignment(.leading) 48 } else if let obtainStr { // withdrawal 49 Text(minimalistic ? transferMini(amountStr.0) 50 : transferMaxi(amountStr.0, obtainStr.0)) 51 .accessibilityLabel(minimalistic ? transferMini(amountStr.1) 52 : transferMaxi(amountStr.1, obtainStr.1)) 53 .talerFont(.body) 54 .multilineTextAlignment(.leading) 55 } else { /* should NEVER happen */ } 56 if let restrictions { 57 ForEach(restrictions) { restriction in 58 if let hintsI18n = restriction.human_hint_i18n { 59 RestrictionsV(hintsI18n: hintsI18n, 60 human_hint: restriction.human_hint) 61 } 62 } 63 } 64 } 65 } 66 } 67 // MARK: - 68 struct RestrictionsV: View { 69 let hintsI18n: HintDict 70 var human_hint: String? 71 72 @State private var selectedLanguage = Locale.preferredLanguageCode 73 74 var body: some View { 75 if !hintsI18n.isEmpty { 76 // let sortedDict = OrderedDictionary(uniqueKeys: hintsI18n.keys, values: hintsI18n.values) 77 // var sorted: OrderedDictionary<String:String> 78 let sortedDict = OrderedDictionary(uncheckedUniqueKeysWithValues: hintsI18n.sorted { $0.key < $1.key }) 79 Picker("Restriction:", selection: $selectedLanguage) { 80 ForEach(sortedDict.keys, id: \.self) { 81 Text(sortedDict[$0] ?? "missing hint") 82 } 83 } 84 .accentColor(.primary) 85 .pickerStyle(.menu) 86 .padding(.top) 87 .task { 88 if !sortedDict.keys.contains(selectedLanguage) { 89 selectedLanguage = sortedDict.keys.first! 90 } 91 } 92 } else if let hint = human_hint { 93 let mark = Image(systemName: EXCLAMATION) 94 Text("\(mark) \(hint)") // verbatim: doesn't work here, will not show the image. Thus we must set this to "Don't translate" 95 .padding(.top) 96 } 97 } 98 } 99 // MARK: - 100 struct PayeeZip: View { 101 let receiverZip: String 102 @State var isCopied: Bool? = false 103 var body: some View { 104 HStack { 105 VStack(alignment: .leading) { 106 Text("Zip code:") 107 .talerFont(.subheadline) 108 Text(receiverZip) 109 .monospacedDigit() 110 .foregroundStyle(isCopied == true ? Color.secondary : .primary) 111 .padding(.leading) 112 } .frame(maxWidth: .infinity, alignment: .leading) 113 .accessibilityElement(children: .combine) 114 .accessibilityLabel(Text("Zip code", comment: "a11y")) 115 CopyButton(receiverZip, isCopied: $isCopied, vertical: true) 116 .accessibilityLabel(Text("Copy the zip code", comment: "a11y")) 117 .disabled(false) 118 } .padding(.top, -8) 119 } 120 } 121 // MARK: - 122 struct PayeeReceiver: View { 123 let receiverStr: String 124 @State var isCopied: Bool? = false 125 var body: some View { 126 HStack { 127 VStack(alignment: .leading) { 128 Text("Recipient:") 129 .talerFont(.subheadline) 130 Text(receiverStr) 131 .monospacedDigit() 132 .foregroundStyle(isCopied == true ? Color.secondary : .primary) 133 .padding(.leading) 134 } .frame(maxWidth: .infinity, alignment: .leading) 135 .accessibilityElement(children: .combine) 136 .accessibilityLabel(Text("Recipient", comment: "a11y")) 137 CopyButton(receiverStr, isCopied: $isCopied, vertical: true) 138 .accessibilityLabel(Text("Copy the recipient", comment: "a11y")) 139 .disabled(false) 140 } .padding(.top, -8) 141 } 142 } 143 // MARK: - 144 struct PayeeTown: View { 145 let receiverTown: String 146 @State var isCopied: Bool? = false 147 var body: some View { 148 HStack { 149 VStack(alignment: .leading) { 150 Text("City:") 151 .talerFont(.subheadline) 152 Text(receiverTown) 153 .monospacedDigit() 154 .foregroundStyle(isCopied == true ? Color.secondary : .primary) 155 .padding(.leading) 156 } .frame(maxWidth: .infinity, alignment: .leading) 157 .accessibilityElement(children: .combine) 158 .accessibilityLabel(Text("City", comment: "a11y")) 159 CopyButton(receiverTown, isCopied: $isCopied, vertical: true) 160 .accessibilityLabel(Text("Copy the city", comment: "a11y")) 161 .disabled(false) 162 } .padding(.top, -8) 163 } 164 } 165 // MARK: - 166 struct Cryptocode: View { 167 let cryptoString: String 168 let chQRr: String? 169 170 @State var isCopied: Bool? = false 171 var body: some View { 172 let isChQRr = chQRr != nil 173 HStack { 174 Text(chQRr ?? cryptoString) 175 .foregroundStyle(isCopied == true ? Color.secondary : .primary) 176 .monospacedDigit() 177 .accessibilityLabel(isChQRr ? Text("QR reference", comment: "a11y") 178 : Text("Cryptocode", comment: "a11y")) 179 .frame(maxWidth: .infinity, alignment: .leading) 180 CopyButton(chQRr ?? cryptoString, isCopied: $isCopied, vertical: true) 181 .accessibilityLabel(isChQRr ? Text("Copy the QR reference", comment: "a11y") 182 : Text("Copy the cryptocode", comment: "a11y")) 183 .disabled(false) 184 } .padding(.leading) 185 } 186 } 187 // MARK: - 188 struct IbanCode: View { 189 let iban: String 190 @State var isCopied: Bool? = false 191 var body: some View { 192 HStack { 193 VStack(alignment: .leading) { 194 Text("IBAN:") // TODO: BBAN 195 .talerFont(.subheadline) 196 Text(iban) 197 .monospacedDigit() 198 .foregroundStyle(isCopied == true ? Color.secondary : .primary) 199 .padding(.leading) 200 } .frame(maxWidth: .infinity, alignment: .leading) 201 .accessibilityElement(children: .combine) 202 .accessibilityLabel(Text("IBAN of the recipient", comment: "a11y")) // TODO: BBAN 203 CopyButton(iban, isCopied: $isCopied, vertical: true) 204 .accessibilityLabel(Text("Copy the IBAN", comment: "a11y")) // TODO: BBAN 205 .disabled(false) 206 } // .padding(.top, -8) 207 } 208 } 209 // MARK: - 210 struct AmountCode: View { 211 let amountStr: (String, String) 212 let amountValue: String // string representation of the value, formatted as "`integer`.`fraction`" 213 @State var isCopied: Bool? = false 214 var body: some View { 215 HStack { 216 VStack(alignment: .leading) { 217 Text("Amount:") 218 .talerFont(.subheadline) 219 Text(amountStr.0) 220 .accessibilityLabel(amountStr.1) 221 .monospacedDigit() 222 .foregroundStyle(isCopied == true ? Color.secondary : .primary) 223 .padding(.leading) 224 } .frame(maxWidth: .infinity, alignment: .leading) 225 .accessibilityElement(children: .combine) 226 .accessibilityLabel(Text("Amount to transfer", comment: "a11y")) 227 CopyButton(amountValue, isCopied: $isCopied, vertical: true) 228 // only digits + separator, no currency name or symbol 229 .accessibilityLabel(Text("Copy the amount", comment: "a11y")) 230 .disabled(false) 231 } .padding(.top, -8) 232 } 233 } 234 // MARK: - 235 struct XTalerCode: View { 236 let xTaler: String 237 @State var isCopied: Bool? = false 238 var body: some View { 239 HStack { 240 VStack(alignment: .leading) { 241 Text("Account:") 242 .talerFont(.subheadline) 243 Text(xTaler) 244 .monospacedDigit() 245 .foregroundStyle(isCopied == true ? Color.secondary : .primary) 246 .padding(.leading) 247 } .frame(maxWidth: .infinity, alignment: .leading) 248 .accessibilityElement(children: .combine) 249 .accessibilityLabel(Text("account of the recipient", comment: "a11y")) 250 CopyButton(xTaler, isCopied: $isCopied, vertical: true) 251 .accessibilityLabel(Text("Copy the account", comment: "a11y")) 252 .disabled(false) 253 } .padding(.top, -8) 254 } 255 } 256 // MARK: - 257 struct ManualDetailsWireV: View { 258 let stack: CallStack 259 let reservePub: String 260 let payto: PayTo 261 262 let restrictions: [AccountRestriction]? // only if restrictions apply 263 // let iban: String? // TODO: BBAN 264 let amountValue: String // string representation of the value, formatted as "`integer`.`fraction`" 265 let amountStr: (String, String) 266 let obtainStr: (String, String)? // only for withdrawal 267 let debitIBAN: String? // only for deposit auth 268 269 @AppStorage("minimalistic") var minimalistic: Bool = false 270 let navTitle = String(localized: "Wire transfer", comment: "ViewTitle of wire-transfer instructions") 271 272 private func step3(_ amountS: String) -> String { 273 let amountNBS = amountS.nbs 274 let bePatient = String(localized: "Depending on your bank the transfer can take from minutes to two working days, please be patient.") 275 if let debitIBAN { 276 return minimalistic ? String(localized: "Transfer \(amountNBS) from \(debitIBAN).") 277 : String(localized: "Finish the wire transfer of \(amountNBS) in your banking app or website to verify your bank account \(debitIBAN).") + "\n" + bePatient 278 } 279 return minimalistic ? String(localized: "Transfer \(amountNBS).") 280 : String(localized: "Finish the wire transfer of \(amountNBS) in your banking app or website, then this withdrawal will proceed automatically.") + "\n" + bePatient 281 } 282 283 /// The subject of the wire transfer 284 private var cryptoString: String { 285 // chQRr only consists of digits - no prefix possible 286 if let chQRr = payto.chQRr, !chQRr.isEmpty { 287 return chQRr 288 } 289 // if let messageStr = payto.messageStr, !messageStr.isEmpty { 290 // return messageStr 291 // } 292 return debitIBAN != nil ? "KYC:" + reservePub : reservePub 293 } 294 295 // @ViewBuilder func cyclosCode() -> some View { 296 // HStack { 297 // VStack(alignment: .leading) { 298 // Text("Cyclos:") 299 // .talerFont(.subheadline) 300 // Text(cyclos) 301 // .monospacedDigit() 302 // .padding(.leading) 303 // } .frame(maxWidth: .infinity, alignment: .leading) 304 // .accessibilityElement(children: .combine) 305 // .accessibilityLabel(Text("cyclos account of the recipient", comment: "a11y")) 306 // CopyButton(textToCopy: cyclos, vertical: true) 307 // .accessibilityLabel(Text("Copy the cyclos account", comment: "a11y")) 308 // .disabled(false) 309 // } .padding(.top, -8) 310 // } 311 312 @ViewBuilder func step2(_ isChQrr: Bool) -> some View { 313 Text(isChQrr ? (minimalistic ? "**Step 2:** Copy+Paste this QR-Reference:" 314 : "**Step 2:** Copy this code and paste it into the QR-Reference field in your banking app or bank website:") 315 : (minimalistic ? "**Step 2:** Copy+Paste this subject:" 316 : "**Step 2:** Copy this code and paste it into the subject/purpose field (or “Message to recipient”) in your banking app or bank website:")) 317 .talerFont(.body) 318 .multilineTextAlignment(.leading) 319 } 320 321 var body: some View { 322 if let receiverStr = payto.receiver { 323 List { 324 let warningIcon = Image(systemName: WARNING) 325 let note = Text("**Note: Don't forget to copy and paste the code in Step 2.**") 326 let manda = debitIBAN == nil ? String(localized: "This is mandatory, otherwise your money will not arrive in this wallet.") 327 : String(localized: "This is mandatory, otherwise the verification will fail.") 328 let mandatory = Text("\(warningIcon) \(note)\n\(manda)") 329 .bold() 330 .talerFont(.body) 331 .multilineTextAlignment(.leading) 332 .listRowSeparator(.hidden) 333 let step1i = Text(minimalistic ? "**Step 1:** Copy+Paste recipient and IBAN:" 334 : "**Step 1:** If you don't already have it in your banking favorites list, then copy and paste recipient and IBAN into the recipient/IBAN fields in your banking app or website (and save it as favorite for the next time):") // TODO: BBAN 335 .talerFont(.body) 336 .multilineTextAlignment(.leading) 337 .padding(.top) 338 let step1x = Text(minimalistic ? "**Step 1:** Copy+Paste recipient and account:" 339 : "**Step 1:** Copy and paste recipient and account into the corresponding fields in your banking app or website:") 340 .talerFont(.body) 341 .multilineTextAlignment(.leading) 342 .padding(.top) 343 let step3A11y = String(localized: "Step 3: \(step3(amountStr.1))", comment: "a11y") 344 let step3Head: LocalizedStringKey = "**Step 3:** \(step3(amountStr.0))" 345 let step3 = Text(step3Head) 346 .accessibilityLabel(step3A11y) 347 .talerFont(.body) 348 .multilineTextAlignment(.leading) 349 350 Group { 351 TransferRestrictionsV(amountStr: amountStr, 352 obtainStr: obtainStr, 353 debitIBAN: debitIBAN, 354 restrictions: restrictions) 355 .listRowSeparator(.visible) 356 if !minimalistic { 357 mandatory 358 } 359 if let iban = payto.iban { 360 step1i 361 PayeeReceiver(receiverStr: receiverStr) 362 if let receiverZip = payto.postalCode { 363 if !receiverZip.isEmpty { 364 PayeeZip(receiverZip: receiverZip) 365 } 366 } 367 if let receiverTown = payto.town { 368 if !receiverTown.isEmpty { 369 PayeeTown(receiverTown: receiverTown) 370 } 371 } 372 IbanCode(iban: iban) 373 } else if let cyclos = payto.cyclos, !cyclos.isEmpty { 374 step1x 375 PayeeReceiver(receiverStr: receiverStr) 376 // cyclosCode() 377 } else if let xTaler = payto.xTaler { 378 step1x 379 PayeeReceiver(receiverStr: receiverStr) 380 XTalerCode(xTaler: xTaler) 381 } 382 AmountCode(amountStr: amountStr, amountValue: amountValue) 383 step2(payto.chQRr != nil) 384 Cryptocode(cryptoString: cryptoString, chQRr: payto.chQRr) 385 // .padding(.top) 386 step3 // .padding(.top, 6) 387 }.listRowSeparator(.hidden) 388 } 389 .navigationTitle(navTitle) 390 .onAppear() { 391 // symLog.log("onAppear") 392 DebugViewC.shared.setViewID(VIEW_WITHDRAW_INSTRUCTIONS, stack: stack.push()) 393 } 394 } // if receiverStr 395 } 396 } 397 398 // MARK: - 399 #if DEBUG 400 //struct ManualDetailsWire_Previews: PreviewProvider { 401 // static var previews: some View { 402 // let common = TransactionCommon(type: .withdrawal, 403 // transactionId: "someTxID", 404 // timestamp: Timestamp(from: 1_666_666_000_000), 405 // txState: TransactionState(major: .done), 406 // txActions: []) 407 // amountEffective: Amount(currency: LONGCURRENCY, cent: 110), 408 // amountRaw: Amount(currency: LONGCURRENCY, cent: 220), 409 // let payto = "payto://iban/SANDBOXX/DE159593?receiver-name=Exchange+Company" 410 // let details = WithdrawalDetails(type: .manual, 411 // reservePub: "ReSeRvEpUbLiC_KeY_FoR_WiThDrAwAl", 412 // reserveIsReady: false, 413 // confirmed: false) 414 // List { 415 // ManualDetailsWireV(stack: CallStack("Preview"), 416 // details: details, 417 // receiverStr: <#T##String#>, 418 // iban: <#T##String?#>, 419 // xTaler: <#T##String#>, 420 // amountStr: <#T##String#>, 421 // obtainStr: <#T##String#>, 422 // account: T##ExchangeAccountDetails) 423 // } 424 // } 425 //} 426 #endif