From 6b978f9fda4cadd985fb75fe3a3fc9ff28132c99 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 7 Jan 2025 18:50:30 +0700 Subject: [PATCH 1/4] compare order total vs merchant input to validate --- .../PointOfSaleCollectCashView.swift | 37 +++++++++++++++++-- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift index 289a768498f..b8056b0a5c9 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift @@ -1,4 +1,5 @@ import SwiftUI +import class WooFoundation.CurrencyFormatter struct PointOfSaleCollectCashView: View { @Environment(\.colorScheme) var colorScheme @@ -9,6 +10,8 @@ struct PointOfSaleCollectCashView: View { @State private var isLoading: Bool = false @State private var errorMessage: String? + private let currencyFormatter: CurrencyFormatter = WooFoundation.CurrencyFormatter(currencySettings: ServiceLocator.currencySettings) + let orderTotal: String private var formattedOrderTotal: String { @@ -16,10 +19,26 @@ struct PointOfSaleCollectCashView: View { } private func validateAmount() -> Bool { - // TODO: - // Validate amount entered vs order total - // https://github.com/woocommerce/woocommerce-ios/issues/14749 - return true + guard let orderDecimal = parseCurrency(orderTotal), + let inputDecimal = parseCurrency(textFieldAmountInput) else { + errorMessage = "Invalid amount. Please try again." + return false + } + switch inputDecimal.compare(orderDecimal) { + case .orderedAscending: + // inputDecimal < orderDecimal + errorMessage = "Not enough cash to cover the order." + return false + case .orderedDescending: + // inputDecimal > orderDecimal + let changeDue = inputDecimal.subtracting(orderDecimal) + errorMessage = "Change due: \(formatAsCurrency(changeDue))" + return true + case .orderedSame: + // inputDecimal == orderDecimal + errorMessage = nil + return true + } } @StateObject private var textFieldViewModel = FormattableAmountTextFieldViewModel(size: .extraLarge, @@ -112,6 +131,16 @@ struct PointOfSaleCollectCashView: View { } } +private extension PointOfSaleCollectCashView { + func parseCurrency(_ amountString: String) -> NSDecimalNumber? { + currencyFormatter.convertToDecimal(amountString, locale: .current) + } + + private func formatAsCurrency(_ amount: NSDecimalNumber) -> String { + currencyFormatter.formatAmount(amount) ?? "$0.00" + } +} + private extension PointOfSaleCollectCashView { enum Constants { static let buttonSpacing: CGFloat = 12 From 83b7f601cdf4a4bbb25a0c13770aa68f8f6b78de Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 7 Jan 2025 19:08:19 +0700 Subject: [PATCH 2/4] separate change due and error strings when validating --- .../PointOfSaleCollectCashView.swift | 64 ++++++++++++------- 1 file changed, 40 insertions(+), 24 deletions(-) diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift index b8056b0a5c9..0ecb6fc2a95 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift @@ -9,6 +9,7 @@ struct PointOfSaleCollectCashView: View { @State private var textFieldAmountInput: String = "" @State private var isLoading: Bool = false @State private var errorMessage: String? + @State private var changeDueMessage: String? private let currencyFormatter: CurrencyFormatter = WooFoundation.CurrencyFormatter(currencySettings: ServiceLocator.currencySettings) @@ -18,29 +19,6 @@ struct PointOfSaleCollectCashView: View { String.localizedStringWithFormat(Localization.backNavigationSubtitle, orderTotal) } - private func validateAmount() -> Bool { - guard let orderDecimal = parseCurrency(orderTotal), - let inputDecimal = parseCurrency(textFieldAmountInput) else { - errorMessage = "Invalid amount. Please try again." - return false - } - switch inputDecimal.compare(orderDecimal) { - case .orderedAscending: - // inputDecimal < orderDecimal - errorMessage = "Not enough cash to cover the order." - return false - case .orderedDescending: - // inputDecimal > orderDecimal - let changeDue = inputDecimal.subtracting(orderDecimal) - errorMessage = "Change due: \(formatAsCurrency(changeDue))" - return true - case .orderedSame: - // inputDecimal == orderDecimal - errorMessage = nil - return true - } - } - @StateObject private var textFieldViewModel = FormattableAmountTextFieldViewModel(size: .extraLarge, locale: Locale.autoupdatingCurrent, storeCurrencySettings: ServiceLocator.currencySettings, @@ -73,8 +51,15 @@ struct PointOfSaleCollectCashView: View { FormattableAmountTextField(viewModel: textFieldViewModel, style: .pos) .onChange(of: textFieldViewModel.amount) { newValue in textFieldAmountInput = newValue + updateChangeDueMessage() } + if let changeDue = changeDueMessage { + Text(changeDue) + .font(.posBodyRegular) + .foregroundColor(.green) + } + if let errorMessage = errorMessage { Text(errorMessage) .font(POSFontStyle.posBodyRegular) @@ -83,7 +68,7 @@ struct PointOfSaleCollectCashView: View { Button(action: { Task { @MainActor in - guard validateAmount() else { + guard validateAmountOnSubmit() else { return } isLoading = true @@ -121,6 +106,7 @@ struct PointOfSaleCollectCashView: View { .background(backgroundColor) .padding() .animation(.easeInOut, value: errorMessage) + .animation(.easeInOut, value: changeDueMessage) .onChange(of: textFieldAmountInput) { _ in errorMessage = nil } @@ -139,6 +125,36 @@ private extension PointOfSaleCollectCashView { private func formatAsCurrency(_ amount: NSDecimalNumber) -> String { currencyFormatter.formatAmount(amount) ?? "$0.00" } + + private func updateChangeDueMessage() { + guard let orderDecimal = parseCurrency(orderTotal), + let inputDecimal = parseCurrency(textFieldAmountInput) else { + changeDueMessage = nil + return + } + + if inputDecimal.compare(orderDecimal) == .orderedDescending { + let changeDue = inputDecimal.subtracting(orderDecimal) + changeDueMessage = "Change due: \(formatAsCurrency(changeDue))" + } else { + changeDueMessage = nil + } + } + + private func validateAmountOnSubmit() -> Bool { + guard let orderDecimal = parseCurrency(orderTotal), + let inputDecimal = parseCurrency(textFieldAmountInput) else { + errorMessage = "Invalid amount. Please try again." + return false + } + + if inputDecimal.compare(orderDecimal) == .orderedAscending { + errorMessage = "Amount must be more or equal to total" + return false + } + errorMessage = nil + return true + } } private extension PointOfSaleCollectCashView { From fa0bbc3d5ecb9d1c6e5834604f2a4f25662b5177 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 7 Jan 2025 19:12:54 +0700 Subject: [PATCH 3/4] use textSuccess color for change due --- .../POS/Presentation/PointOfSaleCollectCashView.swift | 2 +- WooCommerce/Classes/POS/Utils/Color+WooCommercePOS.swift | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift index 0ecb6fc2a95..7b0edec6a69 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift @@ -57,7 +57,7 @@ struct PointOfSaleCollectCashView: View { if let changeDue = changeDueMessage { Text(changeDue) .font(.posBodyRegular) - .foregroundColor(.green) + .foregroundColor(.posTextSuccess) } if let errorMessage = errorMessage { diff --git a/WooCommerce/Classes/POS/Utils/Color+WooCommercePOS.swift b/WooCommerce/Classes/POS/Utils/Color+WooCommercePOS.swift index 662c3624819..d155c32b5ce 100644 --- a/WooCommerce/Classes/POS/Utils/Color+WooCommercePOS.swift +++ b/WooCommerce/Classes/POS/Utils/Color+WooCommercePOS.swift @@ -87,6 +87,15 @@ extension Color { ) } + static var posTextSuccess: Color { + Color( + UIColor( + light: UIColor(red: 10.0/255.0, green: 148.0/255.0, blue: 0.0/255.0, alpha: 1.0), + dark: UIColor(red: 10.0/255.0, green: 148.0/255.0, blue: 0.0/255.0, alpha: 1.0) + ) + ) + } + // MARK: - Buttons static var posPrimaryButtonBackground: Color { From 1339f75b0a4e44baff8eb0fd6f811293521d1896 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 7 Jan 2025 19:20:45 +0700 Subject: [PATCH 4/4] localized strings --- .../PointOfSaleCollectCashView.swift | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift index 7b0edec6a69..cdbe8e468d7 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleCollectCashView.swift @@ -135,7 +135,7 @@ private extension PointOfSaleCollectCashView { if inputDecimal.compare(orderDecimal) == .orderedDescending { let changeDue = inputDecimal.subtracting(orderDecimal) - changeDueMessage = "Change due: \(formatAsCurrency(changeDue))" + changeDueMessage = String.localizedStringWithFormat(Localization.changeDueMessage, formatAsCurrency(changeDue)) } else { changeDueMessage = nil } @@ -144,12 +144,12 @@ private extension PointOfSaleCollectCashView { private func validateAmountOnSubmit() -> Bool { guard let orderDecimal = parseCurrency(orderTotal), let inputDecimal = parseCurrency(textFieldAmountInput) else { - errorMessage = "Invalid amount. Please try again." + errorMessage = Localization.failedToCollectCashPayment return false } if inputDecimal.compare(orderDecimal) == .orderedAscending { - errorMessage = "Amount must be more or equal to total" + errorMessage = Localization.cashPaymentAmountNotEnough return false } errorMessage = nil @@ -192,10 +192,21 @@ private extension PointOfSaleCollectCashView { comment: "Button to mark a cash payment as completed" ) static let failedToCollectCashPayment = NSLocalizedString( - "pointOfSale.cashview.failedToCollectCashPayment.draft", + "pointOfSale.cashview.failedtocollectcashpayment.errormessage", value: "Error trying to process payment. Try again.", comment: "Error message when the system fails to collect a cash payment." ) + static let cashPaymentAmountNotEnough = NSLocalizedString( + "pointOfSale.cashview.cashpaymentamountnotenough.errormessage", + value: "Amount must be more or equal to total.", + comment: "Error message when the cash amount entered is less than the order total." + ) + static let changeDueMessage = NSLocalizedString( + "pointOfSale.cashview.changedue", + value: "Change due: %1$@", + comment: "Change due when the cash amount entered exceeds the order total." + + "Reads as 'Change due: $1.23'" + ) } }