From 2f295b09e269a4bd99f4523697659f08e74b02b3 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 5 Aug 2025 15:44:10 +0700 Subject: [PATCH 1/5] check and delete auto-drafts on exit POS --- .../POS/Controllers/PointOfSaleOrderController.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/WooCommerce/Classes/POS/Controllers/PointOfSaleOrderController.swift b/WooCommerce/Classes/POS/Controllers/PointOfSaleOrderController.swift index 98031bd8b6c..92a827e15e0 100644 --- a/WooCommerce/Classes/POS/Controllers/PointOfSaleOrderController.swift +++ b/WooCommerce/Classes/POS/Controllers/PointOfSaleOrderController.swift @@ -144,10 +144,20 @@ protocol PointOfSaleOrderControllerProtocol { } func clearOrder() { + clearAutoDraftIfNeeded(for: order) order = nil orderState = .idle } + private func clearAutoDraftIfNeeded(for order: Order?) { + if let order, order.status == .autoDraft { + DispatchQueue.main.async { [weak self] in + let action = OrderAction.deleteOrder(siteID: order.siteID, order: order, deletePermanently: true) { _ in } + self?.stores.dispatch(action) + } + } + } + private func celebrate() { celebration.celebrate() } From 7ea9b21d6e67787d762e0b1b8e35475e30b1dfd4 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 5 Aug 2025 15:51:56 +0700 Subject: [PATCH 2/5] update release notes --- RELEASE-NOTES.txt | 1 + 1 file changed, 1 insertion(+) 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 From 4c4ed833d803a6c30c792bbb7be4781213aac0d8 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 5 Aug 2025 16:43:47 +0700 Subject: [PATCH 3/5] add tests --- .../PointOfSaleOrderControllerTests.swift | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderControllerTests.swift b/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderControllerTests.swift index b3a0baae6ea..79b0b52b016 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 + 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 + 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 + sut.clearOrder() + + // Then + #expect(deleteActionWasDispatched == false) + #expect(sut.orderState == .idle) + } + @MainActor struct AnalyticsTests { private let analytics: WooAnalytics From f4d7c91ef9b6968d4d4e3c7c39ad28521e23630a Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 5 Aug 2025 17:11:11 +0700 Subject: [PATCH 4/5] add delay for testing the dispatch --- .../POS/Controllers/PointOfSaleOrderControllerTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderControllerTests.swift b/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderControllerTests.swift index 79b0b52b016..53544590e8d 100644 --- a/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderControllerTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderControllerTests.swift @@ -621,6 +621,7 @@ struct PointOfSaleOrderControllerTests { // When sut.clearOrder() + try await Task.sleep(nanoseconds: 100_000_000) // Then #expect(deleteActionWasDispatched == true) From fc8199744306bc9699c824bb1062645899c67f7a Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 5 Aug 2025 17:30:57 +0700 Subject: [PATCH 5/5] make clearOrder() async --- .../PointOfSaleOrderController.swift | 18 +++++++++++------- .../POS/Models/PointOfSaleAggregateModel.swift | 8 ++++++-- .../PointOfSalePreviewOrderController.swift | 2 +- .../PointOfSaleOrderControllerTests.swift | 7 +++---- .../Mocks/MockPointOfSaleOrderController.swift | 2 +- 5 files changed, 22 insertions(+), 15 deletions(-) diff --git a/WooCommerce/Classes/POS/Controllers/PointOfSaleOrderController.swift b/WooCommerce/Classes/POS/Controllers/PointOfSaleOrderController.swift index 92a827e15e0..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,16 +143,20 @@ protocol PointOfSaleOrderControllerProtocol { } } - func clearOrder() { - clearAutoDraftIfNeeded(for: order) + func clearOrder() async { + await clearAutoDraftIfNeeded(for: order) order = nil orderState = .idle } - private func clearAutoDraftIfNeeded(for order: Order?) { - if let order, order.status == .autoDraft { - DispatchQueue.main.async { [weak self] in - let action = OrderAction.deleteOrder(siteID: order.siteID, order: order, deletePermanently: true) { _ in } + 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) } } 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 53544590e8d..62658f57d1e 100644 --- a/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderControllerTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderControllerTests.swift @@ -589,7 +589,7 @@ struct PointOfSaleOrderControllerTests { } // When - sut.clearOrder() + await sut.clearOrder() // Then #expect(deleteActionWasDispatched == false) @@ -620,8 +620,7 @@ struct PointOfSaleOrderControllerTests { } // When - sut.clearOrder() - try await Task.sleep(nanoseconds: 100_000_000) + await sut.clearOrder() // Then #expect(deleteActionWasDispatched == true) @@ -651,7 +650,7 @@ struct PointOfSaleOrderControllerTests { } // When - sut.clearOrder() + await sut.clearOrder() // Then #expect(deleteActionWasDispatched == false) 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 }