From 79a92f609e832995252abeeed6060452b8957df1 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Tue, 17 Jun 2025 21:22:51 +0300 Subject: [PATCH 1/9] Split card and cash PointOfSalePaymentState Turn cash and card relationship from OR (enum) to AND (struct), allowing them to co-exist together. Keeping both cash and card payment state in a single variable can cause occasional issues when card payment event unexpectedly arrives and changes the state while cash payment was in progress. --- .../POS/Models/PointOfSalePaymentState.swift | 75 ++++++++++++------- 1 file changed, 49 insertions(+), 26 deletions(-) diff --git a/WooCommerce/Classes/POS/Models/PointOfSalePaymentState.swift b/WooCommerce/Classes/POS/Models/PointOfSalePaymentState.swift index ff25b76859f..06f07180250 100644 --- a/WooCommerce/Classes/POS/Models/PointOfSalePaymentState.swift +++ b/WooCommerce/Classes/POS/Models/PointOfSalePaymentState.swift @@ -1,8 +1,21 @@ import Foundation -enum PointOfSalePaymentState: Equatable { - case card(PointOfSaleCardPaymentState) - case cash(PointOfSaleCashPaymentState) +struct PointOfSalePaymentState: Equatable { + var card: PointOfSaleCardPaymentState + var cash: PointOfSaleCashPaymentState + + init(card: PointOfSaleCardPaymentState = .idle, cash: PointOfSaleCashPaymentState = .idle) { + self.card = card + self.cash = cash + } + + var shownFullScreen: Bool { + if cash != .idle { + return cash.shownFullScreen + } + + return card.shownFullScreen + } } enum PointOfSaleCardPaymentState: Equatable { @@ -19,43 +32,44 @@ enum PointOfSaleCardPaymentState: Equatable { } enum PointOfSaleCashPaymentState: Equatable { + case idle case collectingCash case paymentSuccess } -extension PointOfSalePaymentState { +extension PointOfSaleCardPaymentState { init?(from cardPaymentEvent: CardPresentPaymentEvent, using paymentEventPresentationStyleDependencies: PointOfSaleCardPresentPaymentEventPresentationStyle.Dependencies) { switch cardPaymentEvent { case .idle: - self = .card(.idle) + self = .idle case .show(.validatingOrder): - self = .card(.validatingOrder) + self = .validatingOrder case .show(.preparingForPayment): - self = .card(.preparingReader) + self = .preparingReader case .show(.tapSwipeOrInsertCard): - self = .card(.acceptingCard) + self = .acceptingCard case .show(.cardInserted): - self = .card(.cardInserted) + self = .cardInserted case .show(.processing), .show(.displayReaderMessage): - self = .card(.processingPayment) + self = .processingPayment case .show(.paymentError): if case let .show(eventDetails) = cardPaymentEvent, case let .message(messageType) = PointOfSaleCardPresentPaymentEventPresentationStyle( for: eventDetails, dependencies: paymentEventPresentationStyleDependencies), case .validatingOrderError = messageType { - self = .card(.validatingOrderError) + self = .validatingOrderError } else { - self = .card(.paymentError) + self = .paymentError } case .show(.paymentCaptureError): - self = .card(.paymentError) + self = .paymentError case .show(.paymentSuccess): - self = .card(.cardPaymentSuccessful) + self = .cardPaymentSuccessful case .show(.paymentIntentCreationError): - self = .card(.paymentIntentCreationError) + self = .paymentIntentCreationError default: return nil } @@ -63,19 +77,28 @@ extension PointOfSalePaymentState { var shownFullScreen: Bool { switch self { - case .card(.processingPayment), - .card(.paymentError), - .card(.cardPaymentSuccessful): + case .processingPayment, + .paymentError, + .cardPaymentSuccessful: return true - case .card(.idle), - .card(.validatingOrder), - .card(.validatingOrderError), - .card(.paymentIntentCreationError), - .card(.preparingReader), - .card(.acceptingCard), - .card(.cardInserted): + case .idle, + .validatingOrder, + .validatingOrderError, + .paymentIntentCreationError, + .preparingReader, + .acceptingCard, + .cardInserted: + return false + } + } +} + +extension PointOfSaleCashPaymentState { + var shownFullScreen: Bool { + switch self { + case .idle: return false - case .cash: + case .collectingCash, .paymentSuccess: return true } } From f97797865271a66ced1899f0bb4b1d0c32d0c1f6 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Tue, 17 Jun 2025 21:23:18 +0300 Subject: [PATCH 2/9] Update CartViewHelper to use card payment state --- .../POS/ViewHelpers/CartViewHelper.swift | 31 ++++++++----------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/WooCommerce/Classes/POS/ViewHelpers/CartViewHelper.swift b/WooCommerce/Classes/POS/ViewHelpers/CartViewHelper.swift index d3d216ddd9a..dcca305c839 100644 --- a/WooCommerce/Classes/POS/ViewHelpers/CartViewHelper.swift +++ b/WooCommerce/Classes/POS/ViewHelpers/CartViewHelper.swift @@ -12,7 +12,7 @@ struct CartViewHelper { func shouldPreventCartEditing(orderState: PointOfSaleOrderState, paymentState: PointOfSalePaymentState) -> Bool { - guard paymentState.allowsCartEditing else { + guard paymentState.card.allowsCartEditing else { return true } return orderState.isSyncing @@ -40,26 +40,21 @@ struct CartViewHelper { } } -private extension PointOfSalePaymentState { +private extension PointOfSaleCardPaymentState { var allowsCartEditing: Bool { switch self { - case .card(let cardPaymentState): - switch cardPaymentState { - case .processingPayment, - .paymentError, - .cardPaymentSuccessful, - .validatingOrder, - .preparingReader, - .cardInserted: - return false - case .idle, - .validatingOrderError, - .paymentIntentCreationError, - .acceptingCard: - return true - } - case .cash: + case .processingPayment, + .paymentError, + .cardPaymentSuccessful, + .validatingOrder, + .preparingReader, + .cardInserted: return false + case .idle, + .validatingOrderError, + .paymentIntentCreationError, + .acceptingCard: + return true } } } From dda4a6db62b37982caa20ec695ff32314567cf97 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Tue, 17 Jun 2025 21:23:35 +0300 Subject: [PATCH 3/9] Update TotalsViewHelper to use card and cash payment states --- .../POS/ViewHelpers/TotalsViewHelper.swift | 62 ++++++++++++------- 1 file changed, 38 insertions(+), 24 deletions(-) diff --git a/WooCommerce/Classes/POS/ViewHelpers/TotalsViewHelper.swift b/WooCommerce/Classes/POS/ViewHelpers/TotalsViewHelper.swift index f3fdc5c3c0e..e2e836437b5 100644 --- a/WooCommerce/Classes/POS/ViewHelpers/TotalsViewHelper.swift +++ b/WooCommerce/Classes/POS/ViewHelpers/TotalsViewHelper.swift @@ -2,33 +2,37 @@ import Foundation struct TotalsViewHelper { func shouldShowTotalsFields(for paymentState: PointOfSalePaymentState) -> Bool { - switch paymentState { - case .card(let cardPaymentState): - switch cardPaymentState { - case .idle, - .acceptingCard, - .cardInserted, - .validatingOrder, - .validatingOrderError, - .paymentIntentCreationError, - .preparingReader: - return true - case .processingPayment, - .paymentError, - .cardPaymentSuccessful: - return false - } - case .cash: + guard paymentState.cash == .idle else { + return false + } + + switch paymentState.card { + case .idle, + .acceptingCard, + .cardInserted, + .validatingOrder, + .validatingOrderError, + .paymentIntentCreationError, + .preparingReader: + return true + case .processingPayment, + .paymentError, + .cardPaymentSuccessful: return false } } func shouldShowDisconnectedMessage(readerConnectionStatus: CardPresentPaymentReaderConnectionStatus, - paymentState: PointOfSaleCardPaymentState) -> Bool { + paymentState: PointOfSalePaymentState) -> Bool { guard readerConnectionStatus == .disconnected else { return false } - switch paymentState { + + guard paymentState.cash == .idle else { + return false + } + + switch paymentState.card { case .idle, .acceptingCard, .preparingReader: @@ -47,8 +51,11 @@ struct TotalsViewHelper { func shouldShowCollectCashPaymentButton(orderState: PointOfSaleOrderState, paymentState: PointOfSalePaymentState, cardReaderConnectionStatus: CardPresentPaymentReaderConnectionStatus) -> Bool { - guard orderState != .syncing, - case .card(let cardState) = paymentState else { + guard orderState != .syncing else { + return false + } + + guard paymentState.cash == .idle else { return false } @@ -60,7 +67,7 @@ struct TotalsViewHelper { return true } - switch cardState { + switch paymentState.card { case .validatingOrderError, .paymentIntentCreationError, .acceptingCard: @@ -71,8 +78,15 @@ struct TotalsViewHelper { } func shouldApplyPadding(paymentState: PointOfSalePaymentState) -> Bool { - switch paymentState { - case .card(.cardPaymentSuccessful), .cash(.paymentSuccess), .cash(.collectingCash), .card(.paymentError): + switch paymentState.cash { + case .collectingCash, .paymentSuccess: + return false + case .idle: + break + } + + switch paymentState.card { + case .cardPaymentSuccessful, .paymentError: return false default: return true From 78d70652f20b6a8a0379860bf08af0ffceb8ab03 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Tue, 17 Jun 2025 21:23:58 +0300 Subject: [PATCH 4/9] Update the views to use card and cash payment states --- .../Presentation/POSFloatingControlView.swift | 2 +- .../PointOfSaleDashboardView.swift | 2 +- .../Classes/POS/Presentation/TotalsView.swift | 139 +++++++++--------- 3 files changed, 71 insertions(+), 72 deletions(-) diff --git a/WooCommerce/Classes/POS/Presentation/POSFloatingControlView.swift b/WooCommerce/Classes/POS/Presentation/POSFloatingControlView.swift index abf1aa93531..bbf39d8e646 100644 --- a/WooCommerce/Classes/POS/Presentation/POSFloatingControlView.swift +++ b/WooCommerce/Classes/POS/Presentation/POSFloatingControlView.swift @@ -69,7 +69,7 @@ struct POSFloatingControlView: View { } .background(backgroundColor) .cornerRadius(Constants.cornerRadius) - .disabled(posModel.paymentState == .card(.processingPayment)) + .disabled(posModel.paymentState.card == .processingPayment) CardReaderConnectionStatusView() .foregroundStyle(fontColor) diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift index 2bfffc0b67c..9ce39f4fffc 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift @@ -80,7 +80,7 @@ struct PointOfSaleDashboardView: View { .environment(\.floatingControlAreaSize, CGSizeMake(floatingSize.width + Constants.floatingControlHorizontalOffset, floatingSize.height + Constants.floatingControlVerticalOffset)) - .environment(\.posBackgroundAppearance, posModel.paymentState != .card(.processingPayment) ? .primary : .secondary) + .environment(\.posBackgroundAppearance, posModel.paymentState.card != .processingPayment ? .primary : .secondary) .animation(.easeInOut, value: itemsViewState.containerState == .loading) .background(Color.posSurface) .navigationBarBackButtonHidden(true) diff --git a/WooCommerce/Classes/POS/Presentation/TotalsView.swift b/WooCommerce/Classes/POS/Presentation/TotalsView.swift index b331d43c582..d12521309ad 100644 --- a/WooCommerce/Classes/POS/Presentation/TotalsView.swift +++ b/WooCommerce/Classes/POS/Presentation/TotalsView.swift @@ -93,14 +93,21 @@ struct TotalsView: View { } private var backgroundColor: Color { - switch posModel.paymentState { - case .card(.processingPayment): - .posPrimary - case .cash(.collectingCash): - .posSurfaceBright + switch posModel.paymentState.card { + case .processingPayment: + return .posPrimary default: - .clear + break } + + switch posModel.paymentState.cash { + case .collectingCash: + return .posSurfaceBright + default: + break + } + + return .clear } } @@ -214,7 +221,7 @@ private extension TotalsView { /// Hide totals fields with animation after a delay when starting to processing a payment /// - Parameter isShowing private func hideTotalsFieldsWithDelay(_ isShowing: Bool) { - guard !isShowing && posModel.paymentState == .card(.processingPayment) else { + guard !isShowing && posModel.paymentState.card == .processingPayment else { self.isShowingTotalsFields = isShowing return } @@ -229,10 +236,10 @@ private extension TotalsView { private extension TotalsView { @ViewBuilder private var paymentView: some View { - switch posModel.paymentState { - case .card(let cardPaymentState): + switch posModel.paymentState.cash { + case .idle: if TotalsViewHelper().shouldShowDisconnectedMessage(readerConnectionStatus: posModel.cardReaderConnectionStatus, - paymentState: cardPaymentState) { + paymentState: posModel.paymentState) { PointOfSaleCardPresentPaymentReaderDisconnectedMessageView { posModel.connectCardReader() } @@ -248,20 +255,17 @@ private extension TotalsView { } } } - case .cash(let cashPaymentState): - switch cashPaymentState { - case .collectingCash: - if case .loaded(let total) = posModel.orderState { - PointOfSaleCollectCashView(orderTotal: total.orderTotal) - .transition(.move(edge: .trailing)) - } - case .paymentSuccess: - if case .loaded(let total) = posModel.orderState { - PointOfSaleCardPresentPaymentInLineMessage( - messageType: .paymentSuccess( - viewModel: .init(formattedOrderTotal: total.orderTotal, - paymentMethod: .cash))) - } + case .collectingCash: + if case .loaded(let total) = posModel.orderState { + PointOfSaleCollectCashView(orderTotal: total.orderTotal) + .transition(.move(edge: .trailing)) + } + case .paymentSuccess: + if case .loaded(let total) = posModel.orderState { + PointOfSaleCardPresentPaymentInLineMessage( + messageType: .paymentSuccess( + viewModel: .init(formattedOrderTotal: total.orderTotal, + paymentMethod: .cash))) } } } @@ -305,12 +309,8 @@ private extension TotalsView { switch posModel.cardReaderConnectionStatus { case .connected, .disconnecting, .cancellingConnection: - switch posModel.paymentState { - case .card: - return posModel.cardPresentPaymentInlineMessage != nil - case .cash: - return true - } + // Show card payment UI if there's a message, or cash payment UI when not idle + return posModel.cardPresentPaymentInlineMessage != nil || posModel.paymentState.cash != .idle case .disconnected: // Since the reader is disconnected, this will show the "Connect your reader" CTA button view. return true @@ -322,49 +322,48 @@ private extension TotalsView { return .primary } - switch posModel.paymentState { - case .card(let cardPaymentState): - switch cardPaymentState { - case .validatingOrderError, - .paymentIntentCreationError: + switch posModel.paymentState.cash { + case .idle: + break + case .collectingCash: + return PaymentViewLayout(backgroundColor: backgroundColor, + topPadding: POSPadding.none, + bottomPadding: nil, + sidePadding: POSPadding.none) + case .paymentSuccess: + return PaymentViewLayout(backgroundColor: backgroundColor, + topPadding: POSPadding.none, + bottomPadding: POSPadding.none, + sidePadding: POSPadding.none) + } + + switch posModel.paymentState.card { + case .validatingOrderError, + .paymentIntentCreationError: + return .outlined + case .paymentError: + return PaymentViewLayout(backgroundColor: backgroundColor, + topPadding: POSPadding.none, + bottomPadding: POSPadding.none, + sidePadding: POSPadding.none) + case .cardPaymentSuccessful: + return PaymentViewLayout(backgroundColor: backgroundColor, + topPadding: POSPadding.none, + bottomPadding: POSPadding.none, + sidePadding: POSPadding.none) + case .idle, + .acceptingCard, + .cardInserted, + .validatingOrder, + .preparingReader, + .processingPayment: + if TotalsViewHelper().shouldShowDisconnectedMessage(readerConnectionStatus: posModel.cardReaderConnectionStatus, + paymentState: posModel.paymentState) { return .outlined - case .paymentError: - return PaymentViewLayout(backgroundColor: backgroundColor, - topPadding: POSPadding.none, - bottomPadding: POSPadding.none, - sidePadding: POSPadding.none) - case .cardPaymentSuccessful: - return PaymentViewLayout(backgroundColor: backgroundColor, - topPadding: POSPadding.none, - bottomPadding: POSPadding.none, - sidePadding: POSPadding.none) - case .idle, - .acceptingCard, - .cardInserted, - .validatingOrder, - .preparingReader, - .processingPayment: - if TotalsViewHelper().shouldShowDisconnectedMessage(readerConnectionStatus: posModel.cardReaderConnectionStatus, - paymentState: cardPaymentState) { - return .outlined - } else { - return .primary - } - } - case .cash(let cashPaymentState): - switch cashPaymentState { - case .collectingCash: - return PaymentViewLayout(backgroundColor: backgroundColor, - topPadding: POSPadding.none, - bottomPadding: nil, - sidePadding: POSPadding.none) - case .paymentSuccess: - return PaymentViewLayout(backgroundColor: backgroundColor, - topPadding: POSPadding.none, - bottomPadding: POSPadding.none, - sidePadding: POSPadding.none) } } + + return .primary } } From cfb157f64e42bd16f027fca7d0f588a1f4fc32e5 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Tue, 17 Jun 2025 21:24:14 +0300 Subject: [PATCH 5/9] Update aggregate model to set separate cash and card payment states --- .../Models/PointOfSaleAggregateModel.swift | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift index 82b1651188a..16898641f5f 100644 --- a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift +++ b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift @@ -111,7 +111,7 @@ protocol PointOfSaleAggregateModelProtocol { popularPurchasableItemsController: PointOfSaleItemsControllerProtocol, barcodeScanService: PointOfSaleBarcodeScanServiceProtocol, soundPlayer: PointOfSaleSoundPlayerProtocol = PointOfSaleSoundPlayer(), - paymentState: PointOfSalePaymentState = .card(.idle)) { + paymentState: PointOfSalePaymentState = PointOfSalePaymentState()) { self.purchasableItemsController = itemsController self.purchasableItemsSearchController = purchasableItemsSearchController self.couponsController = couponsController @@ -177,7 +177,7 @@ extension PointOfSaleAggregateModel { private func setStateForEditing() { orderStage = .building - paymentState = .card(.idle) + paymentState = PointOfSalePaymentState() cardPresentPaymentInlineMessage = nil } } @@ -311,20 +311,20 @@ extension PointOfSaleAggregateModel { func startCashPayment() async { analytics.track(.pointOfSaleCashPaymentTapped) try? await cardPresentPaymentService.cancelPayment() - paymentState = .cash(.collectingCash) + paymentState.cash = .collectingCash } @MainActor func cancelCashPayment() async { analytics.track(.pointOfSaleBackToCheckoutFromCashTapped) - paymentState = .card(.idle) + paymentState.cash = .idle if case .connected = cardReaderConnectionStatus { await collectCardPayment() } } private func cashPaymentSuccess() { - paymentState = .cash(.paymentSuccess) + paymentState.cash = .paymentSuccess collectOrderPaymentAnalyticsTracker.trackSuccessfulCashPayment() } @@ -435,24 +435,24 @@ private extension PointOfSaleAggregateModel { .store(in: &cancellables) cardPresentPaymentService.paymentEventPublisher - .compactMap { [weak self] paymentEvent -> PointOfSalePaymentState? in + .compactMap { [weak self] paymentEvent -> PointOfSaleCardPaymentState? in guard let self else { return nil } - let newPaymentState = PointOfSalePaymentState(from: paymentEvent, - using: presentationStyleDeterminerDependencies) + let newCardPaymentState = PointOfSaleCardPaymentState(from: paymentEvent, + using: presentationStyleDeterminerDependencies) - if case .card(.acceptingCard) = newPaymentState { + if case .acceptingCard = newCardPaymentState { collectOrderPaymentAnalyticsTracker.trackCardReaderReady() } - if case .card(.processingPayment) = newPaymentState { + if case .processingPayment = newCardPaymentState { collectOrderPaymentAnalyticsTracker.trackCardReaderTapped() } - return newPaymentState + return newCardPaymentState } - .sink(receiveValue: { [weak self] paymentState in - self?.paymentState = paymentState + .sink(receiveValue: { [weak self] cardPaymentState in + self?.paymentState.card = cardPaymentState }) .store(in: &cancellables) From b38b2cf42402dfb7df1a187beef48a6068a177e0 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Tue, 17 Jun 2025 21:26:09 +0300 Subject: [PATCH 6/9] Update tests for card and cash payment states --- .../Mocks/MockPointOfSaleAggregateModel.swift | 2 +- .../PointOfSaleAggregateModelTests.swift | 20 +++---- .../POS/ViewHelpers/CartViewHelperTests.swift | 14 ++--- .../ViewHelpers/TotalsViewHelperTests.swift | 52 +++++++++++++------ 4 files changed, 53 insertions(+), 35 deletions(-) diff --git a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleAggregateModel.swift b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleAggregateModel.swift index aa47705fd2d..118f79ca64c 100644 --- a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleAggregateModel.swift +++ b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleAggregateModel.swift @@ -45,7 +45,7 @@ final class MockPointOfSaleAggregateModel: PointOfSaleAggregateModelProtocol { couponsSearchController: PointOfSaleSearchingItemsControllerProtocol = MockPointOfSaleCouponsController(), orderStage: PointOfSaleOrderStage = .building, orderState: PointOfSaleOrderState = .idle, - paymentState: PointOfSalePaymentState = .card(.idle)) { + paymentState: PointOfSalePaymentState = PointOfSalePaymentState()) { self.cardReaderConnectionStatus = cardReaderConnectionStatus self.purchasableItemsController = purchasableItemsController self.purchasableItemsSearchController = purchasableItemsSearchController diff --git a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift index 6f329e5b6fd..9ff64d87384 100644 --- a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift @@ -539,7 +539,7 @@ struct PointOfSaleAggregateModelTests { soundPlayer: MockPointOfSaleSoundPlayer()) // Then - #expect(sut.paymentState == .card(.idle)) + #expect(sut.paymentState == PointOfSalePaymentState(card: .idle, cash: .idle)) } @available(iOS 17.0, *) @@ -558,13 +558,13 @@ struct PointOfSaleAggregateModelTests { popularPurchasableItemsController: MockPointOfSaleItemsController(), barcodeScanService: MockPointOfSaleBarcodeScanService(), soundPlayer: MockPointOfSaleSoundPlayer(), - paymentState: .card(.cardPaymentSuccessful)) + paymentState: PointOfSalePaymentState(card: .cardPaymentSuccessful, cash: .idle)) // When sut.startNewCart() // Then - #expect(sut.paymentState == .card(.idle)) + #expect(sut.paymentState == PointOfSalePaymentState(card: .idle, cash: .idle)) } @available(iOS 17.0, *) @@ -611,13 +611,13 @@ struct PointOfSaleAggregateModelTests { popularPurchasableItemsController: MockPointOfSaleItemsController(), barcodeScanService: MockPointOfSaleBarcodeScanService(), soundPlayer: MockPointOfSaleSoundPlayer(), - paymentState: .card(.cardPaymentSuccessful)) + paymentState: PointOfSalePaymentState(card: .cardPaymentSuccessful, cash: .idle)) // When sut.addMoreToCart() // Then - #expect(sut.paymentState == .card(.idle)) + #expect(sut.paymentState == PointOfSalePaymentState(card: .idle, cash: .idle)) } @available(iOS 17.0, *) @@ -675,7 +675,7 @@ struct PointOfSaleAggregateModelTests { // Then #expect(cardPresentPaymentService.cancelPaymentCalled == true) - #expect(sut.paymentState == .cash(.collectingCash)) + #expect(sut.paymentState == PointOfSalePaymentState(card: .idle, cash: .collectingCash)) } @available(iOS 17.0, *) @@ -700,7 +700,7 @@ struct PointOfSaleAggregateModelTests { await sut.startCashPayment() // Then - #expect(sut.paymentState == .cash(.collectingCash)) + #expect(sut.paymentState == PointOfSalePaymentState(card: .idle, cash: .collectingCash)) } @available(iOS 17.0, *) @@ -721,13 +721,13 @@ struct PointOfSaleAggregateModelTests { barcodeScanService: MockPointOfSaleBarcodeScanService(), soundPlayer: MockPointOfSaleSoundPlayer()) await sut.startCashPayment() - #expect(sut.paymentState == .cash(.collectingCash)) + #expect(sut.paymentState == PointOfSalePaymentState(card: .idle, cash: .collectingCash)) // When await sut.cancelCashPayment() // Then - #expect(sut.paymentState == .card(.idle)) + #expect(sut.paymentState == PointOfSalePaymentState(card: .idle, cash: .idle)) } @available(iOS 17.0, *) @@ -753,7 +753,7 @@ struct PointOfSaleAggregateModelTests { #expect(sut.orderStage == .finalizing) await sut.startCashPayment() - #expect(sut.paymentState == .cash(.collectingCash)) + #expect(sut.paymentState == PointOfSalePaymentState(card: .idle, cash: .collectingCash)) // When await sut.cancelCashPayment() diff --git a/WooCommerce/WooCommerceTests/POS/ViewHelpers/CartViewHelperTests.swift b/WooCommerce/WooCommerceTests/POS/ViewHelpers/CartViewHelperTests.swift index d02533a3a3e..ce17b780ca2 100644 --- a/WooCommerce/WooCommerceTests/POS/ViewHelpers/CartViewHelperTests.swift +++ b/WooCommerce/WooCommerceTests/POS/ViewHelpers/CartViewHelperTests.swift @@ -10,7 +10,7 @@ struct CartViewHelperTests { // When, Then #expect(sut.shouldPreventCartEditing(orderState: .syncing, - paymentState: .card(.idle)) == true) + paymentState: PointOfSalePaymentState(card: .idle)) == true) } @Test func shouldPreventCartEditing_when_card_paymentState_cardPaymentSuccessful() async throws { @@ -23,7 +23,7 @@ struct CartViewHelperTests { // When, Then #expect(sut.shouldPreventCartEditing(orderState: orderLoaded, - paymentState: .card(.cardPaymentSuccessful)) == true) + paymentState: PointOfSalePaymentState(card: .cardPaymentSuccessful)) == true) } @Test func shouldPreventCartEditing_when_card_paymentState_processingPayment() async throws { @@ -36,7 +36,7 @@ struct CartViewHelperTests { // When, Then #expect(sut.shouldPreventCartEditing(orderState: orderLoaded, - paymentState: .card(.processingPayment)) == true) + paymentState: PointOfSalePaymentState(card: .processingPayment)) == true) } @Test func shouldPreventCartEditing_false_when_card_paymentState_acceptingCard() async throws { @@ -49,7 +49,7 @@ struct CartViewHelperTests { // When, Then #expect(sut.shouldPreventCartEditing(orderState: orderLoaded, - paymentState: .card(.acceptingCard)) == false) + paymentState: PointOfSalePaymentState(card: .acceptingCard)) == false) } @Test func shouldPreventCartEditing_false_when_card_paymentState_validatingOrderError() async throws { @@ -62,7 +62,7 @@ struct CartViewHelperTests { // When, Then #expect(sut.shouldPreventCartEditing(orderState: orderLoaded, - paymentState: .card(.validatingOrderError)) == false) + paymentState: PointOfSalePaymentState(card: .validatingOrderError)) == false) } @Test func shouldPreventCartEditing_when_card_paymentState_cardInserted() async throws { @@ -75,7 +75,7 @@ struct CartViewHelperTests { // When, Then #expect(sut.shouldPreventCartEditing(orderState: orderLoaded, - paymentState: .card(.cardInserted)) == true) + paymentState: PointOfSalePaymentState(card: .cardInserted)) == true) } @Test func shouldPreventCartEditing_false_when_card_paymentState_paymentIntentCreationError() async throws { @@ -88,7 +88,7 @@ struct CartViewHelperTests { // Then #expect(sut.shouldPreventCartEditing(orderState: orderLoaded, - paymentState: .card(.paymentIntentCreationError)) == false) + paymentState: PointOfSalePaymentState(card: .paymentIntentCreationError)) == false) } @Test(arguments: zip([0, 1, 2, 3], [nil, "1 item", "2 items", "3 items"])) diff --git a/WooCommerce/WooCommerceTests/POS/ViewHelpers/TotalsViewHelperTests.swift b/WooCommerce/WooCommerceTests/POS/ViewHelpers/TotalsViewHelperTests.swift index 95b7bf13e62..95b8e621460 100644 --- a/WooCommerce/WooCommerceTests/POS/ViewHelpers/TotalsViewHelperTests.swift +++ b/WooCommerce/WooCommerceTests/POS/ViewHelpers/TotalsViewHelperTests.swift @@ -12,7 +12,7 @@ struct TotalsViewHelperTests { readerConnectionStatus: CardPresentPaymentReaderConnectionStatus, paymentState: PointOfSaleCardPaymentState) { #expect(TotalsViewHelper().shouldShowDisconnectedMessage(readerConnectionStatus: readerConnectionStatus, - paymentState: paymentState)) + paymentState: PointOfSalePaymentState(card: paymentState, cash: .idle))) } @Test(arguments: [ @@ -26,7 +26,7 @@ struct TotalsViewHelperTests { readerConnectionStatus: CardPresentPaymentReaderConnectionStatus, paymentState: PointOfSaleCardPaymentState) { #expect(TotalsViewHelper().shouldShowDisconnectedMessage(readerConnectionStatus: readerConnectionStatus, - paymentState: paymentState) == false) + paymentState: PointOfSalePaymentState(card: paymentState, cash: .idle)) == false) } @Test(arguments: [ @@ -45,22 +45,22 @@ struct TotalsViewHelperTests { readerConnectionStatus: CardPresentPaymentReaderConnectionStatus, paymentState: PointOfSaleCardPaymentState) { #expect(TotalsViewHelper().shouldShowDisconnectedMessage(readerConnectionStatus: readerConnectionStatus, - paymentState: paymentState) == false) + paymentState: PointOfSalePaymentState(card: paymentState, cash: .idle)) == false) } @Test(arguments: [ - (PointOfSalePaymentState.card(.validatingOrderError)), - (PointOfSalePaymentState.card(.acceptingCard)), - (PointOfSalePaymentState.card(.cardInserted)), - (PointOfSalePaymentState.card(.paymentIntentCreationError)) + (PointOfSaleCardPaymentState.validatingOrderError), + (PointOfSaleCardPaymentState.acceptingCard), + (PointOfSaleCardPaymentState.cardInserted), + (PointOfSaleCardPaymentState.paymentIntentCreationError) ]) func test_shouldShowCollectCashPaymentButton_returns_true_for_supported_states( - paymentState: PointOfSalePaymentState) { + cardPaymentState: PointOfSaleCardPaymentState) { #expect(TotalsViewHelper().shouldShowCollectCashPaymentButton(orderState: .loaded(.init(cartTotal: "10", orderTotal: "10", taxTotal: "10", orderTotalDecimal: 0)), - paymentState: paymentState, + paymentState: PointOfSalePaymentState(card: cardPaymentState, cash: .idle), cardReaderConnectionStatus: .connected(.init(name: "", batteryLevel: nil)))) } @@ -70,7 +70,7 @@ struct TotalsViewHelperTests { orderTotal: "10", taxTotal: "10", orderTotalDecimal: 0)), - paymentState: .card(.idle), + paymentState: PointOfSalePaymentState(card: .idle, cash: .idle), cardReaderConnectionStatus: .disconnected)) } @@ -80,7 +80,7 @@ struct TotalsViewHelperTests { orderTotal: "0", taxTotal: "0", orderTotalDecimal: 0)), - paymentState: .card(.idle), + paymentState: PointOfSalePaymentState(card: .idle, cash: .idle), cardReaderConnectionStatus: .connected(.init(name: "", batteryLevel: nil)))) } @@ -90,19 +90,37 @@ struct TotalsViewHelperTests { orderTotal: "10", taxTotal: "10", orderTotalDecimal: 10)), - paymentState: .card(.idle), + paymentState: PointOfSalePaymentState(card: .idle, cash: .idle), cardReaderConnectionStatus: .connected(.init(name: "", batteryLevel: nil))) == false) } @Test(arguments: [ - (PointOfSalePaymentState.card(.validatingOrderError)), - (PointOfSalePaymentState.card(.acceptingCard)), - (PointOfSalePaymentState.card(.cardInserted)) + (PointOfSaleCardPaymentState.validatingOrderError), + (PointOfSaleCardPaymentState.acceptingCard), + (PointOfSaleCardPaymentState.cardInserted) ]) func test_shouldShowCollectCashPaymentButton_returns_false_when_order_syncing( - paymentState: PointOfSalePaymentState) { + cardPaymentState: PointOfSaleCardPaymentState) { #expect(TotalsViewHelper().shouldShowCollectCashPaymentButton(orderState: .syncing, - paymentState: paymentState, + paymentState: PointOfSalePaymentState(card: cardPaymentState, cash: .idle), + cardReaderConnectionStatus: .connected(.init(name: "", batteryLevel: nil))) == false) + } + + @Test func test_shouldShowCollectCashPaymentButton_returns_false_when_cash_collecting() { + #expect(TotalsViewHelper().shouldShowCollectCashPaymentButton(orderState: .loaded(.init(cartTotal: "10", + orderTotal: "10", + taxTotal: "10", + orderTotalDecimal: 0)), + paymentState: PointOfSalePaymentState(card: .idle, cash: .collectingCash), + cardReaderConnectionStatus: .connected(.init(name: "", batteryLevel: nil))) == false) + } + + @Test func test_shouldShowCollectCashPaymentButton_returns_false_when_cash_payment_success() { + #expect(TotalsViewHelper().shouldShowCollectCashPaymentButton(orderState: .loaded(.init(cartTotal: "10", + orderTotal: "10", + taxTotal: "10", + orderTotalDecimal: 0)), + paymentState: PointOfSalePaymentState(card: .idle, cash: .paymentSuccess), cardReaderConnectionStatus: .connected(.init(name: "", batteryLevel: nil))) == false) } From b094cf0c83962d25784027c604128348e37053d8 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Tue, 17 Jun 2025 22:17:38 +0300 Subject: [PATCH 7/9] Introduce activePaymentMethod for cleaner state management --- .../POS/Models/PointOfSalePaymentState.swift | 14 +- .../Classes/POS/Presentation/TotalsView.swift | 132 +++++++++--------- .../POS/ViewHelpers/TotalsViewHelper.swift | 132 ++++++++++-------- .../PointOfSaleAggregateModelTests.swift | 28 ++++ 4 files changed, 181 insertions(+), 125 deletions(-) diff --git a/WooCommerce/Classes/POS/Models/PointOfSalePaymentState.swift b/WooCommerce/Classes/POS/Models/PointOfSalePaymentState.swift index 06f07180250..37ba387e0d4 100644 --- a/WooCommerce/Classes/POS/Models/PointOfSalePaymentState.swift +++ b/WooCommerce/Classes/POS/Models/PointOfSalePaymentState.swift @@ -9,12 +9,20 @@ struct PointOfSalePaymentState: Equatable { self.cash = cash } - var shownFullScreen: Bool { + var activePaymentMethod: PointOfSalePaymentMethod { if cash != .idle { - return cash.shownFullScreen + return .cash } + return .card + } - return card.shownFullScreen + var shownFullScreen: Bool { + switch activePaymentMethod { + case .cash: + return cash.shownFullScreen + case .card: + return card.shownFullScreen + } } } diff --git a/WooCommerce/Classes/POS/Presentation/TotalsView.swift b/WooCommerce/Classes/POS/Presentation/TotalsView.swift index d12521309ad..cc43faf7f98 100644 --- a/WooCommerce/Classes/POS/Presentation/TotalsView.swift +++ b/WooCommerce/Classes/POS/Presentation/TotalsView.swift @@ -93,21 +93,22 @@ struct TotalsView: View { } private var backgroundColor: Color { - switch posModel.paymentState.card { - case .processingPayment: - return .posPrimary - default: - break - } - - switch posModel.paymentState.cash { - case .collectingCash: - return .posSurfaceBright - default: - break + switch posModel.paymentState.activePaymentMethod { + case .cash: + switch posModel.paymentState.cash { + case .collectingCash: + return .posSurfaceBright + default: + return .clear + } + case .card: + switch posModel.paymentState.card { + case .processingPayment: + return .posPrimary + default: + return .clear + } } - - return .clear } } @@ -236,8 +237,27 @@ private extension TotalsView { private extension TotalsView { @ViewBuilder private var paymentView: some View { - switch posModel.paymentState.cash { - case .idle: + switch posModel.paymentState.activePaymentMethod { + case .cash: + // Show cash payment UI when cash payment is active + switch posModel.paymentState.cash { + case .collectingCash: + if case .loaded(let total) = posModel.orderState { + PointOfSaleCollectCashView(orderTotal: total.orderTotal) + .transition(.move(edge: .trailing)) + } + case .paymentSuccess: + if case .loaded(let total) = posModel.orderState { + PointOfSaleCardPresentPaymentInLineMessage( + messageType: .paymentSuccess( + viewModel: .init(formattedOrderTotal: total.orderTotal, + paymentMethod: PointOfSalePaymentMethod.cash))) + } + case .idle: + EmptyView() + } + case .card: + // Show card payment UI when card is the active payment method if TotalsViewHelper().shouldShowDisconnectedMessage(readerConnectionStatus: posModel.cardReaderConnectionStatus, paymentState: posModel.paymentState) { PointOfSaleCardPresentPaymentReaderDisconnectedMessageView { @@ -255,18 +275,6 @@ private extension TotalsView { } } } - case .collectingCash: - if case .loaded(let total) = posModel.orderState { - PointOfSaleCollectCashView(orderTotal: total.orderTotal) - .transition(.move(edge: .trailing)) - } - case .paymentSuccess: - if case .loaded(let total) = posModel.orderState { - PointOfSaleCardPresentPaymentInLineMessage( - messageType: .paymentSuccess( - viewModel: .init(formattedOrderTotal: total.orderTotal, - paymentMethod: .cash))) - } } } } @@ -310,7 +318,12 @@ private extension TotalsView { switch posModel.cardReaderConnectionStatus { case .connected, .disconnecting, .cancellingConnection: // Show card payment UI if there's a message, or cash payment UI when not idle - return posModel.cardPresentPaymentInlineMessage != nil || posModel.paymentState.cash != .idle + switch posModel.paymentState.activePaymentMethod { + case .cash: + return true + case .card: + return posModel.cardPresentPaymentInlineMessage != nil + } case .disconnected: // Since the reader is disconnected, this will show the "Connect your reader" CTA button view. return true @@ -322,44 +335,37 @@ private extension TotalsView { return .primary } - switch posModel.paymentState.cash { - case .idle: - break - case .collectingCash: - return PaymentViewLayout(backgroundColor: backgroundColor, - topPadding: POSPadding.none, - bottomPadding: nil, - sidePadding: POSPadding.none) - case .paymentSuccess: - return PaymentViewLayout(backgroundColor: backgroundColor, - topPadding: POSPadding.none, - bottomPadding: POSPadding.none, - sidePadding: POSPadding.none) - } - - switch posModel.paymentState.card { - case .validatingOrderError, - .paymentIntentCreationError: - return .outlined - case .paymentError: + switch posModel.paymentState.activePaymentMethod { + case .cash: return PaymentViewLayout(backgroundColor: backgroundColor, topPadding: POSPadding.none, - bottomPadding: POSPadding.none, + bottomPadding: posModel.paymentState.cash == .collectingCash ? nil : POSPadding.none, sidePadding: POSPadding.none) - case .cardPaymentSuccessful: - return PaymentViewLayout(backgroundColor: backgroundColor, - topPadding: POSPadding.none, - bottomPadding: POSPadding.none, - sidePadding: POSPadding.none) - case .idle, - .acceptingCard, - .cardInserted, - .validatingOrder, - .preparingReader, - .processingPayment: - if TotalsViewHelper().shouldShowDisconnectedMessage(readerConnectionStatus: posModel.cardReaderConnectionStatus, - paymentState: posModel.paymentState) { + case .card: + switch posModel.paymentState.card { + case .validatingOrderError, + .paymentIntentCreationError: return .outlined + case .paymentError: + return PaymentViewLayout(backgroundColor: backgroundColor, + topPadding: POSPadding.none, + bottomPadding: POSPadding.none, + sidePadding: POSPadding.none) + case .cardPaymentSuccessful: + return PaymentViewLayout(backgroundColor: backgroundColor, + topPadding: POSPadding.none, + bottomPadding: POSPadding.none, + sidePadding: POSPadding.none) + case .idle, + .acceptingCard, + .cardInserted, + .validatingOrder, + .preparingReader, + .processingPayment: + if TotalsViewHelper().shouldShowDisconnectedMessage(readerConnectionStatus: posModel.cardReaderConnectionStatus, + paymentState: posModel.paymentState) { + return .outlined + } } } diff --git a/WooCommerce/Classes/POS/ViewHelpers/TotalsViewHelper.swift b/WooCommerce/Classes/POS/ViewHelpers/TotalsViewHelper.swift index e2e836437b5..9f1d5f896e7 100644 --- a/WooCommerce/Classes/POS/ViewHelpers/TotalsViewHelper.swift +++ b/WooCommerce/Classes/POS/ViewHelpers/TotalsViewHelper.swift @@ -2,23 +2,24 @@ import Foundation struct TotalsViewHelper { func shouldShowTotalsFields(for paymentState: PointOfSalePaymentState) -> Bool { - guard paymentState.cash == .idle else { - return false - } - - switch paymentState.card { - case .idle, - .acceptingCard, - .cardInserted, - .validatingOrder, - .validatingOrderError, - .paymentIntentCreationError, - .preparingReader: - return true - case .processingPayment, - .paymentError, - .cardPaymentSuccessful: + switch paymentState.activePaymentMethod { + case .cash: return false + case .card: + switch paymentState.card { + case .idle, + .acceptingCard, + .cardInserted, + .validatingOrder, + .validatingOrderError, + .paymentIntentCreationError, + .preparingReader: + return true + case .processingPayment, + .paymentError, + .cardPaymentSuccessful: + return false + } } } @@ -28,23 +29,24 @@ struct TotalsViewHelper { return false } - guard paymentState.cash == .idle else { - return false - } - - switch paymentState.card { - case .idle, - .acceptingCard, - .preparingReader: - return true - case .validatingOrder, - .validatingOrderError, - .paymentIntentCreationError, - .processingPayment, - .cardInserted, - .paymentError, - .cardPaymentSuccessful: + switch paymentState.activePaymentMethod { + case .cash: return false + case .card: + switch paymentState.card { + case .idle, + .acceptingCard, + .preparingReader: + return true + case .validatingOrder, + .validatingOrderError, + .paymentIntentCreationError, + .processingPayment, + .cardInserted, + .paymentError, + .cardPaymentSuccessful: + return false + } } } @@ -55,41 +57,53 @@ struct TotalsViewHelper { return false } - guard paymentState.cash == .idle else { + switch paymentState.activePaymentMethod { + case .cash: return false - } + case .card: + if cardReaderConnectionStatus == .disconnected { + return true + } - if cardReaderConnectionStatus == .disconnected { - return true - } - - if case let .loaded(totals) = orderState, totals.orderTotalDecimal.isZero { - return true - } + if case let .loaded(totals) = orderState, totals.orderTotalDecimal.isZero { + return true + } - switch paymentState.card { - case .validatingOrderError, - .paymentIntentCreationError, - .acceptingCard: - return true - default: - return false + switch paymentState.card { + case .validatingOrderError, + .paymentIntentCreationError, + .acceptingCard: + return true + case .idle, + .cardInserted, + .validatingOrder, + .preparingReader, + .processingPayment, + .paymentError, + .cardPaymentSuccessful: + return false + } } } func shouldApplyPadding(paymentState: PointOfSalePaymentState) -> Bool { - switch paymentState.cash { - case .collectingCash, .paymentSuccess: - return false - case .idle: - break - } - - switch paymentState.card { - case .cardPaymentSuccessful, .paymentError: + switch paymentState.activePaymentMethod { + case .cash: return false - default: - return true + case .card: + switch paymentState.card { + case .cardPaymentSuccessful, .paymentError: + return false + case .idle, + .acceptingCard, + .cardInserted, + .validatingOrder, + .validatingOrderError, + .paymentIntentCreationError, + .preparingReader, + .processingPayment: + return true + } } } diff --git a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift index 9ff64d87384..6039a4d10f0 100644 --- a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift @@ -540,6 +540,34 @@ struct PointOfSaleAggregateModelTests { // Then #expect(sut.paymentState == PointOfSalePaymentState(card: .idle, cash: .idle)) + #expect(sut.paymentState.activePaymentMethod == .card) + } + + @available(iOS 17.0, *) + @Test func activePaymentMethod_returns_card_when_cash_is_idle() async throws { + // Given + let paymentState = PointOfSalePaymentState(card: .acceptingCard, cash: .idle) + + // Then + #expect(paymentState.activePaymentMethod == .card) + } + + @available(iOS 17.0, *) + @Test func activePaymentMethod_returns_cash_when_cash_is_not_idle() async throws { + // Given + let paymentState = PointOfSalePaymentState(card: .acceptingCard, cash: .collectingCash) + + // Then + #expect(paymentState.activePaymentMethod == .cash) + } + + @available(iOS 17.0, *) + @Test func activePaymentMethod_returns_cash_when_cash_is_paymentSuccess() async throws { + // Given + let paymentState = PointOfSalePaymentState(card: .idle, cash: .paymentSuccess) + + // Then + #expect(paymentState.activePaymentMethod == .cash) } @available(iOS 17.0, *) From c07d5cdf8718eea1304b4b830a5114d335f093ee Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 23 Jun 2025 10:58:55 +0300 Subject: [PATCH 8/9] Remove default parameters from PointOfSalePaymentState initializer --- .../POS/Models/PointOfSaleAggregateModel.swift | 4 ++-- .../POS/Models/PointOfSalePaymentState.swift | 6 +++++- .../POS/Mocks/MockPointOfSaleAggregateModel.swift | 2 +- .../POS/ViewHelpers/CartViewHelperTests.swift | 14 +++++++------- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift index 2e2c2964507..8378f68bee4 100644 --- a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift +++ b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift @@ -111,7 +111,7 @@ protocol PointOfSaleAggregateModelProtocol { popularPurchasableItemsController: PointOfSaleItemsControllerProtocol, barcodeScanService: PointOfSaleBarcodeScanServiceProtocol, soundPlayer: PointOfSaleSoundPlayerProtocol = PointOfSaleSoundPlayer(), - paymentState: PointOfSalePaymentState = PointOfSalePaymentState()) { + paymentState: PointOfSalePaymentState = .idle) { self.purchasableItemsController = itemsController self.purchasableItemsSearchController = purchasableItemsSearchController self.couponsController = couponsController @@ -177,7 +177,7 @@ extension PointOfSaleAggregateModel { private func setStateForEditing() { orderStage = .building - paymentState = PointOfSalePaymentState() + paymentState = .idle cardPresentPaymentInlineMessage = nil } } diff --git a/WooCommerce/Classes/POS/Models/PointOfSalePaymentState.swift b/WooCommerce/Classes/POS/Models/PointOfSalePaymentState.swift index 37ba387e0d4..240813966b1 100644 --- a/WooCommerce/Classes/POS/Models/PointOfSalePaymentState.swift +++ b/WooCommerce/Classes/POS/Models/PointOfSalePaymentState.swift @@ -4,11 +4,15 @@ struct PointOfSalePaymentState: Equatable { var card: PointOfSaleCardPaymentState var cash: PointOfSaleCashPaymentState - init(card: PointOfSaleCardPaymentState = .idle, cash: PointOfSaleCashPaymentState = .idle) { + init(card: PointOfSaleCardPaymentState, cash: PointOfSaleCashPaymentState) { self.card = card self.cash = cash } + static var idle: PointOfSalePaymentState { + .init(card: .idle, cash: .idle) + } + var activePaymentMethod: PointOfSalePaymentMethod { if cash != .idle { return .cash diff --git a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleAggregateModel.swift b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleAggregateModel.swift index 118f79ca64c..9ec59821e65 100644 --- a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleAggregateModel.swift +++ b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleAggregateModel.swift @@ -45,7 +45,7 @@ final class MockPointOfSaleAggregateModel: PointOfSaleAggregateModelProtocol { couponsSearchController: PointOfSaleSearchingItemsControllerProtocol = MockPointOfSaleCouponsController(), orderStage: PointOfSaleOrderStage = .building, orderState: PointOfSaleOrderState = .idle, - paymentState: PointOfSalePaymentState = PointOfSalePaymentState()) { + paymentState: PointOfSalePaymentState = .idle) { self.cardReaderConnectionStatus = cardReaderConnectionStatus self.purchasableItemsController = purchasableItemsController self.purchasableItemsSearchController = purchasableItemsSearchController diff --git a/WooCommerce/WooCommerceTests/POS/ViewHelpers/CartViewHelperTests.swift b/WooCommerce/WooCommerceTests/POS/ViewHelpers/CartViewHelperTests.swift index ce17b780ca2..dc632eddb40 100644 --- a/WooCommerce/WooCommerceTests/POS/ViewHelpers/CartViewHelperTests.swift +++ b/WooCommerce/WooCommerceTests/POS/ViewHelpers/CartViewHelperTests.swift @@ -10,7 +10,7 @@ struct CartViewHelperTests { // When, Then #expect(sut.shouldPreventCartEditing(orderState: .syncing, - paymentState: PointOfSalePaymentState(card: .idle)) == true) + paymentState: PointOfSalePaymentState(card: .idle, cash: .idle)) == true) } @Test func shouldPreventCartEditing_when_card_paymentState_cardPaymentSuccessful() async throws { @@ -23,7 +23,7 @@ struct CartViewHelperTests { // When, Then #expect(sut.shouldPreventCartEditing(orderState: orderLoaded, - paymentState: PointOfSalePaymentState(card: .cardPaymentSuccessful)) == true) + paymentState: PointOfSalePaymentState(card: .cardPaymentSuccessful, cash: .idle)) == true) } @Test func shouldPreventCartEditing_when_card_paymentState_processingPayment() async throws { @@ -36,7 +36,7 @@ struct CartViewHelperTests { // When, Then #expect(sut.shouldPreventCartEditing(orderState: orderLoaded, - paymentState: PointOfSalePaymentState(card: .processingPayment)) == true) + paymentState: PointOfSalePaymentState(card: .processingPayment, cash: .idle)) == true) } @Test func shouldPreventCartEditing_false_when_card_paymentState_acceptingCard() async throws { @@ -49,7 +49,7 @@ struct CartViewHelperTests { // When, Then #expect(sut.shouldPreventCartEditing(orderState: orderLoaded, - paymentState: PointOfSalePaymentState(card: .acceptingCard)) == false) + paymentState: PointOfSalePaymentState(card: .acceptingCard, cash: .idle)) == false) } @Test func shouldPreventCartEditing_false_when_card_paymentState_validatingOrderError() async throws { @@ -62,7 +62,7 @@ struct CartViewHelperTests { // When, Then #expect(sut.shouldPreventCartEditing(orderState: orderLoaded, - paymentState: PointOfSalePaymentState(card: .validatingOrderError)) == false) + paymentState: PointOfSalePaymentState(card: .validatingOrderError, cash: .idle)) == false) } @Test func shouldPreventCartEditing_when_card_paymentState_cardInserted() async throws { @@ -75,7 +75,7 @@ struct CartViewHelperTests { // When, Then #expect(sut.shouldPreventCartEditing(orderState: orderLoaded, - paymentState: PointOfSalePaymentState(card: .cardInserted)) == true) + paymentState: PointOfSalePaymentState(card: .cardInserted, cash: .idle)) == true) } @Test func shouldPreventCartEditing_false_when_card_paymentState_paymentIntentCreationError() async throws { @@ -88,7 +88,7 @@ struct CartViewHelperTests { // Then #expect(sut.shouldPreventCartEditing(orderState: orderLoaded, - paymentState: PointOfSalePaymentState(card: .paymentIntentCreationError)) == false) + paymentState: PointOfSalePaymentState(card: .paymentIntentCreationError, cash: .idle)) == false) } @Test(arguments: zip([0, 1, 2, 3], [nil, "1 item", "2 items", "3 items"])) From db6f305a5adaf5e20955c52b60c2b18d4f9df9e0 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 23 Jun 2025 10:59:07 +0300 Subject: [PATCH 9/9] Move backgroundAppearance to a variable --- .../Classes/POS/Presentation/PointOfSaleDashboardView.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift index 63cbd8b4b17..f5e70d5ff5c 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift @@ -80,7 +80,7 @@ struct PointOfSaleDashboardView: View { .environment(\.floatingControlAreaSize, CGSizeMake(floatingSize.width + Constants.floatingControlHorizontalOffset, floatingSize.height + Constants.floatingControlVerticalOffset)) - .environment(\.posBackgroundAppearance, posModel.paymentState.card != .processingPayment ? .primary : .secondary) + .environment(\.posBackgroundAppearance, backgroundAppearance) .animation(.easeInOut, value: itemsViewState.containerState == .loading) .background(Color.posSurface) .navigationBarBackButtonHidden(true) @@ -143,6 +143,10 @@ struct PointOfSaleDashboardView: View { .animation(.default, value: posModel.paymentState.shownFullScreen) } } + + private var backgroundAppearance: POSBackgroundAppearanceKey.Appearance { + posModel.paymentState.card != .processingPayment ? .primary : .secondary + } } @available(iOS 17.0, *)