Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Do Not Merge] POS MVSU experiment #14291

Draft
wants to merge 4 commits into
base: experiment/pos-aggregate-model-with-multiple-items
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions WooCommerce/Classes/POS/POSItemsService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Foundation
import protocol Yosemite.POSItemProvider
import protocol Yosemite.POSItem

struct POSItemsService {

private let itemProvider: POSItemProvider

init(itemProvider: POSItemProvider) {
self.itemProvider = itemProvider
}

@MainActor
func fetchItems(pageNumber: Int,
currentItems: [any POSDisplayableItem]) async throws -> [any POSDisplayableItem] {
var newItems = try await itemProvider.providePointOfSaleItems(pageNumber: pageNumber)
if pageNumber == 1 {
newItems.insert(POSDiscount(), at: 0)
}

let uniqueNewItems = newItems
.filter { newItem in
!currentItems.contains(where: { $0.id == newItem.itemID })
}
.compactMap(createPOSDisplayableItem(for:))

let allItems = currentItems + uniqueNewItems
return allItems
}
}
61 changes: 2 additions & 59 deletions WooCommerce/Classes/POS/PointOfSaleAggregateModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import Combine

import protocol Yosemite.POSOrderServiceProtocol
import protocol Yosemite.POSItem
import protocol Yosemite.POSItemProvider
import struct Yosemite.POSProduct
import struct Yosemite.Order
import struct Yosemite.OrderItem
Expand All @@ -19,28 +18,23 @@ final class PointOfSaleAggregateModel: ObservableObject {
}

@Published private(set) var orderStage: OrderStage = .building
private var allItems: [any POSDisplayableItem] = []
@Published private(set) var cart: [CartItem] = []
@Published private(set) var orderState: PointOfSaleOrderState = .idle
@Published private(set) var order: Order? = nil
@Published private(set) var connectionStatus: CardPresentPaymentReaderConnectionStatus = .disconnected
@Published private(set) var paymentState: PointOfSalePaymentState = .acceptingCard
@Published private(set) var itemListState: PointOfSaleItemListState = .initialLoading

private let orderService: POSOrderServiceProtocol
let cardPresentPaymentService: CardPresentPaymentFacade
private let itemProvider: POSItemProvider
private let analytics: Analytics

private var cancellables: Set<AnyCancellable> = []
private var startPaymentOnReaderConnection: AnyCancellable?
private var cardReaderDisconnection: AnyCancellable?

