taler-ios

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

PaymentTransactionView.swift (11003B)


      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     @Environment(\.colorScheme) private var colorScheme
     17     @Environment(\.colorSchemeContrast) private var colorSchemeContrast
     18 
     19     @State private var image: Image? = nil
     20 
     21     func compactAddress(_ address: Location?) -> String? {
     22         if let address {
     23             var result: String = EMPTYSTRING
     24             if let street = address.street {
     25                 result.append(street)
     26             }
     27             if let town = address.town {
     28                 if !result.isEmpty {
     29                     result.append(", ")
     30                 }
     31                 result.append(town)
     32             }
     33             if let country = address.country {
     34                 if !result.isEmpty {
     35                     result.append(", ")
     36                 }
     37                 result.append(country)
     38             }
     39             return result.isEmpty ? nil : result
     40         }
     41         return nil
     42     }
     43 
     44     var body: some View {
     45         if let terms {
     46             Section {
     47                 Text(terms.locSummary)
     48                     .talerFont(.title3)
     49             } header: {
     50                 VStack(alignment: .leading) {
     51                     HStack {
     52                         if terms.merchant.logo != nil {
     53                             if let image {
     54                                 image
     55                                     .resizable()
     56                                     .aspectRatio(contentMode: .fit)
     57                                     .frame(maxHeight: 50)
     58                                     .padding(.trailing, 8)
     59                             }
     60                         } else {
     61 #if TALER_NIGHTLY
     62                             let imageName = if #available(iOS 17.0, *) { MERCHANT17 } else { MERCHANT14 }
     63                             Image(systemName: imageName)
     64                                 .resizable()
     65                                 .frame(width: 44, height: 44)
     66 #endif
     67                         }
     68                         VStack(alignment: .leading) {
     69                             let merchant = terms.merchant
     70                             Text(merchant.name)
     71                                 .talerFont(.title3)
     72                             if let address = compactAddress(merchant.address) {
     73                                 Text(address)
     74                                     .talerFont(.body)
     75                             }
     76                         }
     77                     }   .foregroundStyle(Color.primary)
     78                         .padding(.bottom)
     79                         .task {
     80                             if let imageBase64 = terms.merchant.logo {
     81                                 image = Image(imageBase64: imageBase64)
     82                             }
     83                         }
     84                     Text("Summary")
     85                         .talerFont(.title3)
     86                         .foregroundColor(WalletColors().secondary2(colorScheme, colorSchemeContrast))
     87                 }
     88             }
     89         }
     90     }
     91 }
     92 // MARK: -
     93 struct PaymentTransactionView: View {
     94     private let symLog = SymLogV(0)
     95     let stack: CallStack
     96     let common: TransactionCommon
     97     let paymentTransaction: PaymentTransaction
     98     @Binding var scope: ScopeInfo?
     99     @Binding var effective: Amount?
    100     @Binding var possible: Bool?
    101     @Binding var payNow: Bool
    102     @Binding var selectedChoice: Int?
    103 
    104     @EnvironmentObject private var controller: Controller
    105     @EnvironmentObject private var model: WalletModel
    106 
    107     func selectIndex(_ index: Int?, of choices: [ChoiceTriple]) -> Int? {
    108         let index1 = index ?? 0
    109         let select = index1 < choices.count ? index1 : 0
    110         let choice = choices[select]
    111         let selectionDetail = choice.0
    112         if selectionDetail.status == .paymentPossible {
    113             selectedChoice = select
    114             effective = selectionDetail.amountEffective
    115             possible = true
    116             return select
    117         } else {
    118             possible = false
    119         }
    120         return nil
    121     }
    122 
    123     func automaticOrDefault(_ automaticIndex: Int?,_ defaultChoiceIndex: Int?, of choices: [ChoiceTriple]) {
    124         if let automaticIndex {
    125             // Pay Automatically - usually with a subscription token
    126             if let selectedAutomatic = selectIndex(automaticIndex, of: choices) {
    127                 if selectedAutomatic == automaticIndex {
    128                     // check if automatic choice does NOT have an amount
    129                     let choiceTriple = choices[selectedAutomatic]
    130                     let selectionDetail = choiceTriple.0
    131                     if selectionDetail.status == .paymentPossible {
    132                         // we currently don't want to pay MONEY automatically, only tokens
    133                         // TODO: make the maximum value a user default
    134                         if selectionDetail.amountRaw.isZero {
    135                             payNow = true
    136                         }
    137                     }
    138                     return
    139                 } else if let defaultChoiceIndex {
    140                     if selectedAutomatic == defaultChoiceIndex { return }
    141                         // else fall thru and select defaultChoiceIndex
    142                 } else {      // there is no default - keep selectedAutomatic
    143                     return
    144                 }
    145             }
    146         }
    147         _ = selectIndex(defaultChoiceIndex, of: choices)
    148     }
    149 
    150     var isNotClaiming: Bool {
    151         !common.isClaiming
    152     }
    153 
    154     func selectedOrFirst(_ choices: [ChoiceTriple]) -> ChoiceTriple? {
    155         if let selectedChoice {
    156             if selectedChoice < choices.count {
    157                 return choices[selectedChoice]
    158             }
    159         }
    160         if let choice = choices.first {
    161             return choice
    162         }
    163         return nil
    164     }
    165 
    166     var body: some View {
    167 #if PRINT_CHANGES
    168         let _ = Self._printChanges()
    169         let _ = symLog.vlog()
    170 #endif
    171         let details = paymentTransaction.details
    172         if common.isDialog {        // show payment confirmation dialog
    173             let state = common.txState
    174             MerchantHeader(terms: details.contractTerms)
    175                 .task(id: state.hashValue) {
    176                     let choicesTuple = controller.choicesTuple
    177                     let choicesForPayment = choicesTuple.1
    178                     if choicesTuple.0 != common.transactionId || choicesForPayment == nil {
    179                         symLog.log("❗️❗️ task: \(state)")
    180                         await controller.loadChoicesForPayment(stack.push(),
    181                                                           txId: common.transactionId)
    182                     } else {
    183                         symLog.log("❗️❗️ choicesForPayment exists, no need for task: \(state)")
    184                     }
    185                 } // loadChoicesForPayment
    186 
    187             let choicesTuple = controller.choicesTuple
    188             if let txId = choicesTuple.0, let choicesForPayment = choicesTuple.1 {
    189                 if txId == common.transactionId {
    190                     let terms = choicesForPayment.contractTerms
    191                     if let products = terms.products {
    192                         ProductsView(stack: stack.push(),
    193                                      scope: scope,
    194                                   products: products)
    195                     }
    196                     if let (choices, showHeader) = choicesForPayment.choiceTriple() {
    197                         let hasAutomatic = choicesForPayment.automaticExecution ?? false
    198                         let automaticIndex = hasAutomatic ? choicesForPayment.automaticExecutableIndex : nil
    199                         ChoicesView(stack: stack.push(),
    200                             choiceTriples: choices,
    201                             tokenFamilies: terms.tokenFamilies,
    202                                showHeader: showHeader,
    203                            automaticIndex: automaticIndex,
    204                            selectedChoice: $selectedChoice)
    205                         .task {
    206                             automaticOrDefault(automaticIndex, choicesForPayment.defaultChoiceIndex, of: choices)
    207                         }
    208                         .onChange(of: selectedChoice) { newValue in
    209                             if let newValue, newValue < choices.count {
    210                                 let newChoice = choices[newValue]
    211                                 effective = newChoice.0.amountEffective
    212                                 possible = newChoice.0.status == .paymentPossible
    213                                 scope = newChoice.0.scopeInfo
    214                             } else {
    215                                 effective = nil
    216                                 possible = nil
    217                                 scope = nil
    218                             }
    219                         }
    220 
    221                         if let choice = selectedOrFirst(choices) {
    222                             let summary = details.info?.locSummary ??
    223                                           String(localized: "No summary", comment: "OrderShortInfo.summary")
    224                             let selectionDetail: ChoiceSelectionDetail = choice.0
    225     //                        let contractChoice: ContractChoice = choice.1
    226                             PaymentView2(stack: stack.push(),           // TODO: details.info.merchant.name
    227                                           paid: false,
    228                                            raw: selectionDetail.amountRaw,
    229                                      effective: effective,              // V0?
    230                                     firstScope: scope,
    231                                        baseURL: nil,
    232                                        summary: summary,
    233                                 balanceDetails: selectionDetail.balanceDetails)
    234                         }
    235                     } else { // should never happen...
    236                         // TODO: show some error
    237                     }
    238                 } else {    // cached data outdated
    239                     // TODO: purge and reload
    240                 }
    241             }
    242         } else if isNotClaiming { // show finished payment
    243             PaidTransactionDetailV(stack: stack.push(),
    244                                paymentTx: paymentTransaction)           // TODO: details.info.merchant.name
    245             ThreeAmountsSheet(stack: stack.push(),
    246                               scope: scope,
    247                              common: common,
    248                           topAbbrev: String(localized: "Price:", comment: "mini"),
    249                            topTitle: String(localized: "Price (net):"),
    250                             baseURL: nil,               // TODO: baseURL
    251                              noFees: nil,               // TODO: noFees
    252                       feeIsNegative: false,
    253                               large: true,
    254                             summary: details.info?.locSummary ?? EMPTYSTRING)
    255         } // show finished payment
    256     } // body
    257 } // PaymentTransactionView