diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index e823639d18c..b8d93b67904 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -8,6 +8,7 @@ - [*] Shipping Labels: Made HS tariff number field required in customs form for EU destinations [https://github.com/woocommerce/woocommerce-ios/pull/15946] - [*] Order Details > Edit Shipping/Billing Address: Added map-based address lookup support for iOS 17+. [https://github.com/woocommerce/woocommerce-ios/pull/15964] - [*] Order Creation: Prevent subscription products to be added to an order [https://github.com/woocommerce/woocommerce-ios/pull/15960] +- [*] Point of Sale: Remove temporary orders from storage on exiting POS mode [https://github.com/woocommerce/woocommerce-ios/pull/15975] - [internal] Replace COTS_DEVICE reader model name with TAP_TO_PAY_DEVICE. [https://github.com/woocommerce/woocommerce-ios/pull/15961] 22.9 diff --git a/WooCommerce/Classes/POS/Controllers/PointOfSaleOrderController.swift b/WooCommerce/Classes/POS/Controllers/PointOfSaleOrderController.swift index 98031bd8b6c..ca27ab54dee 100644 --- a/WooCommerce/Classes/POS/Controllers/PointOfSaleOrderController.swift +++ b/WooCommerce/Classes/POS/Controllers/PointOfSaleOrderController.swift @@ -35,7 +35,7 @@ protocol PointOfSaleOrderControllerProtocol { @discardableResult func syncOrder(for cart: Cart, retryHandler: @escaping () async -> Void) async -> Result func sendReceipt(recipientEmail: String) async throws - func clearOrder() + func clearOrder() async func collectCashPayment(changeDueAmount: String?) async throws } @@ -143,11 +143,25 @@ protocol PointOfSaleOrderControllerProtocol { } } - func clearOrder() { + func clearOrder() async { + await clearAutoDraftIfNeeded(for: order) order = nil orderState = .idle } + private func clearAutoDraftIfNeeded(for order: Order?) async { + guard let order, order.status == .autoDraft else { return } + + await withCheckedContinuation { continuation in + Task { @MainActor [weak self] in + let action = OrderAction.deleteOrder(siteID: order.siteID, order: order, deletePermanently: true) { _ in + continuation.resume() + } + self?.stores.dispatch(action) + } + } + } + private func celebrate() { celebration.celebrate() } diff --git a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift index e40ffa2a39f..1b144cdf8bb 100644 --- a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift +++ b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift @@ -173,7 +173,9 @@ extension PointOfSaleAggregateModel { func startNewCart() { removeAllItemsFromCart() - orderController.clearOrder() + Task { + await orderController.clearOrder() + } setStateForEditing() viewStateCoordinator.reset() } @@ -621,7 +623,9 @@ extension PointOfSaleAggregateModel { // Before exiting Point of Sale, we warn the merchant about losing their in-progress order. // We need to clear it down as any accidental retention can cause issues especially when reconnecting card readers. - orderController.clearOrder() + Task { + await orderController.clearOrder() + } // Ideally, we could rely on the POS being deallocated to cancel all these. Since we have memory leak issues, // cancelling them explicitly helps reduce the risk of user-visible bugs while we work on the memory leaks. diff --git a/WooCommerce/Classes/POS/Utils/PointOfSalePreviewOrderController.swift b/WooCommerce/Classes/POS/Utils/PointOfSalePreviewOrderController.swift index 8945ddf0d33..7ba75466005 100644 --- a/WooCommerce/Classes/POS/Utils/PointOfSalePreviewOrderController.swift +++ b/WooCommerce/Classes/POS/Utils/PointOfSalePreviewOrderController.swift @@ -18,7 +18,7 @@ class PointOfSalePreviewOrderController: PointOfSaleOrderControllerProtocol { func sendReceipt(recipientEmail: String) async throws { } - func clearOrder() { } + func clearOrder() async { } func collectCashPayment(changeDueAmount: String?) async throws {} } diff --git a/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderControllerTests.swift b/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderControllerTests.swift index b3a0baae6ea..62658f57d1e 100644 --- a/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderControllerTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderControllerTests.swift @@ -572,6 +572,91 @@ struct PointOfSaleOrderControllerTests { } } + @MainActor + @available(iOS 17.0, *) + @Test func clearOrder_when_no_order_then_does_not_dispatch_delete_action() async throws { + // Given + let mockStores = MockStoresManager(sessionManager: .testingInstance) + let sut = PointOfSaleOrderController(orderService: mockOrderService, + receiptService: mockReceiptService, + stores: mockStores) + + var deleteActionWasDispatched = false + mockStores.whenReceivingAction(ofType: OrderAction.self) { action in + if case .deleteOrder = action { + deleteActionWasDispatched = true + } + } + + // When + await sut.clearOrder() + + // Then + #expect(deleteActionWasDispatched == false) + #expect(sut.orderState == .idle) + } + + @MainActor + @available(iOS 17.0, *) + @Test func clearOrder_when_autodraft_order_then_dispatches_delete_action() async throws { + // Given + let mockStores = MockStoresManager(sessionManager: .testingInstance) + let sut = PointOfSaleOrderController(orderService: mockOrderService, + receiptService: mockReceiptService, + stores: mockStores) + + let autoDraftOrder = Order.fake().copy(siteID: 123, status: .autoDraft) + mockOrderService.orderToReturn = autoDraftOrder + await sut.syncOrder(for: .init(purchasableItems: [makeItem()]), retryHandler: {}) + + var deleteActionWasDispatched = false + var dispatchedOrder: Order? + mockStores.whenReceivingAction(ofType: OrderAction.self) { action in + if case let .deleteOrder(_, order, deletePermanently, _) = action { + deleteActionWasDispatched = true + dispatchedOrder = order + #expect(deletePermanently == true) + } + } + + // When + await sut.clearOrder() + + // Then + #expect(deleteActionWasDispatched == true) + #expect(dispatchedOrder?.orderID == autoDraftOrder.orderID) + #expect(dispatchedOrder?.status == .autoDraft) + #expect(sut.orderState == .idle) + } + + @MainActor + @available(iOS 17.0, *) + @Test func clearOrder_when_completed_order_then_does_not_dispatch_delete_action() async throws { + // Given + let mockStores = MockStoresManager(sessionManager: .testingInstance) + let sut = PointOfSaleOrderController(orderService: mockOrderService, + receiptService: mockReceiptService, + stores: mockStores) + + let completedOrder = Order.fake().copy(status: .completed) + mockOrderService.orderToReturn = completedOrder + await sut.syncOrder(for: .init(purchasableItems: [makeItem()]), retryHandler: {}) + + var deleteActionWasDispatched = false + mockStores.whenReceivingAction(ofType: OrderAction.self) { action in + if case .deleteOrder = action { + deleteActionWasDispatched = true + } + } + + // When + await sut.clearOrder() + + // Then + #expect(deleteActionWasDispatched == false) + #expect(sut.orderState == .idle) + } + @MainActor struct AnalyticsTests { private let analytics: WooAnalytics diff --git a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderController.swift b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderController.swift index 575935eb212..133c0e7ca8e 100644 --- a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderController.swift +++ b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderController.swift @@ -37,7 +37,7 @@ final class MockPointOfSaleOrderController: PointOfSaleOrderControllerProtocol { } var clearOrderWasCalled: Bool = false - func clearOrder() { + func clearOrder() async { clearOrderWasCalled = true }