diff --git a/Networking/Networking/Model/Product/ProductBundleItem.swift b/Networking/Networking/Model/Product/ProductBundleItem.swift index 7fb670f2d6e..e735d614bfd 100644 --- a/Networking/Networking/Model/Product/ProductBundleItem.swift +++ b/Networking/Networking/Model/Product/ProductBundleItem.swift @@ -3,7 +3,7 @@ import Codegen /// Represents an item in a Product Bundle /// -public struct ProductBundleItem: Codable, Equatable, GeneratedCopiable, GeneratedFakeable { +public struct ProductBundleItem: Codable, Equatable, Hashable, GeneratedCopiable, GeneratedFakeable { /// Bundled item ID public let bundledItemID: Int64 diff --git a/WooCommerce/Classes/POS/Presentation/CartView.swift b/WooCommerce/Classes/POS/Presentation/CartView.swift index c4371ed449c..383e24b8e84 100644 --- a/WooCommerce/Classes/POS/Presentation/CartView.swift +++ b/WooCommerce/Classes/POS/Presentation/CartView.swift @@ -56,7 +56,7 @@ struct CartView: View { ) } .padding(.leading, Constants.itemHorizontalPadding) - .renderedIf(posModel.cart.isNotEmpty && posModel.orderStage == .building) + .renderedIf(shouldShowClearCartButton) } } .frame(maxWidth: .infinity, alignment: .leading) @@ -147,6 +147,12 @@ private extension CartView { orderState: posModel.orderState, paymentState: posModel.paymentState) } + + var shouldShowClearCartButton: Bool { + viewHelper.shouldShowClearCartButton( + cart: posModel.cart, + orderStage: posModel.orderStage) + } } private extension CartView { diff --git a/WooCommerce/Classes/POS/ViewHelpers/CartViewHelper.swift b/WooCommerce/Classes/POS/ViewHelpers/CartViewHelper.swift index 035f340b841..29ac1f88af7 100644 --- a/WooCommerce/Classes/POS/ViewHelpers/CartViewHelper.swift +++ b/WooCommerce/Classes/POS/ViewHelpers/CartViewHelper.swift @@ -17,6 +17,10 @@ final class CartViewHelper { } return orderState.isSyncing } + + func shouldShowClearCartButton(cart: [CartItem], orderStage: PointOfSaleOrderStage) -> Bool { + cart.isNotEmpty && orderStage == .building + } } private extension PointOfSalePaymentState { diff --git a/WooCommerce/WooCommerceTests/POS/ViewModels/CartViewHelperTests.swift b/WooCommerce/WooCommerceTests/POS/ViewModels/CartViewHelperTests.swift index 289ab84b149..eff14551f99 100644 --- a/WooCommerce/WooCommerceTests/POS/ViewModels/CartViewHelperTests.swift +++ b/WooCommerce/WooCommerceTests/POS/ViewModels/CartViewHelperTests.swift @@ -1,3 +1,4 @@ +import Foundation import Testing @testable import WooCommerce @@ -68,4 +69,20 @@ struct CartViewHelperTests { #expect(sut.itemsInCartLabel(for: count) == expected) } + @Test func shouldShowClearCartButton_empty_cart_false() async throws { + #expect(sut.shouldShowClearCartButton(cart: [], orderStage: .building) == false) + #expect(sut.shouldShowClearCartButton(cart: [], orderStage: .finalizing) == false) + } + + @Test func shouldShowClearCartButton_items_in_cart_and_building_true() async throws { + #expect(sut.shouldShowClearCartButton(cart: [makeItem()], orderStage: .building) == true) + } + + @Test func shouldShowClearCartButton_items_in_cart_and_finalizing_false() async throws { + #expect(sut.shouldShowClearCartButton(cart: [makeItem()], orderStage: .finalizing) == false) + } +} + +private func makeItem() -> CartItem { + CartItem(id: UUID(), item: MockPOSItem(name: "Item", formattedPrice: "$1.00"), quantity: 1) } diff --git a/Yosemite/Yosemite/PointOfSale/POSProduct.swift b/Yosemite/Yosemite/PointOfSale/POSProduct.swift index 2e46e13e7e1..f246dfbfc5f 100644 --- a/Yosemite/Yosemite/PointOfSale/POSProduct.swift +++ b/Yosemite/Yosemite/PointOfSale/POSProduct.swift @@ -14,7 +14,7 @@ struct POSProduct: POSOrderableItem, OrderSyncProductTypeProtocol, Equatable { let bundledItems: [ProductBundleItem] = [] } -extension POSProduct { +extension POSProduct: Hashable { func toOrderSyncProductInput(quantity: Decimal) -> OrderSyncProductInput { OrderSyncProductInput(product: .product(self), quantity: quantity) } diff --git a/Yosemite/Yosemite/Stores/Order/OrderSyncProductInput.swift b/Yosemite/Yosemite/Stores/Order/OrderSyncProductInput.swift index 3adc28df955..f5589803681 100644 --- a/Yosemite/Yosemite/Stores/Order/OrderSyncProductInput.swift +++ b/Yosemite/Yosemite/Stores/Order/OrderSyncProductInput.swift @@ -1,12 +1,23 @@ -public protocol OrderSyncProductTypeProtocol { +import Networking + +public protocol OrderSyncProductTypeProtocol: Hashable { var price: String { get } var productID: Int64 { get } var productType: ProductType { get } var bundledItems: [ProductBundleItem] { get } + + func isEqual(to: any OrderSyncProductTypeProtocol) -> Bool } extension Product: OrderSyncProductTypeProtocol {} +public extension OrderSyncProductTypeProtocol where Self: Equatable { + func isEqual(to other: any OrderSyncProductTypeProtocol) -> Bool { + guard let other = other as? Self else { return false } + return self == other + } +} + /// Product input for an `OrderSynchronizer` type. /// public struct OrderSyncProductInput { @@ -27,24 +38,26 @@ public struct OrderSyncProductInput { /// Types of products the synchronizer supports /// public enum ProductType: Hashable { - case product(OrderSyncProductTypeProtocol) + case product(any OrderSyncProductTypeProtocol) case variation(ProductVariation) public func hash(into hasher: inout Hasher) { switch self { case .product(let product): - hasher.combine("product-\(product.productID)") + hasher.combine("productType-product") + hasher.combine(product) case .variation(let variation): - hasher.combine("variation-\(variation.productVariationID)") + hasher.combine("productType-variation") + hasher.combine(variation) } } public static func == (lhs: OrderSyncProductInput.ProductType, rhs: OrderSyncProductInput.ProductType) -> Bool { switch (lhs, rhs) { - case (.product(let l), .product(let r)): - return l.productID == r.productID - case (.variation(let l), .variation(let r)): - return l.productVariationID == r.productVariationID + case (.product(let lhsProduct), .product(let rhsProduct)): + return lhsProduct.isEqual(to: rhsProduct) + case (.variation(let lhsVariation), .variation(let rhsVariation)): + return lhsVariation == rhsVariation default: return false } diff --git a/Yosemite/Yosemite/Stores/Order/ProductInputTransformer.swift b/Yosemite/Yosemite/Stores/Order/ProductInputTransformer.swift index 49608c9b877..3ff599b8c8d 100644 --- a/Yosemite/Yosemite/Stores/Order/ProductInputTransformer.swift +++ b/Yosemite/Yosemite/Stores/Order/ProductInputTransformer.swift @@ -83,7 +83,7 @@ public struct ProductInputTransformer { quantity: Decimal, discount: Decimal? = nil, bundleConfiguration: [BundledProductConfiguration] = [], - allProducts: [OrderSyncProductTypeProtocol], + allProducts: [any OrderSyncProductTypeProtocol], allProductVariations: Set, defaultDiscount: Decimal) -> OrderSyncProductInput? { // Finds the product or productVariation associated with the order item. diff --git a/Yosemite/Yosemite/Tools/POS/POSOrderService.swift b/Yosemite/Yosemite/Tools/POS/POSOrderService.swift index 2a42f5a0fa7..cccbcd64075 100644 --- a/Yosemite/Yosemite/Tools/POS/POSOrderService.swift +++ b/Yosemite/Yosemite/Tools/POS/POSOrderService.swift @@ -124,7 +124,7 @@ public final class POSOrderService: POSOrderServiceProtocol { } } -private struct POSOrderSyncProductType: OrderSyncProductTypeProtocol { +private struct POSOrderSyncProductType: OrderSyncProductTypeProtocol, Hashable { let productID: Int64 let price: String // Not used in POS but have to be included for the app usage.