taler-ios

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

ThreeAmountsSection.swift (10856B)


      1 /*
      2  * This file is part of GNU Taler, ©2022-25 Taler Systems S.A.
      3  * See LICENSE.md
      4  */
      5 /**
      6  * @author Marc Stibane
      7  */
      8 import SwiftUI
      9 import taler_swift
     10 
     11 struct ThreeAmountsSheet: View {    // should be in a separate file
     12     let stack: CallStack
     13     let scope: ScopeInfo?
     14     var common: TransactionCommon
     15     var topAbbrev: String
     16     var topTitle: String
     17     var bottomTitle: String?
     18     var bottomAbbrev: String?
     19     let baseURL: String?
     20     let noFees: Bool?                       // true if exchange charges no fees at all
     21     var feeIsNegative: Bool?                // show fee with minus (or plus) sign, or no sign if nil
     22     let large: Bool               // set to false for QR or IBAN
     23     let summary: String?
     24 
     25 #if DEBUG
     26     @AppStorage("developerMode") var developerMode: Bool = true
     27 #else
     28     @AppStorage("developerMode") var developerMode: Bool = false
     29 #endif
     30 
     31     var body: some View {
     32         let incoming = common.isIncoming
     33         let pending = common.isPending || common.isFinalizing
     34         let dialog = common.isDialog
     35         let isDone = common.isDone
     36         let incomplete = !(isDone || pending || dialog)
     37         let raw = common.amountRaw
     38         let effective: Amount? = incomplete ? nil : common.amountEffective
     39         let fee: Amount? = incomplete ? nil : common.fee()
     40 
     41         let defaultBottomTitle  = incoming ? (pending ? String(localized: "Pending amount to obtain:")
     42                                                       : String(localized: "Obtained amount:") )
     43                                            : (pending ? String(localized: "Amount to pay:")
     44                                                       : String(localized: "Paid amount:") )
     45         let defaultBottomAbbrev = incoming ? (pending ? String(localized: "Pending:", comment: "mini")
     46                                                       : String(localized: "Obtained:", comment: "mini") )
     47                                            : (pending ? String(localized: "Pay:", comment: "mini")
     48                                                       : String(localized: "Paid:", comment: "mini") )
     49         let majorLcl = common.txState.major.localizedState
     50         let txStateLcl = developerMode && pending ? (common.txState.minor?.localizedState ?? majorLcl)
     51                                                   : majorLcl
     52         ThreeAmountsSection(stack: stack.push(),
     53                             scope: scope,
     54                          topTitle: topTitle,
     55                         topAbbrev: topAbbrev,
     56                         topAmount: raw,
     57                            noFees: noFees,
     58                               fee: fee,
     59                     feeIsNegative: feeIsNegative,
     60                       bottomTitle: bottomTitle ?? defaultBottomTitle,
     61                      bottomAbbrev: bottomAbbrev ?? defaultBottomAbbrev,
     62                      bottomAmount: effective,
     63                             large: large,
     64                     pendingDialog: pending || dialog,
     65                            isDone: isDone,
     66                          incoming: incoming,
     67                           baseURL: baseURL,
     68                        txStateLcl: txStateLcl,
     69                           summary: summary)
     70     }
     71 }
     72 
     73 struct ProductImage: Codable, Hashable {
     74     var imageBase64: String
     75 
     76     var image: Image? {
     77         Image(imageBase64)
     78     }
     79 }
     80 
     81 // MARK: -
     82 struct ThreeAmountsSection: View {
     83     let stack: CallStack
     84     let scope: ScopeInfo?
     85     var topTitle: String
     86     var topAbbrev: String
     87     var topAmount: Amount
     88     let noFees: Bool?                       // true if exchange charges no fees at all
     89     var fee: Amount?                        // nil = don't show fee line, zero = no fee for this tx
     90     var feeIsNegative: Bool?                // show fee with minus (or plus) sign, or no sign if nil
     91     var bottomTitle: String
     92     var bottomAbbrev: String
     93     var bottomAmount: Amount?               // nil = incomplete (aborted, timed out)
     94     let large: Bool
     95     let pendingDialog: Bool
     96     let isDone: Bool
     97     let incoming: Bool
     98     let baseURL: String?
     99     let txStateLcl: String?                 // localizedState
    100     let summary: String?
    101 
    102     @EnvironmentObject private var controller: Controller
    103     @Environment(\.colorScheme) private var colorScheme
    104     @Environment(\.colorSchemeContrast) private var colorSchemeContrast
    105     @AppStorage("minimalistic") var minimalistic: Bool = false
    106 
    107     @State private var currencyInfo: CurrencyInfo = CurrencyInfo.zero(UNKNOWN)
    108 
    109     @MainActor
    110     private func viewDidLoad() async {
    111         if let scope {
    112             currencyInfo = controller.info(for: scope) ?? CurrencyInfo.zero(UNKNOWN)
    113         }
    114     }
    115 
    116 
    117     var body: some View {
    118         let currency = currencyInfo.currency
    119         let labelColor = WalletColors().labelColor
    120         let bottomColor = pendingDialog ? WalletColors().pendingColor(incoming)
    121                                         : WalletColors().transactionColor(incoming)
    122         let hasNoFees = noFees ?? false
    123         Section {
    124 //            if let summary {
    125 //                Text(summary)
    126 //                    .talerFont(.title3)
    127 //                    .padding(.bottom)
    128 //            }
    129 
    130             if currency != "UNKNOWN" {
    131                 if pendingDialog || isDone {
    132                     Text(pendingDialog ? "Payment will be made in \(currency)"
    133                                        : "Payment was made in \(currency)")
    134                         .talerFont(.callout)
    135                         .padding(.top, 4)
    136                 }
    137                 AmountRowV(stack: stack.push(),
    138                            title: minimalistic ? topAbbrev : topTitle,
    139                           amount: topAmount,
    140                            scope: scope,
    141                       isNegative: nil,
    142                            color: labelColor,
    143                            large: false)
    144                     .padding(.bottom, 4)
    145                 if hasNoFees == false {     // otherwise raw==effective
    146                     if let fee {
    147                         let title = minimalistic ? String(localized: "Exchange fee (short):", defaultValue: "Fee:",     comment: "short version")
    148                                                  : String(localized: "Exchange fee (long):", defaultValue: "Fee:",     comment: "long version")
    149                         AmountRowV(stack: stack.push(),
    150                                    title: title,
    151                                   amount: fee,
    152                                    scope: scope,
    153                               isNegative: fee.isZero ? nil : feeIsNegative,
    154                                    color: labelColor,
    155                                    large: false)
    156                         .padding(.bottom, 4)
    157                     }
    158                     if let bottomAmount {
    159                         AmountRowV(stack: stack.push(),
    160                                    title: minimalistic ? bottomAbbrev : bottomTitle,
    161                                   amount: bottomAmount,
    162                                    scope: scope,
    163                               isNegative: nil,
    164                                    color: bottomColor,
    165                                    large: large)
    166                     }
    167                 }
    168                 let serviceURL = scope?.url ?? baseURL
    169                 if let serviceURL {
    170                     VStack(alignment: .leading) {
    171                                    // TODO: "Issued by" for withdrawals
    172                         Text(minimalistic ? "Payment service:" : "Using payment service:")
    173                             .multilineTextAlignment(.leading)
    174                             .talerFont(.body)
    175                         Text(serviceURL.trimURL)
    176                             .frame(maxWidth: .infinity, alignment: .trailing)
    177                             .multilineTextAlignment(.center)
    178                             .talerFont(large ? .title3 : .body)
    179 //                            .fontWeight(large ? .medium : .regular)  // @available(iOS 16.0, *)
    180                             .foregroundColor(labelColor)
    181                     }
    182                     .padding(.top, 4)
    183                     .frame(maxWidth: .infinity, alignment: .leading)
    184                     .listRowSeparator(.hidden)
    185                     .accessibilityElement(children: .combine)
    186                 }
    187             }
    188         } header: {
    189             let header = scope?.url?.trimURL ?? scope?.currency ?? (summary != nil ? "Summary" : nil)
    190             if let header {
    191                 Text(header)
    192                     .talerFont(.title3)
    193                     .foregroundColor(WalletColors().secondary2(colorScheme, colorSchemeContrast))
    194             }
    195         }
    196         .task { await viewDidLoad() }
    197         .onChange(of: scope) { newVal in
    198             if let newVal {
    199                 currencyInfo = controller.info(for: newVal) ?? CurrencyInfo.zero(UNKNOWN)
    200             }
    201         }
    202     }
    203 }
    204 // MARK: -
    205 #if  DEBUG
    206 struct ThreeAmounts_Previews: PreviewProvider {
    207     @MainActor
    208     struct StateContainer: View {
    209 //        @State private var previewD: CurrencyInfo = CurrencyInfo.zero(DEMOCURRENCY)
    210 //        @State private var previewT: CurrencyInfo = CurrencyInfo.zero(TESTCURRENCY)
    211 
    212         var body: some View {
    213             let scope = ScopeInfo.zero(LONGCURRENCY)
    214             let common = TransactionCommon(type: .withdrawal,
    215                                   transactionId: "someTxID",
    216                                       timestamp: Timestamp(from: 1_666_666_000_000),
    217                                          scopes: [scope],
    218                                         txState: TransactionState(major: .done),
    219                                       txActions: [],
    220                                       amountRaw: Amount(currency: LONGCURRENCY, cent: 20),
    221                                 amountEffective: Amount(currency: LONGCURRENCY, cent: 10))
    222 //            let test = Amount(currency: TESTCURRENCY, cent: 123)
    223 //            let demo = Amount(currency: DEMOCURRENCY, cent: 123456)
    224             List {
    225                 ThreeAmountsSheet(stack: CallStack("Preview"),
    226                                   scope: scope,
    227                                  common: common, 
    228                               topAbbrev: "Withdrawal",
    229                                topTitle: "Withdrawal",
    230                                 baseURL: DEMOEXCHANGE,
    231                                  noFees: false,
    232                                   large: 1==0, summary: nil)
    233                 .safeAreaInset(edge: .bottom) {
    234                     Button(String("Preview")) {}
    235                         .buttonStyle(TalerButtonStyle(type: .prominent))
    236                         .padding(.horizontal)
    237                         .disabled(true)
    238                 }
    239             }
    240         }
    241     }
    242 
    243     static var previews: some View {
    244         StateContainer()
    245 //          .environment(\.sizeCategory, .extraExtraLarge)    Canvas Device Settings
    246     }
    247 }
    248 #endif