diff --git a/Modules/Sources/Yosemite/PointOfSale/OrderList/PointOfSaleOrderListFetchStrategy.swift b/Modules/Sources/Yosemite/PointOfSale/OrderList/POSOrderListFetchStrategy.swift similarity index 70% rename from Modules/Sources/Yosemite/PointOfSale/OrderList/PointOfSaleOrderListFetchStrategy.swift rename to Modules/Sources/Yosemite/PointOfSale/OrderList/POSOrderListFetchStrategy.swift index 97a288a6a9d..8371a8f39b3 100644 --- a/Modules/Sources/Yosemite/PointOfSale/OrderList/PointOfSaleOrderListFetchStrategy.swift +++ b/Modules/Sources/Yosemite/PointOfSale/OrderList/POSOrderListFetchStrategy.swift @@ -1,7 +1,7 @@ import Foundation import struct NetworkingCore.PagedItems -public protocol PointOfSaleOrderListFetchStrategy { +public protocol POSOrderListFetchStrategy { func fetchOrders(pageNumber: Int) async throws -> PagedItems func loadOrder(orderID: Int64) async throws -> POSOrder var supportsCaching: Bool { get } @@ -9,18 +9,18 @@ public protocol PointOfSaleOrderListFetchStrategy { var id: String { get } } -extension PointOfSaleOrderListFetchStrategy { +extension POSOrderListFetchStrategy { var id: String { String(describing: type(of: self)) } } -struct PointOfSaleDefaultOrderListFetchStrategy: PointOfSaleOrderListFetchStrategy { - private let orderListService: PointOfSaleOrderListServiceProtocol +struct POSDefaultOrderListFetchStrategy: POSOrderListFetchStrategy { + private let orderListService: POSOrderListServiceProtocol let supportsCaching: Bool = true var showsLoadingWithItems: Bool = true - init(orderListService: PointOfSaleOrderListServiceProtocol) { + init(orderListService: POSOrderListServiceProtocol) { self.orderListService = orderListService } @@ -33,14 +33,14 @@ struct PointOfSaleDefaultOrderListFetchStrategy: PointOfSaleOrderListFetchStrate } } -struct PointOfSaleSearchOrderListFetchStrategy: PointOfSaleOrderListFetchStrategy { - private let orderListService: PointOfSaleOrderListServiceProtocol +struct POSSearchOrderListFetchStrategy: POSOrderListFetchStrategy { + private let orderListService: POSOrderListServiceProtocol private let searchTerm: String var supportsCaching: Bool = false var showsLoadingWithItems = false - init(orderListService: PointOfSaleOrderListServiceProtocol, searchTerm: String) { + init(orderListService: POSOrderListServiceProtocol, searchTerm: String) { self.orderListService = orderListService self.searchTerm = searchTerm } diff --git a/Modules/Sources/Yosemite/PointOfSale/OrderList/PointOfSaleOrderListFetchStrategyFactory.swift b/Modules/Sources/Yosemite/PointOfSale/OrderList/POSOrderListFetchStrategyFactory.swift similarity index 58% rename from Modules/Sources/Yosemite/PointOfSale/OrderList/PointOfSaleOrderListFetchStrategyFactory.swift rename to Modules/Sources/Yosemite/PointOfSale/OrderList/POSOrderListFetchStrategyFactory.swift index eebaf996b6b..47da00588c8 100644 --- a/Modules/Sources/Yosemite/PointOfSale/OrderList/PointOfSaleOrderListFetchStrategyFactory.swift +++ b/Modules/Sources/Yosemite/PointOfSale/OrderList/POSOrderListFetchStrategyFactory.swift @@ -3,12 +3,12 @@ import class Networking.AlamofireNetwork import class Networking.OrdersRemote import class WooFoundationCore.CurrencyFormatter -public protocol PointOfSaleOrderListFetchStrategyFactoryProtocol { - func defaultStrategy() -> PointOfSaleOrderListFetchStrategy - func searchStrategy(searchTerm: String) -> PointOfSaleOrderListFetchStrategy +public protocol POSOrderListFetchStrategyFactoryProtocol { + func defaultStrategy() -> POSOrderListFetchStrategy + func searchStrategy(searchTerm: String) -> POSOrderListFetchStrategy } -public final class PointOfSaleOrderListFetchStrategyFactory: PointOfSaleOrderListFetchStrategyFactoryProtocol { +public final class POSOrderListFetchStrategyFactory: POSOrderListFetchStrategyFactoryProtocol { private let siteID: Int64 private let ordersRemote: OrdersRemote private let currencyFormatter: CurrencyFormatter @@ -22,9 +22,9 @@ public final class PointOfSaleOrderListFetchStrategyFactory: PointOfSaleOrderLis self.currencyFormatter = currencyFormatter } - public func defaultStrategy() -> PointOfSaleOrderListFetchStrategy { - PointOfSaleDefaultOrderListFetchStrategy( - orderListService: PointOfSaleOrderListService( + public func defaultStrategy() -> POSOrderListFetchStrategy { + POSDefaultOrderListFetchStrategy( + orderListService: POSOrderListService( siteID: siteID, ordersRemote: ordersRemote, currencyFormatter: currencyFormatter @@ -32,9 +32,9 @@ public final class PointOfSaleOrderListFetchStrategyFactory: PointOfSaleOrderLis ) } - public func searchStrategy(searchTerm: String) -> PointOfSaleOrderListFetchStrategy { - PointOfSaleSearchOrderListFetchStrategy( - orderListService: PointOfSaleOrderListService( + public func searchStrategy(searchTerm: String) -> POSOrderListFetchStrategy { + POSSearchOrderListFetchStrategy( + orderListService: POSOrderListService( siteID: siteID, ordersRemote: ordersRemote, currencyFormatter: currencyFormatter diff --git a/Modules/Sources/Yosemite/PointOfSale/OrderList/PointOfSaleOrderListService.swift b/Modules/Sources/Yosemite/PointOfSale/OrderList/POSOrderListService.swift similarity index 84% rename from Modules/Sources/Yosemite/PointOfSale/OrderList/PointOfSaleOrderListService.swift rename to Modules/Sources/Yosemite/PointOfSale/OrderList/POSOrderListService.swift index d02178e4c84..1fdf96161a6 100644 --- a/Modules/Sources/Yosemite/PointOfSale/OrderList/PointOfSaleOrderListService.swift +++ b/Modules/Sources/Yosemite/PointOfSale/OrderList/POSOrderListService.swift @@ -5,7 +5,7 @@ import struct NetworkingCore.Order import protocol NetworkingCore.POSOrdersRemoteProtocol import class WooFoundationCore.CurrencyFormatter -public final class PointOfSaleOrderListService: PointOfSaleOrderListServiceProtocol { +public final class POSOrderListService: POSOrderListServiceProtocol { private let ordersRemote: POSOrdersRemoteProtocol private let siteID: Int64 private let mapper: POSOrderMapper @@ -39,9 +39,9 @@ public final class PointOfSaleOrderListService: PointOfSaleOrderListServiceProto hasMorePages: pagedOrders.hasMorePages, totalItems: pagedOrders.totalItems) } catch AFError.explicitlyCancelled { - throw PointOfSaleOrderListServiceError.requestCancelled + throw POSOrderListServiceError.requestCancelled } catch { - throw PointOfSaleOrderListServiceError.requestFailed + throw POSOrderListServiceError.requestFailed } } @@ -65,9 +65,9 @@ public final class PointOfSaleOrderListService: PointOfSaleOrderListServiceProto hasMorePages: pagedOrders.hasMorePages, totalItems: pagedOrders.totalItems) } catch AFError.explicitlyCancelled { - throw PointOfSaleOrderListServiceError.requestCancelled + throw POSOrderListServiceError.requestCancelled } catch { - throw PointOfSaleOrderListServiceError.requestFailed + throw POSOrderListServiceError.requestFailed } } @@ -76,9 +76,9 @@ public final class PointOfSaleOrderListService: PointOfSaleOrderListServiceProto let order = try await ordersRemote.loadPOSOrder(siteID: siteID, orderID: orderID) return mapper.map(order: order) } catch AFError.explicitlyCancelled { - throw PointOfSaleOrderListServiceError.requestCancelled + throw POSOrderListServiceError.requestCancelled } catch { - throw PointOfSaleOrderListServiceError.requestFailed + throw POSOrderListServiceError.requestFailed } } } diff --git a/Modules/Sources/Yosemite/PointOfSale/OrderList/PointOfSaleOrderListServiceProtocol.swift b/Modules/Sources/Yosemite/PointOfSale/OrderList/POSOrderListServiceProtocol.swift similarity index 75% rename from Modules/Sources/Yosemite/PointOfSale/OrderList/PointOfSaleOrderListServiceProtocol.swift rename to Modules/Sources/Yosemite/PointOfSale/OrderList/POSOrderListServiceProtocol.swift index 8415555fd18..db842c6672f 100644 --- a/Modules/Sources/Yosemite/PointOfSale/OrderList/PointOfSaleOrderListServiceProtocol.swift +++ b/Modules/Sources/Yosemite/PointOfSale/OrderList/POSOrderListServiceProtocol.swift @@ -1,12 +1,12 @@ import Foundation import struct NetworkingCore.PagedItems -public enum PointOfSaleOrderListServiceError: Error, Equatable { +public enum POSOrderListServiceError: Error, Equatable { case requestFailed case requestCancelled } -public protocol PointOfSaleOrderListServiceProtocol { +public protocol POSOrderListServiceProtocol { func providePointOfSaleOrders(pageNumber: Int) async throws -> PagedItems func searchPointOfSaleOrders(searchTerm: String, pageNumber: Int) async throws -> PagedItems func loadOrder(orderID: Int64) async throws -> POSOrder diff --git a/Modules/Tests/YosemiteTests/PointOfSale/PointOfSaleOrderServiceTests.swift b/Modules/Tests/YosemiteTests/PointOfSale/OrderList/POSOrderListServiceTests.swift similarity index 92% rename from Modules/Tests/YosemiteTests/PointOfSale/PointOfSaleOrderServiceTests.swift rename to Modules/Tests/YosemiteTests/PointOfSale/OrderList/POSOrderListServiceTests.swift index 30ff81bc467..5c5060ce1a5 100644 --- a/Modules/Tests/YosemiteTests/PointOfSale/PointOfSaleOrderServiceTests.swift +++ b/Modules/Tests/YosemiteTests/PointOfSale/OrderList/POSOrderListServiceTests.swift @@ -5,15 +5,15 @@ import struct NetworkingCore.Order import enum NetworkingCore.OrderStatusEnum import WooFoundation -final class PointOfSaleOrderServiceTests: XCTestCase { +final class POSOrderListServiceTests: XCTestCase { private let siteID: Int64 = 13092 - private var orderProvider: PointOfSaleOrderListServiceProtocol! + private var orderProvider: POSOrderListServiceProtocol! private var mockOrdersRemote: MockPOSOrdersRemote! override func setUp() { super.setUp() mockOrdersRemote = MockPOSOrdersRemote() - orderProvider = PointOfSaleOrderListService( + orderProvider = POSOrderListService( siteID: siteID, ordersRemote: mockOrdersRemote, currencyFormatter: CurrencyFormatter(currencySettings: CurrencySettings()) @@ -27,14 +27,14 @@ final class PointOfSaleOrderServiceTests: XCTestCase { } func test_PointOfSaleOrderServiceProtocol_when_fails_request_with_requestFailed_then_throws_error() async throws { - let expectedError = PointOfSaleOrderListServiceError.requestFailed + let expectedError = POSOrderListServiceError.requestFailed mockOrdersRemote.mockPagedOrdersResult = .failure(expectedError) do { _ = try await orderProvider.providePointOfSaleOrders(pageNumber: 1) XCTFail("Expected an error, but got success.") } catch { - XCTAssertEqual(error as? PointOfSaleOrderListServiceError, expectedError) + XCTAssertEqual(error as? POSOrderListServiceError, expectedError) } } @@ -111,7 +111,7 @@ final class PointOfSaleOrderServiceTests: XCTestCase { do { _ = try await orderProvider.providePointOfSaleOrders(pageNumber: 1) XCTFail("Expected error to be thrown") - } catch PointOfSaleOrderListServiceError.requestFailed { + } catch POSOrderListServiceError.requestFailed { // Expected } catch { XCTFail("Unexpected error occurred: \(error)") @@ -144,7 +144,7 @@ final class PointOfSaleOrderServiceTests: XCTestCase { } } -private extension PointOfSaleOrderServiceTests { +private extension POSOrderListServiceTests { enum TestError: Error { case expectedError } diff --git a/WooCommerce/Classes/POS/Controllers/PointOfSaleOrderListController.swift b/WooCommerce/Classes/POS/Controllers/POSOrderListController.swift similarity index 88% rename from WooCommerce/Classes/POS/Controllers/PointOfSaleOrderListController.swift rename to WooCommerce/Classes/POS/Controllers/POSOrderListController.swift index 90d8f01935f..6ab3ef8d471 100644 --- a/WooCommerce/Classes/POS/Controllers/PointOfSaleOrderListController.swift +++ b/WooCommerce/Classes/POS/Controllers/POSOrderListController.swift @@ -1,15 +1,15 @@ import Foundation import Observation -import enum Yosemite.PointOfSaleOrderListServiceError -import protocol Yosemite.PointOfSaleOrderListServiceProtocol -import protocol Yosemite.PointOfSaleOrderListFetchStrategyFactoryProtocol -import protocol Yosemite.PointOfSaleOrderListFetchStrategy +import enum Yosemite.POSOrderListServiceError +import protocol Yosemite.POSOrderListServiceProtocol +import protocol Yosemite.POSOrderListFetchStrategyFactoryProtocol +import protocol Yosemite.POSOrderListFetchStrategy import struct Yosemite.POSOrder import struct Yosemite.POSOrderItem import struct Yosemite.POSOrderRefund import class Yosemite.Store -protocol PointOfSaleOrderListControllerProtocol { +protocol POSOrderListControllerProtocol { var ordersViewState: POSOrderListState { get } var selectedOrder: POSOrder? { get } func loadOrders() async @@ -19,18 +19,18 @@ protocol PointOfSaleOrderListControllerProtocol { func updateOrder(orderID: Int64) async throws } -protocol PointOfSaleSearchingOrderListControllerProtocol: PointOfSaleOrderListControllerProtocol { +protocol POSSearchingOrderListControllerProtocol: POSOrderListControllerProtocol { func searchOrders(searchTerm: String) async func clearSearchOrders() } -@Observable final class PointOfSaleOrderListController: PointOfSaleSearchingOrderListControllerProtocol { +@Observable final class POSOrderListController: POSSearchingOrderListControllerProtocol { var ordersViewState: POSOrderListState private var strategyPaginationTracker: [String: AsyncPaginationTracker] = [:] - private var fetchStrategy: PointOfSaleOrderListFetchStrategy + private var fetchStrategy: POSOrderListFetchStrategy private var cachedOrders: [POSOrder] = [] private(set) var selectedOrder: POSOrder? - private let orderListFetchStrategyFactory: PointOfSaleOrderListFetchStrategyFactoryProtocol + private let orderListFetchStrategyFactory: POSOrderListFetchStrategyFactoryProtocol private var paginationTracker: AsyncPaginationTracker { if let existing = strategyPaginationTracker[fetchStrategy.id] { return existing @@ -40,7 +40,7 @@ protocol PointOfSaleSearchingOrderListControllerProtocol: PointOfSaleOrderListCo return tracker } - init(orderListFetchStrategyFactory: PointOfSaleOrderListFetchStrategyFactoryProtocol, + init(orderListFetchStrategyFactory: POSOrderListFetchStrategyFactoryProtocol, initialState: POSOrderListState = .loading([])) { self.ordersViewState = initialState self.orderListFetchStrategyFactory = orderListFetchStrategyFactory @@ -133,7 +133,7 @@ protocol PointOfSaleSearchingOrderListControllerProtocol: PointOfSaleOrderListCo } return pagedOrders.hasMorePages - } catch PointOfSaleOrderListServiceError.requestCancelled { + } catch POSOrderListServiceError.requestCancelled { return true } } diff --git a/WooCommerce/Classes/POS/Models/PointOfSaleOrderListModel.swift b/WooCommerce/Classes/POS/Models/POSOrderListModel.swift similarity index 70% rename from WooCommerce/Classes/POS/Models/PointOfSaleOrderListModel.swift rename to WooCommerce/Classes/POS/Models/POSOrderListModel.swift index 2162bfe1846..b27035f104a 100644 --- a/WooCommerce/Classes/POS/Models/PointOfSaleOrderListModel.swift +++ b/WooCommerce/Classes/POS/Models/POSOrderListModel.swift @@ -2,11 +2,11 @@ import Foundation import Observation import struct Yosemite.POSOrder -@Observable final class PointOfSaleOrderListModel { - let ordersController: PointOfSaleSearchingOrderListControllerProtocol +@Observable final class POSOrderListModel { + let ordersController: POSSearchingOrderListControllerProtocol let receiptSender: POSReceiptSending - init(ordersController: PointOfSaleSearchingOrderListControllerProtocol, + init(ordersController: POSSearchingOrderListControllerProtocol, receiptSender: POSReceiptSending) { self.ordersController = ordersController self.receiptSender = receiptSender diff --git a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/PointOfSaleItemListEmptyView.swift b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/PointOfSaleItemListEmptyView.swift index 6d57fc3b062..dfdd9ddbf09 100644 --- a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/PointOfSaleItemListEmptyView.swift +++ b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/PointOfSaleItemListEmptyView.swift @@ -1,9 +1,17 @@ import SwiftUI -struct PointOfSaleItemListEmptyView: View { +protocol POSEmptyViewModelProtocol { + var title: String { get } + var subtitle: String { get } + var hint: String? { get } + var buttonTitle: String? { get } + var iconName: String { get } +} + +struct POSListEmptyView: View { @Environment(\.dynamicTypeSize) private var dynamicTypeSize @Environment(\.floatingControlAreaSize) private var floatingControlAreaSize: CGSize - private let viewModel: PointOfSaleItemListEmptyViewModel + private let viewModel: any POSEmptyViewModelProtocol private let onAction: (() -> Void)? @@ -11,7 +19,7 @@ struct PointOfSaleItemListEmptyView: View { @Environment(\.keyboardObserver) private var keyboard - init(viewModel: PointOfSaleItemListEmptyViewModel, onAction: (() -> Void)? = nil) { + init(viewModel: any POSEmptyViewModelProtocol, onAction: (() -> Void)? = nil) { self.viewModel = viewModel self.onAction = onAction } @@ -84,7 +92,7 @@ struct PointOfSaleItemListEmptyView: View { } } -private extension PointOfSaleItemListEmptyView { +private extension POSListEmptyView { enum Constants { static let iconSize: CGFloat = 88 } @@ -243,7 +251,7 @@ struct PointOfSaleItemListEmptyViewModel { // MARK: - Preview #Preview { - PointOfSaleItemListEmptyView( + POSListEmptyView( viewModel: PointOfSaleItemListEmptyViewModel( itemListType: .coupons(search: false), baseItem: .root @@ -252,10 +260,14 @@ struct PointOfSaleItemListEmptyViewModel { } #Preview { - PointOfSaleItemListEmptyView( + POSListEmptyView( viewModel: PointOfSaleItemListEmptyViewModel( itemListType: .products(search: true), baseItem: .root ) ) {} } + +// MARK: - Protocol Conformance + +extension PointOfSaleItemListEmptyViewModel: POSEmptyViewModelProtocol {} diff --git a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/PointOfSaleItemListFullscreenErrorView.swift b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/PointOfSaleItemListFullscreenErrorView.swift index 03bed0e00f0..a09e859d2f3 100644 --- a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/PointOfSaleItemListFullscreenErrorView.swift +++ b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/PointOfSaleItemListFullscreenErrorView.swift @@ -12,7 +12,7 @@ struct PointOfSaleItemListFullscreenErrorView: View { var body: some View { PointOfSaleItemListFullscreenView { - PointOfSaleItemListErrorView(error: error, onAction: onAction) + POSListErrorView(error: error, onAction: onAction) } } } diff --git a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/PointOfSaleItemListFullscreenView.swift b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/PointOfSaleItemListFullscreenView.swift index 6222f88d24e..040aa367a9c 100644 --- a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/PointOfSaleItemListFullscreenView.swift +++ b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/PointOfSaleItemListFullscreenView.swift @@ -30,7 +30,7 @@ private enum Localization { #Preview { PointOfSaleItemListFullscreenView( content: { - PointOfSaleItemListErrorView( + POSListErrorView( error: .init(errorType: .productsLoadError, title: "Error", subtitle: "Something went wrong", buttonText: "Fix it")) }) } diff --git a/WooCommerce/Classes/POS/Presentation/Item Selector/ChildItemList.swift b/WooCommerce/Classes/POS/Presentation/Item Selector/ChildItemList.swift index 4c7a305aaf0..5f70d6f5d7e 100644 --- a/WooCommerce/Classes/POS/Presentation/Item Selector/ChildItemList.swift +++ b/WooCommerce/Classes/POS/Presentation/Item Selector/ChildItemList.swift @@ -87,8 +87,8 @@ private extension ChildItemList { var emptyView: some View { VStack { headerView - PointOfSaleItemListEmptyView( - viewModel: PointOfSaleItemListEmptyViewModel( + POSListEmptyView( + viewModel: POSListEmptyViewModel( itemListType: .products(search: false), baseItem: node)) { Task { @@ -106,7 +106,7 @@ private extension ChildItemList { Spacer() } - PointOfSaleItemListErrorView(error: error, onAction: { + POSListErrorView(error: error, onAction: { Task { await itemsController.loadItems(base: node) } diff --git a/WooCommerce/Classes/POS/Presentation/Item Selector/ItemList.swift b/WooCommerce/Classes/POS/Presentation/Item Selector/ItemList.swift index 39d61189c90..e08985ac4cd 100644 --- a/WooCommerce/Classes/POS/Presentation/Item Selector/ItemList.swift +++ b/WooCommerce/Classes/POS/Presentation/Item Selector/ItemList.swift @@ -113,7 +113,7 @@ struct ItemList: View { GhostItemCardView() } case .inlineError(_, let errorState, .pagination): - ItemListErrorCardView(errorState: errorState, + POSListInlineErrorView(errorState: errorState, buttonAction: { Task { @MainActor in await itemsController.loadNextItems(base: node) @@ -127,7 +127,7 @@ struct ItemList: View { @ViewBuilder var headerRows: some View { switch state { case .inlineError(_, let errorState, .refresh): - ItemListErrorCardView(errorState: errorState, + POSListInlineErrorView(errorState: errorState, buttonAction: { Task { @MainActor in await itemsController.loadItems(base: .root) diff --git a/WooCommerce/Classes/POS/Presentation/Item Selector/ItemListErrorCardView.swift b/WooCommerce/Classes/POS/Presentation/Item Selector/ItemListErrorCardView.swift deleted file mode 100644 index 275ab3afc6d..00000000000 --- a/WooCommerce/Classes/POS/Presentation/Item Selector/ItemListErrorCardView.swift +++ /dev/null @@ -1,54 +0,0 @@ -import SwiftUI - -struct ItemListErrorCardView: View { - let errorState: PointOfSaleErrorState - let buttonAction: () -> Void - - @ScaledMetric private var scale: CGFloat = 1.0 - - var body: some View { - HStack(spacing: Constants.cardSpacing) { - POSErrorExclamationMark(size: .small) - .frame(width: min(Constants.productCardSize * scale, Constants.maximumProductCardSize), - height: Constants.productCardSize * scale) - - VStack(alignment: .leading, spacing: Constants.textSpacing) { - Text(errorState.title) - .lineLimit(2) - .foregroundStyle(Color.posOnSurface) - .multilineTextAlignment(.leading) - .font(Constants.itemTitleFont) - - Text(errorState.subtitle) - .foregroundStyle(Color.posOnSurfaceVariantHighest) - .font(Constants.itemDetailFont) - } - .padding(.horizontal, Constants.horizontalTextPadding * (1 / scale)) - .padding(.vertical, Constants.verticalTextPadding * (1 / scale)) - - Spacer() - - Button { - buttonAction() - } label: { - Text(errorState.buttonText) - .font(Constants.itemTitleFont) - } - .buttonStyle(POSOutlinedButtonStyle(size: .normal)) - .fixedSize(horizontal: true, vertical: false) - .padding(Constants.accessoryButtonPadding * (1 / scale)) - } - .frame(maxWidth: .infinity, idealHeight: Constants.productCardSize * scale) - .background(Constants.backgroundColor) - .posItemCardBorderStyles() - } -} - -private typealias Constants = PointOfSaleItemListCardConstants - -#Preview { - ItemListErrorCardView( - errorState: .errorOnLoadingVariationsNextPage(), - buttonAction: {} - ) -} diff --git a/WooCommerce/Classes/POS/Presentation/ItemListView.swift b/WooCommerce/Classes/POS/Presentation/ItemListView.swift index 697bbc93098..481dae9314c 100644 --- a/WooCommerce/Classes/POS/Presentation/ItemListView.swift +++ b/WooCommerce/Classes/POS/Presentation/ItemListView.swift @@ -293,8 +293,8 @@ private extension ItemListView { var emptyView: some View { switch selectedItemListType { case .products: - PointOfSaleItemListEmptyView( - viewModel: PointOfSaleItemListEmptyViewModel( + POSListEmptyView( + viewModel: POSListEmptyViewModel( itemListType: selectedItemListType, baseItem: .root)) { Task { @@ -302,8 +302,8 @@ private extension ItemListView { } } case .coupons: - PointOfSaleItemListEmptyView( - viewModel: PointOfSaleItemListEmptyViewModel( + POSListEmptyView( + viewModel: POSListEmptyViewModel( itemListType: selectedItemListType, baseItem: .root)) { showCouponCreationModal = true @@ -315,14 +315,14 @@ private extension ItemListView { func errorView(_ errorState: PointOfSaleErrorState) -> some View { switch errorState.errorType { case .couponsDisabled: - PointOfSaleItemListErrorView(error: errorState, onAction: { + POSListErrorView(error: errorState, onAction: { Task { await posModel.couponsController.enableCoupons() ServiceLocator.analytics.track(.couponSettingEnabled) } }) default: - PointOfSaleItemListErrorView(error: errorState, onAction: { + POSListErrorView(error: errorState, onAction: { Task { await itemsController(selectedItemListType).loadItems(base: .root) } diff --git a/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrderDetailsView.swift b/WooCommerce/Classes/POS/Presentation/Orders/POSDetailsView.swift similarity index 97% rename from WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrderDetailsView.swift rename to WooCommerce/Classes/POS/Presentation/Orders/POSDetailsView.swift index 51fdca001fe..a6f8752a64c 100644 --- a/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrderDetailsView.swift +++ b/WooCommerce/Classes/POS/Presentation/Orders/POSDetailsView.swift @@ -5,13 +5,13 @@ import struct Yosemite.POSOrderRefund import enum Yosemite.OrderStatusEnum import typealias Yosemite.OrderItemAttribute -struct PointOfSaleOrderDetailsView: View { +struct POSOrderDetailsView: View { let order: POSOrder let onBack: () -> Void @Environment(\.horizontalSizeClass) private var horizontalSizeClass @Environment(\.siteTimezone) private var siteTimezone - @Environment(PointOfSaleOrderListModel.self) private var orderListModel + @Environment(POSOrderListModel.self) private var orderListModel @State private var isShowingEmailReceiptView: Bool = false private var shouldShowBackButton: Bool { @@ -60,7 +60,7 @@ struct PointOfSaleOrderDetailsView: View { // MARK: - Main Sections -private extension PointOfSaleOrderDetailsView { +private extension POSOrderDetailsView { @ViewBuilder func productsSection(_ order: POSOrder) -> some View { VStack(alignment: .leading, spacing: POSSpacing.medium) { @@ -108,7 +108,7 @@ private extension PointOfSaleOrderDetailsView { // MARK: - Header Components -private extension PointOfSaleOrderDetailsView { +private extension POSOrderDetailsView { @ViewBuilder func headerBottomContent(for order: POSOrder) -> some View { VStack(alignment: .leading, spacing: POSSpacing.xSmall) { @@ -131,7 +131,7 @@ private extension PointOfSaleOrderDetailsView { // MARK: - Product Components -private extension PointOfSaleOrderDetailsView { +private extension POSOrderDetailsView { @ViewBuilder func productRow(item: POSOrderItem) -> some View { HStack(alignment: .top, spacing: POSSpacing.medium) { @@ -188,7 +188,7 @@ private extension PointOfSaleOrderDetailsView { // MARK: - Totals Components -private extension PointOfSaleOrderDetailsView { +private extension POSOrderDetailsView { @ViewBuilder func productsSubtotalRow(_ order: POSOrder) -> some View { totalsRow( @@ -299,7 +299,7 @@ private extension PointOfSaleOrderDetailsView { // MARK: - Actions -private extension PointOfSaleOrderDetailsView { +private extension POSOrderDetailsView { enum POSOrderDetailsAction: Identifiable, CaseIterable { case emailReceipt @@ -436,7 +436,7 @@ private enum Localization { #if DEBUG #Preview("Order Details") { - PointOfSaleOrderDetailsView( + POSOrderDetailsView( order: POSPreviewHelpers.makePreviewOrder(), onBack: {} ) diff --git a/WooCommerce/Classes/POS/Presentation/Orders/POSOrderDetailsEmptyView.swift b/WooCommerce/Classes/POS/Presentation/Orders/POSOrderDetailsEmptyView.swift new file mode 100644 index 00000000000..16b67c4b1dd --- /dev/null +++ b/WooCommerce/Classes/POS/Presentation/Orders/POSOrderDetailsEmptyView.swift @@ -0,0 +1,45 @@ +import SwiftUI + +struct POSOrderDetailsEmptyView: View { + var body: some View { + VStack(spacing: 0) { + POSPageHeaderView( + title: Localization.title, + backButtonConfiguration: nil + ) + + VStack { + Spacer() + + Text(Localization.noOrderToDisplay) + .font(.posBodyLargeRegular()) + .foregroundStyle(Color.posOnSurfaceVariantHighest) + .multilineTextAlignment(.center) + + Spacer() + } + } + .background(Color.posSurface) + .navigationBarHidden(true) + } +} + +private enum Localization { + static let title = NSLocalizedString( + "pos.orderDetailsEmptyView.ordersTitle", + value: "Order", + comment: "Title at the header for the Order Details empty view." + ) + + static let noOrderToDisplay = NSLocalizedString( + "pos.orderDetailsEmptyView.noOrderToDisplay", + value: "No order to display", + comment: "Text appearing in the order details pane when there are no orders available." + ) +} + +#if DEBUG +#Preview { + POSOrderDetailsEmptyView() +} +#endif diff --git a/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrderDetailsLoadingView.swift b/WooCommerce/Classes/POS/Presentation/Orders/POSOrderDetailsLoadingView.swift similarity index 98% rename from WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrderDetailsLoadingView.swift rename to WooCommerce/Classes/POS/Presentation/Orders/POSOrderDetailsLoadingView.swift index 7bc2aaac6af..144700da5f7 100644 --- a/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrderDetailsLoadingView.swift +++ b/WooCommerce/Classes/POS/Presentation/Orders/POSOrderDetailsLoadingView.swift @@ -1,6 +1,6 @@ import SwiftUI -struct PointOfSaleOrderDetailsLoadingView: View { +struct POSOrderDetailsLoadingView: View { var body: some View { VStack(spacing: 0) { POSPageHeaderView( @@ -177,6 +177,6 @@ private enum Localization { #if DEBUG #Preview { - PointOfSaleOrderDetailsLoadingView() + POSOrderDetailsLoadingView() } #endif diff --git a/WooCommerce/Classes/POS/Presentation/Orders/POSOrderListEmptyViewModel.swift b/WooCommerce/Classes/POS/Presentation/Orders/POSOrderListEmptyViewModel.swift new file mode 100644 index 00000000000..e773785c34d --- /dev/null +++ b/WooCommerce/Classes/POS/Presentation/Orders/POSOrderListEmptyViewModel.swift @@ -0,0 +1,63 @@ +import Foundation + +struct POSOrderListEmptyViewModel: POSListEmptyViewModelProtocol { + let isSearching: Bool + + var title: String { + isSearching ? Localization.emptyOrdersSearchTitle : Localization.emptyOrdersTitle + } + + var subtitle: String { + isSearching ? Localization.emptyOrdersSearchSubtitle : Localization.emptyOrdersSubtitle + } + + var hint: String? { + isSearching ? Localization.emptyOrdersSearchHint : nil + } + + var buttonTitle: String? { + isSearching ? nil : Localization.emptyOrdersButtonTitle + } + + var iconName: String { + PointOfSaleAssets.magnifierNotFound.imageName + } +} + +private enum Localization { + static let emptyOrdersTitle = NSLocalizedString( + "pos.orderListView.emptyOrdersTitle", + value: "No orders yet", + comment: "Title appearing when there are no orders to display." + ) + + static let emptyOrdersSubtitle = NSLocalizedString( + "pos.orderListView.emptyOrdersSubtitle", + value: "Orders will appear here once you start processing sales on the POS.", + comment: "Subtitle appearing when there are no orders to display." + ) + + static let emptyOrdersButtonTitle = NSLocalizedString( + "pos.orderListView.emptyOrdersButtonTitle", + value: "Refresh", + comment: "Button text for refreshing orders when list is empty." + ) + + static let emptyOrdersSearchTitle = NSLocalizedString( + "pos.orderListView.emptyOrdersSearchTitle", + value: "No orders found", + comment: "Title appearing when order search returns no results." + ) + + static let emptyOrdersSearchSubtitle = NSLocalizedString( + "pos.orderListView.emptyOrdersSearchSubtitle", + value: "We couldn't find any orders matching your search.", + comment: "Subtitle appearing when order search returns no results." + ) + + static let emptyOrdersSearchHint = NSLocalizedString( + "pos.orderListView.emptyOrdersSearchHint", + value: "Try adjusting your search term.", + comment: "Hint text suggesting to modify search terms when no orders are found." + ) +} diff --git a/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrderListView.swift b/WooCommerce/Classes/POS/Presentation/Orders/POSOrderListView.swift similarity index 68% rename from WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrderListView.swift rename to WooCommerce/Classes/POS/Presentation/Orders/POSOrderListView.swift index d02281fb979..4d2273bca91 100644 --- a/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrderListView.swift +++ b/WooCommerce/Classes/POS/Presentation/Orders/POSOrderListView.swift @@ -2,10 +2,11 @@ import SwiftUI import struct Yosemite.POSOrder import enum Yosemite.OrderPaymentMethod -struct PointOfSaleOrderListView: View { +struct POSOrderListView: View { let onClose: () -> Void - @Environment(PointOfSaleOrderListModel.self) private var orderListModel + @Environment(POSOrderListModel.self) private var orderListModel + @Environment(\.keyboardObserver) private var keyboardObserver @StateObject private var infiniteScrollTriggerDeterminer = ThresholdInfiniteScrollTriggerDeterminer() @State private var isSearching: Bool = false @@ -32,7 +33,7 @@ struct PointOfSaleOrderListView: View { )], backButtonConfiguration: isSearching ? nil : .init(state: .enabled, action: onClose, buttonIcon: "xmark"), trailingContent: { - if !isSearching { + if !isSearching && !ordersViewState.orders.isEmpty { POSPageHeaderActionButton( systemName: "magnifyingglass", backgroundColor: .posSurface, @@ -64,44 +65,26 @@ struct PointOfSaleOrderListView: View { ) .animation(.easeInOut(duration: Constants.animationDuration), value: isSearching) - InfiniteScrollView( - triggerDeterminer: infiniteScrollTriggerDeterminer, - loadMore: { - guard case .loaded(_, let hasMoreItems) = ordersViewState, hasMoreItems else { return } - await orderListModel.ordersController.loadNextOrders() - }, - content: { - LazyVStack(spacing: 8) { - headerRows - - switch ordersViewState { - case .empty: - // TODO: WOOMOB-1139 - Text("No orders") - case .error(let errorState): - ItemListErrorCardView(errorState: errorState) { - Task { @MainActor in - await orderListModel.ordersController.loadOrders() - } - } - default: - let orders = ordersViewState.orders - ForEach(orders, id: \.id) { order in - Button(action: { - orderListModel.ordersController.selectOrder(order) - }) { - OrderRowView(order: order, isSelected: orderListModel.ordersController.selectedOrder?.id == order.id) - } - .buttonStyle(PlainButtonStyle()) - } - .animation(.default, value: orders.first?.id) - } - - footerRows + switch ordersViewState { + case .empty: + POSListEmptyView( + viewModel: POSOrderListEmptyViewModel(isSearching: isSearching) + ) { + Task { @MainActor in + await orderListModel.ordersController.loadOrders() } - .padding(.horizontal) } - ) + .padding(.bottom, keyboardObserver.keyboardHeight) + case .error(let errorState): + POSListErrorView(error: errorState) { + Task { @MainActor in + await orderListModel.ordersController.loadOrders() + } + } + .padding(.bottom, keyboardObserver.keyboardHeight) + default: + listView + } } .animation(.default, value: orderListModel.ordersController.ordersViewState.isEmpty) .background(Color.posSurfaceBright) @@ -118,7 +101,7 @@ struct PointOfSaleOrderListView: View { private var headerRows: some View { switch ordersViewState { case .inlineError(_, let errorState, .refresh): - ItemListErrorCardView(errorState: errorState) { + POSListInlineErrorView(errorState: errorState) { Task { @MainActor in await orderListModel.ordersController.loadOrders() } @@ -128,6 +111,38 @@ struct PointOfSaleOrderListView: View { } } + @ViewBuilder + private var listView: some View { + InfiniteScrollView( + triggerDeterminer: infiniteScrollTriggerDeterminer, + loadMore: { + guard case .loaded(_, let hasMoreItems) = ordersViewState, hasMoreItems else { return } + await orderListModel.ordersController.loadNextOrders() + }, + content: { + LazyVStack(spacing: POSSpacing.small) { + headerRows + + let orders = ordersViewState.orders + ForEach(orders, id: \.id) { order in + Button(action: { + orderListModel.ordersController.selectOrder(order) + }) { + OrderRowView(order: order, isSelected: orderListModel.ordersController.selectedOrder?.id == order.id) + } + .buttonStyle(PlainButtonStyle()) + } + .animation(.default, value: orders.first?.id) + + footerRows + } + .padding(.horizontal) + .padding(.bottom, POSPadding.medium) + } + ) + .scrollDismissesKeyboard(.immediately) + } + @ViewBuilder private var footerRows: some View { switch ordersViewState { @@ -142,7 +157,7 @@ struct PointOfSaleOrderListView: View { GhostOrderRowView() } case .inlineError(_, let errorState, .pagination): - ItemListErrorCardView(errorState: errorState) { + POSListInlineErrorView(errorState: errorState) { Task { @MainActor in await orderListModel.ordersController.loadNextOrders() } @@ -206,10 +221,6 @@ private struct OrderRowView: View { } } -private extension OrderRowView { - // No additional helpers needed - using shared PointOfSaleOrderBadgeView -} - private struct GhostOrderRowView: View { @ScaledMetric private var scale: CGFloat = 1.0 @Environment(\.dynamicTypeSize) var dynamicTypeSize @@ -317,7 +328,7 @@ struct PointOfSaleOrderBadgeView: View { // MARK: - Search -private extension PointOfSaleOrderListView { +private extension POSOrderListView { func setSearch(_ isSearchingValue: Bool) { if isSearchingValue { isSearching = true @@ -331,9 +342,9 @@ private extension PointOfSaleOrderListView { } final class POSOrderSearchable: POSSearchable { - private let ordersController: PointOfSaleSearchingOrderListControllerProtocol + private let ordersController: POSSearchingOrderListControllerProtocol - init(ordersController: PointOfSaleSearchingOrderListControllerProtocol) { + init(ordersController: POSSearchingOrderListControllerProtocol) { self.ordersController = ordersController } @@ -363,12 +374,16 @@ private enum Constants { static let searchControlID = "searchControl" } -private enum Localization { - static let ordersTitle = NSLocalizedString( - "pos.orderListView.ordersTitle", - value: "Orders", - comment: "Title at the header for the Orders view.") +extension POSOrderListView { + enum Localization { + static let ordersTitle = NSLocalizedString( + "pos.orderListView.ordersTitle", + value: "Orders", + comment: "Title at the header for the Orders view.") + } +} +private enum Localization { static let searchFieldPlaceholder = NSLocalizedString( "pos.orderListView.searchFieldPlaceholder", value: "Search orders", @@ -378,12 +393,82 @@ private enum Localization { #if DEBUG #Preview("List") { - NavigationSplitView { - PointOfSaleOrderListView(onClose: {}) + NavigationSplitView(columnVisibility: .constant(.all)) { + POSOrderListView(onClose: {}) + .navigationSplitViewColumnWidth(450) + .environment(POSPreviewHelpers.makePreviewOrdersModel(state: .loaded([POSPreviewHelpers.makePreviewOrder()], hasMoreItems: false))) + } detail: { + Text("Detail View") + } + .navigationSplitViewStyle(.balanced) +} + +#Preview("Empty State") { + NavigationSplitView(columnVisibility: .constant(.all)) { + POSOrderListView(onClose: {}) + .navigationSplitViewColumnWidth(450) + .environment(POSPreviewHelpers.makePreviewOrdersModel(state: .empty)) + } detail: { + Text("Detail View") + } +} + +#Preview("Error State") { + NavigationSplitView(columnVisibility: .constant(.all)) { + POSOrderListView(onClose: {}) + .navigationSplitViewColumnWidth(450) + .environment(POSPreviewHelpers.makePreviewOrdersModel(state: .error(.errorOnLoadingOrders()))) + } detail: { + Text("Detail View") + } +} + +#Preview("Loading State") { + NavigationSplitView(columnVisibility: .constant(.all)) { + POSOrderListView(onClose: {}) + .navigationSplitViewColumnWidth(450) + .environment(POSPreviewHelpers.makePreviewOrdersModel(state: .loading([]))) + } detail: { + Text("Detail View") + } +} + +#Preview("Inline Error - Refresh") { + NavigationSplitView(columnVisibility: .constant(.all)) { + POSOrderListView(onClose: {}) + .navigationSplitViewColumnWidth(450) + .environment(POSPreviewHelpers.makePreviewOrdersModel( + state: .inlineError([POSPreviewHelpers.makePreviewOrder()], + error: .errorOnLoadingOrders(), + context: .refresh) + )) + } detail: { + Text("Detail View") + } +} + +#Preview("Inline Error - Pagination") { + NavigationSplitView(columnVisibility: .constant(.all)) { + POSOrderListView(onClose: {}) .navigationSplitViewColumnWidth(450) - .environment(POSPreviewHelpers.makePreviewOrdersModel()) + .environment(POSPreviewHelpers.makePreviewOrdersModel( + state: .inlineError([POSPreviewHelpers.makePreviewOrder()], + error: .errorOnLoadingOrdersNextPage(), + context: .pagination) + )) } detail: { Text("Detail View") } } + +#Preview("Search Empty State") { + NavigationSplitView(columnVisibility: .constant(.all)) { + POSOrderListView(onClose: {}) + .navigationSplitViewColumnWidth(450) + .environment(POSPreviewHelpers.makePreviewOrdersModel(state: .empty)) + } detail: { + Text("Detail View") + } +} + #endif diff --git a/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrdersView.swift b/WooCommerce/Classes/POS/Presentation/Orders/POSOrdersView.swift similarity index 54% rename from WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrdersView.swift rename to WooCommerce/Classes/POS/Presentation/Orders/POSOrdersView.swift index 5abf77a0be7..c757752f7ae 100644 --- a/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrdersView.swift +++ b/WooCommerce/Classes/POS/Presentation/Orders/POSOrdersView.swift @@ -1,52 +1,75 @@ import SwiftUI import UIKit -struct PointOfSaleOrdersView: View { +struct POSOrdersView: View { @Binding var isPresented: Bool - @Environment(PointOfSaleOrderListModel.self) private var orderListModel + @Environment(POSOrderListModel.self) private var orderListModel @Environment(\.horizontalSizeClass) private var horizontalSizeClass var body: some View { - CustomNavigationSplitView(selection: Binding( - get: { orderListModel.ordersController.selectedOrder }, - set: { orderListModel.ordersController.selectOrder($0) } - )) { _ in - PointOfSaleOrderListView() { - isPresented = false - } - } detail: { selection in - PointOfSaleOrderDetailsView( - order: selection, - onBack: { - orderListModel.ordersController.selectOrder(nil) + switch orderListModel.ordersController.ordersViewState { + case .error(let error): + errorView(error) + default: + CustomNavigationSplitView(selection: Binding( + get: { orderListModel.ordersController.selectedOrder }, + set: { orderListModel.ordersController.selectOrder($0) } + )) { _ in + POSOrderListView() { + isPresented = false + } + } detail: { selection in + POSOrderDetailsView( + order: selection, + onBack: { + orderListModel.ordersController.selectOrder(nil) + } + ) + } detailPlaceholderView: { + if orderListModel.ordersController.ordersViewState.isLoading { + POSOrderDetailsLoadingView() + } else { + POSOrderDetailsEmptyView() + } + } setDefaultValue: { + if orderListModel.ordersController.selectedOrder == nil, + let firstOrder = orderListModel.ordersController.ordersViewState.orders.first { + orderListModel.ordersController.selectOrder(firstOrder) } - ) - } detailPlaceholderView: { - if orderListModel.ordersController.ordersViewState.isLoading { - PointOfSaleOrderDetailsLoadingView() - } else { - PointOfSaleOrderDetailsEmptyView() } - } setDefaultValue: { - if orderListModel.ordersController.selectedOrder == nil, - let firstOrder = orderListModel.ordersController.ordersViewState.orders.first { + .onChange(of: orderListModel.ordersController.ordersViewState.orders) { oldOrders, newOrders in + guard horizontalSizeClass == .regular else { return } + + guard let firstOrder = newOrders.first else { + return + } + + if let selectedOrder = orderListModel.ordersController.selectedOrder, newOrders.map(\.id).contains(selectedOrder.id) { + return + } + orderListModel.ordersController.selectOrder(firstOrder) } + .animation(.default, value: orderListModel.ordersController.ordersViewState.orders.isEmpty) } - .onChange(of: orderListModel.ordersController.ordersViewState.orders) { oldOrders, newOrders in - guard horizontalSizeClass == .regular else { return } + } - guard let firstOrder = newOrders.first else { - return - } + @ViewBuilder + private func errorView(_ error: PointOfSaleErrorState) -> some View { + VStack(spacing: 0) { + POSPageHeaderView( + title: POSOrderListView.Localization.ordersTitle, + backButtonConfiguration: .init(state: .enabled, action: { + isPresented = false + })) + .posHeaderBackButtonIcon(systemName: "xmark") - if let selectedOrder = orderListModel.ordersController.selectedOrder, newOrders.map(\.id).contains(selectedOrder.id) { - return + POSListErrorView(error: error) { + Task { @MainActor in + await orderListModel.ordersController.loadOrders() + } } - - orderListModel.ordersController.selectOrder(firstOrder) } - .animation(.default, value: orderListModel.ordersController.ordersViewState.orders.isEmpty) } } @@ -122,7 +145,7 @@ private enum Constants { #if DEBUG #Preview("Orders View") { - PointOfSaleOrdersView(isPresented: .constant(true)) - .environment(POSPreviewHelpers.makePreviewOrdersModel()) + POSOrdersView(isPresented: .constant(true)) + .environment(POSPreviewHelpers.makePreviewOrdersModel(state: .empty)) } #endif diff --git a/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrderDetailsEmptyView.swift b/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrderDetailsEmptyView.swift deleted file mode 100644 index 1d5241d8921..00000000000 --- a/WooCommerce/Classes/POS/Presentation/Orders/PointOfSaleOrderDetailsEmptyView.swift +++ /dev/null @@ -1,29 +0,0 @@ -import SwiftUI - -struct PointOfSaleOrderDetailsEmptyView: View { - var body: some View { - // TODO: WOOMOB-1136 - VStack(spacing: 0) { - POSPageHeaderView( - title: "Orders", - backButtonConfiguration: nil - ) - - VStack { - Spacer() - Text("No Orders Loaded") - .font(.posBodyLargeRegular()) - .foregroundStyle(Color.posOnSurfaceVariantHighest) - Spacer() - } - } - .background(Color.posSurface) - .navigationBarHidden(true) - } -} - -#if DEBUG -#Preview { - PointOfSaleOrderDetailsEmptyView() -} -#endif diff --git a/WooCommerce/Classes/POS/Presentation/POSFloatingControlView.swift b/WooCommerce/Classes/POS/Presentation/POSFloatingControlView.swift index 222ac20de56..54b378aaf0e 100644 --- a/WooCommerce/Classes/POS/Presentation/POSFloatingControlView.swift +++ b/WooCommerce/Classes/POS/Presentation/POSFloatingControlView.swift @@ -63,7 +63,7 @@ struct POSFloatingControlView: View { PointOfSaleBarcodeScannerSetup(isPresented: $showBarcodeScanningModal) } .posFullScreenCover(isPresented: $showOrders) { - PointOfSaleOrdersView(isPresented: $showOrders) + POSOrdersView(isPresented: $showOrders) } .frame(height: Constants.size) .background(Color.clear) diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift index dca2d462486..bf84c798723 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift @@ -7,7 +7,7 @@ struct PointOfSaleEntryPointView: View { @StateObject private var posModalManager = POSModalManager() @StateObject private var posSheetManager = POSSheetManager() @StateObject private var posCoverManager = POSFullScreenCoverManager() - @State private var orderListModel: PointOfSaleOrderListModel + @State private var orderListModel: POSOrderListModel @State private var posEntryPointController: POSEntryPointController @Environment(\.horizontalSizeClass) private var horizontalSizeClass @@ -29,7 +29,7 @@ struct PointOfSaleEntryPointView: View { purchasableItemsSearchController: PointOfSaleSearchingItemsControllerProtocol, couponsController: PointOfSaleCouponsControllerProtocol, couponsSearchController: PointOfSaleSearchingItemsControllerProtocol, - ordersController: PointOfSaleSearchingOrderListControllerProtocol, + ordersController: POSSearchingOrderListControllerProtocol, onPointOfSaleModeActiveStateChange: @escaping ((Bool) -> Void), cardPresentPaymentService: CardPresentPaymentFacade, orderController: PointOfSaleOrderControllerProtocol, @@ -55,7 +55,7 @@ struct PointOfSaleEntryPointView: View { self.popularPurchasableItemsController = popularPurchasableItemsController self.barcodeScanService = barcodeScanService self.posEntryPointController = POSEntryPointController(eligibilityChecker: posEligibilityChecker) - self.orderListModel = PointOfSaleOrderListModel(ordersController: ordersController, receiptSender: receiptSender) + self.orderListModel = POSOrderListModel(ordersController: ordersController, receiptSender: receiptSender) self.siteTimezone = siteTimezone } @@ -109,7 +109,7 @@ struct PointOfSaleEntryPointView: View { purchasableItemsSearchController: PointOfSalePreviewItemsController(), couponsController: PointOfSalePreviewCouponsController(), couponsSearchController: PointOfSalePreviewCouponsController(), - ordersController: PointOfSalePreviewOrderListController(), + ordersController: POSConfigurablePreviewOrderListController(), onPointOfSaleModeActiveStateChange: { _ in }, cardPresentPaymentService: CardPresentPaymentPreviewService(), orderController: PointOfSalePreviewOrderController(), diff --git a/WooCommerce/Classes/POS/Presentation/Reusable Views/POSListEmptyView.swift b/WooCommerce/Classes/POS/Presentation/Reusable Views/POSListEmptyView.swift new file mode 100644 index 00000000000..c178b9de856 --- /dev/null +++ b/WooCommerce/Classes/POS/Presentation/Reusable Views/POSListEmptyView.swift @@ -0,0 +1,269 @@ +import SwiftUI + +protocol POSListEmptyViewModelProtocol { + var title: String { get } + var subtitle: String { get } + var hint: String? { get } + var buttonTitle: String? { get } + var iconName: String { get } +} + +struct POSListEmptyView: View { + @Environment(\.dynamicTypeSize) private var dynamicTypeSize + @Environment(\.floatingControlAreaSize) private var floatingControlAreaSize: CGSize + private let viewModel: any POSListEmptyViewModelProtocol + + private let onAction: (() -> Void)? + + @State private var viewWidth: CGFloat = 0 + + @Environment(\.keyboardObserver) private var keyboard + + init(viewModel: any POSListEmptyViewModelProtocol, onAction: (() -> Void)? = nil) { + self.viewModel = viewModel + self.onAction = onAction + } + + var body: some View { + ScrollableVStack { + Spacer() + VStack(alignment: .center, spacing: POSSpacing.none) { + let shouldShowIcon: Bool = !dynamicTypeSize.isAccessibilitySize && !keyboard.isFullSizeKeyboardVisible + + if shouldShowIcon { + icon + + Spacer().frame(height: PointOfSaleEmptyErrorStateViewLayout.imageAndTextSpacing) + } + + Text(viewModel.title) + .accessibilityAddTraits(.isHeader) + .foregroundStyle(Color.posOnSurface) + .font(.posHeadingBold) + .multilineTextAlignment(.center) + + Spacer().frame(height: PointOfSaleEmptyErrorStateViewLayout.textSpacing) + + Text(viewModel.subtitle) + .foregroundStyle(Color.posOnSurface) + .font(.posBodyLargeRegular()) + .padding([.leading, .trailing]) + .multilineTextAlignment(.center) + + if let hint = viewModel.hint { + Spacer().frame(height: POSSpacing.small) + Text(hint) + .foregroundStyle(Color.posOnSurfaceVariantHighest) + .font(.posBodyLargeRegular()) + .padding([.leading, .trailing]) + .multilineTextAlignment(.center) + } + + Spacer().frame(height: PointOfSaleEmptyErrorStateViewLayout.textAndButtonSpacing) + + if let onAction, let buttonTitle = viewModel.buttonTitle { + Button(action: { + onAction() + }, label: { + Text(buttonTitle) + }) + .buttonStyle(POSOutlinedButtonStyle(size: .normal)) + .frame(width: viewWidth / 2) + .padding([.leading, .trailing]) + } + } + Spacer() + } + .multilineTextAlignment(.center) + .padding(.bottom, !keyboard.isFullSizeKeyboardVisible ? floatingControlAreaSize.height : 0) + .animation(.default, value: keyboard.keyboardHeight) + .measureWidth { width in + viewWidth = width + } + } + + @ViewBuilder + private var icon: some View { + Image(decorative: viewModel.iconName) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: Constants.iconSize, height: Constants.iconSize) + .foregroundColor(.posOnSurfaceVariantHighest) + } +} + +private extension POSListEmptyView { + enum Constants { + static let iconSize: CGFloat = 88 + } +} + +struct POSListEmptyViewModel: POSListEmptyViewModelProtocol { + let itemListType: ItemListType + let baseItem: ItemListBaseItem + + var title: String { + switch (baseItem, itemListType) { + case (.root, .products(search: true)): + return Localization.emptyProductsSearchTitle + case (.root, .products(search: false)): + return Localization.emptyProductsTitle + case (.root, .coupons): + return Localization.emptyCouponsTitle + case (.parent, .products): + return Localization.emptyVariableParentProductTitle + default: + assertionFailure("No title defined for \(baseItem)") + return Localization.emptyProductsTitle + } + } + + var subtitle: String { + switch (baseItem, itemListType) { + case (.root, .products(search: true)): + return Localization.emptyProductsSearchSubtitle + case (.root, .products(search: false)): + return Localization.emptyProductsSubtitle + case (.root, .coupons(search: false)): + return Localization.emptyCouponsSubtitle + case (.root, .coupons(search: true)): + return Localization.emptyCouponSearchSubtitle + case (.parent, .products): + return Localization.emptyVariableParentProductSubtitle + default: + assertionFailure("No subtitle defined for \(baseItem)") + return Localization.emptyProductsTitle + } + } + + var hint: String? { + switch (baseItem, itemListType) { + case (.root, .products(search: true)): + return Localization.emptyProductsSearchHint + case (.root, .products(search: false)): + return Localization.emptyProductsHint + case (.root, .coupons): + return nil + case (.parent, .products): + return Localization.emptyVariableParentProductHint + default: + assertionFailure("No hint defined for \(baseItem)") + return Localization.emptyProductsTitle + } + } + + var buttonTitle: String? { + switch (baseItem, itemListType) { + case (.root, .coupons(search: false)): + return Localization.emptyCouponsButtonTitle + case (.root, .products(search: false)): + return Localization.emptyProductsButtonTitle + case (.parent, .products): + return Localization.emptyProductsButtonTitle + default: + return nil + } + } + + var iconName: String { + switch itemListType { + case .coupons(search: false): + PointOfSaleAssets.coupons.imageName + default: + PointOfSaleAssets.magnifierNotFound.imageName + } + } + + enum Localization { + static let emptyProductsTitle = NSLocalizedString( + "pos.pointOfSaleItemListEmptyView.emptyProductsTitle.2", + value: "No supported products found", + comment: "Text appearing on screen when there are no products to load." + ) + static let emptyProductsSubtitle = NSLocalizedString( + "pos.pointOfSaleItemListEmptyView.emptyProductsSubtitle.1", + value: "POS currently only supports simple and variable products.", + comment: "Subtitle text on screen when there are no products to load." + ) + static let emptyProductsHint = NSLocalizedString( + "pos.pointOfSaleItemListEmptyView.emptyProductsHint.1", + value: "To add one, exit POS and go to Products.", + comment: "Text hinting the merchant to create a product." + ) + static let emptyProductsSearchTitle = NSLocalizedString( + "pos.pointOfSaleItemListEmptyView.emptyProductsSearchTitle.2", + value: "No products found", + comment: "Text appearing on screen when a POS product search returns no results." + ) + static let emptyProductsSearchSubtitle = NSLocalizedString( + "pos.pointOfSaleItemListEmptyView.emptyProductsSearchSubtitle.2", + value: "We couldn't find any matching products — try adjusting your search term.", + comment: "Subtitle text suggesting to modify search terms when no products are found in the POS product search." + ) + static let emptyProductsSearchHint = NSLocalizedString( + "pos.pointOfSaleItemListEmptyView.emptyProductsSearchHint", + value: "Variation names can't be searched, so use the parent product name.", + comment: "Text providing additional search tips when no products are found in the POS product search." + ) + static let emptyVariableParentProductTitle = NSLocalizedString( + "pos.pointOfSaleItemListEmptyView.emptyVariableParentProductTitle.2", + value: "No supported variations found", + comment: "Text appearing on screen when there are no variations to load." + ) + static let emptyVariableParentProductSubtitle = NSLocalizedString( + "pos.pointOfSaleItemListEmptyView.emptyVariableParentProductSubtitle", + value: "POS only supports enabled, non-downloadable variations.", + comment: "Subtitle text on screen when there are no products to load." + ) + static let emptyVariableParentProductHint = NSLocalizedString( + "pos.pointOfSaleItemListEmptyView.emptyVariableParentProductHint", + value: "To add one, exit POS and edit this product in the Products tab.", + comment: "Text hinting the merchant to create a product." + ) + static let emptyCouponsTitle = NSLocalizedString( + "pos.pointOfSaleItemListEmptyView.emptyCouponsTitle2", + value: "No coupons found", + comment: "Text appearing on the coupon list screen when there's no coupons found." + ) + static let emptyCouponsSubtitle = NSLocalizedString( + "pos.pointOfSaleItemListEmptyView.emptyCouponsSubtitle.2", + value: "Coupons can be an effective way to drive business. Would you like to create one?", + comment: "Text appearing on the coupons list screen as subtitle when there's no coupons found." + ) + static let emptyCouponsButtonTitle = NSLocalizedString( + "pos.pointOfSaleItemListEmptyView.noCouponsFoundButtonTitleButtonTitle", + value: "Create coupon", + comment: "Text for the button appearing on the coupons list screen when there's no coupons found." + ) + static let emptyCouponSearchSubtitle = NSLocalizedString( + "pos.pointOfSaleItemListEmptyView.emptyCouponSearchSubtitle.2", + value: "We couldn’t find any coupons with that name — try adjusting your search term.", + comment: "Text appearing on the coupons list screen as subtitle when there's no coupons found." + ) + static let emptyProductsButtonTitle = NSLocalizedString( + "pos.pointOfSaleItemListEmptyView.emptyProductsButtonTitle", + value: "Refresh", + comment: "Text for the button appearing on the products list screen when there are no products found." + ) + } +} + +// MARK: - Preview + +#Preview { + POSListEmptyView( + viewModel: POSListEmptyViewModel( + itemListType: .coupons(search: false), + baseItem: .root + ) + ) {} +} + +#Preview { + POSListEmptyView( + viewModel: POSListEmptyViewModel( + itemListType: .products(search: true), + baseItem: .root + ) + ) {} +} diff --git a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/PointOfSaleItemListErrorView.swift b/WooCommerce/Classes/POS/Presentation/Reusable Views/POSListErrorView.swift similarity index 89% rename from WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/PointOfSaleItemListErrorView.swift rename to WooCommerce/Classes/POS/Presentation/Reusable Views/POSListErrorView.swift index 10425c8eaa4..36f72af057d 100644 --- a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/PointOfSaleItemListErrorView.swift +++ b/WooCommerce/Classes/POS/Presentation/Reusable Views/POSListErrorView.swift @@ -1,9 +1,9 @@ import SwiftUI /// A view that displays an error message with a retry CTA when the list of POS items fails to load. -struct PointOfSaleItemListErrorView: View { +struct POSListErrorView: View { @Environment(\.floatingControlAreaSize) private var floatingControlAreaSize: CGSize - private let viewModel: PointOfSaleItemListErrorViewModel + private let viewModel: POSListErrorViewModell private let onAction: (() -> Void)? @State private var viewWidth: CGFloat = 0 @@ -11,7 +11,7 @@ struct PointOfSaleItemListErrorView: View { @Environment(\.keyboardObserver) private var keyboard init(error: PointOfSaleErrorState, onAction: (() -> Void)? = nil) { - self.viewModel = PointOfSaleItemListErrorViewModel(error: error) + self.viewModel = POSListErrorViewModell(error: error) self.onAction = onAction } @@ -67,7 +67,7 @@ struct PointOfSaleItemListErrorView: View { } } -struct PointOfSaleItemListErrorViewModel { +struct POSListErrorViewModell { let title: String let subtitle: String let buttonText: String @@ -87,9 +87,9 @@ struct PointOfSaleItemListErrorViewModel { } #Preview { - PointOfSaleItemListErrorView(error: .errorCouponsDisabled, onAction: {}) + POSListErrorView(error: .errorCouponsDisabled, onAction: {}) } #Preview { - PointOfSaleItemListErrorView(error: .errorOnLoadingCoupons(), onAction: {}) + POSListErrorView(error: .errorOnLoadingCoupons(), onAction: {}) } diff --git a/WooCommerce/Classes/POS/Presentation/Reusable Views/POSListInlineErrorView.swift b/WooCommerce/Classes/POS/Presentation/Reusable Views/POSListInlineErrorView.swift new file mode 100644 index 00000000000..fda2d7299e9 --- /dev/null +++ b/WooCommerce/Classes/POS/Presentation/Reusable Views/POSListInlineErrorView.swift @@ -0,0 +1,91 @@ +import SwiftUI + +struct POSListInlineErrorView: View { + let errorState: PointOfSaleErrorState + let buttonAction: () -> Void + + @ScaledMetric private var scale: CGFloat = 1.0 + + var body: some View { + ViewThatFits { + largerView + compactView + } + .frame(maxWidth: .infinity, idealHeight: Constants.productCardSize * scale) + .background(Constants.backgroundColor) + .posItemCardBorderStyles() + } + + @ViewBuilder + private var largerView: some View { + HStack(spacing: Constants.cardSpacing) { + POSErrorExclamationMark(size: .small) + .frame(width: min(Constants.productCardSize * scale, Constants.maximumProductCardSize), + height: Constants.productCardSize * scale) + VStack(alignment: .leading, spacing: Constants.textSpacing) { + Text(errorState.title) + .lineLimit(2) + .foregroundStyle(Color.posOnSurface) + .multilineTextAlignment(.leading) + .font(Constants.itemTitleFont) + + Text(errorState.subtitle) + .foregroundStyle(Color.posOnSurfaceVariantHighest) + .font(Constants.itemDetailFont) + } + .padding(.horizontal, Constants.horizontalTextPadding * (1 / scale)) + .padding(.vertical, Constants.verticalTextPadding * (1 / scale)) + + Spacer() + + button + .buttonStyle(POSOutlinedButtonStyle(size: .normal)) + .fixedSize(horizontal: true, vertical: false) + .padding(Constants.accessoryButtonPadding * (1 / scale)) + } + } + + @ViewBuilder + private var compactView: some View { + HStack(spacing: POSSpacing.small) { + POSErrorExclamationMark(size: .small) + HStack(spacing: 0) { + VStack(alignment: .leading, spacing: Constants.textSpacing) { + Text(errorState.title) + .lineLimit(2) + .foregroundStyle(Color.posOnSurface) + .multilineTextAlignment(.leading) + .font(POSFontStyle.posBodyMediumBold) + Text(errorState.subtitle) + .foregroundStyle(Color.posOnSurfaceVariantHighest) + .font(POSFontStyle.posBodyMediumRegular()) + } + Spacer() + button + .buttonStyle(POSOutlinedButtonStyle(size: .extraSmall)) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, POSSpacing.medium * (1 / scale)) + .padding(.vertical, POSSpacing.medium * (1 / scale)) + } + + @ViewBuilder + private var button: some View { + Button { + buttonAction() + } label: { + Text(errorState.buttonText) + .font(Constants.itemTitleFont) + } + } +} + +private typealias Constants = PointOfSaleItemListCardConstants + +#Preview { + POSListInlineErrorView( + errorState: .errorOnLoadingVariationsNextPage(), + buttonAction: {} + ) +} diff --git a/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift b/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift index f22a57e4016..8168ad9472f 100644 --- a/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift +++ b/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift @@ -122,8 +122,8 @@ private extension POSTabCoordinator { fetchStrategyFactory: posCouponFetchStrategyFactory), couponsSearchController: PointOfSaleCouponsController(itemProvider: posCouponProvider, fetchStrategyFactory: posCouponFetchStrategyFactory), - ordersController: PointOfSaleOrderListController( - orderListFetchStrategyFactory: PointOfSaleOrderListFetchStrategyFactory( + ordersController: POSOrderListController( + orderListFetchStrategyFactory: POSOrderListFetchStrategyFactory( siteID: siteID, credentials: credentials, currencyFormatter: CurrencyFormatter(currencySettings: currencySettings) diff --git a/WooCommerce/Classes/POS/Utils/PreviewHelpers.swift b/WooCommerce/Classes/POS/Utils/PreviewHelpers.swift index 0dff7f140a9..13de21e337d 100644 --- a/WooCommerce/Classes/POS/Utils/PreviewHelpers.swift +++ b/WooCommerce/Classes/POS/Utils/PreviewHelpers.swift @@ -26,8 +26,8 @@ import struct Yosemite.POSOrder import struct Yosemite.POSOrderItem import struct Yosemite.POSOrderRefund import typealias Yosemite.OrderItemAttribute -import class Yosemite.PointOfSaleOrderListService -import class Yosemite.PointOfSaleOrderListFetchStrategyFactory +import class Yosemite.POSOrderListService +import class Yosemite.POSOrderListFetchStrategyFactory // MARK: - PreviewProvider helpers // @@ -237,8 +237,10 @@ struct POSPreviewHelpers { ) } - static func makePreviewOrdersModel() -> PointOfSaleOrderListModel { - return PointOfSaleOrderListModel(ordersController: PointOfSalePreviewOrderListController(), receiptSender: POSReceiptSenderPreview()) + static func makePreviewOrdersModel(state: POSOrderListState) -> POSOrderListModel { + return POSOrderListModel( + ordersController: POSConfigurablePreviewOrderListController(state: state), + receiptSender: POSReceiptSenderPreview()) } static func makePreviewOrder() -> POSOrder { @@ -283,8 +285,10 @@ struct POSPreviewHelpers { } // MARK: - Preview Orders Controller -final class PointOfSalePreviewOrderListController: PointOfSaleSearchingOrderListControllerProtocol { - var ordersViewState: POSOrderListState { +final class POSConfigurablePreviewOrderListController: POSSearchingOrderListControllerProtocol { + let ordersViewState: POSOrderListState + + init(state: POSOrderListState? = nil) { let orders = [ POSOrder( id: 1, @@ -369,8 +373,7 @@ final class PointOfSalePreviewOrderListController: PointOfSaleSearchingOrderList formattedNetAmount: "$69.51" ) ] - - return .loaded(orders, hasMoreItems: false) + self.ordersViewState = state ?? .loaded(orders, hasMoreItems: false) } var selectedOrder: POSOrder? { diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index c7089765d9b..25264b754d2 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -31,12 +31,12 @@ 011D7A352CEC87B70007C187 /* CardPresentModalErrorEmailSent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 011D7A342CEC87B60007C187 /* CardPresentModalErrorEmailSent.swift */; }; 011DF3442C53A5CF000AFDD9 /* PointOfSaleCardPresentPaymentValidatingOrderMessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 011DF3432C53A5CF000AFDD9 /* PointOfSaleCardPresentPaymentValidatingOrderMessageViewModel.swift */; }; 011DF3462C53A919000AFDD9 /* PointOfSaleCardPresentPaymentActivityIndicatingMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 011DF3452C53A919000AFDD9 /* PointOfSaleCardPresentPaymentActivityIndicatingMessageView.swift */; }; - 012ACB742E5C830500A49458 /* PointOfSaleOrderListController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012ACB732E5C830500A49458 /* PointOfSaleOrderListController.swift */; }; - 012ACB762E5C83EC00A49458 /* PointOfSaleOrderListControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012ACB752E5C83EC00A49458 /* PointOfSaleOrderListControllerTests.swift */; }; + 012ACB742E5C830500A49458 /* POSOrderListController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012ACB732E5C830500A49458 /* POSOrderListController.swift */; }; + 012ACB762E5C83EC00A49458 /* POSOrderListControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012ACB752E5C83EC00A49458 /* POSOrderListControllerTests.swift */; }; 012ACB782E5C84A200A49458 /* POSOrdersViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012ACB772E5C84A200A49458 /* POSOrdersViewState.swift */; }; - 012ACB7A2E5C84D200A49458 /* MockPointOfSaleOrderListService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012ACB792E5C84D200A49458 /* MockPointOfSaleOrderListService.swift */; }; - 012ACB7C2E5C9BD400A49458 /* PointOfSaleOrderListModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012ACB7B2E5C9BD400A49458 /* PointOfSaleOrderListModel.swift */; }; - 012ACB822E5D8DCD00A49458 /* MockPointOfSaleOrderListFetchStrategyFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012ACB812E5D8DCD00A49458 /* MockPointOfSaleOrderListFetchStrategyFactory.swift */; }; + 012ACB7A2E5C84D200A49458 /* MockPOSOrderListService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012ACB792E5C84D200A49458 /* MockPOSOrderListService.swift */; }; + 012ACB7C2E5C9BD400A49458 /* POSOrderListModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012ACB7B2E5C9BD400A49458 /* POSOrderListModel.swift */; }; + 012ACB822E5D8DCD00A49458 /* MockPOSOrderListFetchStrategyFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012ACB812E5D8DCD00A49458 /* MockPOSOrderListFetchStrategyFactory.swift */; }; 01309A7F2DC4F39E00B77527 /* PointOfSaleCardPresentPaymentCardInsertedMessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01309A7E2DC4F39A00B77527 /* PointOfSaleCardPresentPaymentCardInsertedMessageViewModel.swift */; }; 01309A812DC4F45300B77527 /* CardPresentModalCardInserted.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01309A802DC4F44700B77527 /* CardPresentModalCardInserted.swift */; }; 01309A832DC4F89D00B77527 /* PointOfSaleCardPresentPaymentCardInsertedMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01309A822DC4F89400B77527 /* PointOfSaleCardPresentPaymentCardInsertedMessageView.swift */; }; @@ -91,16 +91,15 @@ 01AB2D122DDC7AD300AA67FD /* PointOfSaleItemListAnalyticsTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01AB2D112DDC7AD100AA67FD /* PointOfSaleItemListAnalyticsTrackerTests.swift */; }; 01AB2D142DDC7CD200AA67FD /* POSItemActionHandlerFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01AB2D132DDC7CD000AA67FD /* POSItemActionHandlerFactoryTests.swift */; }; 01AB2D162DDC8CDA00AA67FD /* MockAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01AB2D152DDC8CD600AA67FD /* MockAnalytics.swift */; }; - 01ABA0282E57579300829DC0 /* PointOfSaleOrderListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01ABA0252E57579300829DC0 /* PointOfSaleOrderListView.swift */; }; - 01ABA0292E57579300829DC0 /* PointOfSaleOrderDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01ABA0242E57579300829DC0 /* PointOfSaleOrderDetailsView.swift */; }; - 01ABA02A2E57579300829DC0 /* PointOfSaleOrdersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01ABA0262E57579300829DC0 /* PointOfSaleOrdersView.swift */; }; + 01ABA0282E57579300829DC0 /* POSOrderListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01ABA0252E57579300829DC0 /* POSOrderListView.swift */; }; + 01ABA0292E57579300829DC0 /* POSDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01ABA0242E57579300829DC0 /* POSDetailsView.swift */; }; + 01ABA02A2E57579300829DC0 /* POSOrdersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01ABA0262E57579300829DC0 /* POSOrdersView.swift */; }; 01ADC1362C9AB4810036F7D2 /* PointOfSaleCardPresentPaymentIntentCreationErrorMessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01ADC1352C9AB4810036F7D2 /* PointOfSaleCardPresentPaymentIntentCreationErrorMessageViewModel.swift */; }; 01ADC1382C9AB6050036F7D2 /* PointOfSaleCardPresentPaymentIntentCreationErrorMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01ADC1372C9AB6050036F7D2 /* PointOfSaleCardPresentPaymentIntentCreationErrorMessageView.swift */; }; 01B3A1F22DB6D48800286B7F /* ItemListType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01B3A1F12DB6D48800286B7F /* ItemListType.swift */; }; 01B744E22D2FCA1400AEB3F4 /* PushNotificationBackgroundSynchronizerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01B744E12D2FCA1300AEB3F4 /* PushNotificationBackgroundSynchronizerFactory.swift */; }; - 01B7AFBD2E707FB30004BE9D /* PointOfSaleOrderListModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01B7AFBB2E707FB30004BE9D /* PointOfSaleOrderListModelTests.swift */; }; 01B7AFBE2E707FB30004BE9D /* POSOrderListStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01B7AFBC2E707FB30004BE9D /* POSOrderListStateTests.swift */; }; - 01B7AFC02E70801A0004BE9D /* MockPointOfSaleOrderListController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01B7AFBF2E7080180004BE9D /* MockPointOfSaleOrderListController.swift */; }; + 01B7C9CA2E71C8D00004BE9D /* POSOrderListEmptyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01B7C9C92E71C8D00004BE9D /* POSOrderListEmptyViewModel.swift */; }; 01BB6C072D09DC560094D55B /* CardPresentModalLocationPreAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01BB6C062D09DC470094D55B /* CardPresentModalLocationPreAlert.swift */; }; 01BB6C0A2D09E9630094D55B /* LocationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01BB6C092D09E9630094D55B /* LocationService.swift */; }; 01BD77442C58CED400147191 /* PointOfSaleCardPresentPaymentProcessingMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01BD77432C58CED400147191 /* PointOfSaleCardPresentPaymentProcessingMessageView.swift */; }; @@ -110,8 +109,8 @@ 01BD774C2C58D2BE00147191 /* PointOfSaleCardPresentPaymentDisconnectedMessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01BD774B2C58D2BE00147191 /* PointOfSaleCardPresentPaymentDisconnectedMessageViewModel.swift */; }; 01BE94002DDCB1110063541C /* Error+Connectivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01BE93FF2DDCB1110063541C /* Error+Connectivity.swift */; }; 01BE94042DDCC7670063541C /* PointOfSaleEmptyErrorStateViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01BE94032DDCC7650063541C /* PointOfSaleEmptyErrorStateViewLayout.swift */; }; - 01C21AB62E66EB80008E4D77 /* PointOfSaleOrderDetailsLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01C21AB52E66EB70008E4D77 /* PointOfSaleOrderDetailsLoadingView.swift */; }; - 01C21AB82E66EC26008E4D77 /* PointOfSaleOrderDetailsEmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01C21AB72E66EC14008E4D77 /* PointOfSaleOrderDetailsEmptyView.swift */; }; + 01C21AB62E66EB80008E4D77 /* POSOrderDetailsLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01C21AB52E66EB70008E4D77 /* POSOrderDetailsLoadingView.swift */; }; + 01C21AB82E66EC26008E4D77 /* POSOrderDetailsEmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01C21AB72E66EC14008E4D77 /* POSOrderDetailsEmptyView.swift */; }; 01C9C59F2DA3D98400CD81D8 /* CartRowRemoveButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01C9C59E2DA3D97E00CD81D8 /* CartRowRemoveButton.swift */; }; 01D082402C5B9EAB007FE81F /* POSBackgroundAppearanceKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D0823F2C5B9EAB007FE81F /* POSBackgroundAppearanceKey.swift */; }; 01E62EC82DFADF56003A6D9E /* Cart+BarcodeScanError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E62EC72DFADF4B003A6D9E /* Cart+BarcodeScanError.swift */; }; @@ -976,7 +975,7 @@ 20DB185B2CF5D9220018D3E1 /* MockPointOfSaleOrderController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20DB185A2CF5D9220018D3E1 /* MockPointOfSaleOrderController.swift */; }; 20DB185D2CF5E7630018D3E1 /* PointOfSaleOrderControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20DB185C2CF5E7560018D3E1 /* PointOfSaleOrderControllerTests.swift */; }; 20E188842AD059A50053E945 /* TapToPayEducationContactlessLimitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20E188832AD059A50053E945 /* TapToPayEducationContactlessLimitView.swift */; }; - 20EFAEA62D35337F00D35F9C /* ItemListErrorCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20EFAEA52D35337F00D35F9C /* ItemListErrorCardView.swift */; }; + 20EFAEA62D35337F00D35F9C /* POSListInlineErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20EFAEA52D35337F00D35F9C /* POSListInlineErrorView.swift */; }; 20F6A46C2DE5FCEF0066D8CB /* POSItemFetchAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20F6A46B2DE5FCEF0066D8CB /* POSItemFetchAnalytics.swift */; }; 20F7B12D2D12C7B900C08193 /* ItemsContainerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20F7B12C2D12C7B900C08193 /* ItemsContainerState.swift */; }; 20F7B12F2D12CBE700C08193 /* ItemsViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20F7B12E2D12CBE700C08193 /* ItemsViewState.swift */; }; @@ -1620,8 +1619,8 @@ 6856DF20E1BDCC391635F707 /* AgeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6856D1A5F72A36AB3704D19D /* AgeTests.swift */; }; 685A305F2E608F2D001E667B /* POSSettingsStoreViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 685A305E2E608F29001E667B /* POSSettingsStoreViewModelTests.swift */; }; 685A30612E60908C001E667B /* POSSettingsStoreViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 685A30602E60908B001E667B /* POSSettingsStoreViewModel.swift */; }; - 68600A8F2C65BC5500252EDD /* PointOfSaleItemListErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68600A8E2C65BC5500252EDD /* PointOfSaleItemListErrorView.swift */; }; - 68600A912C65BC9C00252EDD /* PointOfSaleItemListEmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68600A902C65BC9C00252EDD /* PointOfSaleItemListEmptyView.swift */; }; + 68600A8F2C65BC5500252EDD /* POSListErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68600A8E2C65BC5500252EDD /* POSListErrorView.swift */; }; + 68600A912C65BC9C00252EDD /* POSListEmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68600A902C65BC9C00252EDD /* POSListEmptyView.swift */; }; 68625DE62D4134D70042B231 /* DynamicVStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68625DE52D4134D50042B231 /* DynamicVStack.swift */; }; 68674D312B6C895D00E93FBD /* ReceiptEligibilityUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68674D302B6C895D00E93FBD /* ReceiptEligibilityUseCaseTests.swift */; }; 686A71B62DC9E5C10006E835 /* POSItemActionHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 686A71B52DC9E5C10006E835 /* POSItemActionHandlerTests.swift */; }; @@ -3255,12 +3254,12 @@ 011D7A342CEC87B60007C187 /* CardPresentModalErrorEmailSent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentModalErrorEmailSent.swift; sourceTree = ""; }; 011DF3432C53A5CF000AFDD9 /* PointOfSaleCardPresentPaymentValidatingOrderMessageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentValidatingOrderMessageViewModel.swift; sourceTree = ""; }; 011DF3452C53A919000AFDD9 /* PointOfSaleCardPresentPaymentActivityIndicatingMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentActivityIndicatingMessageView.swift; sourceTree = ""; }; - 012ACB732E5C830500A49458 /* PointOfSaleOrderListController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleOrderListController.swift; sourceTree = ""; }; - 012ACB752E5C83EC00A49458 /* PointOfSaleOrderListControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleOrderListControllerTests.swift; sourceTree = ""; }; + 012ACB732E5C830500A49458 /* POSOrderListController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSOrderListController.swift; sourceTree = ""; }; + 012ACB752E5C83EC00A49458 /* POSOrderListControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSOrderListControllerTests.swift; sourceTree = ""; }; 012ACB772E5C84A200A49458 /* POSOrdersViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSOrdersViewState.swift; sourceTree = ""; }; - 012ACB792E5C84D200A49458 /* MockPointOfSaleOrderListService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPointOfSaleOrderListService.swift; sourceTree = ""; }; - 012ACB7B2E5C9BD400A49458 /* PointOfSaleOrderListModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleOrderListModel.swift; sourceTree = ""; }; - 012ACB812E5D8DCD00A49458 /* MockPointOfSaleOrderListFetchStrategyFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPointOfSaleOrderListFetchStrategyFactory.swift; sourceTree = ""; }; + 012ACB792E5C84D200A49458 /* MockPOSOrderListService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPOSOrderListService.swift; sourceTree = ""; }; + 012ACB7B2E5C9BD400A49458 /* POSOrderListModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSOrderListModel.swift; sourceTree = ""; }; + 012ACB812E5D8DCD00A49458 /* MockPOSOrderListFetchStrategyFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPOSOrderListFetchStrategyFactory.swift; sourceTree = ""; }; 01309A7E2DC4F39A00B77527 /* PointOfSaleCardPresentPaymentCardInsertedMessageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentCardInsertedMessageViewModel.swift; sourceTree = ""; }; 01309A802DC4F44700B77527 /* CardPresentModalCardInserted.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentModalCardInserted.swift; sourceTree = ""; }; 01309A822DC4F89400B77527 /* PointOfSaleCardPresentPaymentCardInsertedMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentCardInsertedMessageView.swift; sourceTree = ""; }; @@ -3315,16 +3314,17 @@ 01AB2D112DDC7AD100AA67FD /* PointOfSaleItemListAnalyticsTrackerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleItemListAnalyticsTrackerTests.swift; sourceTree = ""; }; 01AB2D132DDC7CD000AA67FD /* POSItemActionHandlerFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSItemActionHandlerFactoryTests.swift; sourceTree = ""; }; 01AB2D152DDC8CD600AA67FD /* MockAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAnalytics.swift; sourceTree = ""; }; - 01ABA0242E57579300829DC0 /* PointOfSaleOrderDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleOrderDetailsView.swift; sourceTree = ""; }; - 01ABA0252E57579300829DC0 /* PointOfSaleOrderListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleOrderListView.swift; sourceTree = ""; }; - 01ABA0262E57579300829DC0 /* PointOfSaleOrdersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleOrdersView.swift; sourceTree = ""; }; + 01ABA0242E57579300829DC0 /* POSDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSDetailsView.swift; sourceTree = ""; }; + 01ABA0252E57579300829DC0 /* POSOrderListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSOrderListView.swift; sourceTree = ""; }; + 01ABA0262E57579300829DC0 /* POSOrdersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSOrdersView.swift; sourceTree = ""; }; 01ADC1352C9AB4810036F7D2 /* PointOfSaleCardPresentPaymentIntentCreationErrorMessageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentIntentCreationErrorMessageViewModel.swift; sourceTree = ""; }; 01ADC1372C9AB6050036F7D2 /* PointOfSaleCardPresentPaymentIntentCreationErrorMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentIntentCreationErrorMessageView.swift; sourceTree = ""; }; 01B3A1F12DB6D48800286B7F /* ItemListType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemListType.swift; sourceTree = ""; }; 01B744E12D2FCA1300AEB3F4 /* PushNotificationBackgroundSynchronizerFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationBackgroundSynchronizerFactory.swift; sourceTree = ""; }; - 01B7AFBB2E707FB30004BE9D /* PointOfSaleOrderListModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleOrderListModelTests.swift; sourceTree = ""; }; + 01B7AFBB2E707FB30004BE9D /* POSOrderListModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSOrderListModelTests.swift; sourceTree = ""; }; 01B7AFBC2E707FB30004BE9D /* POSOrderListStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSOrderListStateTests.swift; sourceTree = ""; }; - 01B7AFBF2E7080180004BE9D /* MockPointOfSaleOrderListController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPointOfSaleOrderListController.swift; sourceTree = ""; }; + 01B7AFBF2E7080180004BE9D /* MockPOSOrderListController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPOSOrderListController.swift; sourceTree = ""; }; + 01B7C9C92E71C8D00004BE9D /* POSOrderListEmptyViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSOrderListEmptyViewModel.swift; sourceTree = ""; }; 01BB6C062D09DC470094D55B /* CardPresentModalLocationPreAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentModalLocationPreAlert.swift; sourceTree = ""; }; 01BB6C092D09E9630094D55B /* LocationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationService.swift; sourceTree = ""; }; 01BD77432C58CED400147191 /* PointOfSaleCardPresentPaymentProcessingMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentProcessingMessageView.swift; sourceTree = ""; }; @@ -3334,8 +3334,8 @@ 01BD774B2C58D2BE00147191 /* PointOfSaleCardPresentPaymentDisconnectedMessageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentDisconnectedMessageViewModel.swift; sourceTree = ""; }; 01BE93FF2DDCB1110063541C /* Error+Connectivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Error+Connectivity.swift"; sourceTree = ""; }; 01BE94032DDCC7650063541C /* PointOfSaleEmptyErrorStateViewLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleEmptyErrorStateViewLayout.swift; sourceTree = ""; }; - 01C21AB52E66EB70008E4D77 /* PointOfSaleOrderDetailsLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleOrderDetailsLoadingView.swift; sourceTree = ""; }; - 01C21AB72E66EC14008E4D77 /* PointOfSaleOrderDetailsEmptyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleOrderDetailsEmptyView.swift; sourceTree = ""; }; + 01C21AB52E66EB70008E4D77 /* POSOrderDetailsLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSOrderDetailsLoadingView.swift; sourceTree = ""; }; + 01C21AB72E66EC14008E4D77 /* POSOrderDetailsEmptyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSOrderDetailsEmptyView.swift; sourceTree = ""; }; 01C9C59E2DA3D97E00CD81D8 /* CartRowRemoveButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CartRowRemoveButton.swift; sourceTree = ""; }; 01D0823F2C5B9EAB007FE81F /* POSBackgroundAppearanceKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSBackgroundAppearanceKey.swift; sourceTree = ""; }; 01E62EC72DFADF4B003A6D9E /* Cart+BarcodeScanError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Cart+BarcodeScanError.swift"; sourceTree = ""; }; @@ -4208,7 +4208,7 @@ 20DB185C2CF5E7560018D3E1 /* PointOfSaleOrderControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleOrderControllerTests.swift; sourceTree = ""; }; 20E014E12CF63671008C823B /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 20E188832AD059A50053E945 /* TapToPayEducationContactlessLimitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TapToPayEducationContactlessLimitView.swift; sourceTree = ""; }; - 20EFAEA52D35337F00D35F9C /* ItemListErrorCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemListErrorCardView.swift; sourceTree = ""; }; + 20EFAEA52D35337F00D35F9C /* POSListInlineErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSListInlineErrorView.swift; sourceTree = ""; }; 20F6A46B2DE5FCEF0066D8CB /* POSItemFetchAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSItemFetchAnalytics.swift; sourceTree = ""; }; 20F7B12C2D12C7B900C08193 /* ItemsContainerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemsContainerState.swift; sourceTree = ""; }; 20F7B12E2D12CBE700C08193 /* ItemsViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemsViewState.swift; sourceTree = ""; }; @@ -4826,8 +4826,8 @@ 6856DCE1638958DA296D690F /* KeyboardStateProviderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardStateProviderTests.swift; sourceTree = ""; }; 685A305E2E608F29001E667B /* POSSettingsStoreViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSSettingsStoreViewModelTests.swift; sourceTree = ""; }; 685A30602E60908B001E667B /* POSSettingsStoreViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSSettingsStoreViewModel.swift; sourceTree = ""; }; - 68600A8E2C65BC5500252EDD /* PointOfSaleItemListErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleItemListErrorView.swift; sourceTree = ""; }; - 68600A902C65BC9C00252EDD /* PointOfSaleItemListEmptyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleItemListEmptyView.swift; sourceTree = ""; }; + 68600A8E2C65BC5500252EDD /* POSListErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSListErrorView.swift; sourceTree = ""; }; + 68600A902C65BC9C00252EDD /* POSListEmptyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSListEmptyView.swift; sourceTree = ""; }; 68625DE52D4134D50042B231 /* DynamicVStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicVStack.swift; sourceTree = ""; }; 68674D302B6C895D00E93FBD /* ReceiptEligibilityUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptEligibilityUseCaseTests.swift; sourceTree = ""; }; 686A71B52DC9E5C10006E835 /* POSItemActionHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSItemActionHandlerTests.swift; sourceTree = ""; }; @@ -6538,6 +6538,9 @@ 2005FC9C2DC37E4D00E12021 /* POSPageHeaderBackButton.swift */, 0295CDBF2D6477C400865E27 /* POSNoticeView.swift */, 021A17202D7036AF006DF7C0 /* DynamicFrameScaler.swift */, + 68600A902C65BC9C00252EDD /* POSListEmptyView.swift */, + 68600A8E2C65BC5500252EDD /* POSListErrorView.swift */, + 20EFAEA52D35337F00D35F9C /* POSListInlineErrorView.swift */, ); path = "Reusable Views"; sourceTree = ""; @@ -6576,11 +6579,12 @@ 01ABA0272E57579300829DC0 /* Orders */ = { isa = PBXGroup; children = ( - 01C21AB72E66EC14008E4D77 /* PointOfSaleOrderDetailsEmptyView.swift */, - 01C21AB52E66EB70008E4D77 /* PointOfSaleOrderDetailsLoadingView.swift */, - 01ABA0242E57579300829DC0 /* PointOfSaleOrderDetailsView.swift */, - 01ABA0252E57579300829DC0 /* PointOfSaleOrderListView.swift */, - 01ABA0262E57579300829DC0 /* PointOfSaleOrdersView.swift */, + 01B7C9C92E71C8D00004BE9D /* POSOrderListEmptyViewModel.swift */, + 01C21AB72E66EC14008E4D77 /* POSOrderDetailsEmptyView.swift */, + 01C21AB52E66EB70008E4D77 /* POSOrderDetailsLoadingView.swift */, + 01ABA0242E57579300829DC0 /* POSDetailsView.swift */, + 01ABA0252E57579300829DC0 /* POSOrderListView.swift */, + 01ABA0262E57579300829DC0 /* POSOrdersView.swift */, ); path = Orders; sourceTree = ""; @@ -7291,11 +7295,9 @@ 0157A9952C4FEA7200866FFD /* PointOfSaleLoadingView.swift */, 2004E2E82C0DFE2B00D62521 /* PointOfSaleCardPresentPaymentAlert.swift */, 2023E2AD2C21D8EA00FC365A /* PointOfSaleCardPresentPaymentInLineMessage.swift */, - 68600A8E2C65BC5500252EDD /* PointOfSaleItemListErrorView.swift */, 20897C9D2D4A68C5008AD16C /* PointOfSaleUnsupportedWidthView.swift */, 021BCDF72D3648CD002E9F15 /* PointOfSaleItemListFullscreenErrorView.swift */, 20018FFF2C80AEAC002C1E4B /* PointOfSaleItemListFullscreenView.swift */, - 68600A902C65BC9C00252EDD /* PointOfSaleItemListEmptyView.swift */, 68C7E5C32C69B3CD00856513 /* PointOfSaleItemListErrorLayout.swift */, 205E79492C204B2D001BA266 /* Reader Messages */, 2023E2AC2C21D8A400FC365A /* Connection Alerts */, @@ -7575,7 +7577,6 @@ isa = PBXGroup; children = ( 0291497A2D2682FF00F7B3B3 /* ItemList.swift */, - 20EFAEA52D35337F00D35F9C /* ItemListErrorCardView.swift */, 026826A42BF59DF60036F959 /* SimpleProductCardView.swift */, 204C20452D35471400E6D9CF /* PointOfSaleItemListCardConstants.swift */, 027ADB6D2D1BF5E3009608DB /* ParentProductCardView.swift */, @@ -7814,10 +7815,10 @@ 02CD3BFC2C35D01600E575C4 /* Mocks */ = { isa = PBXGroup; children = ( - 01B7AFBF2E7080180004BE9D /* MockPointOfSaleOrderListController.swift */, + 01B7AFBF2E7080180004BE9D /* MockPOSOrderListController.swift */, 019460E12E70121A00FCB9AB /* MockPOSReceiptController.swift */, - 012ACB812E5D8DCD00A49458 /* MockPointOfSaleOrderListFetchStrategyFactory.swift */, - 012ACB792E5C84D200A49458 /* MockPointOfSaleOrderListService.swift */, + 012ACB812E5D8DCD00A49458 /* MockPOSOrderListFetchStrategyFactory.swift */, + 012ACB792E5C84D200A49458 /* MockPOSOrderListService.swift */, 01F935582DFC0D4800B50B03 /* MockPointOfSaleSoundPlayer.swift */, 01AB2D152DDC8CD600AA67FD /* MockAnalytics.swift */, 686A71B72DC9EB6D0006E835 /* MockPOSSearchHistoryService.swift */, @@ -8242,7 +8243,7 @@ 68B681152D92577F0098D5CD /* PointOfSaleCouponsController.swift */, 200BA1582CF092280006DC5B /* PointOfSaleItemsController.swift */, 20CF75B92CF4E69000ACCF4A /* PointOfSaleOrderController.swift */, - 012ACB732E5C830500A49458 /* PointOfSaleOrderListController.swift */, + 012ACB732E5C830500A49458 /* POSOrderListController.swift */, ); path = Controllers; sourceTree = ""; @@ -8255,7 +8256,7 @@ 68D7480F2E5DB6D20048CFE9 /* PointOfSaleSettingsControllerTests.swift */, 200BA15D2CF0A9EB0006DC5B /* PointOfSaleItemsControllerTests.swift */, 02E4F26F2E0F2C75003A31E7 /* POSEntryPointControllerTests.swift */, - 012ACB752E5C83EC00A49458 /* PointOfSaleOrderListControllerTests.swift */, + 012ACB752E5C83EC00A49458 /* POSOrderListControllerTests.swift */, ); path = Controllers; sourceTree = ""; @@ -10060,7 +10061,7 @@ 68F151DF2C0DA7800082AEC8 /* Models */ = { isa = PBXGroup; children = ( - 012ACB7B2E5C9BD400A49458 /* PointOfSaleOrderListModel.swift */, + 012ACB7B2E5C9BD400A49458 /* POSOrderListModel.swift */, 01B3A1F12DB6D48800286B7F /* ItemListType.swift */, 20FCBCDC2CE223340082DCA3 /* PointOfSaleAggregateModel.swift */, 68E9F7002E5C499000D45747 /* PointOfSaleSettingsController.swift */, @@ -13200,7 +13201,7 @@ DAD988C72C4A9D49009DE9E3 /* Models */ = { isa = PBXGroup; children = ( - 01B7AFBB2E707FB30004BE9D /* PointOfSaleOrderListModelTests.swift */, + 01B7AFBB2E707FB30004BE9D /* POSOrderListModelTests.swift */, 01B7AFBC2E707FB30004BE9D /* POSOrderListStateTests.swift */, 685A305E2E608F29001E667B /* POSSettingsStoreViewModelTests.swift */, 20FCBCDE2CE241810082DCA3 /* PointOfSaleAggregateModelTests.swift */, @@ -15298,7 +15299,7 @@ 02D681AB29C3F8AC00348510 /* StoreOnboardingPaymentsSetupCoordinator.swift in Sources */, B59D49CD219B587E006BF0AD /* UILabel+OrderStatus.swift in Sources */, 265BCA0C2430E741004E53EE /* ProductCategoryTableViewCell.swift in Sources */, - 012ACB742E5C830500A49458 /* PointOfSaleOrderListController.swift in Sources */, + 012ACB742E5C830500A49458 /* POSOrderListController.swift in Sources */, 02ACD25A2852E11700EC928E /* CloseAccountCoordinator.swift in Sources */, 0191301B2CF4E782008C0C88 /* TapToPayEducationStepViewModel.swift in Sources */, 0258D9492B68E7FE00D280D0 /* ProductsSplitViewWrapperController.swift in Sources */, @@ -15310,9 +15311,9 @@ 016910982E1D019500B731DA /* GameControllerBarcodeObserver.swift in Sources */, EE9D031B2B89E4470077CED1 /* FilterOrdersByProduct+Analytics.swift in Sources */, 20C6E7512CDE4AEA00CD124C /* ItemListState.swift in Sources */, - 01ABA0282E57579300829DC0 /* PointOfSaleOrderListView.swift in Sources */, - 01ABA0292E57579300829DC0 /* PointOfSaleOrderDetailsView.swift in Sources */, - 01ABA02A2E57579300829DC0 /* PointOfSaleOrdersView.swift in Sources */, + 01ABA0282E57579300829DC0 /* POSOrderListView.swift in Sources */, + 01ABA0292E57579300829DC0 /* POSDetailsView.swift in Sources */, + 01ABA02A2E57579300829DC0 /* POSOrdersView.swift in Sources */, 2DB891662E27F0830001B175 /* Address+Shared.swift in Sources */, DEC17AE02D82C513005A6E6D /* WooShippingHazmatDetailView.swift in Sources */, 86967D812B4E21C600C20CA8 /* BlazeAddParameterViewModel.swift in Sources */, @@ -15498,7 +15499,7 @@ CCD2F51C26D697860010E679 /* ShippingLabelServicePackageListViewModel.swift in Sources */, 03076D38290C223E008EE839 /* WooNavigationSheet.swift in Sources */, 022CE91A29BB143000F210E0 /* ProductSelectorNavigationView.swift in Sources */, - 012ACB7C2E5C9BD400A49458 /* PointOfSaleOrderListModel.swift in Sources */, + 012ACB7C2E5C9BD400A49458 /* POSOrderListModel.swift in Sources */, B99686E02A13C8CC00D1AF62 /* ScanToPayView.swift in Sources */, 02B191502CCF27F300CF38C9 /* PointOfSaleCardPresentPaymentOnboardingView.swift in Sources */, CE070A3E2BBC608A00017578 /* GiftCardsReportCardViewModel.swift in Sources */, @@ -16317,7 +16318,7 @@ 0300201029C0EBA400B09777 /* ReaderConnectionUnderlyingErrorDisplaying.swift in Sources */, EEB4E2DA29B5F8FC00371C3C /* CouponLineDetailsViewModel.swift in Sources */, B6C78B8E293BAE68008934A1 /* AnalyticsHubLastMonthRangeData.swift in Sources */, - 01C21AB62E66EB80008E4D77 /* PointOfSaleOrderDetailsLoadingView.swift in Sources */, + 01C21AB62E66EB80008E4D77 /* POSOrderDetailsLoadingView.swift in Sources */, B517EA18218B232700730EC4 /* StringFormatter+Notes.swift in Sources */, 318109DC25E5B51900EE0BE7 /* ImageTableViewCell.swift in Sources */, 262EB5B0298C7C3A009DCC36 /* SupportFormsDataSources.swift in Sources */, @@ -16397,6 +16398,7 @@ DEC2962526C122DF005A056B /* ShippingLabelCustomsFormInputViewModel.swift in Sources */, 02F4F50F237AFC1E00E13A9C /* ImageAndTitleAndTextTableViewCell.swift in Sources */, 26E7EE7029300F6200793045 /* DeltaTag.swift in Sources */, + 01B7C9CA2E71C8D00004BE9D /* POSOrderListEmptyViewModel.swift in Sources */, 021E2A1C23AA0DD100B1DE07 /* ProductBackordersSettingListSelectorCommand.swift in Sources */, 26F94E21267A41BE00DB6CCF /* ProductAddOnsListViewModel.swift in Sources */, 45F5A3C123DF206B007D40E5 /* ShippingInputFormatter.swift in Sources */, @@ -16644,7 +16646,7 @@ CC53FB3527551A6E00C4CA4F /* ProductRow.swift in Sources */, CC04918D292BB74500F719D8 /* StatsDataTextFormatter.swift in Sources */, 2662D90A26E16B3600E25611 /* FilterListSelector.swift in Sources */, - 68600A8F2C65BC5500252EDD /* PointOfSaleItemListErrorView.swift in Sources */, + 68600A8F2C65BC5500252EDD /* POSListErrorView.swift in Sources */, DECE1400279A595200816ECD /* Coupon+Woo.swift in Sources */, B6C78B90293BAF37008934A1 /* AnalyticsHubLastYearRangeData.swift in Sources */, 314265B12645A07800500598 /* CardReaderSettingsConnectedViewController.swift in Sources */, @@ -17023,7 +17025,7 @@ 26B9875D273C6A830090E8CA /* SimplePaymentsNoteViewModel.swift in Sources */, CE6302482BAB60AE00E3325C /* CustomerDetailViewModel.swift in Sources */, DEF36DEA2898D3CF00178AC2 /* AuthenticatedWebViewModel.swift in Sources */, - 20EFAEA62D35337F00D35F9C /* ItemListErrorCardView.swift in Sources */, + 20EFAEA62D35337F00D35F9C /* POSListInlineErrorView.swift in Sources */, 26132B3C2C3DA20C004C157F /* PushNotificationBackgroundSynchronizer.swift in Sources */, 6885E2CC2C32B14B004C8D70 /* TotalsViewHelper.swift in Sources */, 45A0E4CB2566B56000D4E8C3 /* NumberOfLinkedProductsTableViewCell.swift in Sources */, @@ -17036,7 +17038,7 @@ 68707A172E570EB200500CD8 /* PointOfSaleSettingsHardwareDetailView.swift in Sources */, CC4D1D8625E6CDDE00B6E4E7 /* RenameAttributesViewModel.swift in Sources */, DEFA3D932897D8930076FAE1 /* NoWooErrorViewModel.swift in Sources */, - 01C21AB82E66EC26008E4D77 /* PointOfSaleOrderDetailsEmptyView.swift in Sources */, + 01C21AB82E66EC26008E4D77 /* POSOrderDetailsEmptyView.swift in Sources */, 209B15672AD85F070094152A /* OperatingSystemVersion+Localization.swift in Sources */, 0220F4952C16DC98003723C2 /* PointOfSaleCardPresentPaymentFoundMultipleReadersView.swift in Sources */, 020A55F127F6C605007843F0 /* CardReaderConnectionAnalyticsTracker.swift in Sources */, @@ -17089,7 +17091,7 @@ B958B4DA2983E3F40010286B /* OrderDurationRecorder.swift in Sources */, CC3B35DB28E5A6830036B097 /* ReviewReply.swift in Sources */, 45D1CF4523BAC2A500945A36 /* ProductTaxClassListSelectorDataSource.swift in Sources */, - 68600A912C65BC9C00252EDD /* PointOfSaleItemListEmptyView.swift in Sources */, + 68600A912C65BC9C00252EDD /* POSListEmptyView.swift in Sources */, 319A626127ACAE3400BC96C3 /* InPersonPaymentsPluginChoicesView.swift in Sources */, 68625DE62D4134D70042B231 /* DynamicVStack.swift in Sources */, 0365986729AFAEFC00F297D3 /* SetUpTapToPayViewModelsOrderedList.swift in Sources */, @@ -17186,7 +17188,6 @@ 269098B627D2C09D001FEB07 /* ShippingInputTransformerTests.swift in Sources */, 02BA128B24616B48008D8325 /* ProductFormActionsFactory+VisibilityTests.swift in Sources */, DEA88F522AAAC1180037273B /* AddEditProductCategoryViewModelTests.swift in Sources */, - 01B7AFC02E70801A0004BE9D /* MockPointOfSaleOrderListController.swift in Sources */, DE2BF4FD2846192B00FBE68A /* CouponAllowedEmailsViewModelTests.swift in Sources */, FEEB2F6E268A2F7B0075A6E0 /* RoleEligibilityUseCaseTests.swift in Sources */, 31E906A326CC91A70099A985 /* CardReaderConnectionControllerTests.swift in Sources */, @@ -17197,7 +17198,7 @@ 3198A1E82694DC7200597213 /* MockKnownReadersProvider.swift in Sources */, DEC51B04276B30F6009F3DF4 /* SystemStatusReportViewModelTests.swift in Sources */, 26B119C224D1CD3500FED5C7 /* WooConstantsTests.swift in Sources */, - 012ACB822E5D8DCD00A49458 /* MockPointOfSaleOrderListFetchStrategyFactory.swift in Sources */, + 012ACB822E5D8DCD00A49458 /* MockPOSOrderListFetchStrategyFactory.swift in Sources */, DE96844D2A332CC2000FBF4E /* ShareProductCoordinatorTests.swift in Sources */, 26100B202722FCAD00473045 /* MockCardPresentPaymentsOnboardingUseCase.swift in Sources */, 263491D5299C923400594566 /* SupportFormViewModelTests.swift in Sources */, @@ -17302,7 +17303,7 @@ DEF657AA2C8AC25C00ACD61E /* BlazeCampaignObjectivePickerViewModelTests.swift in Sources */, EE2EDFE12987A189004E702B /* MockABTestVariationProvider.swift in Sources */, 0273707E24C0047800167204 /* SequenceHelpersTests.swift in Sources */, - 012ACB762E5C83EC00A49458 /* PointOfSaleOrderListControllerTests.swift in Sources */, + 012ACB762E5C83EC00A49458 /* POSOrderListControllerTests.swift in Sources */, DE9A02A32A44441200193ABF /* RequirementsCheckerTests.swift in Sources */, D802547326551D0F001B2CC1 /* CardPresentModalTapCardTests.swift in Sources */, B55BC1F321A8790F0011A0C0 /* StringHTMLTests.swift in Sources */, @@ -17416,7 +17417,6 @@ 45EF798624509B4C00B22BA2 /* ArrayIndexPathTests.swift in Sources */, D8610BDD256F5ABF00A5DF27 /* JetpackErrorViewModelTests.swift in Sources */, 746791632108D7C0007CF1DC /* WooAnalyticsTests.swift in Sources */, - 01B7AFBD2E707FB30004BE9D /* PointOfSaleOrderListModelTests.swift in Sources */, 01B7AFBE2E707FB30004BE9D /* POSOrderListStateTests.swift in Sources */, 200BA15E2CF0A9EB0006DC5B /* PointOfSaleItemsControllerTests.swift in Sources */, 2667BFDD252F61C5008099D4 /* RefundShippingDetailsViewModelTests.swift in Sources */, @@ -17752,7 +17752,7 @@ 02A275C023FE58F6005C560F /* MockImageCache.swift in Sources */, EE6C6B6E2C65DC4100632BDA /* WordPressMediaLibraryPickerDataSourceTests.swift in Sources */, 20BCF6F02B0E48CC00954840 /* WooPaymentsPayoutsOverviewViewModelTests.swift in Sources */, - 012ACB7A2E5C84D200A49458 /* MockPointOfSaleOrderListService.swift in Sources */, + 012ACB7A2E5C84D200A49458 /* MockPOSOrderListService.swift in Sources */, 261AA30E275506DE009530FE /* PaymentMethodsViewModelTests.swift in Sources */, 4524CDA1242D045C00B2F20A /* ProductStatusSettingListSelectorCommandTests.swift in Sources */, 26A280D72B46027A00ACEE87 /* OrderNotificationView.swift in Sources */, diff --git a/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderListControllerTests.swift b/WooCommerce/WooCommerceTests/POS/Controllers/POSOrderListControllerTests.swift similarity index 88% rename from WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderListControllerTests.swift rename to WooCommerce/WooCommerceTests/POS/Controllers/POSOrderListControllerTests.swift index 10b15ab916a..be8162bad3d 100644 --- a/WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleOrderListControllerTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Controllers/POSOrderListControllerTests.swift @@ -1,15 +1,15 @@ import Testing import Foundation @testable import WooCommerce -import enum Yosemite.PointOfSaleOrderListServiceError +import enum Yosemite.POSOrderListServiceError import struct NetworkingCore.Order import Observation import struct Yosemite.POSOrder -final class PointOfSaleOrderListControllerTests { - private let orderListService = MockPointOfSaleOrderListService() - private lazy var fetchStrategyFactory = MockPointOfSaleOrderListFetchStrategyFactory(orderService: orderListService) - private lazy var sut = PointOfSaleOrderListController(orderListFetchStrategyFactory: fetchStrategyFactory) +final class POSOrderListControllerTests { + private let orderListService = MockPOSOrderListService() + private lazy var fetchStrategyFactory = MockPOSOrderListFetchStrategyFactory(orderService: orderListService) + private lazy var sut = POSOrderListController(orderListFetchStrategyFactory: fetchStrategyFactory) @Test func loadOrders_requests_first_page_after_loading_two_pages() async throws { try #require(sut.ordersViewState.isLoading) @@ -25,7 +25,7 @@ final class PointOfSaleOrderListControllerTests { } @Test func loadOrders_results_in_loaded_state() async throws { - let expectedOrders = MockPointOfSaleOrderListService.makeInitialOrders() + let expectedOrders = MockPOSOrderListService.makeInitialOrders() orderListService.orderPages = [expectedOrders] try #require(sut.ordersViewState.isLoading) @@ -35,7 +35,7 @@ final class PointOfSaleOrderListControllerTests { } @Test func loadOrders_with_more_pages_sets_hasMoreItems() async throws { - let expectedOrders = MockPointOfSaleOrderListService.makeInitialOrders() + let expectedOrders = MockPOSOrderListService.makeInitialOrders() try #require(sut.ordersViewState.isLoading) orderListService.shouldSimulateTwoPages = true @@ -46,7 +46,7 @@ final class PointOfSaleOrderListControllerTests { @Test func loadOrders_when_called_multiple_times_then_orders_are_not_duplicated() async throws { try #require(sut.ordersViewState.isLoading) - let expectedOrders = MockPointOfSaleOrderListService.makeInitialOrders() + let expectedOrders = MockPOSOrderListService.makeInitialOrders() orderListService.orderPages = [expectedOrders] await sut.loadOrders() @@ -76,7 +76,7 @@ final class PointOfSaleOrderListControllerTests { } @Test func loadOrders_when_initial_orders_has_orders_but_no_more_pages_then_state_is_loaded_with_initial_orders() async throws { - let initialOrders = MockPointOfSaleOrderListService.makeInitialOrders() + let initialOrders = MockPOSOrderListService.makeInitialOrders() orderListService.orderPages = [initialOrders] try #require(sut.ordersViewState.isLoading) @@ -125,7 +125,7 @@ final class PointOfSaleOrderListControllerTests { } @Test func loadNextOrders_when_hasNextPage_is_false_then_does_not_fetch_next_page() async throws { - let expectedOrders = MockPointOfSaleOrderListService.makeInitialOrders() + let expectedOrders = MockPOSOrderListService.makeInitialOrders() orderListService.orderPages = [expectedOrders] await sut.loadOrders() @@ -160,7 +160,7 @@ final class PointOfSaleOrderListControllerTests { } @Test func loadOrders_when_error_occurs_with_existing_orders_then_shows_inline_error() async throws { - let initialOrders = MockPointOfSaleOrderListService.makeInitialOrders() + let initialOrders = MockPOSOrderListService.makeInitialOrders() orderListService.orderPages = [initialOrders] await sut.loadOrders() @@ -176,7 +176,7 @@ final class PointOfSaleOrderListControllerTests { } @Test func loadOrders_when_cached_data_available_then_shows_cached_data_with_loading_state() async throws { - let initialOrders = MockPointOfSaleOrderListService.makeInitialOrders() + let initialOrders = MockPOSOrderListService.makeInitialOrders() orderListService.orderPages = [initialOrders] // First load - should cache the data @@ -200,7 +200,7 @@ final class PointOfSaleOrderListControllerTests { } @Test func loadOrders_when_no_cached_data_then_starts_with_empty_loading_state() async throws { - let initialOrders = MockPointOfSaleOrderListService.makeInitialOrders() + let initialOrders = MockPOSOrderListService.makeInitialOrders() orderListService.orderPages = [initialOrders] // Initial state should be loading with empty orders @@ -222,8 +222,8 @@ final class PointOfSaleOrderListControllerTests { } @Test func loadOrders_cached_data_is_replaced_with_fresh_data() async throws { - let initialOrders = MockPointOfSaleOrderListService.makeInitialOrders() - let freshOrders = MockPointOfSaleOrderListService.makeSecondPageOrders() + let initialOrders = MockPOSOrderListService.makeInitialOrders() + let freshOrders = MockPOSOrderListService.makeSecondPageOrders() // First load orderListService.orderPages = [initialOrders] @@ -249,8 +249,8 @@ final class PointOfSaleOrderListControllerTests { @Test func clearSearchOrders_immediately_shows_cached_orders() async throws { // Given - let initialOrders = MockPointOfSaleOrderListService.makeInitialOrders() - let searchOrders = MockPointOfSaleOrderListService.makeSearchOrders() + let initialOrders = MockPOSOrderListService.makeInitialOrders() + let searchOrders = MockPOSOrderListService.makeSearchOrders() orderListService.orderPages = [initialOrders] orderListService.searchOrderPages = [searchOrders] @@ -302,7 +302,7 @@ final class PointOfSaleOrderListControllerTests { @Test func searchOrders_uses_search_strategy() async throws { // Given - let searchOrders = MockPointOfSaleOrderListService.makeSearchOrders() + let searchOrders = MockPOSOrderListService.makeSearchOrders() orderListService.searchOrderPages = [searchOrders] // When @@ -319,7 +319,7 @@ final class PointOfSaleOrderListControllerTests { @Test func updateOrder_when_order_loaded_from_API_then_order_list_updates() async throws { // Given - load initial orders - let initialOrders = MockPointOfSaleOrderListService.makeInitialOrders() + let initialOrders = MockPOSOrderListService.makeInitialOrders() orderListService.orderPages = [initialOrders] await sut.loadOrders() @@ -346,7 +346,7 @@ final class PointOfSaleOrderListControllerTests { @Test func updateOrder_when_order_loaded_from_API_then_selected_order_updates() async throws { // Given - let initialOrders = MockPointOfSaleOrderListService.makeInitialOrders() + let initialOrders = MockPOSOrderListService.makeInitialOrders() orderListService.orderPages = [initialOrders] await sut.loadOrders() diff --git a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderListController.swift b/WooCommerce/WooCommerceTests/POS/Mocks/MockPOSOrderListController.swift similarity index 89% rename from WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderListController.swift rename to WooCommerce/WooCommerceTests/POS/Mocks/MockPOSOrderListController.swift index dc4d87347fe..13ccd3cc58e 100644 --- a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderListController.swift +++ b/WooCommerce/WooCommerceTests/POS/Mocks/MockPOSOrderListController.swift @@ -2,7 +2,7 @@ import Foundation @testable import WooCommerce import struct Yosemite.POSOrder -final class MockPointOfSaleOrderListController: PointOfSaleSearchingOrderListControllerProtocol { +final class MockPOSOrderListController: POSOrderListControllerProtocol { var ordersViewState: POSOrderListState = .empty var selectedOrder: POSOrder? var updateOrderCalled = false diff --git a/WooCommerce/WooCommerceTests/POS/Mocks/MockPOSOrderListFetchStrategyFactory.swift b/WooCommerce/WooCommerceTests/POS/Mocks/MockPOSOrderListFetchStrategyFactory.swift new file mode 100644 index 00000000000..b70df2954e0 --- /dev/null +++ b/WooCommerce/WooCommerceTests/POS/Mocks/MockPOSOrderListFetchStrategyFactory.swift @@ -0,0 +1,56 @@ +import Foundation +@testable import WooCommerce +import protocol Yosemite.POSOrderListFetchStrategyFactoryProtocol +import protocol Yosemite.POSOrderListFetchStrategy +import protocol Yosemite.POSOrderListServiceProtocol +import struct NetworkingCore.PagedItems +import struct Yosemite.POSOrder + +final class MockPOSOrderListFetchStrategyFactory: POSOrderListFetchStrategyFactoryProtocol { + private let orderService: POSOrderListServiceProtocol + + init(orderService: POSOrderListServiceProtocol) { + self.orderService = orderService + } + + func defaultStrategy() -> POSOrderListFetchStrategy { + MockPOSOrderListFetchStrategy(orderService: orderService) + } + + func searchStrategy(searchTerm: String) -> POSOrderListFetchStrategy { + MockPOSOrderListSearchFetchStrategy(orderService: orderService, searchTerm: searchTerm) + } +} + +private struct MockPOSOrderListFetchStrategy: POSOrderListFetchStrategy { + let orderService: POSOrderListServiceProtocol + + var supportsCaching: Bool { true } + var showsLoadingWithItems: Bool { true } + var id: String = "default" + + func fetchOrders(pageNumber: Int) async throws -> PagedItems { + try await orderService.providePointOfSaleOrders(pageNumber: pageNumber) + } + + func loadOrder(orderID: Int64) async throws -> Yosemite.POSOrder { + try await orderService.loadOrder(orderID: orderID) + } +} + +private struct MockPOSOrderListSearchFetchStrategy: POSOrderListFetchStrategy { + let orderService: POSOrderListServiceProtocol + let searchTerm: String + + var supportsCaching: Bool { false } + var showsLoadingWithItems: Bool { false } + var id: String = "search" + + func fetchOrders(pageNumber: Int) async throws -> PagedItems { + try await orderService.searchPointOfSaleOrders(searchTerm: searchTerm, pageNumber: pageNumber) + } + + func loadOrder(orderID: Int64) async throws -> Yosemite.POSOrder { + try await orderService.loadOrder(orderID: orderID) + } +} diff --git a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderListService.swift b/WooCommerce/WooCommerceTests/POS/Mocks/MockPOSOrderListService.swift similarity index 92% rename from WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderListService.swift rename to WooCommerce/WooCommerceTests/POS/Mocks/MockPOSOrderListService.swift index 81ecb14778d..12c317228ed 100644 --- a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleOrderListService.swift +++ b/WooCommerce/WooCommerceTests/POS/Mocks/MockPOSOrderListService.swift @@ -5,7 +5,7 @@ import struct NetworkingCore.Order import enum NetworkingCore.OrderStatusEnum import WooFoundation -final class MockPointOfSaleOrderListService: PointOfSaleOrderListServiceProtocol { +final class MockPOSOrderListService: POSOrderListServiceProtocol { var orderPages: [[POSOrder]] = [] var searchOrderPages: [[POSOrder]] = [] var errorToThrow: Error? @@ -29,7 +29,7 @@ final class MockPointOfSaleOrderListService: PointOfSaleOrderListServiceProtocol spyCallCount += 1 if shouldThrowError { - throw PointOfSaleOrderListServiceError.requestFailed + throw POSOrderListServiceError.requestFailed } if let errorToThrow { @@ -43,19 +43,19 @@ final class MockPointOfSaleOrderListService: PointOfSaleOrderListServiceProtocol if shouldSimulateTwoPages { if shouldSimulateThreePages && pageNumber > 1 { return .init( - items: MockPointOfSaleOrderListService.makeSecondPageOrders(), + items: MockPOSOrderListService.makeSecondPageOrders(), hasMorePages: true, totalItems: 6 ) } else if pageNumber > 1 { return .init( - items: MockPointOfSaleOrderListService.makeSecondPageOrders(), + items: MockPOSOrderListService.makeSecondPageOrders(), hasMorePages: false, totalItems: 4 ) } else { return .init( - items: MockPointOfSaleOrderListService.makeInitialOrders(), + items: MockPOSOrderListService.makeInitialOrders(), hasMorePages: shouldSimulateTwoPages, totalItems: 4 ) @@ -76,7 +76,7 @@ final class MockPointOfSaleOrderListService: PointOfSaleOrderListServiceProtocol spyCallCount += 1 if shouldThrowError { - throw PointOfSaleOrderListServiceError.requestFailed + throw POSOrderListServiceError.requestFailed } if let errorToThrow { @@ -97,7 +97,7 @@ final class MockPointOfSaleOrderListService: PointOfSaleOrderListServiceProtocol } // For testing purposes, return filtered results based on search term - let allOrders = MockPointOfSaleOrderListService.makeInitialOrders() + let allOrders = MockPOSOrderListService.makeInitialOrders() let filteredOrders = allOrders.filter { order in order.number.contains(searchTerm) || order.customerEmail?.contains(searchTerm) == true || @@ -116,7 +116,7 @@ final class MockPointOfSaleOrderListService: PointOfSaleOrderListServiceProtocol lastLoadOrderID = orderID if shouldThrowError { - throw PointOfSaleOrderListServiceError.requestFailed + throw POSOrderListServiceError.requestFailed } if let errorToThrow { @@ -128,16 +128,16 @@ final class MockPointOfSaleOrderListService: PointOfSaleOrderListServiceProtocol } // Fallback - find order from existing orders - let allOrders = MockPointOfSaleOrderListService.makeInitialOrders() + MockPointOfSaleOrderListService.makeSecondPageOrders() + let allOrders = MockPOSOrderListService.makeInitialOrders() + MockPOSOrderListService.makeSecondPageOrders() if let foundOrder = allOrders.first(where: { $0.id == orderID }) { return foundOrder } - throw PointOfSaleOrderListServiceError.requestFailed + throw POSOrderListServiceError.requestFailed } } -extension MockPointOfSaleOrderListService { +extension MockPOSOrderListService { static func makeInitialOrders() -> [POSOrder] { let baseDate = Date(timeIntervalSince1970: 1672531200) // Fixed date: Jan 1, 2023 diff --git a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleOrderListModelTests.swift b/WooCommerce/WooCommerceTests/POS/Models/POSOrderListModelTests.swift similarity index 92% rename from WooCommerce/WooCommerceTests/POS/Models/PointOfSaleOrderListModelTests.swift rename to WooCommerce/WooCommerceTests/POS/Models/POSOrderListModelTests.swift index 281d7e3b775..b83b024be48 100644 --- a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleOrderListModelTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Models/POSOrderListModelTests.swift @@ -4,10 +4,10 @@ import Foundation import struct Yosemite.POSOrder import enum NetworkingCore.OrderStatusEnum -final class PointOfSaleOrderListModelTests { - private let mockOrdersController = MockPointOfSaleOrderListController() +final class POSOrderListModelTests { + private let mockOrdersController = MockPOSOrderListController() private let mockReceiptSender = MockPOSReceiptSender() - private lazy var sut = PointOfSaleOrderListModel( + private lazy var sut = POSOrderListModel( ordersController: mockOrdersController, receiptSender: mockReceiptSender ) @@ -53,7 +53,7 @@ final class PointOfSaleOrderListModelTests { mockOrdersController.shouldThrowError = true // When & Then - await #expect(throws: MockPointOfSaleOrderListController.TestError.updateOrderFailed) { + await #expect(throws: MockPOSOrderListController.TestError.updateOrderFailed) { try await sut.sendReceipt(order: testOrder, email: testEmail) }