Skip to content

Commit 1b1372a

Browse files
authored
[Woo POS] Use Observation to fix refresh issue (#15060)
2 parents ab5aa98 + 2a30c2e commit 1b1372a

14 files changed

+290
-208
lines changed

WooCommerce/Classes/Copiable/Models+Copiable.generated.swift

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -50,36 +50,6 @@ extension WooCommerce.AggregateOrderItem {
5050
}
5151
}
5252

53-
extension WooCommerce.ItemsStackState {
54-
func copy(
55-
root: CopiableProp<ItemListState> = .copy,
56-
itemStates: CopiableProp<[POSItem: ItemListState]> = .copy
57-
) -> WooCommerce.ItemsStackState {
58-
let root = root ?? self.root
59-
let itemStates = itemStates ?? self.itemStates
60-
61-
return WooCommerce.ItemsStackState(
62-
root: root,
63-
itemStates: itemStates
64-
)
65-
}
66-
}
67-
68-
extension WooCommerce.ItemsViewState {
69-
func copy(
70-
containerState: CopiableProp<ItemsContainerState> = .copy,
71-
itemsStack: CopiableProp<ItemsStackState> = .copy
72-
) -> WooCommerce.ItemsViewState {
73-
let containerState = containerState ?? self.containerState
74-
let itemsStack = itemsStack ?? self.itemsStack
75-
76-
return WooCommerce.ItemsViewState(
77-
containerState: containerState,
78-
itemsStack: itemsStack
79-
)
80-
}
81-
}
82-
8353
extension WooCommerce.ShippingLabelSelectedRate {
8454
func copy(
8555
packageID: CopiableProp<String> = .copy,

WooCommerce/Classes/POS/Controllers/PointOfSaleItemsController.swift

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,24 @@
11
import Foundation
2-
import Combine
2+
import Observation
33
import enum Yosemite.POSItem
44
import protocol Yosemite.PointOfSaleItemServiceProtocol
55
import struct Yosemite.POSVariableParentProduct
66
import class Yosemite.Store
77

8+
@available(iOS 17.0, *)
89
protocol PointOfSaleItemsControllerProtocol {
9-
var itemsViewStatePublisher: any Publisher<ItemsViewState, Never> { get }
10+
var itemsViewState: ItemsViewState { get }
1011
/// Loads the first page of items for a given base item.
1112
func loadItems(base: ItemListBaseItem) async
1213
/// Loads the next page of items for a given base item.
1314
func loadNextItems(base: ItemListBaseItem) async
1415
}
1516

16-
class PointOfSaleItemsController: PointOfSaleItemsControllerProtocol {
17-
var itemsViewStatePublisher: any Publisher<ItemsViewState, Never> {
18-
$itemsViewState.eraseToAnyPublisher()
19-
}
20-
@Published private var itemsViewState: ItemsViewState =
21-
ItemsViewState(containerState: .loading,
22-
itemsStack: ItemsStackState(root: .loading([]),
23-
itemStates: [:]))
17+
@available(iOS 17.0, *)
18+
@Observable final class PointOfSaleItemsController: PointOfSaleItemsControllerProtocol {
19+
var itemsViewState: ItemsViewState = ItemsViewState(containerState: .loading,
20+
itemsStack: ItemsStackState(root: .loading([]),
21+
itemStates: [:]))
2422
private let paginationTracker: AsyncPaginationTracker
2523
private var childPaginationTrackers: [POSItem: AsyncPaginationTracker] = [:]
2624
private let itemProvider: PointOfSaleItemServiceProtocol
@@ -42,22 +40,22 @@ class PointOfSaleItemsController: PointOfSaleItemsControllerProtocol {
4240

4341
@MainActor
4442
private func loadRootItems() async {
45-
let currentItems = itemsViewState.itemsStack.root.items
46-
let currentItemStates = itemsViewState.itemsStack.itemStates
47-
let containerState: ItemsContainerState = currentItems.isEmpty ? .loading : .content
48-
itemsViewState = .init(containerState: containerState,
49-
itemsStack: ItemsStackState(root: .loading(currentItems),
50-
itemStates: currentItemStates))
43+
let items = itemsViewState.itemsStack.root.items
44+
if items.isEmpty {
45+
itemsViewState.containerState = .loading
46+
} else {
47+
itemsViewState.itemsStack.root = .loading(items)
48+
}
5149

5250
do {
5351
try await paginationTracker.resync { [weak self] pageNumber in
5452
guard let self else { return true }
5553
return try await fetchItems(pageNumber: pageNumber, appendToExistingItems: false)
5654
}
5755
} catch {
58-
itemsViewState = .init(containerState: .error(PointOfSaleErrorState.errorOnLoadingProducts()),
59-
itemsStack: ItemsStackState(root: .loaded([], hasMoreItems: false),
60-
itemStates: [:]))
56+
itemsViewState.containerState = .error(PointOfSaleErrorState.errorOnLoadingProducts())
57+
itemsViewState.itemsStack = ItemsStackState(root: .loaded([], hasMoreItems: false),
58+
itemStates: [:])
6159
}
6260
}
6361

@@ -78,18 +76,19 @@ class PointOfSaleItemsController: PointOfSaleItemsControllerProtocol {
7876
}
7977
let currentItems = itemsViewState.itemsStack.root.items
8078
let currentItemStates = itemsViewState.itemsStack.itemStates
81-
itemsViewState = .init(containerState: .content, itemsStack: ItemsStackState(root: .loading(currentItems),
82-
itemStates: currentItemStates))
79+
itemsViewState.containerState = .content
80+
itemsViewState.itemsStack = ItemsStackState(root: .loading(currentItems),
81+
itemStates: currentItemStates)
8382
do {
8483
_ = try await paginationTracker.ensureNextPageIsSynced { [weak self] pageNumber in
8584
guard let self else { return true }
8685
return try await fetchItems(pageNumber: pageNumber)
8786
}
8887
} catch {
89-
itemsViewState = .init(containerState: .content,
90-
itemsStack: ItemsStackState(root: .inlineError(currentItems,
91-
error: .errorOnLoadingProductsNextPage()),
92-
itemStates: currentItemStates))
88+
itemsViewState.containerState = .content
89+
itemsViewState.itemsStack = ItemsStackState(root: .inlineError(currentItems,
90+
error: .errorOnLoadingProductsNextPage()),
91+
itemStates: currentItemStates)
9392
}
9493
}
9594

@@ -155,6 +154,7 @@ class PointOfSaleItemsController: PointOfSaleItemsControllerProtocol {
155154
}
156155
}
157156

157+
@available(iOS 17.0, *)
158158
private extension PointOfSaleItemsController {
159159
/// Fetches items given a page number and appends new unique items to the `allItems` array.
160160
/// - Parameter pageNumber: Page number to fetch items from.
@@ -171,15 +171,15 @@ private extension PointOfSaleItemsController {
171171
}
172172
allItems.append(contentsOf: uniqueNewItems)
173173
if allItems.isEmpty {
174-
itemsViewState = .init(containerState: .empty,
175-
itemsStack: ItemsStackState(root: .loaded([], hasMoreItems: false),
176-
itemStates: [:]))
174+
itemsViewState.containerState = .empty
175+
itemsViewState.itemsStack = ItemsStackState(root: .loaded([], hasMoreItems: false),
176+
itemStates: [:])
177177
} else {
178178
let itemStates = itemsViewState.itemsStack.itemStates
179179
.filter { allItems.contains($0.key) }
180-
itemsViewState = .init(containerState: .content,
181-
itemsStack: ItemsStackState(root: .loaded(allItems, hasMoreItems: pagedItems.hasMorePages),
182-
itemStates: itemStates))
180+
itemsViewState.containerState = .content
181+
itemsViewState.itemsStack = ItemsStackState(root: .loaded(allItems, hasMoreItems: pagedItems.hasMorePages),
182+
itemStates: itemStates)
183183
}
184184
return pagedItems.hasMorePages
185185
}
@@ -210,7 +210,7 @@ private extension PointOfSaleItemsController {
210210
}
211211

212212
// MARK: - ItemsViewState Updates
213-
213+
@available(iOS 17.0, *)
214214
private extension PointOfSaleItemsController {
215215
func updateState(for parent: POSItem, to state: ItemListState) {
216216
let viewState = itemsViewState
@@ -219,6 +219,6 @@ private extension PointOfSaleItemsController {
219219
states[parent] = state
220220
return states
221221
}()
222-
itemsViewState = viewState.copy(itemsStack: viewState.itemsStack.copy(itemStates: itemStates))
222+
itemsViewState.itemsStack.itemStates = itemStates
223223
}
224224
}

WooCommerce/Classes/POS/Models/ItemListState.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import enum Yosemite.POSItem
2-
import Codegen
32

43
enum ItemListState {
54
case loading(_ currentItems: [POSItem])
@@ -30,4 +29,4 @@ extension ItemListState {
3029
}
3130
}
3231

33-
extension ItemListState: Equatable, GeneratedCopiable {}
32+
extension ItemListState: Equatable {}
Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
11
import Foundation
2-
import Codegen
32
import enum Yosemite.POSItem
3+
import Observation
44

5-
struct ItemsStackState {
6-
let root: ItemListState
7-
let itemStates: [POSItem: ItemListState]
5+
@available(iOS 17.0, *)
6+
@Observable final class ItemsStackState {
7+
var root: ItemListState
8+
var itemStates: [POSItem: ItemListState]
9+
10+
init(root: ItemListState, itemStates: [POSItem: ItemListState]) {
11+
self.root = root
12+
self.itemStates = itemStates
13+
}
814
}
915

10-
extension ItemsStackState: Equatable, GeneratedCopiable {}
16+
@available(iOS 17.0, *)
17+
extension ItemsStackState: Equatable {
18+
static func == (lhs: ItemsStackState, rhs: ItemsStackState) -> Bool {
19+
return lhs.root == rhs.root
20+
&& lhs.itemStates == rhs.itemStates
21+
}
22+
}
Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
11
import Foundation
2-
import Codegen
2+
import Observation
33

4-
struct ItemsViewState {
5-
let containerState: ItemsContainerState
6-
let itemsStack: ItemsStackState
4+
@available(iOS 17.0, *)
5+
@Observable class ItemsViewState {
6+
var containerState: ItemsContainerState
7+
var itemsStack: ItemsStackState
8+
9+
init(containerState: ItemsContainerState, itemsStack: ItemsStackState) {
10+
self.containerState = containerState
11+
self.itemsStack = itemsStack
12+
}
713
}
814

9-
extension ItemsViewState: GeneratedCopiable, Equatable {}
15+
@available(iOS 17.0, *)
16+
extension ItemsViewState: Equatable {
17+
static func == (lhs: ItemsViewState, rhs: ItemsViewState) -> Bool {
18+
return lhs.containerState == rhs.containerState
19+
&& lhs.itemsStack == rhs.itemsStack
20+
}
21+
}

WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import struct Yosemite.POSCartItem
1010
import enum Yosemite.POSItem
1111
import enum Yosemite.SystemStatusAction
1212

13+
@available(iOS 17.0, *)
1314
protocol PointOfSaleAggregateModelProtocol {
1415
var orderStage: PointOfSaleOrderStage { get }
1516

@@ -49,9 +50,7 @@ protocol PointOfSaleAggregateModelProtocol {
4950
var cardPresentPaymentOnboardingViewModel: CardPresentPaymentsOnboardingViewModel?
5051
private var onOnboardingCancellation: (() -> Void)?
5152

52-
private(set) var itemsViewState: ItemsViewState = ItemsViewState(containerState: .loading,
53-
itemsStack: ItemsStackState(root: .loading([]),
54-
itemStates: [:]))
53+
var itemsViewState: ItemsViewState { itemsController.itemsViewState }
5554

5655
private(set) var cart: [CartItem] = []
5756

@@ -79,7 +78,6 @@ protocol PointOfSaleAggregateModelProtocol {
7978
self.orderController = orderController
8079
self.analytics = analytics
8180
self.paymentState = paymentState
82-
publishItemsViewState()
8381
publishCardReaderConnectionStatus()
8482
publishPaymentMessages()
8583
publishOrderState()
@@ -90,13 +88,6 @@ protocol PointOfSaleAggregateModelProtocol {
9088
// MARK: - ItemList
9189
@available(iOS 17.0, *)
9290
extension PointOfSaleAggregateModel {
93-
private func publishItemsViewState() {
94-
itemsController.itemsViewStatePublisher.sink { [weak self] state in
95-
self?.itemsViewState = state
96-
}
97-
.store(in: &cancellables)
98-
}
99-
10091
@MainActor
10192
func loadItems(base: ItemListBaseItem) async {
10293
await itemsController.loadItems(base: base)

WooCommerce/Classes/POS/Presentation/Item Selector/ChildItemList.swift

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,6 @@ struct ChildItemList: View {
3333
}
3434
.background(Color.posPrimaryBackground)
3535
.toolbar(.hidden, for: .navigationBar)
36-
.refreshable {
37-
ServiceLocator.analytics.track(.pointOfSaleVariationsPullToRefresh)
38-
await Task {
39-
await posModel.loadItems(base: .parent(parentItem))
40-
}.value
41-
}
4236
.task {
4337
guard state.items.isEmpty else {
4438
return

WooCommerce/Classes/POS/Presentation/ItemListView.swift

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,6 @@ struct ItemListView: View {
3636
})
3737
.background(Color.posPrimaryBackground)
3838
}
39-
.refreshable {
40-
ServiceLocator.analytics.track(.pointOfSaleProductsPullToRefresh)
41-
await Task {
42-
await posModel.loadItems(base: .root)
43-
}.value
44-
}
4539
.accessibilityElement(children: .contain)
4640
.posModal(isPresented: $showSimpleProductsModal) {
4741
SimpleProductsOnlyInformation(isPresented: $showSimpleProductsModal)
@@ -151,6 +145,10 @@ private extension ItemListView {
151145
switch parentItem {
152146
case let .variableParentProduct(parentProduct):
153147
ChildItemList(parentItem: parentItem, title: parentProduct.name)
148+
.refreshable {
149+
ServiceLocator.analytics.track(.pointOfSaleVariationsPullToRefresh)
150+
await posModel.loadItems(base: .parent(parentItem))
151+
}
154152
default:
155153
EmptyView()
156154
}

WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ struct PointOfSaleDashboardView: View {
8888
HStack {
8989
if posModel.orderStage == .building {
9090
ItemListView()
91+
.refreshable {
92+
ServiceLocator.analytics.track(.pointOfSaleProductsPullToRefresh)
93+
await posModel.loadItems(base: .root)
94+
}
9195
.accessibilitySortPriority(2)
9296
.transition(.move(edge: .leading))
9397
}

WooCommerce/Classes/POS/Utils/PreviewHelpers.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ final class PointOfSalePreviewItemService: PointOfSaleItemServiceProtocol {
5858
}
5959
}
6060

61+
@available(iOS 17.0, *)
6162
final class PointOfSalePreviewItemsController: PointOfSaleItemsControllerProtocol {
6263
@Published var itemsViewState: ItemsViewState = ItemsViewState(containerState: .loading,
6364
itemsStack: ItemsStackState(root: .loading([]),

0 commit comments

Comments
 (0)