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