taler-ios

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

CurrencyInputView.swift (10448B)


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