taler-ios

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

ThreeAmountsSection.swift (12678B)


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