Skip to content

[Woo POS] Ensure auto-draft order removal on exit POS #15975

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: trunk
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ protocol PointOfSaleOrderControllerProtocol {
@discardableResult
func syncOrder(for cart: Cart, retryHandler: @escaping () async -> Void) async -> Result<SyncOrderState, Error>
func sendReceipt(recipientEmail: String) async throws
func clearOrder()
func clearOrder() async
func collectCashPayment(changeDueAmount: String?) async throws
}

Expand Down Expand Up @@ -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()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,9 @@ extension PointOfSaleAggregateModel {

func startNewCart() {
removeAllItemsFromCart()
orderController.clearOrder()
Task {
await orderController.clearOrder()
}
setStateForEditing()
viewStateCoordinator.reset()
}
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class PointOfSalePreviewOrderController: PointOfSaleOrderControllerProtocol {

func sendReceipt(recipientEmail: String) async throws { }

func clearOrder() { }
func clearOrder() async { }

func collectCashPayment(changeDueAmount: String?) async throws {}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ final class MockPointOfSaleOrderController: PointOfSaleOrderControllerProtocol {
}

var clearOrderWasCalled: Bool = false
func clearOrder() {
func clearOrder() async {
clearOrderWasCalled = true
}

Expand Down