diff --git a/WooCommerce/Classes/POS/Analytics/WooAnalyticsEvent+PointOfSale.swift b/WooCommerce/Classes/POS/Analytics/WooAnalyticsEvent+PointOfSale.swift index a448daa6ab6..d29894d287f 100644 --- a/WooCommerce/Classes/POS/Analytics/WooAnalyticsEvent+PointOfSale.swift +++ b/WooCommerce/Classes/POS/Analytics/WooAnalyticsEvent+PointOfSale.swift @@ -37,24 +37,29 @@ extension WooAnalyticsEvent { } static func addItemToCart( - sourceView: WooAnalyticsEvent.PointOfSale.SourceView, + sourceView: WooAnalyticsEvent.PointOfSale.SourceView? = nil, sourceViewType: WooAnalyticsEvent.PointOfSale.SourceViewType, itemType: WooAnalyticsEvent.PointOfSale.ItemType, - productType: WooAnalyticsEvent.PointOfSale.CartItemProductType? = nil + productType: WooAnalyticsEvent.PointOfSale.CartItemProductType? = nil, + error: Error? = nil ) -> WooAnalyticsEvent { var properties: [String: String] = [ - Key.sourceView: sourceView.rawValue, Key.sourceViewType: sourceViewType.rawValue, Key.itemType: itemType.rawValue ] + if let sourceView { + properties[Key.sourceView] = sourceView.rawValue + } + if let productType { properties[Key.productType] = productType.rawValue } return WooAnalyticsEvent( statName: .pointOfSaleAddItemToCart, - properties: properties + properties: properties, + error: error ) } @@ -217,6 +222,7 @@ extension WooAnalyticsEvent.PointOfSale { case list case search case preSearch = "pre_search" + case scanner init(isSearching: Bool, searchTerm: String = "") { switch (isSearching, searchTerm.isEmpty) { @@ -235,6 +241,19 @@ extension WooAnalyticsEvent.PointOfSale { enum ItemType: String { case product case coupon + case loading + case error + + init(cartItem: Cart.PurchasableItem) { + switch cartItem.state { + case .loaded: + self = .product + case .loading: + self = .loading + case .error: + self = .error + } + } } /// Types of products supported in the POS diff --git a/WooCommerce/Classes/POS/Models/Cart+BarcodeScanError.swift b/WooCommerce/Classes/POS/Models/Cart+BarcodeScanError.swift index fb47d8c2752..e3adb93b7dd 100644 --- a/WooCommerce/Classes/POS/Models/Cart+BarcodeScanError.swift +++ b/WooCommerce/Classes/POS/Models/Cart+BarcodeScanError.swift @@ -2,8 +2,9 @@ import Foundation import enum Yosemite.PointOfSaleBarcodeScanError extension Cart { - mutating func updateLoadingItem(id: UUID, with error: PointOfSaleBarcodeScanError) { - guard let index = purchasableItems.firstIndex(where: { $0.id == id }) else { return } + @discardableResult + mutating func updateLoadingItem(id: UUID, with error: PointOfSaleBarcodeScanError) -> Cart.PurchasableItem? { + guard let index = purchasableItems.firstIndex(where: { $0.id == id }) else { return nil } purchasableItems[index] = Cart.PurchasableItem( id: id, @@ -12,6 +13,8 @@ extension Cart { quantity: 1, state: .error ) + + return purchasableItems[index] } private func title(for error: PointOfSaleBarcodeScanError) -> String { @@ -30,7 +33,13 @@ extension Cart { } private func subtitle(for error: PointOfSaleBarcodeScanError) -> String { - switch error { + return error.localizedDescription + } +} + +extension PointOfSaleBarcodeScanError { + var localizedDescription: String { + switch self { case .notFound, .unknown: return Localization.notFound case .downloadableProduct, .unsupportedProductType: @@ -45,10 +54,8 @@ extension Cart { } } } -} -private extension Cart { - enum Localization { + private enum Localization { static let notFound = NSLocalizedString( "pointOfSale.barcodeScan.error.notFound", value: "Unknown scanned item", diff --git a/WooCommerce/Classes/POS/Models/Cart.swift b/WooCommerce/Classes/POS/Models/Cart.swift index 9ee582e6975..cce8a2df833 100644 --- a/WooCommerce/Classes/POS/Models/Cart.swift +++ b/WooCommerce/Classes/POS/Models/Cart.swift @@ -100,20 +100,23 @@ extension Cart { } } - mutating func addLoadingItem() -> UUID { + mutating func addLoadingItem() -> Cart.PurchasableItem { let id = UUID() let loadingItem = PurchasableItem.loading(id: id) purchasableItems.insert(loadingItem, at: purchasableItems.startIndex) - return id + return loadingItem } - mutating func updateLoadingItem(id: UUID, with posItem: POSItem) { - guard let index = purchasableItems.firstIndex(where: { $0.id == id }) else { return } + @discardableResult + mutating func updateLoadingItem(id: UUID, with posItem: POSItem) -> Cart.PurchasableItem? { + guard let index = purchasableItems.firstIndex(where: { $0.id == id }) else { return nil } if let productItem = createPurchasableItem(id: id, from: posItem) { purchasableItems[index] = productItem + return productItem } else { purchasableItems.remove(at: index) + return nil } } diff --git a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift index 82b1651188a..256be5964c9 100644 --- a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift +++ b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift @@ -187,14 +187,40 @@ extension PointOfSaleAggregateModel { extension PointOfSaleAggregateModel { func barcodeScanned(_ barcode: String) { Task { - let placeholderItemID = cart.addLoadingItem() + let placeholderItemID = cart.addLoadingItem().id + + analytics.track( + event: .PointOfSale.addItemToCart( + sourceViewType: .scanner, + itemType: .loading + ) + ) + do throws(PointOfSaleBarcodeScanError) { let item = try await barcodeScanService.getItem(barcode: barcode) - cart.updateLoadingItem(id: placeholderItemID, with: item) + if let cartItem = cart.updateLoadingItem(id: placeholderItemID, with: item) { + analytics.track( + event: .PointOfSale.addItemToCart( + sourceViewType: .scanner, + itemType: .product, + productType: .init(cartItem: cartItem) + ) + ) + } } catch { DDLogInfo("Failed to find item by barcode: \(error)") - cart.updateLoadingItem(id: placeholderItemID, with: error) - await soundPlayer.playSound(.barcodeScanFailure) + if let _ = cart.updateLoadingItem(id: placeholderItemID, with: error) { + // Only play a sound and track analytics if the item still exists in the cart. + await soundPlayer.playSound(.barcodeScanFailure) + + analytics.track( + event: .PointOfSale.addItemToCart( + sourceViewType: .scanner, + itemType: .error, + error: error + ) + ) + } } } } diff --git a/WooCommerce/Classes/POS/Presentation/CartView.swift b/WooCommerce/Classes/POS/Presentation/CartView.swift index ec72321a1f7..76140a677a0 100644 --- a/WooCommerce/Classes/POS/Presentation/CartView.swift +++ b/WooCommerce/Classes/POS/Presentation/CartView.swift @@ -73,13 +73,19 @@ struct CartView: View { ServiceLocator.analytics.track( event: .PointOfSale.itemRemovedFromCart( sourceView: .cart, - itemType: .product, + itemType: .init(cartItem: cartItem), productType: .init(cartItem: cartItem) ) ) posModel.remove(cartItem: cartItem) } : nil, onCancelLoading: { + ServiceLocator.analytics.track( + event: .PointOfSale.itemRemovedFromCart( + sourceView: .cart, + itemType: .loading + ) + ) posModel.cancelLoadingItem(id: cartItem.id) }) .id(cartItem.id) diff --git a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift index 6f329e5b6fd..7fa5f46bd0f 100644 --- a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift @@ -125,12 +125,12 @@ struct PointOfSaleAggregateModelTests { try #require(cart.purchasableItems.isEmpty) // When - let id = cart.addLoadingItem() + let loadingItem = cart.addLoadingItem() // Then #expect(cart.purchasableItems.count == 1) let item = try #require(cart.purchasableItems.first) - #expect(item.id == id) + #expect(item.id == loadingItem.id) guard case .loading = item.state else { throw CartTestError.unexpectedItemStateInCart } @@ -140,11 +140,11 @@ struct PointOfSaleAggregateModelTests { @Test func updateLoadingItem_updates_loading_item_with_simple_product() async throws { // Given var cart = Cart() - let id = cart.addLoadingItem() + let loadingItem = cart.addLoadingItem() let purchasableItem = makePurchasableItem(name: "Test Product") // When - cart.updateLoadingItem(id: id, with: purchasableItem) + cart.updateLoadingItem(id: loadingItem.id, with: purchasableItem) // Then #expect(cart.purchasableItems.count == 1)