Skip to content

Commit

Permalink
[Woo POS] Extract cart logic to CartViewModel (#13191)
Browse files Browse the repository at this point in the history
  • Loading branch information
jaclync authored Jul 1, 2024
2 parents 5ac3e1e + e15efe2 commit 432ea08
Show file tree
Hide file tree
Showing 7 changed files with 315 additions and 131 deletions.
39 changes: 26 additions & 13 deletions WooCommerce/Classes/POS/Presentation/CartView.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import SwiftUI
import protocol Yosemite.POSItem

struct CartView: View {
@ObservedObject private var viewModel: PointOfSaleDashboardViewModel
@ObservedObject private var cartViewModel: CartViewModel

init(viewModel: PointOfSaleDashboardViewModel) {
init(viewModel: PointOfSaleDashboardViewModel, cartViewModel: CartViewModel) {
self.viewModel = viewModel
self.cartViewModel = cartViewModel
}

var body: some View {
Expand All @@ -13,16 +16,17 @@ struct CartView: View {
Text("Cart")
.foregroundColor(Color.posPrimaryTexti3)
Spacer()
if let temsInCartLabel = viewModel.itemsInCartLabel {
if let temsInCartLabel = cartViewModel.itemsInCartLabel {
Text(temsInCartLabel)
.foregroundColor(Color.posPrimaryTexti3)
Button {
viewModel.removeAllItemsFromCart()
cartViewModel.removeAllItemsFromCart()
} label: {
Text("Clear all")
.foregroundColor(Color.init(uiColor: .wooCommercePurple(.shade60)))
}
.padding(.horizontal, 8)
.renderedIf(cartViewModel.canDeleteItemsFromCart)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
Expand All @@ -32,19 +36,19 @@ struct CartView: View {
.foregroundColor(Color.white)
ScrollViewReader { proxy in
ScrollView {
ForEach(viewModel.itemsInCart, id: \.id) { cartItem in
ForEach(cartViewModel.itemsInCart, id: \.id) { cartItem in
ItemRowView(cartItem: cartItem,
onItemRemoveTapped: viewModel.canDeleteItemsFromCart ? {
viewModel.removeItemFromCart(cartItem)
onItemRemoveTapped: cartViewModel.canDeleteItemsFromCart ? {
cartViewModel.removeItemFromCart(cartItem)
} : nil)
.id(cartItem.id)
.background(Color.posBackgroundGreyi3)
.padding(.horizontal, 32)
}
}
.onChange(of: viewModel.itemToScrollToWhenCartUpdated?.id) { _ in
.onChange(of: cartViewModel.itemToScrollToWhenCartUpdated?.id) { _ in
if viewModel.orderStage == .building,
let last = viewModel.itemToScrollToWhenCartUpdated?.id {
let last = cartViewModel.itemToScrollToWhenCartUpdated?.id {
withAnimation {
proxy.scrollTo(last)
}
Expand Down Expand Up @@ -72,7 +76,7 @@ struct CartView: View {
private extension CartView {
var checkoutButton: some View {
Button {
viewModel.submitCart()
cartViewModel.submitCart()
} label: {
HStack {
Spacer()
Expand All @@ -88,7 +92,7 @@ private extension CartView {

var addMoreButton: some View {
Button {
viewModel.addMoreToCart()
cartViewModel.addMoreToCart()
} label: {
Spacer()
Text("Add More")
Expand All @@ -102,9 +106,18 @@ private extension CartView {
}

#if DEBUG
import Combine
#Preview {
CartView(viewModel: PointOfSaleDashboardViewModel(itemProvider: POSItemProviderPreview(),
cardPresentPaymentService: CardPresentPaymentPreviewService(),
orderService: POSOrderPreviewService()))
// TODO:
// Simplify this by mocking `CartViewModel`
// https://github.com/woocommerce/woocommerce-ios/issues/13207
let orderStageSubject = PassthroughSubject<PointOfSaleDashboardViewModel.OrderStage, Never>()
let orderStagePublisher = orderStageSubject.eraseToAnyPublisher()
let dashboardViewModel = PointOfSaleDashboardViewModel(itemProvider: POSItemProviderPreview(),
cardPresentPaymentService: CardPresentPaymentPreviewService(),
orderService: POSOrderPreviewService())
let cartViewModel = CartViewModel(orderStage: orderStagePublisher)

return CartView(viewModel: dashboardViewModel, cartViewModel: cartViewModel)
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,9 @@ private extension PointOfSaleDashboardView {
}

var cartView: some View {
CartView(viewModel: viewModel)
.frame(maxWidth: .infinity)
CartView(viewModel: viewModel,
cartViewModel: viewModel.cartViewModel)
.frame(maxWidth: .infinity)
}

var totalsView: some View {
Expand Down
64 changes: 64 additions & 0 deletions WooCommerce/Classes/POS/ViewModels/CartViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import SwiftUI
import Combine
import protocol Yosemite.POSItem

final class CartViewModel: ObservableObject {
/// Emits cart items when the CTA is tapped to submit the cart.
let cartSubmissionPublisher: AnyPublisher<[CartItem], Never>
private let cartSubmissionSubject: PassthroughSubject<[CartItem], Never> = .init()

/// Emits a signal when the CTA is tapped to update the cart.
let addMoreToCartActionPublisher: AnyPublisher<Void, Never>
private let addMoreToCartActionSubject: PassthroughSubject<Void, Never> = .init()

@Published private(set) var itemsInCart: [CartItem] = []

// It should be synced with the source of truth in `PointOfSaleDashboardViewModel`.
@Published private var orderStage: PointOfSaleDashboardViewModel.OrderStage = .building

var canDeleteItemsFromCart: Bool {
orderStage != .finalizing
}

init(orderStage: AnyPublisher<PointOfSaleDashboardViewModel.OrderStage, Never>) {
cartSubmissionPublisher = cartSubmissionSubject.eraseToAnyPublisher()
addMoreToCartActionPublisher = addMoreToCartActionSubject.eraseToAnyPublisher()
orderStage.assign(to: &$orderStage)
}

func addItemToCart(_ item: POSItem) {
let cartItem = CartItem(id: UUID(), item: item, quantity: 1)
itemsInCart.append(cartItem)
}

func removeItemFromCart(_ cartItem: CartItem) {
itemsInCart.removeAll(where: { $0.id == cartItem.id })
}

func removeAllItemsFromCart() {
itemsInCart.removeAll()
}

var itemToScrollToWhenCartUpdated: CartItem? {
return itemsInCart.last
}

var itemsInCartLabel: String? {
switch itemsInCart.count {
case 0:
return nil
default:
return String.pluralize(itemsInCart.count,
singular: "%1$d item",
plural: "%1$d items")
}
}

func submitCart() {
cartSubmissionSubject.send(itemsInCart)
}

func addMoreToCart() {
addMoreToCartActionSubject.send(())
}
}
122 changes: 55 additions & 67 deletions WooCommerce/Classes/POS/ViewModels/PointOfSaleDashboardViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,9 @@ final class PointOfSaleDashboardViewModel: ObservableObject {
}

let itemSelectorViewModel: ItemSelectorViewModel
private(set) lazy var cartViewModel: CartViewModel = CartViewModel(orderStage: $orderStage.eraseToAnyPublisher())

@Published private(set) var itemsInCart: [CartItem] = [] {
didSet {
checkIfCartEmpty()
}
}
@Published private(set) var isCartCollapsed: Bool = true

// Total amounts
@Published private(set) var formattedCartTotalPrice: String?
Expand Down Expand Up @@ -72,10 +69,16 @@ final class PointOfSaleDashboardViewModel: ObservableObject {
}

@Published private(set) var orderStage: OrderStage = .building
@Published private(set) var paymentState: PointOfSaleDashboardViewModel.PaymentState = .acceptingCard

@Published private(set) var isAddMoreDisabled: Bool = false
@Published var isExitPOSDisabled: Bool = false

private var cancellables: Set<AnyCancellable> = []

// TODO: 12998 - move the following properties to totals view model
@Published private(set) var paymentState: PointOfSaleDashboardViewModel.PaymentState = .acceptingCard
private var itemsInCart: [CartItem] = []

/// Order created the first time the checkout is shown for a given transaction.
/// If the merchant goes back to the product selection screen and makes changes, this should be updated when they return to the checkout.
@Published private var order: POSOrder?
Expand All @@ -86,8 +89,6 @@ final class PointOfSaleDashboardViewModel: ObservableObject {

private let currencyFormatter = CurrencyFormatter(currencySettings: ServiceLocator.currencySettings)

private var cancellables: Set<AnyCancellable> = []

init(itemProvider: POSItemProvider,
cardPresentPaymentService: CardPresentPaymentFacade,
orderService: POSOrderServiceProtocol) {
Expand All @@ -98,64 +99,15 @@ final class PointOfSaleDashboardViewModel: ObservableObject {
self.itemSelectorViewModel = .init(itemProvider: itemProvider)

observeSelectedItemToAddToCart()
observeCartItemsForCollapsedState()
observeCartSubmission()
observeCartAddMoreAction()
observeCartItemsToCheckIfCartIsEmpty()
observeCardPresentPaymentEvents()
observeItemsInCartForCartTotal()
observePaymentStateForButtonDisabledProperties()
}

var canDeleteItemsFromCart: Bool {
return orderStage != .finalizing
}

var isCartCollapsed: Bool {
itemsInCart.isEmpty
}

var itemToScrollToWhenCartUpdated: CartItem? {
return itemsInCart.last
}

func addItemToCart(_ item: POSItem) {
let cartItem = CartItem(id: UUID(), item: item, quantity: 1)
itemsInCart.append(cartItem)
}

func removeItemFromCart(_ cartItem: CartItem) {
itemsInCart.removeAll(where: { $0.id == cartItem.id })
}

func removeAllItemsFromCart() {
itemsInCart.removeAll()
}

var itemsInCartLabel: String? {
switch itemsInCart.count {
case 0:
return nil
default:
return String.pluralize(itemsInCart.count,
singular: "%1$d item",
plural: "%1$d items")
}
}

private func checkIfCartEmpty() {
if itemsInCart.isEmpty {
orderStage = .building
}
}

func submitCart() {
// TODO: https://github.com/woocommerce/woocommerce-ios/issues/12810
orderStage = .finalizing

startSyncingOrder()
}

func addMoreToCart() {
orderStage = .building
}

var areAmountsFullyCalculated: Bool {
return isSyncingOrder == false && formattedOrderTotalTaxPrice != nil && formattedOrderTotalPrice != nil
}
Expand Down Expand Up @@ -208,18 +160,22 @@ final class PointOfSaleDashboardViewModel: ObservableObject {
}

func calculateAmountsTapped() {
startSyncingOrder()
// TODO: 12998 - move to the totals view model
startSyncingOrder(cartItems: itemsInCart)
}

private func startSyncingOrder() {
private func startSyncingOrder(cartItems: [CartItem]) {
// TODO: 12998 - move to the totals view model
// At this point, this should happen in the TotalsViewModel
Task { @MainActor in
await syncOrder(for: itemsInCart)
itemsInCart = cartItems
await syncOrder(for: cartItems)
}
}

func startNewTransaction() {
// clear cart
itemsInCart.removeAll()
cartViewModel.removeAllItemsFromCart()
orderStage = .building
paymentState = .acceptingCard
order = nil
Expand All @@ -234,15 +190,47 @@ final class PointOfSaleDashboardViewModel: ObservableObject {
private extension PointOfSaleDashboardViewModel {
func observeSelectedItemToAddToCart() {
itemSelectorViewModel.selectedItemPublisher.sink { [weak self] selectedItem in
self?.addItemToCart(selectedItem)
self?.cartViewModel.addItemToCart(selectedItem)
}
.store(in: &cancellables)
}

func observeCartItemsForCollapsedState() {
cartViewModel.$itemsInCart
.map { $0.isEmpty }
.assign(to: &$isCartCollapsed)
}

func observeCartSubmission() {
cartViewModel.cartSubmissionPublisher.sink { [weak self] cartItems in
guard let self else { return }
orderStage = .finalizing
startSyncingOrder(cartItems: cartItems)
}
.store(in: &cancellables)
}

func observeCartAddMoreAction() {
cartViewModel.addMoreToCartActionPublisher.sink { [weak self] in
guard let self else { return }
orderStage = .building
}
.store(in: &cancellables)
}

func observeCartItemsToCheckIfCartIsEmpty() {
cartViewModel.$itemsInCart
.filter { $0.isEmpty }
.sink { [weak self] _ in
self?.orderStage = .building
}
.store(in: &cancellables)
}
}

private extension PointOfSaleDashboardViewModel {
func observeItemsInCartForCartTotal() {
$itemsInCart
cartViewModel.$itemsInCart
.map { [weak self] in
guard let self else { return "-" }
let totalValue: Decimal = $0.reduce(0) { partialResult, cartItem in
Expand Down
Loading

0 comments on commit 432ea08

Please sign in to comment.