taler-ios

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

ThreeAmountsSection.swift (11780B)


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