ManualDetailsWireV.swift (20275B)
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: I18nDict 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 private 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 private 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 private 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 private 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 private 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 private 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 private 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 private var isCopied1: Bool? = false 273 @State private 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).") + LINEFEED + 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.") + LINEFEED + 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 .talerFont(.headline) 335 .multilineTextAlignment(.leading) 336 .listRowSeparator(.hidden) 337 let step1i = Text(minimalistic ? "**Step 1:** Copy+Paste recipient and IBAN:" 338 : "**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 339 .talerFont(.body) 340 .multilineTextAlignment(.leading) 341 .padding(.top) 342 let step1x = Text(minimalistic ? "**Step 1:** Copy+Paste recipient and account:" 343 : "**Step 1:** Copy and paste recipient and account into the corresponding fields in your banking app or website:") 344 .talerFont(.body) 345 .multilineTextAlignment(.leading) 346 .padding(.top) 347 let step3A11y = String(localized: "Step 3: \(step3(amountStr.1))", comment: "a11y") 348 let step3Head: LocalizedStringKey = "**Step 3:** \(step3(amountStr.0))" 349 let step3 = Text(step3Head) 350 .accessibilityLabel(step3A11y) 351 .talerFont(.body) 352 .multilineTextAlignment(.leading) 353 354 Group { 355 TransferRestrictionsV(amountStr: amountStr, 356 obtainStr: obtainStr, 357 debitIBAN: debitIBAN, 358 restrictions: restrictions) 359 .listRowSeparator(.visible) 360 if !minimalistic { 361 mandatory 362 } 363 if let iban = payto.iban { 364 step1i 365 PayeeReceiver(receiverStr: receiverStr) 366 if let receiverZip = payto.postalCode { 367 if !receiverZip.isEmpty { 368 PayeeZip(receiverZip: receiverZip) 369 } 370 } 371 if let receiverTown = payto.town { 372 if !receiverTown.isEmpty { 373 PayeeTown(receiverTown: receiverTown) 374 } 375 } 376 IbanCode(iban: iban) 377 } else if let cyclos = payto.cyclos, !cyclos.isEmpty { 378 step1x 379 PayeeReceiver(receiverStr: receiverStr) 380 // cyclosCode() 381 } else if let xTaler = payto.xTaler { 382 step1x 383 PayeeReceiver(receiverStr: receiverStr) 384 XTalerCode(xTaler: xTaler) 385 } 386 AmountCode(amountStr: amountStr, amountValue: amountValue) 387 step2(payto.chQRr != nil) 388 Cryptocode(cryptoString: cryptoString, chQRr: payto.chQRr) 389 // .padding(.top) 390 step3 // .padding(.top, 6) 391 }.listRowSeparator(.hidden) 392 } 393 .navigationTitle(navTitle) 394 .onAppear() { 395 // symLog.log("onAppear") 396 DebugViewC.shared.setViewID(VIEW_WITHDRAW_INSTRUCTIONS, stack: stack.push()) 397 } 398 399 if #available(iOS 16.0, *) { 400 list.toolbar { 401 ToolbarTitleMenu { 402 if let debitPaytoStr { 403 CopyButton(paytoStr, isCopied: $isCopied1, title: "Copy validation payto") 404 CopyButton(debitPaytoStr, isCopied: $isCopied2, title: "Copy deposit payto") 405 } else { 406 CopyButton(paytoStr, isCopied: $isCopied1, title: "Copy transfer payto") 407 } 408 } 409 } 410 } else { 411 list 412 } 413 } // if receiverStr 414 } 415 } 416 417 // MARK: - 418 #if DEBUG 419 //struct ManualDetailsWire_Previews: PreviewProvider { 420 // static var previews: some View { 421 // let common = TransactionCommon(type: .withdrawal, 422 // transactionId: "someTxID", 423 // timestamp: Timestamp(from: 1_666_666_000_000), 424 // txState: TransactionState(major: .done), 425 // txActions: []) 426 // amountEffective: Amount(currency: LONGCURRENCY, cent: 110), 427 // amountRaw: Amount(currency: LONGCURRENCY, cent: 220), 428 // let payto = "payto://iban/SANDBOXX/DE159593?receiver-name=Exchange+Company" 429 // let details = WithdrawalDetails(type: .manual, 430 // reservePub: "ReSeRvEpUbLiC_KeY_FoR_WiThDrAwAl", 431 // reserveIsReady: false, 432 // confirmed: false) 433 // List { 434 // ManualDetailsWireV(stack: CallStack("Preview"), 435 // details: details, 436 // receiverStr: <#T##String#>, 437 // iban: <#T##String?#>, 438 // xTaler: <#T##String#>, 439 // amountStr: <#T##String#>, 440 // obtainStr: <#T##String#>, 441 // account: T##ExchangeAccountDetails) 442 // } 443 // } 444 //} 445 #endif