taler-ios

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

ThreeAmountsSection.swift (12969B)


      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 bottomColor = 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 currency != "UNKNOWN" {
    186                 if pendingDialog || isDone {
    187                     Text(pendingDialog ? "Payment will be made in \(currency)"
    188                                        : "Payment was made in \(currency)")
    189                         .talerFont(.callout)
    190                         .padding(.top, 4)
    191                 }
    192                 AmountRowV(stack: stack.push(),
    193                            title: minimalistic ? topAbbrev : topTitle,
    194                           amount: topAmount,
    195                            scope: scope,
    196                       isNegative: nil,
    197                            color: labelColor,
    198                            large: false)
    199                     .padding(.bottom, 4)
    200                 if hasNoFees == false {     // otherwise raw==effective
    201                     if let fee {
    202                         let title = minimalistic ? String(localized: "Exchange fee (short):", defaultValue: "Fee:",     comment: "short version")
    203                                                  : String(localized: "Exchange fee (long):", defaultValue: "Fee:",     comment: "long version")
    204                         AmountRowV(stack: stack.push(),
    205                                    title: title,
    206                                   amount: fee,
    207                                    scope: scope,
    208                               isNegative: fee.isZero ? nil : feeIsNegative,
    209                                    color: labelColor,
    210                                    large: false)
    211                         .padding(.bottom, 4)
    212                     }
    213                     if let bottomAmount {
    214                         AmountRowV(stack: stack.push(),
    215                                    title: minimalistic ? bottomAbbrev : bottomTitle,
    216                                   amount: bottomAmount,
    217                                    scope: scope,
    218                               isNegative: nil,
    219                                    color: bottomColor,
    220                                    large: large)
    221                     }
    222                 }
    223                 let serviceURL = scope?.url ?? baseURL
    224                 if let serviceURL {
    225                     VStack(alignment: .leading) {
    226                                    // TODO: "Issued by" for withdrawals
    227                         Text(minimalistic ? "Payment service:" : "Using payment service:")
    228                             .multilineTextAlignment(.leading)
    229                             .talerFont(.body)
    230                         Text(serviceURL.trimURL)
    231                             .frame(maxWidth: .infinity, alignment: .trailing)
    232                             .multilineTextAlignment(.center)
    233                             .talerFont(large ? .title3 : .body)
    234 //                            .fontWeight(large ? .medium : .regular)  // @available(iOS 16.0, *)
    235                             .foregroundColor(labelColor)
    236                     }
    237                     .padding(.top, 4)
    238                     .frame(maxWidth: .infinity, alignment: .leading)
    239                     .listRowSeparator(.hidden)
    240                     .accessibilityElement(children: .combine)
    241                 }
    242             }
    243         } header: {
    244             let header = scope?.url?.trimURL ?? scope?.currency ?? summary != nil ? "Summary" : nil
    245             if let header {
    246                 Text(header)
    247                     .talerFont(.title3)
    248                     .foregroundColor(WalletColors().secondary(colorScheme, colorSchemeContrast))
    249             }
    250         }
    251         .task { await viewDidLoad() }
    252         .onChange(of: scope) { newVal in
    253             if let newVal {
    254                 currencyInfo = controller.info(for: newVal) ?? CurrencyInfo.zero(UNKNOWN)
    255             }
    256         }
    257     }
    258 }
    259 // MARK: -
    260 #if  DEBUG
    261 struct ThreeAmounts_Previews: PreviewProvider {
    262     @MainActor
    263     struct StateContainer: View {
    264 //        @State private var previewD: CurrencyInfo = CurrencyInfo.zero(DEMOCURRENCY)
    265 //        @State private var previewT: CurrencyInfo = CurrencyInfo.zero(TESTCURRENCY)
    266 
    267         var body: some View {
    268             let scope = ScopeInfo.zero(LONGCURRENCY)
    269             let common = TransactionCommon(type: .withdrawal,
    270                                   transactionId: "someTxID",
    271                                       timestamp: Timestamp(from: 1_666_666_000_000),
    272                                          scopes: [scope],
    273                                         txState: TransactionState(major: .done),
    274                                       txActions: [],
    275                                       amountRaw: Amount(currency: LONGCURRENCY, cent: 20),
    276                                 amountEffective: Amount(currency: LONGCURRENCY, cent: 10))
    277 //            let test = Amount(currency: TESTCURRENCY, cent: 123)
    278 //            let demo = Amount(currency: DEMOCURRENCY, cent: 123456)
    279             List {
    280                 ThreeAmountsSheet(stack: CallStack("Preview"),
    281                                   scope: scope,
    282                                  common: common, 
    283                               topAbbrev: "Withdrawal",
    284                                topTitle: "Withdrawal",
    285                                 baseURL: DEMOEXCHANGE,
    286                                  noFees: false,
    287                                   large: 1==0, summary: nil)
    288                 .safeAreaInset(edge: .bottom) {
    289                     Button(String("Preview")) {}
    290                         .buttonStyle(TalerButtonStyle(type: .prominent))
    291                         .padding(.horizontal)
    292                         .disabled(true)
    293                 }
    294             }
    295         }
    296     }
    297 
    298     static var previews: some View {
    299         StateContainer()
    300 //          .environment(\.sizeCategory, .extraExtraLarge)    Canvas Device Settings
    301     }
    302 }
    303 #endif