Skip to content

[Woo POS] Make cash payment events independent from card payment events #15769

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

Merged
Merged
26 changes: 13 additions & 13 deletions WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -177,7 +177,7 @@ extension PointOfSaleAggregateModel {

private func setStateForEditing() {
orderStage = .building
paymentState = .card(.idle)
paymentState = PointOfSalePaymentState()
cardPresentPaymentInlineMessage = nil
}
}
Expand Down Expand Up @@ -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()
}

Expand Down Expand Up @@ -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)

Expand Down
83 changes: 57 additions & 26 deletions WooCommerce/Classes/POS/Models/PointOfSalePaymentState.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,29 @@
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 activePaymentMethod: PointOfSalePaymentMethod {
if cash != .idle {
return .cash
}
return .card
}

var shownFullScreen: Bool {
switch activePaymentMethod {
case .cash:
return cash.shownFullScreen
case .card:
return card.shownFullScreen
}
}
}

enum PointOfSaleCardPaymentState: Equatable {
Expand All @@ -19,63 +40,73 @@ 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
}
}

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
case .cash:
}
}
}

extension PointOfSaleCashPaymentState {
var shownFullScreen: Bool {
switch self {
case .idle:
return false
case .collectingCash, .paymentSuccess:
return true
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
101 changes: 53 additions & 48 deletions WooCommerce/Classes/POS/Presentation/TotalsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,21 @@ struct TotalsView: View {
}

private var backgroundColor: Color {
switch posModel.paymentState {
case .card(.processingPayment):
.posPrimary
case .cash(.collectingCash):
.posSurfaceBright
default:
.clear
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
}
}
}
}
Expand Down Expand Up @@ -214,7 +222,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
}
Expand All @@ -229,10 +237,29 @@ private extension TotalsView {
private extension TotalsView {

@ViewBuilder private var paymentView: some View {
switch posModel.paymentState {
case .card(let cardPaymentState):
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: cardPaymentState) {
paymentState: posModel.paymentState) {
PointOfSaleCardPresentPaymentReaderDisconnectedMessageView {
posModel.connectCardReader()
}
Expand All @@ -248,21 +275,6 @@ 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)))
}
}
}
}
}
Expand Down Expand Up @@ -305,11 +317,12 @@ private extension TotalsView {

switch posModel.cardReaderConnectionStatus {
case .connected, .disconnecting, .cancellingConnection:
switch posModel.paymentState {
case .card:
return posModel.cardPresentPaymentInlineMessage != nil
// Show card payment UI if there's a message, or cash payment UI when not 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.
Expand All @@ -322,9 +335,14 @@ private extension TotalsView {
return .primary
}

switch posModel.paymentState {
case .card(let cardPaymentState):
switch cardPaymentState {
switch posModel.paymentState.activePaymentMethod {
case .cash:
return PaymentViewLayout(backgroundColor: backgroundColor,
topPadding: POSPadding.none,
bottomPadding: posModel.paymentState.cash == .collectingCash ? nil : POSPadding.none,
sidePadding: POSPadding.none)
case .card:
switch posModel.paymentState.card {
case .validatingOrderError,
.paymentIntentCreationError:
return .outlined
Expand All @@ -345,26 +363,13 @@ private extension TotalsView {
.preparingReader,
.processingPayment:
if TotalsViewHelper().shouldShowDisconnectedMessage(readerConnectionStatus: posModel.cardReaderConnectionStatus,
paymentState: cardPaymentState) {
paymentState: posModel.paymentState) {
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
}
}

Expand Down
Loading