taler-ios

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

ThreeAmountsSection.swift (12388B)


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