ManualDetailsWireV.swift (20215B)
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 let paytoStr: String 262 let debitPaytoStr: String? 263 264 let restrictions: [AccountRestriction]? // only if restrictions apply 265 // let iban: String? // TODO: BBAN 266 let amountValue: String // string representation of the value, formatted as "`integer`.`fraction`" 267 let amountStr: (String, String) 268 let obtainStr: (String, String)? // only for withdrawal 269 let debitIBAN: String? // only for deposit auth 270 271 @AppStorage("minimalistic") var minimalistic: Bool = false 272 @State var isCopied1: Bool? = false 273 @State var isCopied2: Bool? = false 274 275 let navTitle = String(localized: "Wire transfer", comment: "ViewTitle of wire-transfer instructions") 276 277 private func step3(_ amountS: String) -> String { 278 let amountNBS = amountS.nbs 279 let bePatient = String(localized: "Depending on your bank the transfer can take from minutes to two working days, please be patient.") 280 if let debitIBAN { 281 return minimalistic ? String(localized: "Transfer \(amountNBS) from \(debitIBAN).") 282 : String(localized: "Finish the wire transfer of \(amountNBS) in your banking app or website to verify your bank account \(debitIBAN).") + "\n" + bePatient 283 } 284 return minimalistic ? String(localized: "Transfer \(amountNBS).") 285 : String(localized: "Finish the wire transfer of \(amountNBS) in your banking app or website, then this withdrawal will proceed automatically.") + "\n" + bePatient 286 } 287 288 /// The subject of the wire transfer 289 private var cryptoString: String { 290 // chQRr only consists of digits - no prefix possible 291 if let chQRr = payto.chQRr, !chQRr.isEmpty { 292 return chQRr 293 } 294 // if let messageStr = payto.messageStr, !messageStr.isEmpty { 295 // return messageStr 296 // } 297 return debitIBAN != nil ? "KYC:" + reservePub : reservePub 298 } 299 300 // @ViewBuilder func cyclosCode() -> some View { 301 // HStack { 302 // VStack(alignment: .leading) { 303 // Text("Cyclos:") 304 // .talerFont(.subheadline) 305 // Text(cyclos) 306 // .monospacedDigit() 307 // .padding(.leading) 308 // } .frame(maxWidth: .infinity, alignment: .leading) 309 // .accessibilityElement(children: .combine) 310 // .accessibilityLabel(Text("cyclos account of the recipient", comment: "a11y")) 311 // CopyButton(textToCopy: cyclos, vertical: true) 312 // .accessibilityLabel(Text("Copy the cyclos account", comment: "a11y")) 313 // .disabled(false) 314 // } .padding(.top, -8) 315 // } 316 317 @ViewBuilder func step2(_ isChQrr: Bool) -> some View { 318 Text(isChQrr ? (minimalistic ? "**Step 2:** Copy+Paste this QR-Reference:" 319 : "**Step 2:** Copy this code and paste it into the QR-Reference field in your banking app or bank website:") 320 : (minimalistic ? "**Step 2:** Copy+Paste this subject:" 321 : "**Step 2:** Copy this code and paste it into the subject/purpose field (or “Message to recipient”) in your banking app or bank website:")) 322 .talerFont(.body) 323 .multilineTextAlignment(.leading) 324 } 325 326 var body: some View { 327 if let receiverStr = payto.receiver { 328 let list = List { 329 let warningIcon = Image(systemName: WARNING) 330 let note = Text("**Note: Don't forget to copy and paste the code in Step 2.**") 331 let manda = debitIBAN == nil ? String(localized: "This is mandatory, otherwise your money will not arrive in this wallet.") 332 : String(localized: "This is mandatory, otherwise the verification will fail.") 333 let mandatory = Text("\(warningIcon) \(note)\n\(manda)") 334 .bold() 335 .talerFont(.body) 336 .multilineTextAlignment(.leading) 337 .listRowSeparator(.hidden) 338 let step1i = Text(minimalistic ? "**Step 1:** Copy+Paste recipient and IBAN:" 339 : "**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 340 .talerFont(.body) 341 .multilineTextAlignment(.leading) 342 .padding(.top) 343 let step1x = Text(minimalistic ? "**Step 1:** Copy+Paste recipient and account:" 344 : "**Step 1:** Copy and paste recipient and account into the corresponding fields in your banking app or website:") 345 .talerFont(.body) 346 .multilineTextAlignment(.leading) 347 .padding(.top) 348 let step3A11y = String(localized: "Step 3: \(step3(amountStr.1))", comment: "a11y") 349 let step3Head: LocalizedStringKey = "**Step 3:** \(step3(amountStr.0))" 350 let step3 = Text(step3Head) 351 .accessibilityLabel(step3A11y) 352 .talerFont(.body) 353 .multilineTextAlignment(.leading) 354 355 Group { 356 TransferRestrictionsV(amountStr: amountStr, 357 obtainStr: obtainStr, 358 debitIBAN: debitIBAN, 359 restrictions: restrictions) 360 .listRowSeparator(.visible) 361 if !minimalistic { 362 mandatory 363 } 364 if let iban = payto.iban { 365 step1i 366 PayeeReceiver(receiverStr: receiverStr) 367 if let receiverZip = payto.postalCode { 368 if !receiverZip.isEmpty { 369 PayeeZip(receiverZip: receiverZip) 370 } 371 } 372 if let receiverTown = payto.town { 373 if !receiverTown.isEmpty { 374 PayeeTown(receiverTown: receiverTown) 375 } 376 } 377 IbanCode(iban: iban) 378 } else if let cyclos = payto.cyclos, !cyclos.isEmpty { 379 step1x 380 PayeeReceiver(receiverStr: receiverStr) 381 // cyclosCode() 382 } else if let xTaler = payto.xTaler { 383 step1x 384 PayeeReceiver(receiverStr: receiverStr) 385 XTalerCode(xTaler: xTaler) 386 } 387 AmountCode(amountStr: amountStr, amountValue: amountValue) 388 step2(payto.chQRr != nil) 389 Cryptocode(cryptoString: cryptoString, chQRr: payto.chQRr) 390 // .padding(.top) 391 step3 // .padding(.top, 6) 392 }.listRowSeparator(.hidden) 393 } 394 .navigationTitle(navTitle) 395 .onAppear() { 396 // symLog.log("onAppear") 397 DebugViewC.shared.setViewID(VIEW_WITHDRAW_INSTRUCTIONS, stack: stack.push()) 398 } 399 400 if #available(iOS 16.0, *) { 401 list.toolbar { 402 ToolbarTitleMenu { 403 if let debitPaytoStr { 404 CopyButton(paytoStr, isCopied: $isCopied1, title: "Copy validation payto") 405 CopyButton(debitPaytoStr, isCopied: $isCopied2, title: "Copy deposit payto") 406 } else { 407 CopyButton(paytoStr, isCopied: $isCopied1, title: "Copy transfer payto") 408 } 409 } 410 } 411 } else { 412 list 413 } 414 } // if receiverStr 415 } 416 } 417 418 // MARK: - 419 #if DEBUG 420 //struct ManualDetailsWire_Previews: PreviewProvider { 421 // static var previews: some View { 422 // let common = TransactionCommon(type: .withdrawal, 423 // transactionId: "someTxID", 424 // timestamp: Timestamp(from: 1_666_666_000_000), 425 // txState: TransactionState(major: .done), 426 // txActions: []) 427 // amountEffective: Amount(currency: LONGCURRENCY, cent: 110), 428 // amountRaw: Amount(currency: LONGCURRENCY, cent: 220), 429 // let payto = "payto://iban/SANDBOXX/DE159593?receiver-name=Exchange+Company" 430 // let details = WithdrawalDetails(type: .manual, 431 // reservePub: "ReSeRvEpUbLiC_KeY_FoR_WiThDrAwAl", 432 // reserveIsReady: false, 433 // confirmed: false) 434 // List { 435 // ManualDetailsWireV(stack: CallStack("Preview"), 436 // details: details, 437 // receiverStr: <#T##String#>, 438 // iban: <#T##String?#>, 439 // xTaler: <#T##String#>, 440 // amountStr: <#T##String#>, 441 // obtainStr: <#T##String#>, 442 // account: T##ExchangeAccountDetails) 443 // } 444 // } 445 //} 446 #endif