taler-ios

iOS apps for GNU Taler (wallet)
Log | Files | Refs | README | LICENSE

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