Skip to content

Commit f4a6ae2

Browse files
authored
[Woo POS] Coupons: Add Coupon(s) to Cart (dummy UI) (#15407)
2 parents 0451803 + 26b4ece commit f4a6ae2

File tree

18 files changed

+266
-94
lines changed

18 files changed

+266
-94
lines changed

WooCommerce/Classes/POS/Controllers/PointOfSaleOrderController.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ protocol PointOfSaleOrderControllerProtocol {
2727
var orderState: PointOfSaleInternalOrderState { get }
2828

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

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

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import Foundation
2+
import protocol Yosemite.POSOrderableItem
3+
import enum Yosemite.POSItem
4+
5+
struct Cart {
6+
var items: [CartItem] = []
7+
var coupons: [CartCouponItem] = []
8+
}
9+
10+
struct CartItem {
11+
let id: UUID
12+
let item: POSOrderableItem
13+
let title: String
14+
let subtitle: String?
15+
let quantity: Int
16+
}
17+
18+
struct CartCouponItem {
19+
let id: UUID
20+
let code: String
21+
}
22+
23+
// MARK: - Helper Methods
24+
25+
extension Cart {
26+
mutating func add(_ posItem: POSItem) {
27+
switch posItem {
28+
case .simpleProduct(let simpleProduct):
29+
let productItem = CartItem(id: UUID(), item: simpleProduct, title: simpleProduct.name, subtitle: nil, quantity: 1)
30+
items.insert(productItem, at: items.startIndex)
31+
case .variation(let variation):
32+
let productItem = CartItem(id: UUID(), item: variation, title: variation.parentProductName, subtitle: variation.name, quantity: 1)
33+
items.insert(productItem, at: items.startIndex)
34+
case .variableParentProduct:
35+
return
36+
case .coupon(let coupon):
37+
let couponItem = CartCouponItem(id: UUID(), code: coupon.code)
38+
coupons.insert(couponItem, at: coupons.startIndex)
39+
}
40+
}
41+
42+
mutating func remove(_ cartItem: CartItem) {
43+
items.removeAll { $0.id == cartItem.id }
44+
}
45+
46+
mutating func remove(_ cartCouponItem: CartCouponItem) {
47+
coupons.removeAll { $0.id == cartCouponItem.id }
48+
}
49+
50+
mutating func removeAll() {
51+
items.removeAll()
52+
coupons.removeAll()
53+
}
54+
55+
var isEmpty: Bool {
56+
items.isEmpty && coupons.isEmpty
57+
}
58+
59+
var isNotEmpty: Bool {
60+
return !isEmpty
61+
}
62+
}

WooCommerce/Classes/POS/Models/CartItem.swift

Lines changed: 0 additions & 10 deletions
This file was deleted.

WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import protocol Yosemite.POSOrderableItem
66
import protocol WooFoundation.Analytics
77
import struct Yosemite.Order
88
import struct Yosemite.OrderItem
9-
import struct Yosemite.POSCartItem
9+
import struct Yosemite.POSCoupon
1010
import enum Yosemite.POSItem
1111
import enum Yosemite.SystemStatusAction
1212

@@ -28,9 +28,10 @@ protocol PointOfSaleAggregateModelProtocol {
2828
func loadItems(base: ItemListBaseItem) async
2929
func loadNextItems(base: ItemListBaseItem) async
3030

31-
var cart: [CartItem] { get }
31+
var cart: Cart { get }
3232
func addToCart(_ item: POSItem)
3333
func remove(cartItem: CartItem)
34+
func remove(cartCouponItem: CartCouponItem)
3435
func removeAllItemsFromCart()
3536
func addMoreToCart()
3637
func startNewCart()
@@ -54,7 +55,7 @@ protocol PointOfSaleAggregateModelProtocol {
5455

5556
var itemsViewState: ItemsViewState { itemsController.itemsViewState }
5657

57-
private(set) var cart: [CartItem] = []
58+
private(set) var cart: Cart = .init()
5859

5960
var orderState: PointOfSaleOrderState { orderController.orderState.externalState }
6061
private var internalOrderState: PointOfSaleInternalOrderState { orderController.orderState }
@@ -108,33 +109,19 @@ extension PointOfSaleAggregateModel {
108109
}
109110
}
110111

111-
// MARK: - Cart
112-
113-
private extension POSItem {
114-
var cartItem: CartItem? {
115-
switch self {
116-
case .simpleProduct(let simpleProduct):
117-
return CartItem(id: UUID(), item: simpleProduct, title: simpleProduct.name, subtitle: nil, quantity: 1)
118-
case .variation(let variation):
119-
return CartItem(id: UUID(), item: variation, title: variation.parentProductName, subtitle: variation.name, quantity: 1)
120-
case .variableParentProduct:
121-
return nil
122-
case .coupon:
123-
return nil
124-
}
125-
}
126-
}
127-
128112
@available(iOS 17.0, *)
129113
extension PointOfSaleAggregateModel {
130114
func addToCart(_ item: POSItem) {
131115
trackCustomerInteractionStarted()
132-
guard let cartItem = item.cartItem else { return }
133-
cart.insert(cartItem, at: cart.startIndex)
116+
cart.add(item)
134117
}
135118

136119
func remove(cartItem: CartItem) {
137-
cart.removeAll(where: { $0.id == cartItem.id } )
120+
cart.remove(cartItem)
121+
}
122+
123+
func remove(cartCouponItem: CartCouponItem) {
124+
cart.remove(cartCouponItem)
138125
}
139126

140127
func removeAllItemsFromCart() {
@@ -164,7 +151,7 @@ private extension PointOfSaleAggregateModel {
164151
func trackCustomerInteractionStarted() {
165152
// At the moment we're assumming that an interaction starts simply when the cart is zero
166153
// but a more complex logic will be needed for other cases
167-
if cart.count == 0 {
154+
if cart.isEmpty {
168155
collectOrderPaymentAnalyticsTracker.trackCustomerInteractionStarted()
169156
}
170157
}

WooCommerce/Classes/POS/Presentation/CartView.swift

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,17 @@ struct CartView: View {
1616

1717
@State private var shouldShowItemImages: Bool = false
1818

19+
private var shouldShowCoupons: Bool {
20+
ServiceLocator.featureFlagService.isFeatureFlagEnabled(.enableCouponsInPointOfSale)
21+
}
22+
1923
var body: some View {
2024
VStack {
2125
POSPageHeaderView(title: Localization.cartTitle,
2226
backButtonConfiguration: backButtonConfiguration,
2327
trailingContent: {
2428
DynamicHStack(horizontalAlignment: .trailing, verticalAlignment: .center, spacing: Constants.cartHeaderElementSpacing) {
25-
if let itemsInCartLabel = viewHelper.itemsInCartLabel(for: posModel.cart.count) {
29+
if let itemsInCartLabel = viewHelper.itemsInCartLabel(for: posModel.cart.items.count) {
2630
Text(itemsInCartLabel)
2731
.font(Constants.itemsFont)
2832
.lineLimit(1)
@@ -47,7 +51,11 @@ struct CartView: View {
4751
ScrollViewReader { proxy in
4852
ScrollView {
4953
VStack(spacing: Constants.cartItemSpacing) {
50-
ForEach(posModel.cart, id: \.id) { cartItem in
54+
if shouldShowCoupons {
55+
couponsCartSectionView
56+
}
57+
58+
ForEach(posModel.cart.items, id: \.id) { cartItem in
5159
ItemRowView(cartItem: cartItem,
5260
showImage: $shouldShowItemImages,
5361
onItemRemoveTapped: posModel.orderStage == .building ? {
@@ -58,7 +66,8 @@ struct CartView: View {
5866
.transition(.opacity)
5967
}
6068
}
61-
.animation(Constants.cartAnimation, value: posModel.cart.map(\.id))
69+
.animation(Constants.cartAnimation, value: posModel.cart.items.map(\.id))
70+
.animation(Constants.cartAnimation, value: posModel.cart.coupons.map(\.id))
6271
.background(GeometryReader { geometry in
6372
Color.clear.preference(key: ScrollOffSetPreferenceKey.self,
6473
value: geometry.frame(in: coordinateSpace).origin.y)
@@ -81,7 +90,7 @@ struct CartView: View {
8190
.renderedIf(posModel.orderStage == .finalizing)
8291
}
8392
.coordinateSpace(name: Constants.scrollViewCoordinateSpaceIdentifier)
84-
.onChange(of: posModel.cart.first?.id) { itemToScrollTo in
93+
.onChange(of: posModel.cart.items.first?.id) { itemToScrollTo in
8594
if posModel.orderStage == .building {
8695
withAnimation {
8796
proxy.scrollTo(itemToScrollTo)
@@ -93,7 +102,7 @@ struct CartView: View {
93102
Spacer()
94103
switch posModel.orderStage {
95104
case .building:
96-
if posModel.cart.isEmpty {
105+
if posModel.cart.items.isEmpty {
97106
EmptyView()
98107
} else {
99108
checkoutButton
@@ -263,12 +272,27 @@ private extension CartView {
263272
}
264273
.background(backgroundColor.ignoresSafeArea(.all))
265274
}
275+
276+
var couponsCartSectionView: some View {
277+
VStack {
278+
ForEach(posModel.cart.coupons, id: \.id) { couponItem in
279+
CouponRowView(couponItem: couponItem,
280+
onItemRemoveTapped: posModel.orderStage == .building ? {
281+
posModel.remove(cartCouponItem: couponItem)
282+
} : nil)
283+
.id(couponItem.id)
284+
.transition(.opacity)
285+
}
286+
287+
Spacer(minLength: 48)
288+
}
289+
}
266290
}
267291

268292
@available(iOS 17.0, *)
269293
private extension CartView {
270294
func trackCheckoutTapped() {
271-
let itemsInCart = posModel.cart.count
295+
let itemsInCart = posModel.cart.items.count
272296
ServiceLocator.analytics.track(event: .PointOfSale.checkoutTapped(itemsInCart))
273297
}
274298
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import SwiftUI
2+
3+
struct CouponRowView: View {
4+
private let couponItem: CartCouponItem
5+
private let onItemRemoveTapped: (() -> Void)?
6+
7+
@ScaledMetric private var scale: CGFloat = 1.0
8+
9+
init(couponItem: CartCouponItem, onItemRemoveTapped: (() -> Void)? = nil) {
10+
self.couponItem = couponItem
11+
self.onItemRemoveTapped = onItemRemoveTapped
12+
}
13+
14+
var body: some View {
15+
HStack(spacing: Constants.horizontalElementSpacing) {
16+
Rectangle()
17+
.foregroundColor(.posSurfaceDim)
18+
.overlay {
19+
Text(Image(systemName: "tag.square.fill"))
20+
.font(.posButtonSymbolLarge)
21+
.foregroundColor(.posOnSurfaceVariantLowest)
22+
}
23+
.frame(width: Constants.couponCardSize, height: Constants.couponCardSize)
24+
25+
VStack(alignment: .leading) {
26+
Text(couponItem.code)
27+
.foregroundColor(PointOfSaleItemListCardConstants.titleColor)
28+
.font(Constants.itemTitleFont)
29+
}
30+
.frame(maxWidth: .infinity, alignment: .leading)
31+
32+
if let onItemRemoveTapped {
33+
Button(action: {
34+
onItemRemoveTapped()
35+
}, label: {
36+
Text(Image(systemName: "xmark.circle"))
37+
.font(.posButtonSymbolMedium)
38+
})
39+
.foregroundColor(Color.posOnSurfaceVariantLowest)
40+
}
41+
}
42+
.padding(.trailing, Constants.cardContentHorizontalPadding)
43+
.frame(maxWidth: .infinity, idealHeight: Constants.couponCardSize * scale)
44+
.background(Color.posSurfaceContainerLowest)
45+
.posItemCardBorderStyles()
46+
.padding(.horizontal, Constants.horizontalPadding)
47+
}
48+
}
49+
50+
private extension CouponRowView {
51+
enum Constants {
52+
static let couponCardSize: CGFloat = 96
53+
static let horizontalPadding: CGFloat = POSPadding.medium
54+
static let horizontalElementSpacing: CGFloat = POSSpacing.medium
55+
static let cardContentHorizontalPadding: CGFloat = POSPadding.medium
56+
static let itemTitleFont: POSFontStyle = .posBodySmallBold
57+
}
58+
}
59+
60+
#if DEBUG
61+
@available(iOS 17.0, *)
62+
#Preview(traits: .sizeThatFitsLayout) {
63+
CouponRowView(couponItem: CartCouponItem(id: UUID(), code: "10-Discount")) {}
64+
}
65+
#endif

WooCommerce/Classes/POS/Presentation/ItemListView.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import SwiftUI
22
import enum Yosemite.POSItem
3-
import protocol Yosemite.POSOrderableItem
43

54
@available(iOS 17.0, *)
65
struct ItemListView: View {

WooCommerce/Classes/POS/Utils/PointOfSalePreviewOrderController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class PointOfSalePreviewOrderController: PointOfSaleOrderControllerProtocol {
1212
OrderFactory.newOrder(currency: .USD)
1313
)
1414

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

WooCommerce/Classes/POS/ViewHelpers/CartViewHelper.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ final class CartViewHelper {
1818
return orderState.isSyncing
1919
}
2020

21-
func shouldShowClearCartButton(cart: [CartItem], orderStage: PointOfSaleOrderStage) -> Bool {
21+
func shouldShowClearCartButton(cart: Cart, orderStage: PointOfSaleOrderStage) -> Bool {
2222
cart.isNotEmpty && orderStage == .building
2323
}
2424
}

WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/CardPresentPaymentsOnboardingViewModel.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,11 @@ final class CardPresentPaymentsOnboardingViewModel: ObservableObject, PaymentSet
4545
init(
4646
fixedState: CardPresentPaymentOnboardingState,
4747
fixedUserIsAdministrator: Bool = false,
48+
useCase: CardPresentPaymentsOnboardingUseCaseProtocol = CardPresentPaymentsOnboardingUseCase(),
4849
stores: StoresManager = ServiceLocator.stores) {
4950
self.stores = stores
5051
state = fixedState
51-
useCase = CardPresentPaymentsOnboardingUseCase()
52+
self.useCase = useCase
5253
userIsAdministrator = fixedUserIsAdministrator
5354
updateLearnMoreURL(state: fixedState)
5455
}

0 commit comments

Comments
 (0)