taler-ios

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

CurrencyInputView.swift (10374B)


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