From a6721741c05596a24221c7e5b031cd5fe47666d6 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Fri, 10 Jan 2025 13:16:40 +0000 Subject: [PATCH 1/5] 14696 Use baseItem to load item lists --- .../PointOfSaleItemsController.swift | 21 +++++--- .../Classes/POS/Models/ItemListBaseItem.swift | 7 +++ .../Models/PointOfSaleAggregateModel.swift | 18 +++---- .../Item Selector/ChildItemList.swift | 2 +- .../Presentation/Item Selector/ItemList.swift | 11 ++-- .../POS/Presentation/ItemListView.swift | 2 +- .../PointOfSaleDashboardView.swift | 4 +- .../Classes/POS/Utils/PreviewHelpers.swift | 15 ++++-- .../WooCommerce.xcodeproj/project.pbxproj | 4 ++ .../PointOfSaleItemsControllerTests.swift | 54 +++++++++---------- .../Mocks/MockPointOfSaleAggregateModel.swift | 6 +-- .../Mocks/MockPointOfSaleItemsService.swift | 6 +-- 12 files changed, 80 insertions(+), 70 deletions(-) create mode 100644 WooCommerce/Classes/POS/Models/ItemListBaseItem.swift diff --git a/WooCommerce/Classes/POS/Controllers/PointOfSaleItemsController.swift b/WooCommerce/Classes/POS/Controllers/PointOfSaleItemsController.swift index 383e3a8ac60..7c97377fdfa 100644 --- a/WooCommerce/Classes/POS/Controllers/PointOfSaleItemsController.swift +++ b/WooCommerce/Classes/POS/Controllers/PointOfSaleItemsController.swift @@ -7,10 +7,9 @@ import class Yosemite.Store protocol PointOfSaleItemsControllerProtocol { var itemsViewStatePublisher: any Publisher { get } - func loadInitialItems() async - func loadNextItems() async throws + func loadInitialItems(base: ItemListBaseItem) async + func loadNextItems(base: ItemListBaseItem) async throws func reload() async - func loadInitialChildItems(for parent: POSItem) async } class PointOfSaleItemsController: PointOfSaleItemsControllerProtocol { @@ -30,7 +29,17 @@ class PointOfSaleItemsController: PointOfSaleItemsControllerProtocol { } @MainActor - func loadInitialItems() async { + func loadInitialItems(base: ItemListBaseItem) async { + switch base { + case .root: + await loadInitialRootItems() + case .parent(let parent): + await loadInitialChildItems(for: parent) + } + } + + @MainActor + private func loadInitialRootItems() async { itemsViewState = .init(containerState: .loading, itemsStack: ItemsStackState(root: .loading([]), itemStates: [:])) do { @@ -46,7 +55,7 @@ class PointOfSaleItemsController: PointOfSaleItemsControllerProtocol { } @MainActor - func loadNextItems() async throws { + func loadNextItems(base: ItemListBaseItem) async throws { guard paginationTracker.hasNextPage else { return } @@ -84,7 +93,7 @@ class PointOfSaleItemsController: PointOfSaleItemsControllerProtocol { } @MainActor - func loadInitialChildItems(for parent: POSItem) async { + private func loadInitialChildItems(for parent: POSItem) async { switch parent { case let .variableParentProduct(parentProduct): updateState(for: parent, to: .loading([])) diff --git a/WooCommerce/Classes/POS/Models/ItemListBaseItem.swift b/WooCommerce/Classes/POS/Models/ItemListBaseItem.swift new file mode 100644 index 00000000000..a6a8579483e --- /dev/null +++ b/WooCommerce/Classes/POS/Models/ItemListBaseItem.swift @@ -0,0 +1,7 @@ +import Foundation +import enum Yosemite.POSItem + +enum ItemListBaseItem { + case root + case parent(POSItem) +} diff --git a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift index 61f220e08ca..51d8a2c95f1 100644 --- a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift +++ b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift @@ -23,10 +23,9 @@ protocol PointOfSaleAggregateModelProtocol { func trackCardPaymentsOnboardingShown() var itemsViewState: ItemsViewState { get } - func loadInitialItems() async - func loadNextItems() async throws + func loadInitialItems(base: ItemListBaseItem) async + func loadNextItems(base: ItemListBaseItem) async throws func reload() async - func loadInitialChildItems(for parent: POSItem) async var cart: [CartItem] { get } func addToCart(_ item: POSOrderableItem) @@ -93,24 +92,19 @@ extension PointOfSaleAggregateModel { } @MainActor - func loadInitialItems() async { - await itemsController.loadInitialItems() + func loadInitialItems(base: ItemListBaseItem) async { + await itemsController.loadInitialItems(base: base) } @MainActor - func loadNextItems() async throws { - try await itemsController.loadNextItems() + func loadNextItems(base: ItemListBaseItem) async throws { + try await itemsController.loadNextItems(base: base) } @MainActor func reload() async { await itemsController.reload() } - - @MainActor - func loadInitialChildItems(for parent: POSItem) async { - await itemsController.loadInitialChildItems(for: parent) - } } // MARK: - Cart diff --git a/WooCommerce/Classes/POS/Presentation/Item Selector/ChildItemList.swift b/WooCommerce/Classes/POS/Presentation/Item Selector/ChildItemList.swift index 1008f855581..30c01580946 100644 --- a/WooCommerce/Classes/POS/Presentation/Item Selector/ChildItemList.swift +++ b/WooCommerce/Classes/POS/Presentation/Item Selector/ChildItemList.swift @@ -43,7 +43,7 @@ struct ChildItemList: View { guard state.items.isEmpty else { return } - await posModel.loadInitialChildItems(for: parentItem) + await posModel.loadInitialItems(base: .parent(parentItem)) } } } diff --git a/WooCommerce/Classes/POS/Presentation/Item Selector/ItemList.swift b/WooCommerce/Classes/POS/Presentation/Item Selector/ItemList.swift index b243eb4470b..5dd7cecabc8 100644 --- a/WooCommerce/Classes/POS/Presentation/Item Selector/ItemList.swift +++ b/WooCommerce/Classes/POS/Presentation/Item Selector/ItemList.swift @@ -4,21 +4,16 @@ import struct Yosemite.POSVariableParentProduct /// Displays a list of POS items or placeholder card based on the given state. struct ItemList: View { - enum BaseItem { - case root - case parent(POSItem) - } - @Environment(\.floatingControlAreaSize) private var floatingControlAreaSize: CGSize @EnvironmentObject var posModel: PointOfSaleAggregateModel @StateObject private var infiniteScrollTriggerDeterminer = ThresholdInfiniteScrollTriggerDeterminer() let state: ItemListState - private let node: BaseItem + private let node: ItemListBaseItem private let headerView: HeaderView init(state: ItemListState, - node: BaseItem = .root, + node: ItemListBaseItem = .root, @ViewBuilder headerView: () -> HeaderView = { EmptyView() }) { self.state = state self.node = node @@ -33,7 +28,7 @@ struct ItemList: View { case .loaded(_, let hasMoreItems) = state, hasMoreItems else { return } - try await posModel.loadNextItems() + try await posModel.loadNextItems(base: node) }, content: { LazyVStack { diff --git a/WooCommerce/Classes/POS/Presentation/ItemListView.swift b/WooCommerce/Classes/POS/Presentation/ItemListView.swift index c4354702551..f31d2c7a9b4 100644 --- a/WooCommerce/Classes/POS/Presentation/ItemListView.swift +++ b/WooCommerce/Classes/POS/Presentation/ItemListView.swift @@ -273,7 +273,7 @@ private extension ItemListView { #Preview("Loaded with all product types") { let itemsController = PointOfSalePreviewItemsController() Task { @MainActor in - await itemsController.loadInitialItems() + await itemsController.loadInitialItems(base: .root) } let posModel = PointOfSaleAggregateModel( itemsController: itemsController, diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift index 1e7678da2bf..f14c20e6534 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift @@ -20,7 +20,7 @@ struct PointOfSaleDashboardView: View { case .error(let errorContents): PointOfSaleItemListErrorView(error: errorContents, onRetry: { Task { - await posModel.loadInitialItems() + await posModel.loadInitialItems(base: .root) } }) case .content: @@ -66,7 +66,7 @@ struct PointOfSaleDashboardView: View { supportForm } .task { - await posModel.loadInitialItems() + await posModel.loadInitialItems(base: .root) } } diff --git a/WooCommerce/Classes/POS/Utils/PreviewHelpers.swift b/WooCommerce/Classes/POS/Utils/PreviewHelpers.swift index 28fa4c67864..892257d8bde 100644 --- a/WooCommerce/Classes/POS/Utils/PreviewHelpers.swift +++ b/WooCommerce/Classes/POS/Utils/PreviewHelpers.swift @@ -64,12 +64,17 @@ final class PointOfSalePreviewItemsController: PointOfSaleItemsControllerProtoco itemStates: [:])) var itemsViewStatePublisher: any Publisher { $itemsViewState } - func loadInitialItems() async { - itemsViewState = ItemsViewState(containerState: .content, itemsStack: ItemsStackState(root: .loaded(mockItems, hasMoreItems: true), - itemStates: [:])) + func loadInitialItems(base: ItemListBaseItem) async { + switch base { + case .root: + itemsViewState = ItemsViewState(containerState: .content, itemsStack: ItemsStackState(root: .loaded(mockItems, hasMoreItems: true), + itemStates: [:])) + case .parent(let parent): + await loadInitialChildItems(for: parent) + } } - func loadNextItems() async { + func loadNextItems(base: ItemListBaseItem) async { itemsViewState = ItemsViewState(containerState: .content, itemsStack: ItemsStackState(root: .loading(mockItems), itemStates: [:])) } @@ -79,7 +84,7 @@ final class PointOfSalePreviewItemsController: PointOfSaleItemsControllerProtoco itemStates: [:])) } - func loadInitialChildItems(for parent: POSItem) async { + private func loadInitialChildItems(for parent: POSItem) async { itemsViewState = ItemsViewState( containerState: .content, itemsStack: ItemsStackState( diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index a88f19cb89d..f2d13f00e66 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -879,6 +879,7 @@ 20BCF6F02B0E48CC00954840 /* WooPaymentsPayoutsOverviewViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20BCF6EF2B0E48CC00954840 /* WooPaymentsPayoutsOverviewViewModelTests.swift */; }; 20BCF6F72B0E5AF000954840 /* MockSystemStatusService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20BCF6F62B0E5AEF00954840 /* MockSystemStatusService.swift */; }; 20C6E7512CDE4AEA00CD124C /* ItemListState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20C6E7502CDE4AEA00CD124C /* ItemListState.swift */; }; + 20C909962D3151FA0013BCCF /* ItemListBaseItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20C909952D3151FA0013BCCF /* ItemListBaseItem.swift */; }; 20CC1EDB2AFA8381006BD429 /* InPersonPaymentsMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20CC1EDA2AFA8381006BD429 /* InPersonPaymentsMenu.swift */; }; 20CC1EDD2AFA99DF006BD429 /* InPersonPaymentsMenuViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20CC1EDC2AFA99DF006BD429 /* InPersonPaymentsMenuViewModel.swift */; }; 20CCBF212B0E15C0003102E6 /* WooPaymentsPayoutsCurrencyOverviewViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20CCBF202B0E15C0003102E6 /* WooPaymentsPayoutsCurrencyOverviewViewModelTests.swift */; }; @@ -4053,6 +4054,7 @@ 20BCF6EF2B0E48CC00954840 /* WooPaymentsPayoutsOverviewViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooPaymentsPayoutsOverviewViewModelTests.swift; sourceTree = ""; }; 20BCF6F62B0E5AEF00954840 /* MockSystemStatusService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSystemStatusService.swift; sourceTree = ""; }; 20C6E7502CDE4AEA00CD124C /* ItemListState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemListState.swift; sourceTree = ""; }; + 20C909952D3151FA0013BCCF /* ItemListBaseItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemListBaseItem.swift; sourceTree = ""; }; 20CC1EDA2AFA8381006BD429 /* InPersonPaymentsMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InPersonPaymentsMenu.swift; sourceTree = ""; }; 20CC1EDC2AFA99DF006BD429 /* InPersonPaymentsMenuViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InPersonPaymentsMenuViewModel.swift; sourceTree = ""; }; 20CCBF202B0E15C0003102E6 /* WooPaymentsPayoutsCurrencyOverviewViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooPaymentsPayoutsCurrencyOverviewViewModelTests.swift; sourceTree = ""; }; @@ -9680,6 +9682,7 @@ 20C6E7502CDE4AEA00CD124C /* ItemListState.swift */, 20F7B12C2D12C7B900C08193 /* ItemsContainerState.swift */, 20F7B12E2D12CBE700C08193 /* ItemsViewState.swift */, + 20C909952D3151FA0013BCCF /* ItemListBaseItem.swift */, 20D4AE002D133B43004555B2 /* ItemsStackState.swift */, 68F151E02C0DA7910082AEC8 /* CartItem.swift */, 20D920E92CEF86520023B089 /* PointOfSaleErrorState.swift */, @@ -15414,6 +15417,7 @@ 011D7A332CEC877A0007C187 /* CardPresentModalNonRetryableErrorEmailSent.swift in Sources */, 68AF3C3B2D01481C006F1ED2 /* POSReceiptEligibilityBanner.swift in Sources */, EEBA02A32ADD6005001FE8E4 /* BlazeCampaignDashboardView.swift in Sources */, + 20C909962D3151FA0013BCCF /* ItemListBaseItem.swift in Sources */, 028BAC4722F3B550008BB4AF /* StatsTimeRangeV4+UI.swift in Sources */, 024DF32123744798006658FE /* AztecFormatBarCommandCoordinator.swift in Sources */, B5AA7B3F20ED81C2004DA14F /* UserDefaults+Woo.swift in Sources */, diff --git a/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleItemsControllerTests.swift b/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleItemsControllerTests.swift index 5b0e9fa7648..cd41951052c 100644 --- a/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleItemsControllerTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleItemsControllerTests.swift @@ -20,7 +20,7 @@ final class PointOfSaleItemsControllerTests { try #require(itemsViewState.containerState == .loading) // When - await sut.loadInitialItems() + await sut.loadInitialItems(base: .root) // Then #expect(itemProvider.spyLastRequestedPageNumber == 1) @@ -32,7 +32,7 @@ final class PointOfSaleItemsControllerTests { try #require(itemsViewState.containerState == .loading) // When - await sut.loadInitialItems() + await sut.loadInitialItems(base: .root) // Then #expect(itemsViewState == ItemsViewState(containerState: .content, @@ -47,7 +47,7 @@ final class PointOfSaleItemsControllerTests { itemProvider.shouldSimulateTwoPages = true // When - await sut.loadInitialItems() + await sut.loadInitialItems(base: .root) // Then #expect(itemsViewState == ItemsViewState(containerState: .content, @@ -61,9 +61,9 @@ final class PointOfSaleItemsControllerTests { let expectedItems = MockPointOfSaleItemService.makeInitialItems() // When - await sut.loadInitialItems() - await sut.loadInitialItems() - await sut.loadInitialItems() + await sut.loadInitialItems(base: .root) + await sut.loadInitialItems(base: .root) + await sut.loadInitialItems(base: .root) // Then guard case .loaded(let items, _) = itemsViewState.itemsStack.root else { @@ -119,7 +119,7 @@ final class PointOfSaleItemsControllerTests { try #require(itemsViewState.containerState == .loading) // When - try await sut.loadNextItems() + try await sut.loadNextItems(base: .root) // Then #expect(itemsViewState.containerState == .empty) @@ -134,7 +134,7 @@ final class PointOfSaleItemsControllerTests { try #require(itemsViewState.containerState == .loading) // When - try await sut.loadNextItems() + try await sut.loadNextItems(base: .root) // Then #expect(itemsViewState == ItemsViewState(containerState: .content, @@ -147,10 +147,10 @@ final class PointOfSaleItemsControllerTests { let initialItems = MockPointOfSaleItemService.makeInitialItems() itemProvider.items = initialItems itemProvider.shouldSimulateTwoPages = true - await sut.loadInitialItems() + await sut.loadInitialItems(base: .root) // When - try await sut.loadNextItems() + try await sut.loadNextItems(base: .root) // Then guard case .loaded(let items, _) = itemsViewState.itemsStack.root else { @@ -164,10 +164,10 @@ final class PointOfSaleItemsControllerTests { // Given try #require(itemsViewState.containerState == .loading) itemProvider.shouldSimulateTwoPages = true - await sut.loadInitialItems() + await sut.loadInitialItems(base: .root) // When - try await sut.loadNextItems() + try await sut.loadNextItems(base: .root) // Then #expect(itemProvider.spyLastRequestedPageNumber == 2) @@ -179,10 +179,10 @@ final class PointOfSaleItemsControllerTests { itemProvider.items = initialItems itemProvider.shouldSimulateTwoPages = true itemProvider.shouldSimulateMorePages = true - await sut.loadInitialItems() + await sut.loadInitialItems(base: .root) // When - try await sut.loadNextItems() + try await sut.loadNextItems(base: .root) // Then guard case .loaded(let items, let hasMoreItems) = itemsViewState.itemsStack.root else { @@ -200,7 +200,7 @@ final class PointOfSaleItemsControllerTests { try #require(itemsViewState.containerState == .loading) // When - await sut.loadInitialItems() + await sut.loadInitialItems(base: .root) // Then #expect(itemsViewState.containerState == .empty) @@ -215,7 +215,7 @@ final class PointOfSaleItemsControllerTests { try #require(itemsViewState.containerState == .loading) // When - await sut.loadInitialItems() + await sut.loadInitialItems(base: .root) // Then #expect(itemsViewState.containerState == .error(expectedError)) @@ -226,7 +226,7 @@ final class PointOfSaleItemsControllerTests { try #require(itemsViewState.containerState == .loading) itemProvider.shouldSimulateTwoPages = true - await sut.loadInitialItems() + await sut.loadInitialItems(base: .root) itemProvider.shouldThrowError = true let expectedError = PointOfSaleErrorState(title: "Error loading products", @@ -235,7 +235,7 @@ final class PointOfSaleItemsControllerTests { // When do { - try await sut.loadNextItems() + try await sut.loadNextItems(base: .root) } catch { // Then #expect(itemsViewState.containerState == .error(expectedError)) @@ -245,15 +245,15 @@ final class PointOfSaleItemsControllerTests { @Test func loadNextItems_after_itemProvider_throws_error_then_the_same_page_is_requested_next() async throws { // Given itemProvider.shouldSimulateTwoPages = true - await sut.loadInitialItems() + await sut.loadInitialItems(base: .root) itemProvider.shouldThrowError = true - try? await sut.loadNextItems() + try? await sut.loadNextItems(base: .root) try #require(itemProvider.spyLastRequestedPageNumber == 2) itemProvider.spyLastRequestedPageNumber = 0 // When - try? await sut.loadNextItems() + try? await sut.loadNextItems(base: .root) // Then #expect(itemProvider.spyLastRequestedPageNumber == 2) @@ -276,9 +276,9 @@ final class PointOfSaleItemsControllerTests { @Test func reload_requests_first_page() async throws { // Given itemProvider.shouldSimulateTwoPages = true - await sut.loadInitialItems() + await sut.loadInitialItems(base: .root) - try await sut.loadNextItems() + try await sut.loadNextItems(base: .root) try #require(itemProvider.spyLastRequestedPageNumber == 2) // When @@ -290,12 +290,12 @@ final class PointOfSaleItemsControllerTests { @Test func loadNextItems_when_next_page_is_empty_then_state_is_loaded() async throws { // Given - await sut.loadInitialItems() + await sut.loadInitialItems(base: .root) try #require(itemProvider.spyLastRequestedPageNumber == 1) // When itemProvider.shouldReturnZeroItems = true - try await sut.loadNextItems() + try await sut.loadNextItems(base: .root) // Then #expect(itemsViewState == ItemsViewState(containerState: .content, @@ -306,12 +306,12 @@ final class PointOfSaleItemsControllerTests { @Test func loadNextItems_when_next_page_is_empty_then_the_same_page_is_requested_next() async throws { // Given - await sut.loadInitialItems() + await sut.loadInitialItems(base: .root) try #require(itemProvider.spyLastRequestedPageNumber == 1) // When itemProvider.shouldReturnZeroItems = true - try await sut.loadNextItems() + try await sut.loadNextItems(base: .root) // Then try #require(itemProvider.spyLastRequestedPageNumber == 1) diff --git a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleAggregateModel.swift b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleAggregateModel.swift index 013395dc587..7be9bfc0c04 100644 --- a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleAggregateModel.swift +++ b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleAggregateModel.swift @@ -42,14 +42,12 @@ final class MockPointOfSaleAggregateModel: PointOfSaleAggregateModelProtocol { self.paymentState = paymentState } - func loadInitialItems() async { } + func loadInitialItems(base: ItemListBaseItem) async { } - func loadNextItems() async { } + func loadNextItems(base: ItemListBaseItem) async { } func reload() async { } - func loadInitialChildItems(for parent: POSItem) async { } - var cart: [CartItem] = [] func addToCart(_ item: POSOrderableItem) { } diff --git a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleItemsService.swift b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleItemsService.swift index 189b64e9b6e..1d9783ca1fa 100644 --- a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleItemsService.swift +++ b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleItemsService.swift @@ -6,11 +6,9 @@ import enum Yosemite.POSItem final class MockPointOfSaleItemsController: PointOfSaleItemsControllerProtocol { var itemsViewStatePublisher: any Publisher = Empty() - func loadInitialItems() async { } + func loadInitialItems(base: ItemListBaseItem) async { } - func loadNextItems() async { } + func loadNextItems(base: ItemListBaseItem) async { } func reload() async { } - - func loadInitialChildItems(for parent: Yosemite.POSItem) async { } } From d67a60ffc6eec1b90348be48f116018b6a12601d Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Fri, 10 Jan 2025 15:24:56 +0000 Subject: [PATCH 2/5] 14696 Add page info to variation fetches --- .../Remote/ProductVariationsRemote.swift | 15 ++++- .../Remote/ProductVariationsRemoteTests.swift | 57 ++++++++++++++++++- .../PointOfSale/PointOfSaleItemService.swift | 3 +- .../Remote/MockProductVariationsRemote.swift | 4 +- 4 files changed, 72 insertions(+), 7 deletions(-) diff --git a/Networking/Networking/Remote/ProductVariationsRemote.swift b/Networking/Networking/Remote/ProductVariationsRemote.swift index 76c1085fc75..2da75cbe769 100644 --- a/Networking/Networking/Remote/ProductVariationsRemote.swift +++ b/Networking/Networking/Remote/ProductVariationsRemote.swift @@ -14,7 +14,7 @@ public protocol ProductVariationsRemoteProtocol { completion: @escaping ([ProductVariation]?, Error?) -> Void) func loadVariationsForPointOfSale(for siteID: Int64, parentProductID: Int64, - pageNumber: Int) async throws -> [ProductVariation] + pageNumber: Int) async throws -> PagedItems func loadProductVariation(for siteID: Int64, productID: Int64, variationID: Int64, completion: @escaping (Result) -> Void) func createProductVariation(for siteID: Int64, productID: Int64, @@ -78,7 +78,7 @@ public class ProductVariationsRemote: Remote, ProductVariationsRemoteProtocol { /// - Returns: Variations for the provided parent product. public func loadVariationsForPointOfSale(for siteID: Int64, parentProductID: Int64, - pageNumber: Int = Default.pageNumber) async throws -> [ProductVariation] { + pageNumber: Int = Default.pageNumber) async throws -> PagedItems { let request = productVariationsRequest(for: siteID, productID: parentProductID, variationIDs: [], @@ -86,7 +86,16 @@ public class ProductVariationsRemote: Remote, ProductVariationsRemoteProtocol { pageNumber: pageNumber, pageSize: POSConstants.variationsPerPage) let mapper = ProductVariationListMapper(siteID: siteID, productID: parentProductID) - return try await enqueue(request, mapper: mapper) + + let (variations, responseHeaders) = try await enqueueWithResponseHeaders(request, mapper: mapper) + + // Extracts the total number of pages from the response headers. + // Response header names are case insensitive. + let totalPages = responseHeaders?.first(where: { $0.key.lowercased() == Remote.PaginationHeaderKey.totalPagesCount.lowercased() }) + .flatMap { Int($0.value) } + let hasMorePages = totalPages.map { pageNumber < $0 } ?? true + + return PagedItems(items: variations, hasMorePages: hasMorePages) } private func productVariationsRequest(for siteID: Int64, diff --git a/Networking/NetworkingTests/Remote/ProductVariationsRemoteTests.swift b/Networking/NetworkingTests/Remote/ProductVariationsRemoteTests.swift index 1807ddcf514..85251edf81b 100644 --- a/Networking/NetworkingTests/Remote/ProductVariationsRemoteTests.swift +++ b/Networking/NetworkingTests/Remote/ProductVariationsRemoteTests.swift @@ -160,7 +160,7 @@ final class ProductVariationsRemoteTests: XCTestCase { network.simulateResponse(requestUrlSuffix: "products/\(sampleProductID)/variations", filename: "product-variations-load-all") // When - let variations = try await remote.loadVariationsForPointOfSale(for: sampleSiteID, parentProductID: sampleProductID) + let variations = try await remote.loadVariationsForPointOfSale(for: sampleSiteID, parentProductID: sampleProductID).items // Then XCTAssertEqual(variations.count, 8) @@ -224,6 +224,61 @@ final class ProductVariationsRemoteTests: XCTestCase { XCTAssertEqual(firstVariation.menuOrder, 8) } + func test_loadVariationsForPointOfSale_returns_page_details() async throws { + // Given + let remote = ProductVariationsRemote(network: network) + network.simulateResponse(requestUrlSuffix: "products/\(sampleProductID)/variations", filename: "product-variations-load-all") + + // When + let hasMorePages = try await remote.loadVariationsForPointOfSale(for: sampleSiteID, parentProductID: sampleProductID).hasMorePages + + // Then + XCTAssertTrue(hasMorePages) + } + + func test_loadVariationssForPointOfSale_returns_hasMorePages_based_on_header_with_case_insensitive_name() async throws { + // Given + let remote = ProductVariationsRemote(network: network) + network.responseHeaders = ["X-WP-TotalPages": "5"] + network.simulateResponse(requestUrlSuffix: "products/\(sampleProductID)/variations", filename: "product-variations-load-all") + + // When loading page 1 to 4 + for pageNumber in 1...4 { + let pagedVariations = try await remote.loadVariationsForPointOfSale(for: sampleSiteID, + parentProductID: sampleProductID, + pageNumber: pageNumber) + + // Then + XCTAssertTrue(pagedVariations.hasMorePages) + } + + // When loading page 5 + let pagedVariations = try await remote.loadVariationsForPointOfSale(for: sampleSiteID, + parentProductID: sampleProductID, + pageNumber: 5) + + // Then + XCTAssertFalse(pagedVariations.hasMorePages) + } + + func test_loadVariationssForPointOfSale_returns_hasMorePages_true_when_header_is_not_set() async throws { + // Given + let remote = ProductVariationsRemote(network: network) + network.responseHeaders = nil + network.simulateResponse(requestUrlSuffix: "products/\(sampleProductID)/variations", filename: "product-variations-load-all") + + // When loading the first 5 pages + for pageNumber in 1...5 { + let pagedVariations = try await remote.loadVariationsForPointOfSale(for: sampleSiteID, + parentProductID: sampleProductID, + pageNumber: pageNumber) + + // Then + XCTAssertTrue(pagedVariations.hasMorePages) + } + } + + func test_loadVariationsForPointOfSale_properly_relays_networking_error() async throws { // Given let remote = ProductVariationsRemote(network: network) diff --git a/Yosemite/Yosemite/PointOfSale/PointOfSaleItemService.swift b/Yosemite/Yosemite/PointOfSale/PointOfSaleItemService.swift index d8fc8d41044..e85e2a83b28 100644 --- a/Yosemite/Yosemite/PointOfSale/PointOfSaleItemService.swift +++ b/Yosemite/Yosemite/PointOfSale/PointOfSaleItemService.swift @@ -65,10 +65,11 @@ public final class PointOfSaleItemService: PointOfSaleItemServiceProtocol { } public func providePointOfSaleVariationItems(for parentProduct: POSVariableParentProduct, pageNumber: Int) async throws -> PagedItems { - let variations = try await variationRemote + let pagedVariations = try await variationRemote .loadVariationsForPointOfSale(for: siteID, parentProductID: parentProduct.productID, pageNumber: pageNumber) + let variations = pagedVariations.items return .init( items: variations.compactMap({ variation in let variationName = ProductVariationFormatter().generateName( diff --git a/Yosemite/YosemiteTests/Mocks/Networking/Remote/MockProductVariationsRemote.swift b/Yosemite/YosemiteTests/Mocks/Networking/Remote/MockProductVariationsRemote.swift index db31d99af3f..8a7861f076c 100644 --- a/Yosemite/YosemiteTests/Mocks/Networking/Remote/MockProductVariationsRemote.swift +++ b/Yosemite/YosemiteTests/Mocks/Networking/Remote/MockProductVariationsRemote.swift @@ -91,8 +91,8 @@ extension MockProductVariationsRemote: ProductVariationsRemoteProtocol { // no-op } - func loadVariationsForPointOfSale(for siteID: Int64, parentProductID: Int64, pageNumber: Int) async throws -> [ProductVariation] { - [] + func loadVariationsForPointOfSale(for siteID: Int64, parentProductID: Int64, pageNumber: Int) async throws -> PagedItems { + .init(items: [], hasMorePages: false) } func loadProductVariation(for siteID: Int64, productID: Int64, variationID: Int64, completion: @escaping (Result) -> Void) { From b0098f62862edc97f5cc46d351c7e9df75ca0628 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Fri, 10 Jan 2025 15:25:48 +0000 Subject: [PATCH 3/5] 14696 Load next variation page on child item lists --- .../PointOfSaleItemsController.swift | 60 ++++++++++++++++++- .../Presentation/Item Selector/ItemList.swift | 3 +- .../PointOfSaleItemsControllerTests.swift | 29 +++++++++ .../POS/Mocks/MockPOSItemProvider.swift | 50 +++++++++++++++- .../PointOfSale/PointOfSaleItemService.swift | 2 +- .../PointOfSaleItemServiceTests.swift | 38 +++++++++++- 6 files changed, 174 insertions(+), 8 deletions(-) diff --git a/WooCommerce/Classes/POS/Controllers/PointOfSaleItemsController.swift b/WooCommerce/Classes/POS/Controllers/PointOfSaleItemsController.swift index 7c97377fdfa..ccd7aeb9ab4 100644 --- a/WooCommerce/Classes/POS/Controllers/PointOfSaleItemsController.swift +++ b/WooCommerce/Classes/POS/Controllers/PointOfSaleItemsController.swift @@ -21,6 +21,7 @@ class PointOfSaleItemsController: PointOfSaleItemsControllerProtocol { itemsStack: ItemsStackState(root: .loading([]), itemStates: [:])) private let paginationTracker: AsyncPaginationTracker + private var childPaginationTrackers: [POSItem: AsyncPaginationTracker] = [:] private let itemProvider: PointOfSaleItemServiceProtocol init(itemProvider: PointOfSaleItemServiceProtocol) { @@ -56,6 +57,16 @@ class PointOfSaleItemsController: PointOfSaleItemsControllerProtocol { @MainActor func loadNextItems(base: ItemListBaseItem) async throws { + switch base { + case .root: + try await loadNextRootItems() + case .parent(let parent): + await loadNextChildItems(for: parent) + } + } + + @MainActor + private func loadNextRootItems() async throws { guard paginationTracker.hasNextPage else { return } @@ -97,17 +108,59 @@ class PointOfSaleItemsController: PointOfSaleItemsControllerProtocol { switch parent { case let .variableParentProduct(parentProduct): updateState(for: parent, to: .loading([])) + + let paginationTracker = paginationTracker(for: parent) do { - // TODO-14696: pagination support for variations lists - try await fetchVariationItems(parentProduct: parentProduct, parentItem: parent, pageNumber: Store.Default.firstPageNumber) + try await paginationTracker.syncFirstPage { [weak self] pageNumber in + guard let self else { return true } + return try await fetchVariationItems(parentProduct: parentProduct, parentItem: parent, pageNumber: Store.Default.firstPageNumber) + } } catch { // TODO: 14694 - Handle error from loading initial variations. } + default: assertionFailure("Unsupported parent type for loading child items: \(parent)") return } } + + @MainActor + private func loadNextChildItems(for parent: POSItem) async { + let paginationTracker = paginationTracker(for: parent) + + guard paginationTracker.hasNextPage else { + return + } + let currentItems = itemsViewState.itemsStack.itemStates[parent]?.items ?? [] + updateState(for: parent, to: .loading(currentItems)) + + do { + _ = try await paginationTracker.ensureNextPageIsSynced { [weak self] pageNumber in + guard let self else { return true } + switch parent { + case let .variableParentProduct(parentProduct): + return try await fetchVariationItems(parentProduct: parentProduct, parentItem: parent, pageNumber: pageNumber) + default: + assertionFailure() + return true + } + } + } catch { + // TODO: 14694 - Handle error from loading the next page, like showing an error UI at the end or as an overlay. + updateState(for: parent, to: .error(PointOfSaleErrorState.errorOnLoadingProducts())) + } + } + + private func paginationTracker(for parent: POSItem) -> AsyncPaginationTracker { + if let childPaginationTracker = childPaginationTrackers[parent] { + return childPaginationTracker + } else { + let newChildPaginationTracker = AsyncPaginationTracker() + childPaginationTrackers[parent] = newChildPaginationTracker + return newChildPaginationTracker + } + } } private extension PointOfSaleItemsController { @@ -146,7 +199,7 @@ private extension PointOfSaleItemsController { private func fetchVariationItems(parentProduct: POSVariableParentProduct, parentItem: POSItem, pageNumber: Int, - appendToExistingItems: Bool = true) async throws { + appendToExistingItems: Bool = true) async throws -> Bool { let pagedItems = try await itemProvider.providePointOfSaleVariationItems( for: parentProduct, pageNumber: pageNumber @@ -160,6 +213,7 @@ private extension PointOfSaleItemsController { allItems.append(contentsOf: uniqueNewItems) updateState(for: parentItem, to: .loaded(allItems, hasMoreItems: pagedItems.hasMorePages)) + return pagedItems.hasMorePages } } diff --git a/WooCommerce/Classes/POS/Presentation/Item Selector/ItemList.swift b/WooCommerce/Classes/POS/Presentation/Item Selector/ItemList.swift index 5dd7cecabc8..61528849a27 100644 --- a/WooCommerce/Classes/POS/Presentation/Item Selector/ItemList.swift +++ b/WooCommerce/Classes/POS/Presentation/Item Selector/ItemList.swift @@ -24,8 +24,7 @@ struct ItemList: View { InfiniteScrollView( triggerDeterminer: infiniteScrollTriggerDeterminer, loadMore: { - guard case .root = node, - case .loaded(_, let hasMoreItems) = state, + guard case .loaded(_, let hasMoreItems) = state, hasMoreItems else { return } try await posModel.loadNextItems(base: node) diff --git a/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleItemsControllerTests.swift b/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleItemsControllerTests.swift index cd41951052c..dea4a0555ce 100644 --- a/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleItemsControllerTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleItemsControllerTests.swift @@ -1,6 +1,9 @@ import Testing +import Foundation import Combine @testable import WooCommerce +import struct Yosemite.POSVariableParentProduct +import enum Yosemite.POSItem final class PointOfSaleItemsControllerTests { private let itemProvider: MockPointOfSaleItemService @@ -193,6 +196,32 @@ final class PointOfSaleItemsControllerTests { #expect(items.count == 4) } + @Test func loadNextItems_child_when_simulateFetchNextPage_then_state_is_loaded_with_hasMoreItems() async throws { + // Given + let parentItem = POSItem.variableParentProduct(POSVariableParentProduct(id: UUID(), + name: "Fake Parent", + productImageSource: nil, + productID: 12345)) + let baseItem = ItemListBaseItem.parent(parentItem) + itemProvider.items = [parentItem] + itemProvider.shouldSimulateTwoPagesOfVariations = true + itemProvider.shouldSimulateMorePagesOfVariations = true + + await sut.loadInitialItems(base: .root) + await sut.loadInitialItems(base: baseItem) + + // When + try await sut.loadNextItems(base: baseItem) + + // Then + guard case .loaded(let items, let hasMoreItems) = itemsViewState.itemsStack.itemStates[parentItem] else { + Issue.record("Expected loaded ItemList state, but got \(itemsViewState)") + return + } + #expect(hasMoreItems) + #expect(items.count == 4) + } + @Test func loadInitialItems_when_no_items_then_state_is_loaded_empty() async throws { // Given itemProvider.shouldReturnZeroItems = true diff --git a/WooCommerce/WooCommerceTests/POS/Mocks/MockPOSItemProvider.swift b/WooCommerce/WooCommerceTests/POS/Mocks/MockPOSItemProvider.swift index 9daab9af923..1aaee4ecff0 100644 --- a/WooCommerce/WooCommerceTests/POS/Mocks/MockPOSItemProvider.swift +++ b/WooCommerce/WooCommerceTests/POS/Mocks/MockPOSItemProvider.swift @@ -3,6 +3,7 @@ import protocol Yosemite.PointOfSaleItemServiceProtocol import enum Yosemite.POSItem import protocol Yosemite.POSOrderableItem @testable import struct Yosemite.POSSimpleProduct +@testable import struct Yosemite.POSVariation import struct Yosemite.PagedItems import struct Yosemite.POSVariableParentProduct @@ -29,8 +30,15 @@ final class MockPointOfSaleItemService: PointOfSaleItemServiceProtocol { return .init(items: MockPointOfSaleItemService.makeInitialItems(), hasMorePages: shouldSimulateTwoPages) } + var shouldSimulateTwoPagesOfVariations = false + var shouldSimulateMorePagesOfVariations = false func providePointOfSaleVariationItems(for parentProduct: POSVariableParentProduct, pageNumber: Int) async throws -> PagedItems { - .init(items: [], hasMorePages: false) + if shouldSimulateTwoPagesOfVariations, + pageNumber > 1 { + return .init(items: MockPointOfSaleItemService.makeSecondPageVariationItems(), hasMorePages: shouldSimulateMorePagesOfVariations) + } + + return .init(items: MockPointOfSaleItemService.makeInitialVariationItems(), hasMorePages: shouldSimulateTwoPagesOfVariations) } } @@ -71,6 +79,46 @@ extension MockPointOfSaleItemService { return [.simpleProduct(product3), .simpleProduct(product4)] } + static func makeInitialVariationItems() -> [POSItem] { + let fakeUUID1 = UUID(uuidString: "B04AF636-CF6C-11EF-A45C-FA719FB6C0F0") ?? UUID() + let fakeUUID2 = UUID(uuidString: "B04AF727-CF6C-11EF-A45C-FA719FB6C0F0") ?? UUID() + + let variation1 = POSVariation(id: fakeUUID1, + name: "Choco", + formattedPrice: "$2.00", + price: "2.00", + productID: 1, + variationID: 1) + + let variation2 = POSVariation(id: fakeUUID2, + name: "Vanilla", + formattedPrice: "$2.00", + price: "2.00", + productID: 1, + variationID: 2) + return [.variation(variation1), .variation(variation2)] + } + + static func makeSecondPageVariationItems() -> [POSItem] { + let fakeUUID3 = UUID(uuidString: "B04AF758-CF6C-11EF-A45C-FA719FB6C0F0") ?? UUID() + let fakeUUID4 = UUID(uuidString: "B04AF78A-CF6C-11EF-A45C-FA719FB6C0F0") ?? UUID() + + let variation3 = POSVariation(id: fakeUUID3, + name: "Strawberry", + formattedPrice: "$2.00", + price: "2.00", + productID: 1, + variationID: 3) + + let variation4 = POSVariation(id: fakeUUID4, + name: "Pistachio", + formattedPrice: "$3.00", + price: "2.00", + productID: 1, + variationID: 4) + return [.variation(variation3), .variation(variation4)] + } + enum MockError: Error { case requestFailed } diff --git a/Yosemite/Yosemite/PointOfSale/PointOfSaleItemService.swift b/Yosemite/Yosemite/PointOfSale/PointOfSaleItemService.swift index e85e2a83b28..d9b41e58251 100644 --- a/Yosemite/Yosemite/PointOfSale/PointOfSaleItemService.swift +++ b/Yosemite/Yosemite/PointOfSale/PointOfSaleItemService.swift @@ -86,7 +86,7 @@ public final class PointOfSaleItemService: PointOfSaleItemServiceProtocol { variationID: variation.productVariationID)) }), // TODO-14696: pagination support for variations lists - hasMorePages: false + hasMorePages: pagedVariations.hasMorePages ) } diff --git a/Yosemite/YosemiteTests/PointOfSale/PointOfSaleItemServiceTests.swift b/Yosemite/YosemiteTests/PointOfSale/PointOfSaleItemServiceTests.swift index acd63e8ca82..9ce6201babd 100644 --- a/Yosemite/YosemiteTests/PointOfSale/PointOfSaleItemServiceTests.swift +++ b/Yosemite/YosemiteTests/PointOfSale/PointOfSaleItemServiceTests.swift @@ -40,7 +40,7 @@ final class PointOfSaleItemServiceTests: XCTestCase { } } - func test_PointOfSaleItemServiceProtocol_when_empty_data_for_non_first_page_then_returns_empty_items_and_no_next_page() async throws { + func test_PointOfSaleItemServiceProtocol_when_empty_data_for_non_first_page_of_products_then_returns_empty_items_and_no_next_page() async throws { // Given network.simulateResponse(requestUrlSuffix: "products", filename: "empty-data-array") @@ -215,6 +215,42 @@ final class PointOfSaleItemServiceTests: XCTestCase { XCTAssertEqual(firstVariation.productVariationID, 1275) } + func test_providePointOfSaleVariationItems_returns_variation_page_details_when_load_succeeds() async throws { + // Given + let itemProvider = PointOfSaleItemService(siteID: siteID, + currencySettings: currencySettings, + network: network, + isVariableProductsFeatureEnabled: true) + let parentProductID: Int64 = 123 + + // When + network.responseHeaders = ["X-WP-TotalPages": "5"] + network.simulateResponse(requestUrlSuffix: "products/\(parentProductID)/variations", filename: "product-variations-load-all") + let pagedVariations = try await itemProvider.providePointOfSaleVariationItems( + for: .init( + id: .init(), + name: "Tea", + productImageSource: nil, + productID: parentProductID, + allAttributes: [ + .init( + siteID: siteID, + attributeID: 0, + name: "Size", + position: 4, + visible: true, + variation: true, + options: ["6 piece"] + ) + ] + ), + pageNumber: 1 + ) + + // Then + XCTAssertTrue(pagedVariations.hasMorePages) + } + func test_providePointOfSaleVariationItems_throws_error_when_variations_load_fails() async throws { // Given let itemProvider = PointOfSaleItemService(siteID: siteID, From 3e00a9fa90b30226d290a8761d564a8b63154bde Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Fri, 10 Jan 2025 17:24:21 +0000 Subject: [PATCH 4/5] 14696 Extract child item fetch and futureproof it The switch statement now requires that newly added POSItems are specifically handled. --- .../PointOfSaleItemsController.swift | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/WooCommerce/Classes/POS/Controllers/PointOfSaleItemsController.swift b/WooCommerce/Classes/POS/Controllers/PointOfSaleItemsController.swift index ccd7aeb9ab4..0b3a9715fb2 100644 --- a/WooCommerce/Classes/POS/Controllers/PointOfSaleItemsController.swift +++ b/WooCommerce/Classes/POS/Controllers/PointOfSaleItemsController.swift @@ -105,23 +105,16 @@ class PointOfSaleItemsController: PointOfSaleItemsControllerProtocol { @MainActor private func loadInitialChildItems(for parent: POSItem) async { - switch parent { - case let .variableParentProduct(parentProduct): - updateState(for: parent, to: .loading([])) - - let paginationTracker = paginationTracker(for: parent) - do { - try await paginationTracker.syncFirstPage { [weak self] pageNumber in - guard let self else { return true } - return try await fetchVariationItems(parentProduct: parentProduct, parentItem: parent, pageNumber: Store.Default.firstPageNumber) - } - } catch { - // TODO: 14694 - Handle error from loading initial variations. - } + updateState(for: parent, to: .loading([])) - default: - assertionFailure("Unsupported parent type for loading child items: \(parent)") - return + let paginationTracker = paginationTracker(for: parent) + do { + try await paginationTracker.syncFirstPage { [weak self] pageNumber in + guard let self else { return true } + return try await fetchChildItems(for: parent, pageNumber: Store.Default.firstPageNumber) + } + } catch { + // TODO: 14694 - Handle error from loading initial variations. } } @@ -138,13 +131,7 @@ class PointOfSaleItemsController: PointOfSaleItemsControllerProtocol { do { _ = try await paginationTracker.ensureNextPageIsSynced { [weak self] pageNumber in guard let self else { return true } - switch parent { - case let .variableParentProduct(parentProduct): - return try await fetchVariationItems(parentProduct: parentProduct, parentItem: parent, pageNumber: pageNumber) - default: - assertionFailure() - return true - } + return try await fetchChildItems(for: parent, pageNumber: pageNumber) } } catch { // TODO: 14694 - Handle error from loading the next page, like showing an error UI at the end or as an overlay. @@ -152,6 +139,17 @@ class PointOfSaleItemsController: PointOfSaleItemsControllerProtocol { } } + @MainActor + private func fetchChildItems(for parent: POSItem, pageNumber: Int) async throws -> Bool { + switch parent { + case let .variableParentProduct(parentProduct): + return try await fetchVariationItems(parentProduct: parentProduct, parentItem: parent, pageNumber: pageNumber) + case .simpleProduct, .variation: + assertionFailure("Unsupported parent type for loading child items: \(parent)") + return false + } + } + private func paginationTracker(for parent: POSItem) -> AsyncPaginationTracker { if let childPaginationTracker = childPaginationTrackers[parent] { return childPaginationTracker From 3b339193ad551edbdd26b701187436fd578f995d Mon Sep 17 00:00:00 2001 From: Jaclyn Chen Date: Mon, 13 Jan 2025 10:38:38 +0800 Subject: [PATCH 5/5] Remove outdated TODO comment now that pagination support is implemented. --- Yosemite/Yosemite/PointOfSale/PointOfSaleItemService.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Yosemite/Yosemite/PointOfSale/PointOfSaleItemService.swift b/Yosemite/Yosemite/PointOfSale/PointOfSaleItemService.swift index d9b41e58251..9ed8d7c2cf2 100644 --- a/Yosemite/Yosemite/PointOfSale/PointOfSaleItemService.swift +++ b/Yosemite/Yosemite/PointOfSale/PointOfSaleItemService.swift @@ -85,7 +85,6 @@ public final class PointOfSaleItemService: PointOfSaleItemServiceProtocol { productID: variation.productID, variationID: variation.productVariationID)) }), - // TODO-14696: pagination support for variations lists hasMorePages: pagedVariations.hasMorePages ) }