ManualDetailsWireV.swift (18301B)
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)? 15 let debitIBAN: String? 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 obtainStr { 42 Text(minimalistic ? transferMini(amountStr.0) 43 : transferMaxi(amountStr.0, obtainStr.0)) 44 .accessibilityLabel(minimalistic ? transferMini(amountStr.1) 45 : transferMaxi(amountStr.1, obtainStr.1)) 46 .talerFont(.body) 47 .multilineTextAlignment(.leading) 48 } else if let debitIBAN { 49 Text(minimalistic ? authMini(amountStr.0, debitIBAN) 50 : authMaxi(amountStr.0, debitIBAN)) 51 .accessibilityLabel(minimalistic ? authMini(amountStr.1, debitIBAN) 52 : authMaxi(amountStr.1, debitIBAN)) 53 .talerFont(.body) 54 .multilineTextAlignment(.leading) 55 } 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 ? 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(textToCopy: 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 ? Color.secondary : .primary) 133 .padding(.leading) 134 } .frame(maxWidth: .infinity, alignment: .leading) 135 .accessibilityElement(children: .combine) 136 .accessibilityLabel(Text("Recipient", comment: "a11y")) 137 CopyButton(textToCopy: 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 ? Color.secondary : .primary) 155 .padding(.leading) 156 } .frame(maxWidth: .infinity, alignment: .leading) 157 .accessibilityElement(children: .combine) 158 .accessibilityLabel(Text("City", comment: "a11y")) 159 CopyButton(textToCopy: 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 @State var isCopied: Bool = false 169 var body: some View { 170 HStack { 171 Text(cryptoString) 172 .foregroundStyle(isCopied ? Color.secondary : .primary) 173 .monospacedDigit() 174 .accessibilityLabel(Text("Cryptocode", comment: "a11y")) 175 .frame(maxWidth: .infinity, alignment: .leading) 176 CopyButton(textToCopy: cryptoString, isCopied: $isCopied, vertical: true) 177 .accessibilityLabel(Text("Copy the cryptocode", comment: "a11y")) 178 .disabled(false) 179 } .padding(.leading) 180 } 181 } 182 // MARK: - 183 struct IbanCode: View { 184 let iban: String 185 @State var isCopied: Bool = false 186 var body: some View { 187 HStack { 188 VStack(alignment: .leading) { 189 Text("IBAN:") // TODO: BBAN 190 .talerFont(.subheadline) 191 Text(iban) 192 .monospacedDigit() 193 .foregroundStyle(isCopied ? Color.secondary : .primary) 194 .padding(.leading) 195 } .frame(maxWidth: .infinity, alignment: .leading) 196 .accessibilityElement(children: .combine) 197 .accessibilityLabel(Text("IBAN of the recipient", comment: "a11y")) // TODO: BBAN 198 CopyButton(textToCopy: iban, isCopied: $isCopied, vertical: true) 199 .accessibilityLabel(Text("Copy the IBAN", comment: "a11y")) // TODO: BBAN 200 .disabled(false) 201 } // .padding(.top, -8) 202 } 203 } 204 // MARK: - 205 struct AmountCode: View { 206 let amountStr: (String, String) 207 let amountValue: String // string representation of the value, formatted as "`integer`.`fraction`" 208 @State var isCopied: Bool = false 209 var body: some View { 210 HStack { 211 VStack(alignment: .leading) { 212 Text("Amount:") 213 .talerFont(.subheadline) 214 Text(amountStr.0) 215 .accessibilityLabel(amountStr.1) 216 .monospacedDigit() 217 .foregroundStyle(isCopied ? Color.secondary : .primary) 218 .padding(.leading) 219 } .frame(maxWidth: .infinity, alignment: .leading) 220 .accessibilityElement(children: .combine) 221 .accessibilityLabel(Text("Amount to transfer", comment: "a11y")) 222 CopyButton(textToCopy: amountValue, isCopied: $isCopied, vertical: true) 223 // only digits + separator, no currency name or symbol 224 .accessibilityLabel(Text("Copy the amount", comment: "a11y")) 225 .disabled(false) 226 } .padding(.top, -8) 227 } 228 } 229 // MARK: - 230 struct XTalerCode: View { 231 let xTaler: String 232 @State var isCopied: Bool = false 233 var body: some View { 234 HStack { 235 VStack(alignment: .leading) { 236 Text("Account:") 237 .talerFont(.subheadline) 238 Text(xTaler) 239 .monospacedDigit() 240 .foregroundStyle(isCopied ? Color.secondary : .primary) 241 .padding(.leading) 242 } .frame(maxWidth: .infinity, alignment: .leading) 243 .accessibilityElement(children: .combine) 244 .accessibilityLabel(Text("account of the recipient", comment: "a11y")) 245 CopyButton(textToCopy: xTaler, isCopied: $isCopied, vertical: true) 246 .accessibilityLabel(Text("Copy the account", comment: "a11y")) 247 .disabled(false) 248 } .padding(.top, -8) 249 } 250 } 251 // MARK: - 252 struct ManualDetailsWireV: View { 253 let stack: CallStack 254 let reservePub: String 255 let receiverStr: String 256 let receiverZip: String? 257 let receiverTown: String? 258 let iban: String? // TODO: BBAN 259 let cyclos: String 260 let xTaler: String 261 let amountValue: String // string representation of the value, formatted as "`integer`.`fraction`" 262 let amountStr: (String, String) 263 let obtainStr: (String, String)? // only for withdrawal 264 let debitIBAN: String? // only for deposit auth 265 let account: ExchangeAccountDetails 266 267 @AppStorage("minimalistic") var minimalistic: Bool = false 268 let navTitle = String(localized: "Wire transfer", comment: "ViewTitle of wire-transfer instructions") 269 270 private func step3(_ amountS: String) -> String { 271 let amountNBS = amountS.nbs 272 let bePatient = String(localized: "Depending on your bank the transfer can take from minutes to two working days, please be patient.") 273 if let debitIBAN { 274 return minimalistic ? String(localized: "Transfer \(amountNBS) from \(debitIBAN).") 275 : String(localized: "Finish the wire transfer of \(amountNBS) in your banking app or website to verify your bank account \(debitIBAN).") + "\n" + bePatient 276 } 277 return minimalistic ? String(localized: "Transfer \(amountNBS).") 278 : String(localized: "Finish the wire transfer of \(amountNBS) in your banking app or website, then this withdrawal will proceed automatically.") + "\n" + bePatient 279 } 280 281 // @ViewBuilder func cyclosCode() -> some View { 282 // HStack { 283 // VStack(alignment: .leading) { 284 // Text("Cyclos:") 285 // .talerFont(.subheadline) 286 // Text(cyclos) 287 // .monospacedDigit() 288 // .padding(.leading) 289 // } .frame(maxWidth: .infinity, alignment: .leading) 290 // .accessibilityElement(children: .combine) 291 // .accessibilityLabel(Text("cyclos account of the recipient", comment: "a11y")) 292 // CopyButton(textToCopy: cyclos, vertical: true) 293 // .accessibilityLabel(Text("Copy the cyclos account", comment: "a11y")) 294 // .disabled(false) 295 // } .padding(.top, -8) 296 // } 297 298 299 var body: some View { 300 List { 301 let cryptoString = debitIBAN == nil ? reservePub : "kyc" + reservePub 302 let step2 = Text(minimalistic ? "**Step 2:** Copy+Paste this subject:" 303 : "**Step 2:** Copy this code and paste it into the subject/purpose field (or “Message to recipient”) in your banking app or bank website:") 304 .talerFont(.body) 305 .multilineTextAlignment(.leading) 306 let warningIcon = Image(systemName: WARNING) 307 let note = Text("**Note: Don't forget to copy and paste the code in Step 2.**") 308 let manda = debitIBAN == nil ? String(localized: "This is mandatory, otherwise your money will not arrive in this wallet.") 309 : String(localized: "This is mandatory, otherwise the verification will fail.") 310 let mandatory = Text("\(warningIcon) \(note)\n\(manda)") 311 .bold() 312 .talerFont(.body) 313 .multilineTextAlignment(.leading) 314 .listRowSeparator(.hidden) 315 let step1i = Text(minimalistic ? "**Step 1:** Copy+Paste recipient and IBAN:" 316 : "**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 317 .talerFont(.body) 318 .multilineTextAlignment(.leading) 319 .padding(.top) 320 let step1x = Text(minimalistic ? "**Step 1:** Copy+Paste recipient and account:" 321 : "**Step 1:** Copy and paste recipient and account into the corresponding fields in your banking app or website:") 322 .talerFont(.body) 323 .multilineTextAlignment(.leading) 324 .padding(.top) 325 let step3A11y = String(localized: "Step 3: \(step3(amountStr.1))", comment: "a11y") 326 let step3Head: LocalizedStringKey = "**Step 3:** \(step3(amountStr.0))" 327 let step3 = Text(step3Head) 328 .accessibilityLabel(step3A11y) 329 .talerFont(.body) 330 .multilineTextAlignment(.leading) 331 332 Group { 333 TransferRestrictionsV(amountStr: amountStr, 334 obtainStr: obtainStr, 335 debitIBAN: debitIBAN, 336 restrictions: account.creditRestrictions) 337 .listRowSeparator(.visible) 338 if !minimalistic { 339 mandatory 340 } 341 if let iban { 342 step1i 343 PayeeReceiver(receiverStr: receiverStr) 344 if let receiverZip { 345 if !receiverZip.isEmpty { 346 PayeeZip(receiverZip: receiverZip) 347 } 348 } 349 if let receiverTown { 350 if !receiverTown.isEmpty { 351 PayeeTown(receiverTown: receiverTown) 352 } 353 } 354 IbanCode(iban: iban) 355 } else if !cyclos.isEmpty { 356 step1x 357 PayeeReceiver(receiverStr: receiverStr) 358 // cyclosCode() 359 } else { 360 step1x 361 PayeeReceiver(receiverStr: receiverStr) 362 XTalerCode(xTaler: xTaler) 363 } 364 AmountCode(amountStr: amountStr, amountValue: amountValue) 365 step2 366 Cryptocode(cryptoString: cryptoString) 367 // .padding(.top) 368 step3 // .padding(.top, 6) 369 }.listRowSeparator(.hidden) 370 } 371 .navigationTitle(navTitle) 372 .onAppear() { 373 // symLog.log("onAppear") 374 DebugViewC.shared.setViewID(VIEW_WITHDRAW_INSTRUCTIONS, stack: stack.push()) 375 } 376 } 377 } 378 379 // MARK: - 380 #if DEBUG 381 //struct ManualDetailsWire_Previews: PreviewProvider { 382 // static var previews: some View { 383 // let common = TransactionCommon(type: .withdrawal, 384 // transactionId: "someTxID", 385 // timestamp: Timestamp(from: 1_666_666_000_000), 386 // txState: TransactionState(major: .done), 387 // txActions: []) 388 // amountEffective: Amount(currency: LONGCURRENCY, cent: 110), 389 // amountRaw: Amount(currency: LONGCURRENCY, cent: 220), 390 // let payto = "payto://iban/SANDBOXX/DE159593?receiver-name=Exchange+Company" 391 // let details = WithdrawalDetails(type: .manual, 392 // reservePub: "ReSeRvEpUbLiC_KeY_FoR_WiThDrAwAl", 393 // reserveIsReady: false, 394 // confirmed: false) 395 // List { 396 // ManualDetailsWireV(stack: CallStack("Preview"), 397 // details: details, 398 // receiverStr: <#T##String#>, 399 // iban: <#T##String?#>, 400 // xTaler: <#T##String#>, 401 // amountStr: <#T##String#>, 402 // obtainStr: <#T##String#>, 403 // account: T##ExchangeAccountDetails) 404 // } 405 // } 406 //} 407 #endif