commit 64e8fc4e3d9b5271d657db3e367365e61dbbf9da parent 8c1b94805cae9d8533a96fd373478dcb7dea315f Author: Marc Stibane <marc@taler.net> Date: Thu, 5 Dec 2024 23:01:20 +0100 A11y as tupel Diffstat:
15 files changed, 262 insertions(+), 185 deletions(-)
diff --git a/TalerWallet1/Helper/CurrencySpecification.swift b/TalerWallet1/Helper/CurrencySpecification.swift @@ -38,30 +38,41 @@ extension Locale { extension Amount { func formatted(_ currencyInfo: CurrencyInfo?, isNegative: Bool, - useISO: Bool = false, a11y: String? = nil - ) -> String { + useISO: Bool = false, a11yDecSep: String? = nil + ) -> (String, String) { if let currencyInfo { - return currencyInfo.string(for: valueAsFloatTuple, isNegative: isNegative, currency: currencyStr, - useISO: useISO, a11y: a11y) + let a11y = currencyInfo.string(for: valueAsFloatTuple, + isNegative: isNegative, + currency: currencyStr, + useISO: true, + a11yDecSep: a11yDecSep) + let strg = currencyInfo.string(for: valueAsFloatTuple, + isNegative: isNegative, + currency: currencyStr, + useISO: useISO, + a11yDecSep: nil) + return (strg, a11y) } else { - return valueStr + return (valueStr, valueStr) } } // this is the function to use - func formatted(_ scope: ScopeInfo?, isNegative: Bool, useISO: Bool = false, a11y: String? = nil) -> String { + func formatted(_ scope: ScopeInfo?, isNegative: Bool, + useISO: Bool = false, a11yDecSep: String? = nil + ) -> (String, String) { let controller = Controller.shared if let scope { if let currencyInfo = controller.info(for: scope) { - return self.formatted(currencyInfo, isNegative: isNegative, useISO: useISO, a11y: a11y) + return self.formatted(currencyInfo, isNegative: isNegative, useISO: useISO, a11yDecSep: a11yDecSep) } } - return self.readableDescription + return (self.readableDescription, self.readableDescription) } - func formatted(specs: CurrencySpecification?, isNegative: Bool, scope: ScopeInfo? = nil, - useISO: Bool = false - ) -> String { + func formatted(specs: CurrencySpecification?, isNegative: Bool, + scope: ScopeInfo? = nil, useISO: Bool = false + ) -> (String, String) { if let specs { let formatter = CurrencyFormatter.formatter(currency: currencyStr, specs: specs) let currencyInfo = CurrencyInfo(specs: specs, formatter: formatter) @@ -69,7 +80,7 @@ extension Amount { } else if let scope { return formatted(scope, isNegative: isNegative, useISO: useISO) } - return self.readableDescription + return (self.readableDescription, self.readableDescription) } func inputDigits(_ currencyInfo: CurrencyInfo) -> UInt { @@ -193,7 +204,7 @@ public struct CurrencyInfo: Sendable { // TODO: use valueAsDecimalTuple instead of valueAsFloatTuple func string(for valueTuple: (Double, Double), isNegative: Bool, currency: String, - useISO: Bool = false, a11y: String? = nil) -> String { + useISO: Bool = false, a11yDecSep: String? = nil) -> String { formatter.setUseISO(useISO) let (integer, fraction) = valueTuple if let integerStr = formatter.string(for: isNegative ? -integer : integer) { @@ -227,8 +238,8 @@ public struct CurrencyInfo: Sendable { } } // print(resultStr) - if let a11y { - resultStr = resultStr.replacingOccurrences(of: decimalSeparator, with: a11y) + if let a11yDecSep { + resultStr = resultStr.replacingOccurrences(of: decimalSeparator, with: a11yDecSep) } return currencyString(resultStr, useISO: useISO) } diff --git a/TalerWallet1/Views/Actions/Banking/DepositAmountView.swift b/TalerWallet1/Views/Actions/Banking/DepositAmountView.swift @@ -84,7 +84,7 @@ struct DepositAmountView: View { private func buttonTitle(_ amount: Amount) -> String { if let balance { let amountWithCurrency = amount.formatted(balance.scopeInfo, isNegative: false, useISO: true) - return DepositAmountView.navTitle(amountWithCurrency, true) + return DepositAmountView.navTitle(amountWithCurrency.0, true) } return DepositAmountView.navTitle() // should never happen } diff --git a/TalerWallet1/Views/Actions/Banking/QuiteSomeCoins.swift b/TalerWallet1/Views/Actions/Banking/QuiteSomeCoins.swift @@ -18,13 +18,18 @@ struct CoinData { var tooMany: Bool { numCoins > 999 } let fee: Amount? - func feeLabel(_ scope: ScopeInfo?, feeZero: String?, isNegative: Bool = false) -> String { - return if let fee { - fee.isZero ? feeZero ?? EMPTYSTRING // String(localized: "No withdrawal fee") - : isNegative ? String(localized: "- \(fee.formatted(scope, isNegative: false)) fee") - : String(localized: "+ \(fee.formatted(scope, isNegative: false)) fee") + func feeLabel(_ scope: ScopeInfo?, feeZero: String?, isNegative: Bool = false) -> (String, String) { + if let fee { + let formatted = fee.formatted(scope, isNegative: false) + let feeLabel = fee.isZero ? feeZero ?? EMPTYSTRING // String(localized: "No withdrawal fee") + : isNegative ? String(localized: "- \(formatted.0) fee") + : String(localized: "+ \(formatted.0) fee") + let feeA11Y = fee.isZero ? feeZero ?? EMPTYSTRING // String(localized: "No withdrawal fee") + : isNegative ? String(localized: "- \(formatted.1) fee") + : String(localized: "+ \(formatted.1) fee") + return (feeLabel, feeA11Y) } else { - EMPTYSTRING + return (EMPTYSTRING, EMPTYSTRING) } } } @@ -76,7 +81,8 @@ struct QuiteSomeCoins: View { let feeLabel = coinData.feeLabel(scope, feeZero: String(localized: "No fee"), isNegative: feeIsNegative) - Text(feeLabel) + Text(feeLabel.0) + .accessibilityLabel(feeLabel.1) .foregroundColor(.primary) .talerFont(.body) } diff --git a/TalerWallet1/Views/Actions/Peer2peer/P2PSubjectV.swift b/TalerWallet1/Views/Actions/Peer2peer/P2PSubjectV.swift @@ -22,7 +22,7 @@ struct P2PSubjectV: View { private let symLog = SymLogV(0) let stack: CallStack let scope: ScopeInfo - let feeLabel: String? + let feeLabel: (String, String)? let feeIsNotZero: Bool? // nil = no fees at all, false = no fee for this tx let outgoing: Bool @Binding var amountToTransfer: Amount @@ -34,25 +34,37 @@ struct P2PSubjectV: View { @Environment(\.colorSchemeContrast) private var colorSchemeContrast @AppStorage("minimalistic") var minimalistic: Bool = false - @State private var myFeeLabel: String = EMPTYSTRING + @State private var myFeeLabel: (String, String) = (EMPTYSTRING, EMPTYSTRING) @State private var transactionStarted: Bool = false @FocusState private var isFocused: Bool - private func buttonTitle(_ amount: Amount) -> String { + private func sendTitle(_ amountWithCurrency: String) -> String { + String(localized: "Send \(amountWithCurrency) now", + comment: "amount with currency") + } + private func requTitle(_ amountWithCurrency: String) -> String { + String(localized: "Request \(amountWithCurrency)", + comment: "amount with currency") + } + + private func buttonTitle(_ amount: Amount) -> (String, String) { let amountWithCurrency = amount.formatted(scope, isNegative: false, useISO: true) - return outgoing ? String(localized: "Send \(amountWithCurrency) now", - comment: "amount with currency") - : String(localized: "Request \(amountWithCurrency)", - comment: "amount with currency") + return outgoing ? (sendTitle(amountWithCurrency.0), sendTitle(amountWithCurrency.1)) + : (requTitle(amountWithCurrency.0), requTitle(amountWithCurrency.1)) + } + + private var placeHolder: String { + return outgoing ? String(localized: "Sent with GNU TALER") + : String(localized: "Requested with GNU TALER") } private func subjectTitle(_ amount: Amount) -> String { let amountStr = amount.formatted(scope, isNegative: false) return outgoing ? String(localized: "NavTitle_Send_AmountStr", - defaultValue: "Send \(amountStr)", + defaultValue: "Send \(amountStr.0)", comment: "NavTitle: Send 'amountStr'") : String(localized: "NavTitle_Request_AmountStr", - defaultValue: "Request \(amountStr)", + defaultValue: "Request \(amountStr.0)", comment: "NavTitle: Request 'amountStr'") } @@ -62,8 +74,9 @@ struct P2PSubjectV: View { if let ppCheck = try? await model.checkPeerPushDebit(amountToTransfer, scope: scope) { if let feeAmount = p2pFee(ppCheck: ppCheck) { let feeStr = feeAmount.formatted(scope, isNegative: false) - myFeeLabel = String(localized: "+ \(feeStr) fee") - } else { myFeeLabel = EMPTYSTRING } + myFeeLabel = (String(localized: "+ \(feeStr.0) fee"), + String(localized: "+ \(feeStr.1) fee")) + } else { myFeeLabel = (EMPTYSTRING, EMPTYSTRING) } } else { print("❗️ checkPeerPushDebitM failed") @@ -79,78 +92,68 @@ struct P2PSubjectV: View { ScrollView { VStack (alignment: .leading, spacing: 6) { if let feeIsNotZero { // don't show fee if nil let label = feeLabel ?? myFeeLabel - if label.count > 0 { - Text(label) + if label.0.count > 0 { + Text(label.0) + .accessibilityLabel(label.1) .frame(maxWidth: .infinity, alignment: .trailing) .foregroundColor(WalletColors().secondary(colorScheme, colorSchemeContrast)) .talerFont(.body) } } - if !minimalistic { - Text("Enter subject:") // Purpose - .talerFont(.title3) - .accessibilityAddTraits(.isHeader) - .accessibilityRemoveTraits(.isStaticText) - .padding(.top) + let enterSubject = String(localized: "Enter subject") + let enterColon = String("\(enterSubject):") + if !minimalistic { + Text(enterSubject) // Purpose + .talerFont(.title3) + .accessibilityHidden(true) + .padding(.top) + } + Group { if #available(iOS 16.0, *) { + TextField(placeHolder, text: $summary, axis: .vertical) + } else { + TextField(placeHolder, text: $summary) + } } + .talerFont(.title2) + .accessibilityLabel(enterColon) + .submitLabel(.next) + .focused($isFocused) + .onChange(of: summary) { newValue in + guard isFocused else { return } + guard newValue.contains("\n") else { return } + isFocused = false + summary = newValue.replacingOccurrences(of: LINEFEED, with: EMPTYSTRING) } - Group { if #available(iOS 16.0, *) { - TextField(minimalistic ? "Subject" : EMPTYSTRING, text: $summary, axis: .vertical) - .submitLabel(.next) - .focused($isFocused) - .onChange(of: summary) { newValue in - guard isFocused else { return } - guard newValue.contains(LINEFEED) else { return } - isFocused = false - summary = newValue.replacing(LINEFEED, with: EMPTYSTRING) - } - } else { - TextField("Subject", text: $summary) - .submitLabel(.next) - .focused($isFocused) - .onChange(of: summary) { newValue in - guard isFocused else { return } - guard newValue.contains("\n") else { return } - isFocused = false - summary = newValue.replacingOccurrences(of: LINEFEED, with: EMPTYSTRING) - } - } } // Group for iOS16+ & iOS15 - .talerFont(.title2) - .foregroundColor(WalletColors().fieldForeground) // text color - .background(WalletColors().fieldBackground) - .textFieldStyle(.roundedBorder) - .onAppear { - if !UIAccessibility.isVoiceOverRunning { - symLog.log("dispatching kbd...") - DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) { - isFocused = true // make first responder - raise keybord - symLog.log("...kbd isFocused") - } - } - } - Text(verbatim: "\(summary.count)/100") // maximum 100 characters - .frame(maxWidth: .infinity, alignment: .trailing) - .talerFont(.body) - .accessibilityValue(String(localized: "\(summary.count) characters of 100")) + .foregroundColor(WalletColors().fieldForeground) // text color + .background(WalletColors().fieldBackground) + .textFieldStyle(.roundedBorder) + Text(verbatim: "\(summary.count)/100") // maximum 100 characters + .frame(maxWidth: .infinity, alignment: .trailing) + .talerFont(.body) + .accessibilityLabel(EMPTYSTRING) + .accessibilityValue(String(localized: "\(summary.count) characters of 100")) // TODO: compute max Expiration day from peerPushCheck to disable 30 (and even 7) SelectDays(selected: $expireDays, maxExpiration: THIRTYDAYS, outgoing: outgoing) .disabled(false) .padding(.bottom) - let disabled = (expireDays == 0) || (summary.count < 1) // TODO: check amountAvailable + let disabled = (expireDays == 0) // || (summary.count < 1) // TODO: check amountAvailable let destination = P2PReadyV(stack: stack.push(), scope: scope, - summary: summary, + summary: summary.count > 0 ? summary : placeHolder, expireDays: expireDays, outgoing: outgoing, amountToTransfer: amountToTransfer, transactionStarted: $transactionStarted) NavigationLink(destination: destination) { - Text(buttonTitle(amountToTransfer)) + let buttonTitle = buttonTitle(amountToTransfer) + Text(buttonTitle.0) + .accessibilityLabel(buttonTitle.1) } .buttonStyle(TalerButtonStyle(type: .prominent, disabled: disabled)) .disabled(disabled) - .accessibilityHint(disabled ? String(localized: "enabled when subject and expiration are set") : EMPTYSTRING) + .accessibilityHint(disabled ? String(localized: "enabled when subject and expiration are set", comment: "VoiceOver") + : EMPTYSTRING) }.padding(.horizontal) } // ScrollVStack // .scrollBounceBehavior(.basedOnSize) needs iOS 16.4 .navigationTitle(subjectTitle(amountToTransfer)) diff --git a/TalerWallet1/Views/Actions/Peer2peer/RequestPayment.swift b/TalerWallet1/Views/Actions/Peer2peer/RequestPayment.swift @@ -36,7 +36,7 @@ struct RequestPayment: View { } private func navTitle(_ currency: String, _ condition: Bool = false) -> String { - condition ? String(localized: "NavTitle_Request_Currency)", + condition ? String(localized: "NavTitle_Request_Currency", defaultValue: "Request \(currency)", comment: "NavTitle: Request 'currency'") : String(localized: "NavTitle_Request", @@ -118,7 +118,7 @@ struct RequestPaymentContent: View { @State private var peerPullCheck: CheckPeerPullCreditResponse? = nil @State private var expireDays: UInt = 0 // @State private var feeAmount: Amount? = nil - @State private var feeStr: String = EMPTYSTRING + @State private var feeString = (EMPTYSTRING, EMPTYSTRING) @State private var buttonSelected = false @State private var shortcutSelected = false @State private var amountShortcut = Amount.zero(currency: EMPTYSTRING) // Update currency when used @@ -131,9 +131,9 @@ struct RequestPaymentContent: View { } private func buttonAction() { buttonSelected = true } - private func feeLabel(_ feeString: String) -> String { - feeString.count > 0 ? String(localized: "- \(feeString) fee") - : EMPTYSTRING + private func feeLabel(_ feeStr: String) -> String { + feeStr.count > 0 ? String(localized: "- \(feeStr) fee") + : EMPTYSTRING } private func fee(raw: Amount, effective: Amount) -> Amount? { @@ -169,16 +169,16 @@ struct RequestPaymentContent: View { let raw = ppCheck.amountRaw let effective = ppCheck.amountEffective if let fee = fee(raw: raw, effective: effective) { - feeStr = fee.formatted(balance.scopeInfo, isNegative: true) - symLog.log("Fee = \(feeStr)") - + feeString = fee.formatted(balance.scopeInfo, isNegative: false) + symLog.log("Fee = \(feeString.0)") + peerPullCheck = ppCheck - let feeLabel = feeLabel(feeStr) + let feeLabel = (feeLabel(feeString.0), feeLabel(feeString.1)) // announce("\(amountVoiceOver), \(feeLabel)") return ComputeFeeResult(insufficient: false, - feeAmount: fee, - feeStr: feeLabel, - numCoins: ppCheck.numCoins) + feeAmount: fee, + feeStr: feeLabel, // TODO: feeLabelA11y + numCoins: ppCheck.numCoins) } else { peerPullCheck = nil } diff --git a/TalerWallet1/Views/Actions/Peer2peer/SendAmountView.swift b/TalerWallet1/Views/Actions/Peer2peer/SendAmountView.swift @@ -27,7 +27,7 @@ struct SendAmountView: View { @State private var expireDays = SEVENDAYS @State private var insufficient = false // @State private var feeAmount: Amount? = nil - @State private var feeStr: String = EMPTYSTRING + @State private var feeString = (EMPTYSTRING, EMPTYSTRING) @State private var buttonSelected = false @State private var shortcutSelected = false @State private var amountShortcut = Amount.zero(currency: EMPTYSTRING) // Update currency when used @@ -48,9 +48,9 @@ struct SendAmountView: View { comment: "NavTitle: Send") } - private func feeLabel(_ feeString: String) -> String { - feeString.count > 0 ? String(localized: "+ \(feeString) fee") - : EMPTYSTRING + private func feeLabel(_ feeStr: String) -> String { + feeStr.count > 0 ? String(localized: "+ \(feeStr) fee") + : EMPTYSTRING } private func fee(raw: Amount, effective: Amount) -> Amount? { @@ -85,12 +85,12 @@ struct SendAmountView: View { let raw = ppCheck.amountRaw let effective = ppCheck.amountEffective if let fee = fee(raw: raw, effective: effective) { - feeStr = fee.formatted(balance.scopeInfo, isNegative: false) - symLog.log("Fee = \(feeStr)") + feeString = fee.formatted(balance.scopeInfo, isNegative: false) + symLog.log("Fee = \(feeString.0)") let insufficient = (try? effective > amountAvailable) ?? true peerPushCheck = ppCheck - let feeLabel = feeLabel(feeStr) + let feeLabel = (feeLabel(feeString.0), feeLabel(feeString.1)) // announce("\(amountVoiceOver), \(feeLabel)") return ComputeFeeResult(insufficient: insufficient, feeAmount: fee, @@ -142,7 +142,7 @@ struct SendAmountView: View { let inputDestination = P2PSubjectV(stack: stack.push(), scope: balance.scopeInfo, - feeLabel: feeLabel(feeStr), + feeLabel: (feeLabel(feeString.0), feeLabel(feeString.1)), feeIsNotZero: feeIsNotZero(), outgoing: true, amountToTransfer: $amountToTransfer, // from the textedit diff --git a/TalerWallet1/Views/HelperViews/AmountInputV.swift b/TalerWallet1/Views/HelperViews/AmountInputV.swift @@ -12,19 +12,19 @@ import SymLog struct ComputeFeeResult { let insufficient: Bool let feeAmount: Amount? - let feeStr: String + let feeStr: (String, String) let numCoins: Int? static func zero() -> ComputeFeeResult { ComputeFeeResult(insufficient: false, feeAmount: nil, - feeStr: EMPTYSTRING, + feeStr: (EMPTYSTRING, EMPTYSTRING), numCoins: 0) } static func insufficient() -> ComputeFeeResult { ComputeFeeResult(insufficient: true, feeAmount: nil, - feeStr: EMPTYSTRING, + feeStr: (EMPTYSTRING, EMPTYSTRING), numCoins: -1) } } @@ -52,7 +52,7 @@ struct AmountInputV: View { @Environment(\.colorSchemeContrast) private var colorSchemeContrast @State private var feeAmount: Amount? = nil - @State private var feeStr: String = EMPTYSTRING + @State private var feeStr = (EMPTYSTRING, EMPTYSTRING) @State private var numCoins: Int? struct Flags { diff --git a/TalerWallet1/Views/HelperViews/AmountV.swift b/TalerWallet1/Views/HelperViews/AmountV.swift @@ -16,7 +16,7 @@ struct AmountV: View { let useISO: Bool let strikethrough: Bool let large: Bool // set to false for QR or IBAN - let a11y: String? + let a11yDecSep: String? @EnvironmentObject private var controller: Controller @@ -35,30 +35,36 @@ struct AmountV: View { } } - private func amountStr(_ currencyInfo : CurrencyInfo?) -> String { + private func amountStr(_ currencyInfo : CurrencyInfo?) -> (String, String) { let dontShow = (isNegative == nil) let showSign: Bool = isNegative ?? false - let amountFormatted: String + let readable = amount.readableDescription + let amountFormatted: (String, String) if let currencyInfo { amountFormatted = amount.formatted(currencyInfo, isNegative: false, - useISO: useISO, a11y: a11y) + useISO: useISO, a11yDecSep: a11yDecSep) } else { - amountFormatted = amount.readableDescription + amountFormatted = (readable, readable) } - let amountStr = dontShow ? amountFormatted - : showSign ? "- \(amountFormatted)" - : "+ \(amountFormatted)" - return amountStr + let amountStr = dontShow ? amountFormatted.0 + : showSign ? "- \(amountFormatted.0)" + : "+ \(amountFormatted.0)" + let amountA11y = dontShow ? amountFormatted.1 + : showSign ? "- \(amountFormatted.1)" + : "+ \(amountFormatted.1)" + + return (amountStr, amountA11y) } var body: some View { - Text(amountStr(currencyInfo)) + let amountTuple = amountStr(currencyInfo) + Text(amountTuple.0) .strikethrough(strikethrough, color: WalletColors().attention) .multilineTextAlignment(.center) .talerFont(large ? .title : .title2) // .fontWeight(large ? .medium : .regular) // @available(iOS 16.0, *) .monospacedDigit() - .accessibilityLabel(amount.readableDescription) // TODO: locale.leadingCurrencySymbol + .accessibilityLabel(amountTuple.1) // TODO: locale.leadingCurrencySymbol .task(id: controller.currencyTicker) { await currencyTickerChanged() } // .onLongPressGesture(minimumDuration: 0.3) { // showValue = true @@ -78,7 +84,7 @@ extension AmountV { self.useISO = false self.strikethrough = false self.large = false - self.a11y = nil + self.a11yDecSep = nil } init(_ scope: ScopeInfo?, _ amount: Amount, isNegative: Bool?) { self.stack = nil @@ -88,7 +94,7 @@ extension AmountV { self.useISO = false self.strikethrough = false self.large = false - self.a11y = nil + self.a11yDecSep = nil } init(_ scope: ScopeInfo?, _ amount: Amount, isNegative: Bool?, strikethrough: Bool) { self.stack = nil @@ -98,7 +104,7 @@ extension AmountV { self.useISO = false self.strikethrough = strikethrough self.large = false - self.a11y = nil + self.a11yDecSep = nil } init(stack: CallStack?, scope: ScopeInfo?, amount: Amount, isNegative: Bool?, strikethrough: Bool, large: Bool = false) { @@ -109,7 +115,7 @@ extension AmountV { self.useISO = false self.strikethrough = strikethrough self.large = false - self.a11y = nil + self.a11yDecSep = nil } } // MARK: - diff --git a/TalerWallet1/Views/HelperViews/CurrencyField.swift b/TalerWallet1/Views/HelperViews/CurrencyField.swift @@ -63,7 +63,9 @@ struct CurrencyField: View { ZStack { // Text view to display the formatted currency // Set as priority so CurrencyInputField size doesn't affect parent - let text = Text(amount.formatted(currencyInfo, isNegative: false)) + let formatted = amount.formatted(currencyInfo, isNegative: false) + let text = Text(formatted.0) + .accessibilityLabel(formatted.1) .layoutPriority(1) // make the textfield use the whole width for tapping inside to become active .frame(maxWidth: .infinity, alignment: .trailing) diff --git a/TalerWallet1/Views/HelperViews/CurrencyInputView.swift b/TalerWallet1/Views/HelperViews/CurrencyInputView.swift @@ -48,15 +48,16 @@ struct ShortcutButton: View { let shortie = Amount(currency: currency, cent: UInt64(shortcut)) // TODO: adapt for ¥ let title = shortie.formatted(scope, isNegative: false) let shortcutLabel = String(localized: "Shortcut", comment: "VoiceOver: $50,$25,$10,$5 shortcut buttons") + let a11yLabel = "\(shortcutLabel) \(title.1)" Button(action: { action(shortcut, currencyField)} ) { - Text(title) + Text(title.0) .lineLimit(1) .talerFont(.callout) } // .frame(maxWidth: .infinity) .disabled(isDisabled(shortie: shortie)) .buttonStyle(.bordered) - .accessibilityLabel("\(shortcutLabel) \(title)") + .accessibilityLabel(a11yLabel) } } // MARK: - @@ -128,7 +129,8 @@ struct CurrencyInputView: View { return title } if let available { - return availableString(available.formatted(scope, isNegative: false)) + let formatted = available.formatted(scope, isNegative: false) + return availableString(formatted.0) } return nil } diff --git a/TalerWallet1/Views/HelperViews/QRCodeDetailView.swift b/TalerWallet1/Views/HelperViews/QRCodeDetailView.swift @@ -25,17 +25,35 @@ struct QRCodeDetailView: View { currencyInfo = controller.info(for: scope) } - private func amountStr(_ currencyInfo : CurrencyInfo?) -> String { - let amountFormatted: String + private func amountStr(_ currencyInfo : CurrencyInfo?) -> (String, String) { + let amountFormatted: (String, String) + let readable = amount.readableDescription if let currencyInfo { amountFormatted = amount.formatted(currencyInfo, isNegative: false, - useISO: false, a11y: nil) + useISO: false, a11yDecSep: nil) } else { - amountFormatted = amount.readableDescription + amountFormatted = (readable, readable) } return amountFormatted } + private func requesting(_ amountS: String) -> String { + minimalistic ? String(localized: "(payer) 1 mini", + defaultValue: "Requesting \(amountS)", + comment: "e.g. '5,3 €'") + : String(localized: "(payer) 1", + defaultValue: "To receive \(amountS), have the payer scan this QR code to complete the payment.", + comment: "e.g. '5,3 €'") + } + private func sending(_ amountS: String) -> String { + minimalistic ? String(localized: "(payee) 1 mini", + defaultValue: "Sending \(amountS)", + comment: "e.g. '$ 7.41'") + : String(localized: "(payee) 1", + defaultValue: "To send \(amountS), let the payee scan this QR code to accept/receive the payment.", + comment: "e.g. '$ 7.41'") + } + var body: some View { if talerURI.count > 10 { Section { @@ -56,19 +74,10 @@ struct QRCodeDetailView: View { .accessibilityLabel("QR Code") .listRowSeparator(.hidden) let amountStr = amountStr(currencyInfo) - let scanLong = incoming ? (minimalistic ? String(localized: "(payer) 1 mini", - defaultValue: "Requesting \(amountStr)", - comment: "e.g. '5,3 €'") - : String(localized: "(payer) 1", - defaultValue: "To receive \(amountStr), have the payer scan this QR code.", - comment: "e.g. '5,3 €'")) - : (minimalistic ? String(localized: "(payee) 1 mini", - defaultValue: "Sending \(amountStr)", - comment: "e.g. '$ 7.41'") - : String(localized: "(payee) 1", - defaultValue: "Sending \(amountStr) - let the payee scan this QR code to receive the payment.", - comment: "e.g. '$ 7.41'")) - Text(scanLong) + let scanLong = incoming ? (requesting(amountStr.0), requesting(amountStr.1)) + : (sending(amountStr.0), sending(amountStr.1)) + Text(scanLong.0) + .accessibilityLabel(scanLong.1) .multilineTextAlignment(.leading) .talerFont(.title3) .listRowSeparator(.hidden) diff --git a/TalerWallet1/Views/HelperViews/ScopePicker.swift b/TalerWallet1/Views/HelperViews/ScopePicker.swift @@ -8,7 +8,7 @@ import SwiftUI import taler_swift -fileprivate func formattedAmount(_ balance: Balance, _ currencyInfo: CurrencyInfo) -> String { +fileprivate func formattedAmount(_ balance: Balance, _ currencyInfo: CurrencyInfo) -> (String, String) { let amount = balance.available return amount.formatted(currencyInfo, isNegative: false, useISO: false) } @@ -17,8 +17,11 @@ fileprivate func urlOrCurrency(_ balance: Balance) -> String { balance.scopeInfo.url?.trimURL ?? balance.scopeInfo.currency } -fileprivate func pickerRow(_ balance: Balance, _ currencyInfo: CurrencyInfo) -> String { - String("\(urlOrCurrency(balance)):\t\(formattedAmount(balance, currencyInfo).nbs)") +fileprivate func pickerRow(_ balance: Balance, _ currencyInfo: CurrencyInfo) -> (String, String) { + let formatted = formattedAmount(balance, currencyInfo) + let urlOrCurrency = urlOrCurrency(balance) + return (String("\(urlOrCurrency):\t\(formatted.0.nbs)"), + String("\(urlOrCurrency): \(formatted.1)")) } struct ScopePicker: View { @@ -40,12 +43,14 @@ struct ScopePicker: View { let balance = controller.balances[selected] let currencyInfo = controller.info(for: balance.scopeInfo, controller.currencyTicker) let available = balance.available - let availableA11y = available.formatted(currencyInfo, isNegative: false, useISO: true, a11y: ".") + let availableA11y = available.formatted(currencyInfo, isNegative: false, + useISO: true, a11yDecSep: ".") let url = balance.scopeInfo.url?.trimURL ?? EMPTYSTRING // let a11yLabel = url + ", " + availableA11y + let choose = String(localized: "Choose the payment service.", comment: "VoiceOver") + let disabled = (count == 1) HStack(alignment: .firstTextBaseline) { - let disabled = (count == 1) Text("via", comment: "ScopePicker") .accessibilityHidden(true) .foregroundColor(disabled ? .secondary : .primary) @@ -59,7 +64,9 @@ struct ScopePicker: View { ForEach(0..<controller.balances.count, id: \.self) { index in let balance = controller.balances[index] let currencyInfo = controller.info(for: balance.scopeInfo, controller.currencyTicker) - Text(pickerRow(balance, currencyInfo)) + let pickerRow = pickerRow(balance, currencyInfo) + Text(pickerRow.0) + .accessibilityLabel(pickerRow.1) .tag(index) // .selectionDisabled(balance.available.isZero) needs iOS 17 } @@ -74,7 +81,7 @@ struct ScopePicker: View { } .talerFont(.picker) // .accessibilityLabel(a11yLabel) - .accessibilityHint(String(localized: "Choose the payment service.", comment: "a11y")) + .accessibilityHint(disabled ? EMPTYSTRING : choose) .task() { withAnimation { selected = value } } @@ -108,12 +115,14 @@ struct ScopeDropDown: View { func dropDownRow(_ balance: Balance, _ currencyInfo: CurrencyInfo, _ first: Bool = false) -> some View { let urlOrCurrency = urlOrCurrency(balance) let text = Text(urlOrCurrency) - let amount = Text(formattedAmount(balance, currencyInfo).nbs) - let a11yAmount = balance.available.readableDescription + let formatted = formattedAmount(balance, currencyInfo) + let amount = Text(formatted.0.nbs) if first { + let a11yLabel = String(localized: "via \(urlOrCurrency)", comment: "VoiceOver") text - .accessibilityLabel("via \(urlOrCurrency)") + .accessibilityLabel(a11yLabel) } else { + let a11yLabel = "\(urlOrCurrency), \(formatted.1)" let hLayout = HStack(alignment: .firstTextBaseline) { text Spacer() @@ -132,7 +141,7 @@ struct ScopeDropDown: View { vLayout } .accessibilityElement(children: .combine) - .accessibilityLabel("\(urlOrCurrency), \(a11yAmount)") + .accessibilityLabel(a11yLabel) } } @@ -161,7 +170,7 @@ struct ScopeDropDown: View { chevron.foregroundColor(.clear) .accessibilityHidden(true) } - }) // .accessibilityElement(children: .combine) + }) .disabled(rowDisabled) .padding(.horizontal, radius / 2) .frame(maxWidth: .infinity, alignment: .leading) @@ -172,21 +181,25 @@ struct ScopeDropDown: View { let balance = controller.balances[selection] let currencyInfo = controller.info(for: balance.scopeInfo, controller.currencyTicker) let noAmount = !showDropdown - Button(action: buttonAction) { - HStack(alignment: .firstTextBaseline) { + Group { + if disabled { dropDownRow(balance, currencyInfo, noAmount) - .accessibilityAddTraits(.isSelected) - Spacer() - if !disabled { - chevron.rotationEffect(.degrees((showDropdown ? -180 : 0))) - .accessibilityHidden(true) + } else { + Button(action: buttonAction) { + HStack(alignment: .firstTextBaseline) { + dropDownRow(balance, currencyInfo, noAmount) + .accessibilityAddTraits(.isSelected) + Spacer() + chevron.rotationEffect(.degrees((showDropdown ? -180 : 0))) + .accessibilityHidden(true) + } } } } .padding(.vertical, 4) .padding(.horizontal, radius / 2) .frame(maxWidth: .infinity, alignment: .leading) -// .border(.red) +// .border(.red) if (showDropdown) { if #available(iOS 17.0, *) { // let toomany = controller.balances.count > maxItemDisplayed diff --git a/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawURIView.swift b/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawURIView.swift @@ -73,8 +73,8 @@ struct WithdrawURIView: View { baseUrl: exchange.exchangeBaseUrl, scope: nil) { // TODO: scope let fee = try? details.amountRaw - details.amountEffective - let feeStr = fee?.formatted(currencyInfo, isNegative: true) ?? "nix" - symLog.log("Fee = \(feeStr)") + let feeStr = fee?.formatted(currencyInfo, isNegative: true) ?? ("0", "0") + symLog.log("Fee = \(feeStr.0)") let insufficient = if let amountAvailable { (try? details.amountRaw < amountAvailable) ?? true } else { @@ -82,7 +82,8 @@ struct WithdrawURIView: View { } withdrawalDetails = details return ComputeFeeResult(insufficient: insufficient, feeAmount: fee, - feeStr: feeLabel(feeStr), numCoins: details.numCoins) + feeStr: (feeLabel(feeStr.0), feeLabel(feeStr.1)), + numCoins: details.numCoins) } } else { symLog.log("No exchange!") diff --git a/TalerWallet1/Views/Transactions/ManualDetailsV.swift b/TalerWallet1/Views/Transactions/ManualDetailsV.swift @@ -38,12 +38,14 @@ struct SegmentControl: View { let detail = accountDetails[index] let specs = detail.currencySpecification let amount = detail.transferAmount - let amountStr = amount?.formatted(specs: specs, isNegative: false, useISO: false) ?? EMPTYSTRING + let formatted = amount?.formatted(specs: specs, isNegative: false, useISO: false) + ?? (EMPTYSTRING, EMPTYSTRING) let bankName = detail.bankLabel - let a11yLabel = bankName != nil ? (bankName! + SPACE + amountStr) : amountStr + let a11yLabel = bankName != nil ? (bankName! + SPACE + formatted.1) + : formatted.1 // let _ = print(amountStr) VStack(spacing: 6) { - Text(amountStr) + Text(formatted.0) .talerFont(.title3) if let bankName { Text(bankName) @@ -94,10 +96,12 @@ struct AccountPicker: View { isNegative: false, useISO: false) // let _ = print(amountStr) if let bankName = detail.bankLabel { - Text(bankName + ": " + amountStr) + Text(bankName + ": " + amountStr.0) + .accessibilityLabel(bankName + ": " + amountStr.1) .tag(index) } else { - Text(amountStr) + Text(amountStr.0) + .accessibilityLabel(amountStr.1) .tag(index) } } @@ -177,7 +181,8 @@ struct ManualDetailsV: View { } } else if let amount = account.transferAmount { if let bankName = account.bankLabel { - Text(bankName + ": " + amountStr) + Text(bankName + ": " + amountStr.0) + .accessibilityLabel(bankName + ": " + amountStr.1) // } else { // Text(amountStr) } diff --git a/TalerWallet1/Views/Transactions/ManualDetailsWireV.swift b/TalerWallet1/Views/Transactions/ManualDetailsWireV.swift @@ -10,20 +10,30 @@ import OrderedCollections import taler_swift struct TransferRestrictionsV: View { - let amountStr: String - let obtainStr: String + let amountStr: (String, String) + let obtainStr: (String, String) let restrictions: [AccountRestriction]? @AppStorage("minimalistic") var minimalistic: Bool = false @State private var selectedLanguage = Locale.preferredLanguageCode + private func transferMini(_ amountS: String) -> String { + let amountNBS = amountS.nbs + return String(localized: "Transfer \(amountNBS) to the payment service.") + } + private func transferMaxi(_ amountS: String, _ obtainS: String) -> String { + let amountNBS = amountS.nbs + let obtainNBS = obtainS.nbs + return String(localized: "You need to transfer \(amountNBS) from your regular bank account to the payment service to receive \(obtainNBS) as electronic cash in this wallet.") + } + var body: some View { VStack(alignment: .leading) { - let amountNBS = amountStr.nbs - let obtainNBS = obtainStr.nbs - Text(minimalistic ? "Transfer \(amountNBS) to the payment service." - : "You need to transfer \(amountNBS) from your regular bank account to the payment service to receive \(obtainNBS) as electronic cash in this wallet.") + Text(minimalistic ? transferMini(amountStr.0) + : transferMaxi(amountStr.0, obtainStr.0)) + .accessibilityLabel(minimalistic ? transferMini(amountStr.1) + : transferMaxi(amountStr.1, obtainStr.1)) .talerFont(.body) .multilineTextAlignment(.leading) if let restrictions { @@ -53,13 +63,19 @@ struct ManualDetailsWireV: View { let iban: String? let xTaler: String let amountValue: String // string representation of the value, formatted as "`integer`.`fraction`" - let amountStr: String - let obtainStr: String + let amountStr: (String, String) + let obtainStr: (String, String) let account: WithdrawalExchangeAccountDetails @AppStorage("minimalistic") var minimalistic: Bool = false let navTitle = String(localized: "Wire transfer", comment: "ViewTitle of wire-transfer instructions") + private func step3(_ amountS: String) -> String { + let amountNBS = amountS.nbs + return minimalistic ? String(localized: "Transfer \(amountNBS).") + : String(localized: "Finish the wire transfer of \(amountNBS) in your banking app or website, then this withdrawal will proceed automatically. Depending on your bank the transfer can take from minutes to two working days, please be patient.") + } + var body: some View { List { let cryptocode = HStack { @@ -103,7 +119,8 @@ struct ManualDetailsWireV: View { VStack(alignment: .leading) { Text("Amount:") .talerFont(.subheadline) - Text(amountStr) + Text(amountStr.0) + .accessibilityLabel(amountStr.1) .monospacedDigit() .padding(.leading) } .frame(maxWidth: .infinity, alignment: .leading) @@ -146,9 +163,11 @@ struct ManualDetailsWireV: View { .talerFont(.body) .multilineTextAlignment(.leading) .padding(.top) - let amountNBS = amountStr.nbs - let step3 = Text(minimalistic ? "**Step 3:** Transfer \(amountNBS)." - : "**Step 3:** Finish the wire transfer of \(amountNBS) in your banking app or website, then this withdrawal will proceed automatically. Depending on your bank the transfer can take from minutes to two working days, please be patient.") + let step3A11y = step3(amountStr.1) + let step3Str = step3(amountStr.0) + let step3Head = String(localized: "**Step 3:**") + let step3 = Text(step3Head + step3Str) + .accessibilityLabel(step3Head + step3A11y) .talerFont(.body) .multilineTextAlignment(.leading)