taler-ios

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

ManualDetailsWireV.swift (19500B)


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