taler-ios

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

CurrencyInputView.swift (10127B)


      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 fileprivate let replaceable = 500
     12 fileprivate let shortcutValues = [5000,2500,1000]        // TODO: adapt for ¥
     13 
     14 struct ShortcutButton: View {
     15     let scope: ScopeInfo?
     16     let currency: String
     17     let currencyField: CurrencyField
     18     let shortcut: Int
     19     let available: Amount?                      // disable if available < value
     20     let action: (Int, CurrencyField) -> Void
     21 
     22     func makeButton(with newShortcut: Int) -> ShortcutButton {
     23         ShortcutButton(scope: scope,
     24                     currency: currency,
     25                currencyField: currencyField,
     26                     shortcut: newShortcut,
     27                    available: available,
     28                       action: action)
     29     }
     30 
     31     func isDisabled(shortie: Amount) -> Bool {
     32         if let available {
     33             return available.value < shortie.value
     34         }
     35         return false
     36     }
     37 
     38     var body: some View {
     39 #if PRINT_CHANGES
     40         let _ = Self._printChanges()
     41 //        let _ = symLog.vlog()       // just to get the # to compare it with .onAppear & onDisappear
     42 #endif
     43         let shortie = Amount(currency: currency, cent: UInt64(shortcut))        // TODO: adapt for ¥
     44         let title = shortie.formatted(scope, isNegative: false)
     45         let shortcutLabel = String(localized: "Shortcut", comment: "a11y: $50,$25,$10,$5 shortcut buttons")
     46         let a11yLabel = "\(shortcutLabel) \(title.1)"
     47         Button(action: { action(shortcut, currencyField)} ) {
     48             Text(title.0)
     49                 .lineLimit(1)
     50                 .talerFont(.callout)
     51         }
     52 //            .frame(maxWidth: .infinity)
     53             .disabled(isDisabled(shortie: shortie))
     54             .buttonStyle(.bordered)
     55             .accessibilityLabel(a11yLabel)
     56     }
     57 }
     58 // MARK: -
     59 struct CurrencyInputView: View {
     60     let scope: ScopeInfo?
     61     @Binding var amount: Amount         // the `value´
     62     let amountLastUsed: Amount
     63     let available: Amount?
     64     let title: String?
     65     let a11yTitle: String
     66     let shortcutAction: ((_ amount: Amount) -> Void)?
     67 
     68     @EnvironmentObject private var controller: Controller
     69     
     70     @State private var hasBeenShown = false
     71     @State private var useShortcut = 0
     72 
     73     @MainActor
     74     func action(shortcut: Int, currencyField: CurrencyField) {
     75         let shortie = Amount(currency: amount.currencyStr, cent: UInt64(shortcut))      // TODO: adapt for ¥
     76         if let shortcutAction {
     77             shortcutAction(shortie)
     78         } else {
     79             useShortcut = shortcut
     80             currencyField.updateText(amount: shortie)
     81             amount = shortie
     82             currencyField.resignFirstResponder()
     83         }
     84     }
     85 
     86     @MainActor
     87     func shortcut(for value: Int,_ currencyField: CurrencyField) -> ShortcutButton {
     88         var shortcut = value
     89         if value == replaceable {
     90             if !amountLastUsed.isZero {
     91                 let lastUsedD = amountLastUsed.value
     92                 let lastUsedI = lround(lastUsedD * 100)
     93                 if !shortcutValues.contains(lastUsedI) {
     94                     shortcut = lastUsedI
     95         }   }   }
     96         return ShortcutButton(scope: scope,
     97                            currency: amount.currencyStr,
     98                       currencyField: currencyField,
     99                            shortcut: shortcut,
    100                           available: available,
    101                              action: action)
    102     }
    103 
    104     @MainActor
    105     func shortcuts(_ currencyField: CurrencyField, _ currencyInfo: CurrencyInfo) -> [ShortcutButton] {
    106         var buttons: [ShortcutButton] = []
    107         if let commonAmounts = currencyInfo.commonAmounts {
    108             buttons = commonAmounts.prefix(4).map { amount in
    109                 shortcut(for: Int(amount.centValue), currencyField)
    110             }
    111         } else {
    112             buttons = shortcutValues.map { value in
    113                 shortcut(for: value, currencyField)
    114             }
    115             buttons.append(shortcut(for: replaceable, currencyField))
    116         }
    117         return buttons
    118     }
    119 
    120     func availableString(_ availableStr: String) -> String {
    121         String(localized: "Available for transfer: \(availableStr)")
    122     }
    123 
    124     var a11yLabel: String {        // format currency for a11y
    125         availableString(available?.readableDescription ?? String(localized: "unknown"))
    126     }
    127 
    128     func heading() -> (String, String)? {
    129         if let title {
    130             return (title, title)
    131         }
    132         if let available {
    133             let formatted = available.formatted(scope, isNegative: false)
    134             return (availableString(formatted.0), availableString(formatted.1))
    135         }
    136         return nil
    137     }
    138 
    139     func currencyInfo() -> CurrencyInfo {
    140         if let scope {
    141             return controller.info(for: scope, controller.currencyTicker)
    142         } else {
    143             return controller.info2(for: amount.currencyStr, controller.currencyTicker)
    144         }
    145     }
    146 
    147     var body: some View {
    148 #if PRINT_CHANGES
    149         let _ = Self._printChanges()
    150 //        let _ = symLog.vlog()       // just to get the # to compare it with .onAppear & onDisappear
    151 #endif
    152         let currencyInfo = currencyInfo()
    153         let currencyField = CurrencyField(currencyInfo, amount: $amount)
    154         VStack (alignment: .center) {   // center shortcut buttons
    155             if let heading = heading() {
    156                 Text(heading.0)
    157                     .padding(.horizontal, 4)
    158                     .padding(.top)
    159                     .frame(maxWidth: .infinity, alignment: title != nil ? .leading : .trailing)
    160                     .talerFont(.title2)
    161                     .accessibilityLabel(heading.1)
    162                     .padding(.bottom, -6)
    163             }
    164             currencyField
    165                 .accessibilityLabel(a11yTitle)
    166                 .frame(maxWidth: .infinity, alignment: .trailing)
    167                 .foregroundColor(WalletColors().fieldForeground)     // text color
    168 //                .background(WalletColors().fieldBackground)       // problem: white corners
    169                 .talerFont(.title2)
    170                 .textFieldStyle(.roundedBorder)
    171                 .onTapGesture {
    172                     if useShortcut != 0 {
    173                         amount = Amount.zero(currency: amount.currencyStr)
    174                         useShortcut = 0
    175                     }
    176                 }
    177             if #available(iOS 16.4, *) {
    178                 let shortcuts = shortcuts(currencyField, currencyInfo)
    179                 ViewThatFits(in: .horizontal) {
    180                     HStack {
    181                         ForEach(shortcuts, id: \.shortcut) {
    182                             $0.accessibilityAddTraits($0.shortcut == useShortcut ? .isSelected : [])
    183                         }
    184                     }
    185                     VStack {
    186                         let count = shortcuts.count
    187                         let half = count / 2
    188                         HStack {
    189                             Spacer()
    190                             ForEach(0..<half, id: \.self) { index in
    191                                 let thisShortcut = shortcuts[index]
    192                                 thisShortcut
    193                                     .accessibilityAddTraits(thisShortcut.shortcut == useShortcut ? .isSelected : [])
    194                                 Spacer()
    195                             }
    196                         }
    197                         HStack {
    198                             Spacer()
    199                             ForEach(half..<count, id: \.self) { index in
    200                                 let thisShortcut = shortcuts[index]
    201                                 thisShortcut
    202                                     .accessibilityAddTraits(thisShortcut.shortcut == useShortcut ? .isSelected : [])
    203                                 Spacer()
    204                             }
    205                         }
    206                     }
    207                     VStack {
    208                         ForEach(shortcuts, id: \.shortcut) {
    209                             $0.accessibilityAddTraits($0.shortcut == useShortcut ? .isSelected : [])
    210                         }
    211                     }
    212                 }
    213                 .padding(.vertical, 6)
    214             } // iOS 16+ only
    215         }.onAppear {   // make CurrencyField show the keyboard after 0.4 seconds
    216 #if OIM
    217             let oimModeActive = controller.oimModeActive
    218 #else
    219             let oimModeActive = false
    220 #endif
    221             if hasBeenShown {
    222 //                print("❗️Yikes: CurrencyInputView hasBeenShown")
    223             } else if !UIAccessibility.isVoiceOverRunning && !oimModeActive {
    224 //                print("❗️CurrencyInputView❗️")
    225                 DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) {
    226                     hasBeenShown = true
    227                     if !oimModeActive {
    228                         if !currencyField.becomeFirstResponder() {
    229                             print("❗️Yikes❗️ cannot becomeFirstResponder")
    230                         }
    231                     }
    232                 }
    233             }
    234         }.onDisappear {
    235             currencyField.resignFirstResponder()
    236             hasBeenShown = false
    237         }
    238 #if OIM
    239         .onChange(of: controller.oimModeActive) {_ in
    240             currencyField.resignFirstResponder()
    241         }
    242 #endif
    243     }
    244 }
    245 // MARK: -
    246 #if DEBUG
    247 //fileprivate struct Previews: PreviewProvider {
    248 //    @MainActor
    249 //    struct StateContainer: View {
    250 //        @State var amountToPreview = Amount(currency: LONGCURRENCY, cent: 0)
    251 //        @State var amountLastUsed = Amount(currency: LONGCURRENCY, cent: 170)
    252 //        @State private var previewL: CurrencyInfo = CurrencyInfo.zero(LONGCURRENCY)
    253 //        var body: some View {
    254 //            CurrencyInputView(amount: $amountToPreview,
    255 //                              scope: <#ScopeInfo#>,
    256 //                      amountLastUsed: amountLastUsed,
    257 //                           available: Amount(currency: LONGCURRENCY, cent: 2000),
    258 //                               title: "Amount to withdraw:",
    259 //                      shortcutAction: nil)
    260 //        }
    261 //    }
    262 //    static var previews: some View {
    263 //        StateContainer()
    264 //    }
    265 //}
    266 #endif