Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ private extension POSOrderDetailsView {

@ViewBuilder
func refundsSection(_ order: POSOrder) -> some View {
ForEach(order.refunds, id: \.refundID) { refund in
ForEach(order.refunds.sorted(by: { $0.refundID < $1.refundID }), id: \.refundID) { refund in
refundRow(refund: refund)
divider
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ struct POSOrderListEmptyViewModel: POSListEmptyViewModelProtocol {
}

var icon: Image {
PointOfSaleAssets.magnifierNotFound.decorativeImage
isSearching ? PointOfSaleAssets.magnifierNotFound.decorativeImage : PointOfSaleAssets.noOrders.decorativeImage
}
}

Expand All @@ -33,15 +33,15 @@ private enum Localization {
)

static let emptyOrdersSubtitle = NSLocalizedString(
"pos.orderListView.emptyOrdersSubtitle",
value: "Orders will appear here once you start processing sales on the POS.",
"pos.orderListView.emptyOrdersSubtitle2",
value: "Explore how you can increase your store sales.",
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."
"pos.orderListView.emptyOrdersButtonTitle2",
value: "Learn more",
comment: "Button text for opening an information view when orders when list is empty."
)

static let emptyOrdersSearchTitle = NSLocalizedString(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,15 @@ import struct Yosemite.POSOrder
import enum Yosemite.OrderPaymentMethod

struct POSOrderListView: View {
@Binding var isSearching: Bool
@Binding var searchTerm: String
let onClose: () -> Void

@Environment(POSOrderListModel.self) private var orderListModel
@Environment(\.keyboardObserver) private var keyboardObserver
@Environment(\.posAnalytics) private var analytics
@Environment(\.siteTimezone) private var siteTimezone
@StateObject private var infiniteScrollTriggerDeterminer = ThresholdInfiniteScrollTriggerDeterminer()

@State private var isSearching: Bool = false
@State private var searchTerm: String = ""
@Namespace private var searchTransition

private var ordersViewState: POSOrderListState {
orderListModel.ordersController.ordersViewState
}
Expand Down Expand Up @@ -46,8 +43,11 @@ struct POSOrderListView: View {
setSearch(true)
}
.accessibilityLabel(Localization.searchButtonAccessibilityLabel)
.matchedGeometryEffect(id: Constants.searchControlID, in: searchTransition)
.transition(.opacity.combined(with: .scale))
.transition(.asymmetric(
insertion: .scale.combined(with: .opacity)
.animation(.easeInOut(duration: Constants.animationDuration).delay(Constants.animationDuration)),
removal: .opacity.animation(.easeInOut(duration: Constants.animationDuration * 0.5))
))
}

if isSearching {
Expand All @@ -59,8 +59,12 @@ struct POSOrderListView: View {
}
)
.posSearchTextFieldUnfocusedBorderColor(.posOutlineVariant)
.matchedGeometryEffect(id: Constants.searchControlID, in: searchTransition)
.transition(.opacity.combined(with: .move(edge: .leading)))
.transition(.asymmetric(
insertion: .opacity.combined(with: .move(edge: .trailing))
.animation(.easeInOut(duration: Constants.animationDuration).delay(Constants.animationDuration * 0.5)),
removal: .opacity.combined(with: .move(edge: .trailing))
.animation(.easeInOut(duration: Constants.animationDuration))
))
.onChange(of: searchTerm) { _, newValue in
if newValue.isEmpty {
orderListModel.ordersController.clearSearchOrders()
Expand All @@ -71,23 +75,21 @@ struct POSOrderListView: View {
)
.animation(.easeInOut(duration: Constants.animationDuration), value: isSearching)

switch ordersViewState {
case .empty:
switch (ordersViewState, isSearching) {
case (.empty, true):
POSListEmptyView(
viewModel: POSOrderListEmptyViewModel(isSearching: isSearching)
viewModel: POSOrderListEmptyViewModel(isSearching: true)
) {
Task { @MainActor in
await orderListModel.ordersController.loadOrders()
}
}
.padding(.bottom, keyboardObserver.keyboardHeight)
case .error(let errorState):
case (.error(let errorState), true):
POSListErrorView(error: errorState) {
Task { @MainActor in
await orderListModel.ordersController.loadOrders()
}
}
.padding(.bottom, keyboardObserver.keyboardHeight)
default:
listView
}
Expand All @@ -99,9 +101,6 @@ struct POSOrderListView: View {
analytics.track(event: WooAnalyticsEvent.PointOfSale.ordersListPullToRefresh())
await orderListModel.ordersController.refreshOrders()
}
.task {
await orderListModel.ordersController.loadOrders()
}
}

@ViewBuilder
Expand All @@ -128,7 +127,7 @@ struct POSOrderListView: View {
await orderListModel.ordersController.loadNextOrders()
},
content: {
LazyVStack(spacing: POSSpacing.small) {
LazyVStack(spacing: POSSpacing.medium) {
headerRows
.id(Constants.scrollTopID)

Expand Down Expand Up @@ -384,7 +383,6 @@ private enum Constants {
static let orderCardMinHeight: CGFloat = 112
static let maximumOrderCardHeight: CGFloat = Constants.orderCardMinHeight * 2
static let animationDuration: CGFloat = 0.2
static let searchControlID = "searchControl"
static let scrollTopID = "orderListViewTopID"
}

Expand Down Expand Up @@ -450,7 +448,7 @@ private enum Localization {
#if DEBUG
#Preview("List") {
NavigationSplitView(columnVisibility: .constant(.all)) {
POSOrderListView(onClose: {})
POSOrderListView(isSearching: .constant(false), searchTerm: .constant(""), onClose: {})
.navigationSplitViewColumnWidth(450)
.environment(POSPreviewHelpers.makePreviewOrdersModel(state: POSPreviewHelpers.loadedState()))
} detail: {
Expand All @@ -459,19 +457,19 @@ private enum Localization {
.navigationSplitViewStyle(.balanced)
}

#Preview("Empty State") {
#Preview("Empty State in Search") {
NavigationSplitView(columnVisibility: .constant(.all)) {
POSOrderListView(onClose: {})
POSOrderListView(isSearching: .constant(true), searchTerm: .constant(""), onClose: {})
.navigationSplitViewColumnWidth(450)
.environment(POSPreviewHelpers.makePreviewOrdersModel(state: .empty))
} detail: {
Text("Detail View")
}
}

#Preview("Error State") {
#Preview("Error State in Search") {
NavigationSplitView(columnVisibility: .constant(.all)) {
POSOrderListView(onClose: {})
POSOrderListView(isSearching: .constant(true), searchTerm: .constant(""), onClose: {})
.navigationSplitViewColumnWidth(450)
.environment(POSPreviewHelpers.makePreviewOrdersModel(state: .error(.errorOnLoadingOrders())))
} detail: {
Expand All @@ -481,7 +479,7 @@ private enum Localization {

#Preview("Loading State") {
NavigationSplitView(columnVisibility: .constant(.all)) {
POSOrderListView(onClose: {})
POSOrderListView(isSearching: .constant(false), searchTerm: .constant(""), onClose: {})
.navigationSplitViewColumnWidth(450)
.environment(POSPreviewHelpers.makePreviewOrdersModel(state: .loading([])))
} detail: {
Expand All @@ -491,7 +489,7 @@ private enum Localization {

#Preview("Inline Error - Refresh") {
NavigationSplitView(columnVisibility: .constant(.all)) {
POSOrderListView(onClose: {})
POSOrderListView(isSearching: .constant(false), searchTerm: .constant(""), onClose: {})
.navigationSplitViewColumnWidth(450)
.environment(POSPreviewHelpers.makePreviewOrdersModel(
state: .inlineError(POSPreviewHelpers.makePreviewOrders(),
Expand All @@ -505,7 +503,7 @@ private enum Localization {

#Preview("Inline Error - Pagination") {
NavigationSplitView(columnVisibility: .constant(.all)) {
POSOrderListView(onClose: {})
POSOrderListView(isSearching: .constant(false), searchTerm: .constant(""), onClose: {})
.navigationSplitViewColumnWidth(450)
.environment(POSPreviewHelpers.makePreviewOrdersModel(
state: .inlineError(POSPreviewHelpers.makePreviewOrders(),
Expand All @@ -519,7 +517,7 @@ private enum Localization {

#Preview("Search Empty State") {
NavigationSplitView(columnVisibility: .constant(.all)) {
POSOrderListView(onClose: {})
POSOrderListView(isSearching: .constant(true), searchTerm: .constant(""), onClose: {})
.navigationSplitViewColumnWidth(450)
.environment(POSPreviewHelpers.makePreviewOrdersModel(state: .empty))
} detail: {
Expand Down
87 changes: 73 additions & 14 deletions Modules/Sources/PointOfSale/Presentation/Orders/POSOrdersView.swift
Original file line number Diff line number Diff line change
@@ -1,23 +1,37 @@
import SwiftUI
import UIKit
import struct WooFoundation.WooAnalyticsEvent
import struct WooFoundation.SafariView

struct POSOrdersView: View {
@Binding var isPresented: Bool
@Environment(POSOrderListModel.self) private var orderListModel
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@Environment(\.posAnalytics) private var analytics
@State private var isSearching: Bool = false
@State private var searchTerm: String = ""
@State private var showBlog = false

var body: some View {
switch orderListModel.ordersController.ordersViewState {
case .error(let error):
contentView
.task {
await orderListModel.ordersController.loadOrders()
}
}

@ViewBuilder
private var contentView: some View {
switch (orderListModel.ordersController.ordersViewState, isSearching) {
case (.error(let error), false):
errorView(error)
case (.empty, false):
emptyView()
default:
CustomNavigationSplitView(selection: Binding(
get: { orderListModel.ordersController.selectedOrder },
set: { orderListModel.ordersController.selectOrder($0) }
)) { _ in
POSOrderListView() {
POSOrderListView(isSearching: $isSearching, searchTerm: $searchTerm) {
isPresented = false
}
} detail: { selection in
Expand Down Expand Up @@ -65,20 +79,55 @@ struct POSOrdersView: View {

@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")
ZStack {
VStack {
Spacer()
POSListErrorView(error: error) {
Task { @MainActor in
await orderListModel.ordersController.loadOrders()
}
}
Spacer()
}

VStack {
POSPageHeaderView(
title: POSOrderListView.Localization.ordersTitle,
backButtonConfiguration: .init(state: .enabled, action: {
isPresented = false
}))
.posHeaderBackButtonIcon(systemName: "xmark")
Spacer()
}
}
}

POSListErrorView(error: error) {
Task { @MainActor in
await orderListModel.ordersController.loadOrders()
@ViewBuilder
private func emptyView() -> some View {
ZStack {
VStack {
Spacer()
POSListEmptyView(
viewModel: POSOrderListEmptyViewModel(isSearching: false)
) {
showBlog = true
}
Spacer()
}

VStack {
POSPageHeaderView(
title: POSOrderListView.Localization.ordersTitle,
backButtonConfiguration: .init(state: .enabled, action: {
isPresented = false
}))
.posHeaderBackButtonIcon(systemName: "xmark")
Spacer()
}
}
.posFullScreenCover(isPresented: $showBlog) {
SafariView(url: POSConstants.URLs.wooCommerceBlog.asURL())
}
}
}

Expand Down Expand Up @@ -158,8 +207,18 @@ private enum Constants {
}

#if DEBUG
#Preview("Orders View") {
#Preview("Orders View List") {
POSOrdersView(isPresented: .constant(true))
.environment(POSPreviewHelpers.makePreviewOrdersModel(state: POSPreviewHelpers.loadedState()))
}

#Preview("Orders View Empty") {
POSOrdersView(isPresented: .constant(true))
.environment(POSPreviewHelpers.makePreviewOrdersModel(state: .empty))
}

#Preview("Orders View Error") {
POSOrdersView(isPresented: .constant(true))
.environment(POSPreviewHelpers.makePreviewOrdersModel(state: .error(.errorOnLoadingOrders())))
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ enum PointOfSaleAssets: CaseIterable {
case netum1228BCHIDBarcode
case netum1228BCPairBarcode
case testEan13Barcode
case noOrders

var image: Image {
Image(imageName, bundle: .module)
Expand Down Expand Up @@ -67,6 +68,8 @@ enum PointOfSaleAssets: CaseIterable {
"netum-1228bc-hid-barcode"
case .netum1228BCPairBarcode:
"netum-1228bc-pair-barcode"
case .noOrders:
"pos-no-orders"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ struct POSListEmptyView: View {
}, label: {
Text(buttonTitle)
})
.buttonStyle(POSOutlinedButtonStyle(size: .normal))
.buttonStyle(POSFilledButtonStyle(size: .normal))
.frame(width: viewWidth / 2)
.padding([.leading, .trailing])
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "pos-no-orders.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.
4 changes: 4 additions & 0 deletions Modules/Sources/PointOfSale/Utils/POSConstants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ enum POSConstants {
///
case inPersonPaymentsLearnMoreWCPay =
"https://woocommerce.com/document/woocommerce-payments/in-person-payments/getting-started-with-in-person-payments/"

/// URL for WooCommerce blog
///
case wooCommerceBlog = "https://woocommerce.com/blog/"
}
}

Expand Down