Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ protocol PointOfSaleOrderControllerProtocol {
var orderState: PointOfSaleInternalOrderState { get }

@discardableResult
func syncOrder(for cartProducts: [CartItem], retryHandler: @escaping () async -> Void) async -> Result<SyncOrderState, Error>
func syncOrder(for cart: Cart, retryHandler: @escaping () async -> Void) async -> Result<SyncOrderState, Error>
func sendReceipt(recipientEmail: String) async throws
func clearOrder()
func collectCashPayment() async throws
Expand Down Expand Up @@ -63,9 +63,9 @@ protocol PointOfSaleOrderControllerProtocol {
private var order: Order? = nil

@MainActor @discardableResult
func syncOrder(for cartItems: [CartItem],
func syncOrder(for cart: Cart,
retryHandler: @escaping () async -> Void) async -> Result<SyncOrderState, Error> {
let posCartItems = cartItems.map {
let posCartItems = cart.items.map {
POSCartItem(item: $0.item, quantity: Decimal($0.quantity))
}

Expand Down
62 changes: 62 additions & 0 deletions WooCommerce/Classes/POS/Models/Cart.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import Foundation
import protocol Yosemite.POSOrderableItem
import enum Yosemite.POSItem

struct Cart {
var items: [CartItem] = []
var coupons: [CartCouponItem] = []
}

struct CartItem {
let id: UUID
let item: POSOrderableItem
let title: String
let subtitle: String?
let quantity: Int
}

struct CartCouponItem {
let id: UUID
let code: String
}

// MARK: - Helper Methods

extension Cart {
mutating func add(_ posItem: POSItem) {
switch posItem {
case .simpleProduct(let simpleProduct):
let productItem = CartItem(id: UUID(), item: simpleProduct, title: simpleProduct.name, subtitle: nil, quantity: 1)
items.insert(productItem, at: items.startIndex)
case .variation(let variation):
let productItem = CartItem(id: UUID(), item: variation, title: variation.parentProductName, subtitle: variation.name, quantity: 1)
items.insert(productItem, at: items.startIndex)
case .variableParentProduct:
return
case .coupon(let coupon):
let couponItem = CartCouponItem(id: UUID(), code: coupon.code)
coupons.insert(couponItem, at: coupons.startIndex)
}
}

mutating func remove(_ cartItem: CartItem) {
items.removeAll { $0.id == cartItem.id }
}

mutating func remove(_ cartCouponItem: CartCouponItem) {
coupons.removeAll { $0.id == cartCouponItem.id }
}

mutating func removeAll() {
items.removeAll()
coupons.removeAll()
}

var isEmpty: Bool {
items.isEmpty && coupons.isEmpty
}

var isNotEmpty: Bool {
return !isEmpty
}
}
10 changes: 0 additions & 10 deletions WooCommerce/Classes/POS/Models/CartItem.swift

This file was deleted.

35 changes: 11 additions & 24 deletions WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import protocol Yosemite.POSOrderableItem
import protocol WooFoundation.Analytics
import struct Yosemite.Order
import struct Yosemite.OrderItem
import struct Yosemite.POSCartItem
import struct Yosemite.POSCoupon
import enum Yosemite.POSItem
import enum Yosemite.SystemStatusAction

Expand All @@ -28,9 +28,10 @@ protocol PointOfSaleAggregateModelProtocol {
func loadItems(base: ItemListBaseItem) async
func loadNextItems(base: ItemListBaseItem) async

var cart: [CartItem] { get }
var cart: Cart { get }
func addToCart(_ item: POSItem)
func remove(cartItem: CartItem)
func remove(cartCouponItem: CartCouponItem)
func removeAllItemsFromCart()
func addMoreToCart()
func startNewCart()
Expand All @@ -54,7 +55,7 @@ protocol PointOfSaleAggregateModelProtocol {

var itemsViewState: ItemsViewState { itemsController.itemsViewState }

private(set) var cart: [CartItem] = []
private(set) var cart: Cart = .init()

var orderState: PointOfSaleOrderState { orderController.orderState.externalState }
private var internalOrderState: PointOfSaleInternalOrderState { orderController.orderState }
Expand Down Expand Up @@ -108,33 +109,19 @@ extension PointOfSaleAggregateModel {
}
}

// MARK: - Cart

private extension POSItem {
var cartItem: CartItem? {
switch self {
case .simpleProduct(let simpleProduct):
return CartItem(id: UUID(), item: simpleProduct, title: simpleProduct.name, subtitle: nil, quantity: 1)
case .variation(let variation):
return CartItem(id: UUID(), item: variation, title: variation.parentProductName, subtitle: variation.name, quantity: 1)
case .variableParentProduct:
return nil
case .coupon:
return nil
}
}
}

@available(iOS 17.0, *)
extension PointOfSaleAggregateModel {
func addToCart(_ item: POSItem) {
trackCustomerInteractionStarted()
guard let cartItem = item.cartItem else { return }
cart.insert(cartItem, at: cart.startIndex)
cart.add(item)
}

func remove(cartItem: CartItem) {
cart.removeAll(where: { $0.id == cartItem.id } )
cart.remove(cartItem)
}

func remove(cartCouponItem: CartCouponItem) {
cart.remove(cartCouponItem)
}

func removeAllItemsFromCart() {
Expand Down Expand Up @@ -164,7 +151,7 @@ private extension PointOfSaleAggregateModel {
func trackCustomerInteractionStarted() {
// At the moment we're assumming that an interaction starts simply when the cart is zero
// but a more complex logic will be needed for other cases
if cart.count == 0 {
if cart.isEmpty {
collectOrderPaymentAnalyticsTracker.trackCustomerInteractionStarted()
}
}
Expand Down
36 changes: 30 additions & 6 deletions WooCommerce/Classes/POS/Presentation/CartView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,17 @@ struct CartView: View {

@State private var shouldShowItemImages: Bool = false

private var shouldShowCoupons: Bool {
ServiceLocator.featureFlagService.isFeatureFlagEnabled(.enableCouponsInPointOfSale)
}

var body: some View {
VStack {
POSPageHeaderView(title: Localization.cartTitle,
backButtonConfiguration: backButtonConfiguration,
trailingContent: {
DynamicHStack(horizontalAlignment: .trailing, verticalAlignment: .center, spacing: Constants.cartHeaderElementSpacing) {
if let itemsInCartLabel = viewHelper.itemsInCartLabel(for: posModel.cart.count) {
if let itemsInCartLabel = viewHelper.itemsInCartLabel(for: posModel.cart.items.count) {
Text(itemsInCartLabel)
.font(Constants.itemsFont)
.lineLimit(1)
Expand All @@ -47,7 +51,11 @@ struct CartView: View {
ScrollViewReader { proxy in
ScrollView {
VStack(spacing: Constants.cartItemSpacing) {
ForEach(posModel.cart, id: \.id) { cartItem in
if shouldShowCoupons {
couponsCartSectionView
}

ForEach(posModel.cart.items, id: \.id) { cartItem in
ItemRowView(cartItem: cartItem,
showImage: $shouldShowItemImages,
onItemRemoveTapped: posModel.orderStage == .building ? {
Expand All @@ -58,7 +66,8 @@ struct CartView: View {
.transition(.opacity)
}
}
.animation(Constants.cartAnimation, value: posModel.cart.map(\.id))
.animation(Constants.cartAnimation, value: posModel.cart.items.map(\.id))
.animation(Constants.cartAnimation, value: posModel.cart.coupons.map(\.id))
.background(GeometryReader { geometry in
Color.clear.preference(key: ScrollOffSetPreferenceKey.self,
value: geometry.frame(in: coordinateSpace).origin.y)
Expand All @@ -81,7 +90,7 @@ struct CartView: View {
.renderedIf(posModel.orderStage == .finalizing)
}
.coordinateSpace(name: Constants.scrollViewCoordinateSpaceIdentifier)
.onChange(of: posModel.cart.first?.id) { itemToScrollTo in
.onChange(of: posModel.cart.items.first?.id) { itemToScrollTo in
if posModel.orderStage == .building {
withAnimation {
proxy.scrollTo(itemToScrollTo)
Expand All @@ -93,7 +102,7 @@ struct CartView: View {
Spacer()
switch posModel.orderStage {
case .building:
if posModel.cart.isEmpty {
if posModel.cart.items.isEmpty {
EmptyView()
} else {
checkoutButton
Expand Down Expand Up @@ -263,12 +272,27 @@ private extension CartView {
}
.background(backgroundColor.ignoresSafeArea(.all))
}

var couponsCartSectionView: some View {
VStack {
ForEach(posModel.cart.coupons, id: \.id) { couponItem in
CouponRowView(couponItem: couponItem,
onItemRemoveTapped: posModel.orderStage == .building ? {
posModel.remove(cartCouponItem: couponItem)
} : nil)
.id(couponItem.id)
.transition(.opacity)
}

Spacer(minLength: 48)
}
}
}

@available(iOS 17.0, *)
private extension CartView {
func trackCheckoutTapped() {
let itemsInCart = posModel.cart.count
let itemsInCart = posModel.cart.items.count
ServiceLocator.analytics.track(event: .PointOfSale.checkoutTapped(itemsInCart))
}
}
Expand Down
65 changes: 65 additions & 0 deletions WooCommerce/Classes/POS/Presentation/CouponRowView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import SwiftUI

struct CouponRowView: View {
private let couponItem: CartCouponItem
private let onItemRemoveTapped: (() -> Void)?

@ScaledMetric private var scale: CGFloat = 1.0

init(couponItem: CartCouponItem, onItemRemoveTapped: (() -> Void)? = nil) {
self.couponItem = couponItem
self.onItemRemoveTapped = onItemRemoveTapped
}

var body: some View {
HStack(spacing: Constants.horizontalElementSpacing) {
Rectangle()
.foregroundColor(.posSurfaceDim)
.overlay {
Text(Image(systemName: "tag.square.fill"))
.font(.posButtonSymbolLarge)
.foregroundColor(.posOnSurfaceVariantLowest)
}
.frame(width: Constants.couponCardSize, height: Constants.couponCardSize)

VStack(alignment: .leading) {
Text(couponItem.code)
.foregroundColor(PointOfSaleItemListCardConstants.titleColor)
.font(Constants.itemTitleFont)
}
.frame(maxWidth: .infinity, alignment: .leading)

if let onItemRemoveTapped {
Button(action: {
onItemRemoveTapped()
}, label: {
Text(Image(systemName: "xmark.circle"))
.font(.posButtonSymbolMedium)
})
.foregroundColor(Color.posOnSurfaceVariantLowest)
}
}
.padding(.trailing, Constants.cardContentHorizontalPadding)
.frame(maxWidth: .infinity, idealHeight: Constants.couponCardSize * scale)
.background(Color.posSurfaceContainerLowest)
.posItemCardBorderStyles()
.padding(.horizontal, Constants.horizontalPadding)
}
}

private extension CouponRowView {
enum Constants {
static let couponCardSize: CGFloat = 96
static let horizontalPadding: CGFloat = POSPadding.medium
static let horizontalElementSpacing: CGFloat = POSSpacing.medium
static let cardContentHorizontalPadding: CGFloat = POSPadding.medium
static let itemTitleFont: POSFontStyle = .posBodySmallBold
}
}

#if DEBUG
@available(iOS 17.0, *)
#Preview(traits: .sizeThatFitsLayout) {
CouponRowView(couponItem: CartCouponItem(id: UUID(), code: "10-Discount")) {}
}
#endif
1 change: 0 additions & 1 deletion WooCommerce/Classes/POS/Presentation/ItemListView.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import SwiftUI
import enum Yosemite.POSItem
import protocol Yosemite.POSOrderableItem

@available(iOS 17.0, *)
struct ItemListView: View {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class PointOfSalePreviewOrderController: PointOfSaleOrderControllerProtocol {
OrderFactory.newOrder(currency: .USD)
)

func syncOrder(for cartProducts: [CartItem], retryHandler: @escaping () async -> Void) async -> Result<SyncOrderState, Error> {
func syncOrder(for cart: Cart, retryHandler: @escaping () async -> Void) async -> Result<SyncOrderState, Error> {
return .success(.newOrder)
}

Expand Down
2 changes: 1 addition & 1 deletion WooCommerce/Classes/POS/ViewHelpers/CartViewHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ final class CartViewHelper {
return orderState.isSyncing
}

func shouldShowClearCartButton(cart: [CartItem], orderStage: PointOfSaleOrderStage) -> Bool {
func shouldShowClearCartButton(cart: Cart, orderStage: PointOfSaleOrderStage) -> Bool {
cart.isNotEmpty && orderStage == .building
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,11 @@ final class CardPresentPaymentsOnboardingViewModel: ObservableObject, PaymentSet
init(
fixedState: CardPresentPaymentOnboardingState,
fixedUserIsAdministrator: Bool = false,
useCase: CardPresentPaymentsOnboardingUseCaseProtocol = CardPresentPaymentsOnboardingUseCase(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯

stores: StoresManager = ServiceLocator.stores) {
self.stores = stores
state = fixedState
useCase = CardPresentPaymentsOnboardingUseCase()
self.useCase = useCase
userIsAdministrator = fixedUserIsAdministrator
updateLearnMoreURL(state: fixedState)
}
Expand Down
Loading