taler-ios

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

CurrencyInputView.swift (9740B)


      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) -> [ShortcutButton] {
    106         var buttons = shortcutValues.map { value in
    107             shortcut(for: value, currencyField)
    108         }
    109         buttons.append(shortcut(for: replaceable, currencyField))
    110         return buttons
    111     }
    112 
    113     func availableString(_ availableStr: String) -> String {
    114         String(localized: "Available for transfer: \(availableStr)")
    115     }
    116 
    117     var a11yLabel: String {        // format currency for a11y
    118         availableString(available?.readableDescription ?? String(localized: "unknown"))
    119     }
    120 
    121     func heading() -> String? {
    122         if let title {
    123             return title
    124         }
    125         if let available {
    126             let formatted = available.formatted(scope, isNegative: false)
    127             return availableString(formatted.0)
    128         }
    129         return nil
    130     }
    131 
    132     func currencyInfo() -> CurrencyInfo {
    133         if let scope {
    134             return controller.info(for: scope, controller.currencyTicker)
    135         } else {
    136             return controller.info2(for: amount.currencyStr, controller.currencyTicker)
    137         }
    138     }
    139 
    140     var body: some View {
    141 #if PRINT_CHANGES
    142         let _ = Self._printChanges()
    143 //        let _ = symLog.vlog()       // just to get the # to compare it with .onAppear & onDisappear
    144 #endif
    145         let currencyInfo = currencyInfo()
    146         let currencyField = CurrencyField(currencyInfo, amount: $amount)
    147         VStack (alignment: .center) {   // center shortcut buttons
    148             if let heading = heading() {
    149                 Text(heading)
    150                     .padding(.horizontal, 4)
    151                     .padding(.top)
    152                     .frame(maxWidth: .infinity, alignment: title != nil ? .leading : .trailing)
    153                     .talerFont(.title2)
    154                     .accessibilityHidden(true)
    155                     .padding(.bottom, -6)
    156             }
    157             currencyField
    158                 .accessibilityLabel(a11yTitle)
    159                 .frame(maxWidth: .infinity, alignment: .trailing)
    160                 .foregroundColor(WalletColors().fieldForeground)     // text color
    161 //                .background(WalletColors().fieldBackground)       // problem: white corners
    162                 .talerFont(.title2)
    163                 .textFieldStyle(.roundedBorder)
    164                 .onTapGesture {
    165                     if useShortcut != 0 {
    166                         amount = Amount.zero(currency: amount.currencyStr)
    167                         useShortcut = 0
    168                     }
    169                 }
    170             if #available(iOS 16.4, *) {
    171                 let shortcuts = shortcuts(currencyField)
    172                 ViewThatFits(in: .horizontal) {
    173                     HStack {
    174                         ForEach(shortcuts, id: \.shortcut) {
    175                             $0.accessibilityAddTraits($0.shortcut == useShortcut ? .isSelected : [])
    176                         }
    177                     }
    178                     VStack {
    179                         let count = shortcuts.count
    180                         let half = count / 2
    181                         HStack {
    182                             Spacer()
    183                             ForEach(0..<half, id: \.self) { index in
    184                                 let thisShortcut = shortcuts[index]
    185                                 thisShortcut
    186                                     .accessibilityAddTraits(thisShortcut.shortcut == useShortcut ? .isSelected : [])
    187                                 Spacer()
    188                             }
    189                         }
    190                         HStack {
    191                             Spacer()
    192                             ForEach(half..<count, id: \.self) { index in
    193                                 let thisShortcut = shortcuts[index]
    194                                 thisShortcut
    195                                     .accessibilityAddTraits(thisShortcut.shortcut == useShortcut ? .isSelected : [])
    196                                 Spacer()
    197                             }
    198                         }
    199                     }
    200                     VStack {
    201                         ForEach(shortcuts, id: \.shortcut) {
    202                             $0.accessibilityAddTraits($0.shortcut == useShortcut ? .isSelected : [])
    203                         }
    204                     }
    205                 }
    206                 .padding(.vertical, 6)
    207             } // iOS 16+ only
    208         }.onAppear {   // make CurrencyField show the keyboard after 0.4 seconds
    209 #if OIM
    210             let oimModeActive = controller.oimModeActive
    211 #else
    212             let oimModeActive = false
    213 #endif
    214             if hasBeenShown {
    215 //                print("❗️Yikes: CurrencyInputView hasBeenShown")
    216             } else if !UIAccessibility.isVoiceOverRunning && !oimModeActive {
    217 //                print("❗️CurrencyInputView❗️")
    218                 DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) {
    219                     hasBeenShown = true
    220                     if !oimModeActive {
    221                         if !currencyField.becomeFirstResponder() {
    222                             print("❗️Yikes❗️ cannot becomeFirstResponder")
    223                         }
    224                     }
    225                 }
    226             }
    227         }.onDisappear {
    228             currencyField.resignFirstResponder()
    229             hasBeenShown = false
    230         }
    231 #if OIM
    232         .onChange(of: controller.oimModeActive) {_ in
    233             currencyField.resignFirstResponder()
    234         }
    235 #endif
    236     }
    237 }
    238 // MARK: -
    239 #if DEBUG
    240 //fileprivate struct Previews: PreviewProvider {
    241 //    @MainActor
    242 //    struct StateContainer: View {
    243 //        @State var amountToPreview = Amount(currency: LONGCURRENCY, cent: 0)
    244 //        @State var amountLastUsed = Amount(currency: LONGCURRENCY, cent: 170)
    245 //        @State private var previewL: CurrencyInfo = CurrencyInfo.zero(LONGCURRENCY)
    246 //        var body: some View {
    247 //            CurrencyInputView(amount: $amountToPreview,
    248 //                              scope: <#ScopeInfo#>,
    249 //                      amountLastUsed: amountLastUsed,
    250 //                           available: Amount(currency: LONGCURRENCY, cent: 2000),
    251 //                               title: "Amount to withdraw:",
    252 //                      shortcutAction: nil)
    253 //        }
    254 //    }
    255 //    static var previews: some View {
    256 //        StateContainer()
    257 //    }
    258 //}
    259 #endif