init(itemProvider: POSItemProvider,
cardPresentPaymentService: CardPresentPaymentFacade,
init(cardPresentPaymentService: CardPresentPaymentFacade,
orderService: POSOrderServiceProtocol,
analytics: Analytics) {
self.itemProvider = itemProvider
self.cardPresentPaymentService = cardPresentPaymentService
self.orderService = orderService
self.analytics = analytics
Expand All @@ -58,57 +52,6 @@ final class PointOfSaleAggregateModel: ObservableObject {
.store(in: &cancellables)
}

@MainActor
func loadInitialItems() async {
do {
itemListState = .initialLoading
try await fetchItems(pageNumber: 1)
} catch {
itemListState = .error(PointOfSaleErrorState.errorOnLoadingProducts())
}
}

@MainActor
func loadItems(pageNumber: Int) async {
do {
itemListState = .loading(allItems)
try await fetchItems(pageNumber: pageNumber)
} catch {
itemListState = .error(PointOfSaleErrorState.errorOnLoadingProducts())
}
}

@MainActor
private func fetchItems(pageNumber: Int) async throws {
var newItems = try await itemProvider.providePointOfSaleItems(pageNumber: pageNumber)
if pageNumber == 1 {
newItems.insert(POSDiscount(), at: 0)
}
let uniqueNewItems = newItems
.filter { newItem in
!allItems.contains(where: { $0.id == newItem.itemID })
}
.compactMap(createPOSDisplayableItem(for:))

allItems.append(contentsOf: uniqueNewItems)

if allItems.count == 0 {
itemListState = .empty
} else {
itemListState = .loaded(allItems)
}
}

@MainActor
func reloadItems() async {
removeAllItems()
await loadItems(pageNumber: 1)
}

func removeAllItems() {
allItems.removeAll()
}

func startNewOrder() {
clearOrder()
removeAllItemsFromCart()
Expand Down Expand Up @@ -137,7 +80,7 @@ final class PointOfSaleAggregateModel: ObservableObject {
@MainActor
func submitCart() async {
orderStage = .finalizing
await startSyncingOrder(with: cart, allItems: allItems.map { $0.item })
await startSyncingOrder(with: cart, allItems: cart.map { $0.item })
}

private func startSyncingOrder(with cartItems: [CartItem], allItems: [POSItem]) async {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,6 @@ private extension CardReaderConnectionStatusView {
import class WooFoundation.MockAnalyticsPreview
#Preview {
let posModel = PointOfSaleAggregateModel(
itemProvider: POSItemProviderPreview(),
cardPresentPaymentService: CardPresentPaymentPreviewService(),
orderService: POSOrderPreviewService(),
analytics: MockAnalyticsPreview())
Expand Down
2 changes: 0 additions & 2 deletions WooCommerce/Classes/POS/Presentation/CartView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,6 @@ import class WooFoundation.MockAnalyticsProviderPreview
// TODO:
// Simplify this by mocking `CartViewModel`
let posModel = PointOfSaleAggregateModel(
itemProvider: POSItemProviderPreview(),
cardPresentPaymentService: CardPresentPaymentPreviewService(),
orderService: POSOrderPreviewService(),
analytics: MockAnalyticsPreview())
Expand All @@ -279,7 +278,6 @@ import class WooFoundation.MockAnalyticsProviderPreview
currencyFormatter: .init(currencySettings: .init()))
let cartViewModel = CartViewModel(analytics: MockAnalyticsPreview(),
posModel: posModel)
let itemsListViewModel = ItemListViewModel(posModel: posModel)
let dashboardViewModel = PointOfSaleDashboardViewModel(
posModel: posModel,
connectivityObserver: POSConnectivityObserverPreview())
Expand Down
56 changes: 30 additions & 26 deletions WooCommerce/Classes/POS/Presentation/ItemListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,24 @@ import SwiftUI
import protocol Yosemite.POSItem

struct ItemListView: View {
@ObservedObject var viewModel: ItemListViewModel
@ObservedObject var posModel: PointOfSaleAggregateModel
@Environment(\.floatingControlAreaSize) var floatingControlAreaSize: CGSize
@Environment(\.dynamicTypeSize) private var dynamicTypeSize

init(viewModel: ItemListViewModel,
posModel: PointOfSaleAggregateModel) {
self.viewModel = viewModel
self.posModel = posModel
}
@Binding var itemListState: PointOfSaleItemListState

var loadNextItems: () async -> Void
var reloadItems: () async -> Void

@State private var isHeaderBannerDismissed: Bool = UserDefaults.standard.bool(forKey: BannerState.isSimpleProductsOnlyBannerDismissedKey)
@State private var showSimpleProductsModal: Bool = false

var body: some View {
VStack {
headerView
.posModal(isPresented: $viewModel.showSimpleProductsModal) {
SimpleProductsOnlyInformation(isPresented: $viewModel.showSimpleProductsModal)
.posModal(isPresented: $showSimpleProductsModal) {
SimpleProductsOnlyInformation(isPresented: $showSimpleProductsModal)
}
switch posModel.itemListState {
switch itemListState {
case .initialLoading, .empty, .error:
// These cases are handled directly in the dashboard, we do not render
// a specific view within the ItemListView to handle them
Expand All @@ -29,7 +29,7 @@ struct ItemListView: View {
}
}
.refreshable {
await posModel.reloadItems()
await reloadItems()
}
.background(Color.posPrimaryBackground)
.accessibilityElement(children: .contain)
Expand All @@ -44,10 +44,10 @@ private extension ItemListView {
VStack {
HStack {
POSHeaderTitleView()
if !viewModel.shouldShowHeaderBanner {
if isHeaderBannerDismissed {
Spacer()
Button(action: {
viewModel.simpleProductsInfoButtonTapped()
showSimpleProductsModal = true
}, label: {
Image(systemName: "info.circle")
.font(.posTitleRegular)
Expand All @@ -56,7 +56,7 @@ private extension ItemListView {
.padding(.trailing, Constants.infoIconPadding)
}
}
if !dynamicTypeSize.isAccessibilitySize, viewModel.shouldShowHeaderBanner {
if !dynamicTypeSize.isAccessibilitySize, !isHeaderBannerDismissed {
bannerCardView
.padding(.horizontal, Constants.bannerCardPadding)
}
Expand Down Expand Up @@ -92,7 +92,8 @@ private extension ItemListView {
.padding(.vertical, Constants.bannerVerticalPadding)
VStack {
Button(action: {
viewModel.dismissBanner()
isHeaderBannerDismissed = true
UserDefaults.standard.set(isHeaderBannerDismissed, forKey: BannerState.isSimpleProductsOnlyBannerDismissedKey)
}, label: {
Image(systemName: "xmark")
.font(.posBodyRegular)
Expand All @@ -110,7 +111,7 @@ private extension ItemListView {
.shadow(color: Color.black.opacity(0.08), radius: 4, y: 2)
.accessibilityAddTraits(.isButton)
.onTapGesture {
viewModel.simpleProductsInfoButtonTapped()
showSimpleProductsModal = true
}
.padding(.bottom, Constants.bannerCardPadding)
}
Expand All @@ -126,7 +127,7 @@ private extension ItemListView {
func listView(_ items: [any POSDisplayableItem]) -> some View {
ScrollView {
VStack {
if dynamicTypeSize.isAccessibilitySize, viewModel.shouldShowHeaderBanner {
if dynamicTypeSize.isAccessibilitySize, !isHeaderBannerDismissed {
bannerCardView
}
ForEach(items, id: \.id) { item in
Expand All @@ -138,13 +139,13 @@ private extension ItemListView {
.background(GeometryReader { proxy in
Color.clear
.onChange(of: proxy.frame(in: .global).maxY) { maxY in
if case .loading = posModel.itemListState {
if case .loading = itemListState {
return
}
let viewHeight = UIScreen.main.bounds.height
if maxY < viewHeight {
Task {
await viewModel.loadNextItems()
await loadNextItems()
}
}
}
Expand Down Expand Up @@ -204,15 +205,18 @@ private extension ItemListView {
}
}


extension ItemListView {
struct BannerState {
static let isSimpleProductsOnlyBannerDismissedKey = "isSimpleProductsOnlyBannerDismissed"
}
}

#if DEBUG
import class WooFoundation.MockAnalyticsPreview
#Preview {
let posModel = PointOfSaleAggregateModel(
itemProvider: POSItemProviderPreview(),
cardPresentPaymentService: CardPresentPaymentPreviewService(),
orderService: POSOrderPreviewService(),
analytics: MockAnalyticsPreview())
ItemListView(viewModel: ItemListViewModel(posModel: posModel),
posModel: posModel)
ItemListView(itemListState: .constant(.initialLoading),
loadNextItems: {},
reloadItems: {})
}
#endif
Loading