Skip to content

Commit b08be97

Browse files
committed
Merge branch 'trunk' into task/15307-moving-to-new-shipment
2 parents ab45fd8 + b69e8ae commit b08be97

File tree

52 files changed

+1175
-276
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+1175
-276
lines changed

Experiments/Experiments/DefaultFeatureFlagService.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public struct DefaultFeatureFlagService: FeatureFlagService {
7272
case .pointOfSale:
7373
return buildConfig == .localDeveloper || buildConfig == .alpha
7474
case .enableCouponsInPointOfSale:
75-
return buildConfig == .localDeveloper || buildConfig == .alpha
75+
return false
7676
case .googleAdsCampaignCreationOnWebView:
7777
return true
7878
case .backgroundTasks:
@@ -97,6 +97,8 @@ public struct DefaultFeatureFlagService: FeatureFlagService {
9797
return buildConfig == .localDeveloper || buildConfig == .alpha
9898
case .notificationSettings:
9999
return true
100+
case .allowMerchantAIAPIKey:
101+
return buildConfig == .localDeveloper || buildConfig == .alpha
100102
default:
101103
return true
102104
}

Experiments/Experiments/FeatureFlag.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,4 +208,8 @@ public enum FeatureFlag: Int {
208208
/// Supports managing notification settings from the app settings
209209
///
210210
case notificationSettings
211+
212+
/// Allows merchants to use their own API keys for AI-powered features
213+
///
214+
case allowMerchantAIAPIKey
211215
}

WooCommerce/Classes/Extensions/UIImage+Woo.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,10 @@ extension UIImage {
569569
return UIImage.gridicon(.cog)
570570
}
571571

572+
static var wandAndRaysInverse: UIImage {
573+
return UIImage(systemName: "wand.and.rays.inverse")!
574+
}
575+
572576
static func prologueBackgroundBubbles(tint: UIColor) -> UIImage {
573577
let image = UIImage(named: "login-prologue-background-bubbles")!
574578
return image.withTintColor(tint)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import Observation
2+
import enum Yosemite.POSItem
3+
import protocol Yosemite.PointOfSaleItemServiceProtocol
4+
5+
@available(iOS 17.0, *)
6+
@Observable final class PointOfSaleCouponsController: PointOfSaleItemsControllerProtocol {
7+
var itemsViewState: ItemsViewState = ItemsViewState(containerState: .loading,
8+
itemsStack: ItemsStackState(root: .loading([]),
9+
itemStates: [:]))
10+
private let paginationTracker: AsyncPaginationTracker
11+
private var childPaginationTrackers: [POSItem: AsyncPaginationTracker] = [:]
12+
private let itemProvider: PointOfSaleItemServiceProtocol
13+
14+
init(itemProvider: PointOfSaleItemServiceProtocol) {
15+
self.itemProvider = itemProvider
16+
self.paginationTracker = .init()
17+
}
18+
19+
@MainActor
20+
func loadItems(base: ItemListBaseItem) async {
21+
debugPrint("🍍 CouponsController::loadItems called")
22+
itemsViewState = ItemsViewState(containerState: .content, itemsStack: .init(root: .loaded([], hasMoreItems: false), itemStates: [:]))
23+
}
24+
25+
func refreshItems(base: ItemListBaseItem) async {
26+
debugPrint("🍍 CouponsController::refreshItems called")
27+
itemsViewState = ItemsViewState(containerState: .content, itemsStack: .init(root: .loaded([], hasMoreItems: false), itemStates: [:]))
28+
}
29+
30+
func loadNextItems(base: ItemListBaseItem) async {
31+
debugPrint("🍍 CouponsController::loadNextItems called")
32+
itemsViewState = ItemsViewState(containerState: .content, itemsStack: .init(root: .loaded([], hasMoreItems: false), itemStates: [:]))
33+
}
34+
}

WooCommerce/Classes/POS/Controllers/PointOfSaleItemsController.swift

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,14 @@ import enum Yosemite.PointOfSaleItemServiceError
66
import struct Yosemite.POSVariableParentProduct
77
import class Yosemite.Store
88

9+
enum ItemType {
10+
case products
11+
case coupons
12+
}
13+
914
@available(iOS 17.0, *)
1015
protocol PointOfSaleItemsControllerProtocol {
16+
///
1117
var itemsViewState: ItemsViewState { get }
1218
/// Loads the first page of items for a given base item.
1319
func loadItems(base: ItemListBaseItem) async
@@ -17,6 +23,8 @@ protocol PointOfSaleItemsControllerProtocol {
1723
func loadNextItems(base: ItemListBaseItem) async
1824
}
1925

26+
27+
2028
@available(iOS 17.0, *)
2129
@Observable final class PointOfSaleItemsController: PointOfSaleItemsControllerProtocol {
2230
var itemsViewState: ItemsViewState = ItemsViewState(containerState: .loading,
@@ -158,14 +166,6 @@ protocol PointOfSaleItemsControllerProtocol {
158166
}
159167
}
160168

161-
@available(iOS 17.0, *)
162-
private extension PointOfSaleItemsController {
163-
func loadPointOfSaleCoupons() {
164-
let posCoupons = itemProvider.providePointOfSaleCoupons()
165-
debugPrint(posCoupons)
166-
}
167-
}
168-
169169
@available(iOS 17.0, *)
170170
private extension PointOfSaleItemsController {
171171
func setLoadingState(base: ItemListBaseItem) {
@@ -202,6 +202,7 @@ private extension PointOfSaleItemsController {
202202
func fetchItems(pageNumber: Int, appendToExistingItems: Bool = true) async throws -> Bool {
203203
do {
204204
let pagedItems = try await itemProvider.providePointOfSaleItems(pageNumber: pageNumber)
205+
205206
let newItems = pagedItems.items
206207
var allItems = appendToExistingItems ? itemsViewState.itemsStack.root.items : []
207208
let uniqueNewItems = newItems.filter { newItem in

WooCommerce/Classes/POS/Controllers/PointOfSaleOrderController.swift

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import protocol Yosemite.POSOrderServiceProtocol
55
import protocol Yosemite.POSReceiptServiceProtocol
66
import struct Yosemite.Order
77
import struct Yosemite.PaymentGateway
8+
import struct Yosemite.POSCart
89
import struct Yosemite.POSCartItem
10+
import struct Yosemite.POSCoupon
911
import enum Yosemite.OrderAction
1012
import enum Yosemite.OrderUpdateField
1113
import class WooFoundation.CurrencyFormatter
@@ -27,7 +29,7 @@ protocol PointOfSaleOrderControllerProtocol {
2729
var orderState: PointOfSaleInternalOrderState { get }
2830

2931
@discardableResult
30-
func syncOrder(for cartProducts: [CartItem], retryHandler: @escaping () async -> Void) async -> Result<SyncOrderState, Error>
32+
func syncOrder(for cart: Cart, retryHandler: @escaping () async -> Void) async -> Result<SyncOrderState, Error>
3133
func sendReceipt(recipientEmail: String) async throws
3234
func clearOrder()
3335
func collectCashPayment() async throws
@@ -63,21 +65,19 @@ protocol PointOfSaleOrderControllerProtocol {
6365
private var order: Order? = nil
6466

6567
@MainActor @discardableResult
66-
func syncOrder(for cartItems: [CartItem],
68+
func syncOrder(for cart: Cart,
6769
retryHandler: @escaping () async -> Void) async -> Result<SyncOrderState, Error> {
68-
let posCartItems = cartItems.map {
69-
POSCartItem(item: $0.item, quantity: Decimal($0.quantity))
70-
}
70+
let posCart = POSCart(cart: cart)
7171

72-
guard !orderState.isSyncing, !posCartItems.matches(order: order) else {
72+
guard !orderState.isSyncing, !posCart.matches(order: order) else {
7373
return .success(.orderNotChanged)
7474
}
7575

7676
orderState = .syncing
7777
let isNewOrder = order == nil
7878

7979
do {
80-
let syncedOrder = try await orderService.syncOrder(cart: posCartItems,
80+
let syncedOrder = try await orderService.syncOrder(cart: posCart,
8181
order: order,
8282
currency: storeCurrency)
8383
self.order = syncedOrder
@@ -249,3 +249,13 @@ extension PointOfSaleOrderController {
249249
case noOrder
250250
}
251251
}
252+
253+
// MARK: - Mapping
254+
255+
private extension POSCart {
256+
init(cart: Cart) {
257+
let items = cart.items.map { POSCartItem(item: $0.item, quantity: Decimal($0.quantity)) }
258+
let coupons = cart.coupons.map { POSCoupon(id: $0.id, code: $0.code) }
259+
self.init(items: items, coupons: coupons)
260+
}
261+
}
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: 24 additions & 26 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()
@@ -52,14 +53,16 @@ protocol PointOfSaleAggregateModelProtocol {
5253
var cardPresentPaymentOnboardingViewModel: CardPresentPaymentsOnboardingViewModel?
5354
private var onOnboardingCancellation: (() -> Void)?
5455

55-
var itemsViewState: ItemsViewState { itemsController.itemsViewState }
56+
var itemsViewState: ItemsViewState { currentController.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 }
6162

63+
private var currentController: PointOfSaleItemsControllerProtocol
6264
private let itemsController: PointOfSaleItemsControllerProtocol
65+
private let couponsController: PointOfSaleItemsControllerProtocol
6366

6467
private let cardPresentPaymentService: CardPresentPaymentFacade
6568
private let orderController: PointOfSaleOrderControllerProtocol
@@ -72,12 +75,15 @@ protocol PointOfSaleAggregateModelProtocol {
7275
private var cancellables: Set<AnyCancellable> = []
7376

7477
init(itemsController: PointOfSaleItemsControllerProtocol,
78+
couponsController: PointOfSaleItemsControllerProtocol,
7579
cardPresentPaymentService: CardPresentPaymentFacade,
7680
orderController: PointOfSaleOrderControllerProtocol,
7781
analytics: Analytics = ServiceLocator.analytics,
7882
collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalyticsTracking,
7983
paymentState: PointOfSalePaymentState = .card(.idle)) {
84+
self.currentController = itemsController // Default current controller set to products
8085
self.itemsController = itemsController
86+
self.couponsController = couponsController
8187
self.cardPresentPaymentService = cardPresentPaymentService
8288
self.orderController = orderController
8389
self.analytics = analytics
@@ -94,47 +100,39 @@ protocol PointOfSaleAggregateModelProtocol {
94100
extension PointOfSaleAggregateModel {
95101
@MainActor
96102
func loadItems(base: ItemListBaseItem) async {
97-
await itemsController.loadItems(base: base)
103+
await currentController.loadItems(base: base)
98104
}
99105

100106
@MainActor
101107
func refreshItems(base: ItemListBaseItem) async {
102-
await itemsController.refreshItems(base: base)
108+
await currentController.refreshItems(base: base)
103109
}
104110

105111
@MainActor
106112
func loadNextItems(base: ItemListBaseItem) async {
107-
await itemsController.loadNextItems(base: base)
113+
await currentController.loadNextItems(base: base)
108114
}
109-
}
110-
111-
// MARK: - Cart
112115

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-
}
116+
func switchToItemType(_ type: ItemType) async {
117+
let newController = type == .products ? itemsController : couponsController
118+
currentController = newController
119+
await refreshItems(base: .root)
125120
}
126121
}
127122

128123
@available(iOS 17.0, *)
129124
extension PointOfSaleAggregateModel {
130125
func addToCart(_ item: POSItem) {
131126
trackCustomerInteractionStarted()
132-
guard let cartItem = item.cartItem else { return }
133-
cart.insert(cartItem, at: cart.startIndex)
127+
cart.add(item)
134128
}
135129

136130
func remove(cartItem: CartItem) {
137-
cart.removeAll(where: { $0.id == cartItem.id } )
131+
cart.remove(cartItem)
132+
}
133+
134+
func remove(cartCouponItem: CartCouponItem) {
135+
cart.remove(cartCouponItem)
138136
}
139137

140138
func removeAllItemsFromCart() {
@@ -164,7 +162,7 @@ private extension PointOfSaleAggregateModel {
164162
func trackCustomerInteractionStarted() {
165163
// At the moment we're assumming that an interaction starts simply when the cart is zero
166164
// but a more complex logic will be needed for other cases
167-
if cart.count == 0 {
165+
if cart.isEmpty {
168166
collectOrderPaymentAnalyticsTracker.trackCustomerInteractionStarted()
169167
}
170168
}

WooCommerce/Classes/POS/Presentation/CardReaderConnection/CardReaderConnectionStatusView.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ private extension CardReaderConnectionStatusView {
171171
#Preview {
172172
let posModel = PointOfSaleAggregateModel(
173173
itemsController: PointOfSalePreviewItemsController(),
174+
couponsController: PointOfSalePreviewItemsController(),
174175
cardPresentPaymentService: CardPresentPaymentPreviewService(),
175176
orderController: PointOfSalePreviewOrderController(),
176177
collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalytics()

0 commit comments

Comments
 (0)