taler-ios

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

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