taler-ios

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

ThreeAmountsSection.swift (12712B)


      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                          products: nil)
     71     }
     72 }
     73 
     74 struct ProductImage: Codable, Hashable {
     75     var imageBase64: String
     76     var description: String
     77     var price: Amount?
     78 
     79     init(_ image: String, _ desc: String, _ price: Amount?) {
     80         self.imageBase64 = image
     81         self.description = desc
     82         self.price = price
     83     }
     84 
     85     var image: Image? {
     86         Image(imageBase64)
     87     }
     88 }
     89 
     90 // MARK: -
     91 struct ThreeAmountsSection: View {
     92     let stack: CallStack
     93     let scope: ScopeInfo?
     94     var topTitle: String
     95     var topAbbrev: String
     96     var topAmount: Amount
     97     let noFees: Bool?                       // true if exchange charges no fees at all
     98     var fee: Amount?                        // nil = don't show fee line, zero = no fee for this tx
     99     var feeIsNegative: Bool?                // show fee with minus (or plus) sign, or no sign if nil
    100     var bottomTitle: String
    101     var bottomAbbrev: String
    102     var bottomAmount: Amount?               // nil = incomplete (aborted, timed out)
    103     let large: Bool
    104     let pendingDialog: Bool
    105     let isDone: Bool
    106     let incoming: Bool
    107     let baseURL: String?
    108     let txStateLcl: String?                 // localizedState
    109     let summary: String?
    110     let products: [Product]?
    111 
    112     @EnvironmentObject private var controller: Controller
    113     @Environment(\.colorScheme) private var colorScheme
    114     @Environment(\.colorSchemeContrast) private var colorSchemeContrast
    115     @AppStorage("minimalistic") var minimalistic: Bool = false
    116 
    117     @State private var productImages: [ProductImage] = []
    118     @State private var currencyInfo: CurrencyInfo = CurrencyInfo.zero(UNKNOWN)
    119 
    120     @MainActor
    121     private func viewDidLoad() async {
    122         var temp: [ProductImage] = []
    123         if let products {
    124             for product in products {
    125                 if let imageBase64 = product.image {
    126                     let productImage = ProductImage(imageBase64, product.description, product.price)
    127                     temp.append(productImage)
    128                 }
    129             }
    130         }
    131         productImages = temp
    132         if let scope {
    133             currencyInfo = controller.info(for: scope) ?? CurrencyInfo.zero(UNKNOWN)
    134         }
    135     }
    136 
    137     @ViewBuilder
    138     var productImageSections: some View {
    139         ForEach(productImages, id: \.self) { productImage in
    140             if let image = productImage.image {
    141                 Section {
    142                     HStack {
    143                         image.resizable()
    144                             .scaledToFill()
    145                             .frame(width: 64, height: 64)
    146                             .accessibilityHidden(true)
    147                         Text(productImage.description)
    148 //                        if let product_id = product.product_id {
    149 //                            Text(product_id)
    150 //                        }
    151                         if let price = productImage.price {
    152                             Spacer()
    153                             AmountV(scope, price, isNegative: nil)
    154                         }
    155                     }.talerFont(.body)
    156                         .accessibilityElement(children: .combine)
    157                 }
    158             }
    159         }
    160     }
    161 
    162     var body: some View {
    163         let currency = currencyInfo.currency
    164         let labelColor = WalletColors().labelColor
    165         let bottomColor = pendingDialog ? WalletColors().pendingColor(incoming)
    166                                         : WalletColors().transactionColor(incoming)
    167         let hasNoFees = noFees ?? false
    168         productImageSections
    169         Section {
    170             if let summary {
    171                 if productImages.isEmpty {  // otherwise we already have rendered the images
    172                     Text(summary)           // and thus don't need a summary
    173                         .talerFont(.title3)
    174                         .padding(.bottom)
    175                 }
    176             }
    177 
    178             if currency != "UNKNOWN" {
    179                 if pendingDialog || isDone {
    180                     Text(pendingDialog ? "Payment will be made in \(currency)"
    181                                        : "Payment was made in \(currency)")
    182                         .talerFont(.callout)
    183                         .padding(.top, 4)
    184                 }
    185                 AmountRowV(stack: stack.push(),
    186                            title: minimalistic ? topAbbrev : topTitle,
    187                           amount: topAmount,
    188                            scope: scope,
    189                       isNegative: nil,
    190                            color: labelColor,
    191                            large: false)
    192                     .padding(.bottom, 4)
    193                 if hasNoFees == false {     // otherwise raw==effective
    194                     if let fee {
    195                         let title = minimalistic ? String(localized: "Exchange fee (short):", defaultValue: "Fee:",     comment: "short version")
    196                                                  : String(localized: "Exchange fee (long):", defaultValue: "Fee:",     comment: "long version")
    197                         AmountRowV(stack: stack.push(),
    198                                    title: title,
    199                                   amount: fee,
    200                                    scope: scope,
    201                               isNegative: fee.isZero ? nil : feeIsNegative,
    202                                    color: labelColor,
    203                                    large: false)
    204                         .padding(.bottom, 4)
    205                     }
    206                     if let bottomAmount {
    207                         AmountRowV(stack: stack.push(),
    208                                    title: minimalistic ? bottomAbbrev : bottomTitle,
    209                                   amount: bottomAmount,
    210                                    scope: scope,
    211                               isNegative: nil,
    212                                    color: bottomColor,
    213                                    large: large)
    214                     }
    215                 }
    216                 let serviceURL = scope?.url ?? baseURL
    217                 if let serviceURL {
    218                     VStack(alignment: .leading) {
    219                                    // TODO: "Issued by" for withdrawals
    220                         Text(minimalistic ? "Payment service:" : "Using payment service:")
    221                             .multilineTextAlignment(.leading)
    222                             .talerFont(.body)
    223                         Text(serviceURL.trimURL)
    224                             .frame(maxWidth: .infinity, alignment: .trailing)
    225                             .multilineTextAlignment(.center)
    226                             .talerFont(large ? .title3 : .body)
    227 //                            .fontWeight(large ? .medium : .regular)  // @available(iOS 16.0, *)
    228                             .foregroundColor(labelColor)
    229                     }
    230                     .padding(.top, 4)
    231                     .frame(maxWidth: .infinity, alignment: .leading)
    232                     .listRowSeparator(.hidden)
    233                     .accessibilityElement(children: .combine)
    234                 }
    235             }
    236         } header: {
    237             let header = scope?.url?.trimURL ?? scope?.currency ?? summary != nil ? "Summary" : nil
    238             if let header {
    239                 Text(header)
    240                     .talerFont(.title3)
    241                     .foregroundColor(WalletColors().secondary(colorScheme, colorSchemeContrast))
    242             }
    243         }
    244         .task { await viewDidLoad() }
    245         .onChange(of: scope) { newVal in
    246             if let newVal {
    247                 currencyInfo = controller.info(for: newVal) ?? CurrencyInfo.zero(UNKNOWN)
    248             }
    249         }
    250     }
    251 }
    252 // MARK: -
    253 #if  DEBUG
    254 struct ThreeAmounts_Previews: PreviewProvider {
    255     @MainActor
    256     struct StateContainer: View {
    257 //        @State private var previewD: CurrencyInfo = CurrencyInfo.zero(DEMOCURRENCY)
    258 //        @State private var previewT: CurrencyInfo = CurrencyInfo.zero(TESTCURRENCY)
    259 
    260         var body: some View {
    261             let scope = ScopeInfo.zero(LONGCURRENCY)
    262             let common = TransactionCommon(type: .withdrawal,
    263                                   transactionId: "someTxID",
    264                                       timestamp: Timestamp(from: 1_666_666_000_000),
    265                                          scopes: [scope],
    266                                         txState: TransactionState(major: .done),
    267                                       txActions: [],
    268                                       amountRaw: Amount(currency: LONGCURRENCY, cent: 20),
    269                                 amountEffective: Amount(currency: LONGCURRENCY, cent: 10))
    270 //            let test = Amount(currency: TESTCURRENCY, cent: 123)
    271 //            let demo = Amount(currency: DEMOCURRENCY, cent: 123456)
    272             List {
    273                 ThreeAmountsSheet(stack: CallStack("Preview"),
    274                                   scope: scope,
    275                                  common: common, 
    276                               topAbbrev: "Withdrawal",
    277                                topTitle: "Withdrawal",
    278                                 baseURL: DEMOEXCHANGE,
    279                                  noFees: false,
    280                                   large: 1==0, summary: nil)
    281                 .safeAreaInset(edge: .bottom) {
    282                     Button(String("Preview")) {}
    283                         .buttonStyle(TalerButtonStyle(type: .prominent))
    284                         .padding(.horizontal)
    285                         .disabled(true)
    286                 }
    287             }
    288         }
    289     }
    290 
    291     static var previews: some View {
    292         StateContainer()
    293 //          .environment(\.sizeCategory, .extraExtraLarge)    Canvas Device Settings
    294     }
    295 }
    296 #endif