taler-ios

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

PaymentTransactionView.swift (8767B)


      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 taler_swift
     10 import SymLog
     11 
     12 // MARK: -
     13 struct MerchantHeader: View {
     14     let terms: MerchantContractTerms?
     15 
     16     @State private var image: Image? = nil
     17 
     18     func summary(_ terms: MerchantContractTerms) -> String {
     19         if let i18nDict = terms.summaryI18n {
     20             if !i18nDict.isEmpty {
     21                 for code in Locale.preferredLanguageCodes {
     22                     if let descI18n = i18nDict[code] {
     23                         return descI18n
     24                     }
     25                 }
     26             }
     27         }
     28         return terms.summary
     29     }
     30 
     31     var body: some View {
     32         if let terms {
     33             Section {
     34                 Text(summary(terms))
     35                     .talerFont(.title3)
     36             } header: {
     37                 HStack {
     38                     Spacer()
     39                     VStack(alignment: .center) {
     40                         if terms.merchant.logo != nil {
     41                             if let image {
     42                                 image
     43                                     .resizable()
     44                                     .aspectRatio(contentMode: .fit)
     45                                     .frame(maxHeight: 50)
     46                             }
     47                         } else {
     48 #if TALER_NIGHTLY
     49                             let imageName = if #available(iOS 17.0, *) { MERCHANT17 } else { MERCHANT14 }
     50                             Image(systemName: imageName)
     51                                 .resizable()
     52                                 .frame(width: 44, height: 44)
     53 #endif
     54                         }
     55                         let merchant = terms.merchant.name
     56                         Text(merchant)
     57                             .talerFont(.title3)
     58                     }   .foregroundStyle(Color(.primary))
     59                         .task {
     60                             if let imageBase64 = terms.merchant.logo {
     61                                 image = Image(imageBase64: imageBase64)
     62                             }
     63                         }
     64                     Spacer()
     65                 }
     66             }
     67         }
     68     }
     69 }
     70 // MARK: -
     71 struct PaymentTransactionView: View {
     72     private let symLog = SymLogV(0)
     73     let stack: CallStack
     74     let common: TransactionCommon
     75     let paymentTransaction: PaymentTransaction
     76     @Binding var scope: ScopeInfo?
     77     @Binding var effective: Amount?
     78     @Binding var payNow: Bool
     79     @Binding var selectedChoice: Int?
     80 
     81     @EnvironmentObject private var controller: Controller
     82     @EnvironmentObject private var model: WalletModel
     83 
     84     func summary(_ info: OrderShortInfo?) -> String? {
     85         if let i18nDict = info?.summary_i18n {
     86             if !i18nDict.isEmpty {
     87                 for code in Locale.preferredLanguageCodes {
     88                     if let i18n = i18nDict[code] {
     89                         return i18n
     90                     }
     91                 }
     92             }
     93         }
     94         if let summary = info?.summary {
     95             return summary
     96         }
     97         return String(localized: "No summary", comment: "OrderShortInfo.summary")
     98     }
     99 
    100     func selectIndex(_ index: Int?, of choices: [ChoiceTriple]) -> Int? {
    101         let index1 = index ?? 0
    102         let select = index1 < choices.count ? index1 : 0
    103         let choice = choices[select]
    104         let selectionDetail = choice.0
    105         if selectionDetail.status == .paymentPossible {
    106             selectedChoice = select
    107             effective = selectionDetail.amountEffective
    108             return select
    109         }
    110         return nil
    111     }
    112 
    113     func automaticOrDefault(_ automaticIndex: Int?,_ defaultChoiceIndex: Int?, of choices: [ChoiceTriple]) {
    114         if let automaticIndex {
    115             // Pay Automatically - usually with a subscription token
    116             if let selectedAutomatic = selectIndex(automaticIndex, of: choices) {
    117                 if selectedAutomatic == automaticIndex {
    118                     // check if automatic choice does NOT have an amount
    119                     let choiceTriple = choices[selectedAutomatic]
    120                     let selectionDetail = choiceTriple.0
    121                     if selectionDetail.status == .paymentPossible {
    122                         // we currently don't want to pay MONEY automatically, only tokens
    123                         // TODO: make the maximum value a user default
    124                         if selectionDetail.amountRaw.isZero {
    125                             payNow = true
    126                         }
    127                     }
    128                     return
    129                 } else if let defaultChoiceIndex {
    130                     if selectedAutomatic == defaultChoiceIndex { return }
    131                         // else fall thru and select defaultChoiceIndex
    132                 } else {      // there is no default - keep selectedAutomatic
    133                     return
    134                 }
    135             }
    136         }
    137         let _ = selectIndex(defaultChoiceIndex, of: choices)
    138     }
    139 
    140     var body: some View {
    141 #if PRINT_CHANGES
    142         let _ = Self._printChanges()
    143         let _ = symLog.vlog()
    144 #endif
    145         let details = paymentTransaction.details
    146         if common.isDialog {        // show payment confirmation dialog
    147             MerchantHeader(terms: details.contractTerms)
    148 
    149             let choicesTuple = controller.choicesTuple
    150             if let txId = choicesTuple.0, let choicesForPayment = choicesTuple.1 {
    151                 if txId == common.transactionId {
    152                     if let (choices, showHeader) = choicesForPayment.choiceTriple() {
    153                         let hasAutomatic = choicesForPayment.automaticExecution ?? false
    154                         let automaticIndex = hasAutomatic ? choicesForPayment.automaticExecutableIndex : nil
    155                         ChoicesView(stack: stack.push(),
    156                              choiceTriple: choices,
    157                                showHeader: showHeader,
    158                            automaticIndex: automaticIndex,
    159                            selectedChoice: $selectedChoice)
    160                         .task {
    161                             automaticOrDefault(automaticIndex, choicesForPayment.defaultChoiceIndex, of:    choices)
    162                         }
    163                         .onChange(of: selectedChoice) { newValue in
    164                             if let newValue, newValue < choices.count {
    165                                 let newChoice = choices[newValue]
    166                                 effective = newChoice.0.amountEffective
    167                                 scope = newChoice.0.scopeInfo
    168                             } else {
    169                                 effective = nil
    170                                 scope = nil
    171                             }
    172                         }
    173 
    174                         if let selectedChoice {
    175                             let choice = choices[selectedChoice]
    176                             let selectionDetail: ChoiceSelectionDetail = choice.0
    177     //                        let contractChoice: ContractChoice = choice.1
    178 
    179                             PaymentView2(stack: stack.push(),           // TODO: details.info.merchant.name
    180                                           paid: false,
    181                                            raw: selectionDetail.amountRaw,
    182                                      effective: effective,
    183                                     firstScope: scope,
    184                                        baseURL: nil,
    185                                        summary: summary(details.info),
    186                                       products: details.info?.products ?? [],
    187                                 balanceDetails: selectionDetail.balanceDetails)
    188                         }
    189                     } else { // should never happen...
    190                         // TODO: show some error
    191                     }
    192                 } else {    // cached data outdated
    193                     // TODO: purge and reload
    194                 }
    195             }
    196         } else { // show finished payment
    197             TransactionPayDetailV(stack: stack.push(),
    198                               paymentTx: paymentTransaction)    // TODO: details.info.merchant.name
    199             ThreeAmountsSheet(stack: stack.push(),
    200                               scope: scope,
    201                              common: common,
    202                           topAbbrev: String(localized: "Price:", comment: "mini"),
    203                            topTitle: String(localized: "Price (net):"),
    204                             baseURL: nil,               // TODO: baseURL
    205                              noFees: nil,               // TODO: noFees
    206                       feeIsNegative: false,
    207                               large: true,
    208                             summary: details.info?.summary ?? EMPTYSTRING)
    209         } // show finished payment
    210     } // body
    211 } // PaymentTransactionView