Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6f9b492
Create posBodySmallBold with underline syle
staskus Oct 23, 2025
27855e0
Update existing posBodySmallBold usage
staskus Oct 23, 2025
8a85286
Create POSCatalogLoadingView to show the syncing catalog state
staskus Oct 23, 2025
10799b3
Allow observing full sync state update stream for POSCatalogSyncCoord…
staskus Oct 23, 2025
45fa0ec
Update POSCatalogSyncCoordinatorTests
staskus Oct 23, 2025
a6139be
Remove initial delay for POSCatalogSync when the app launches
staskus Oct 23, 2025
9b3f30a
Coordinate animation between loading and catalog loading transition
staskus Oct 23, 2025
669ffc0
Publish catalogSyncState from PointOfSaleAggregateModel
staskus Oct 23, 2025
debf355
Check syncState in PointOfSaleDashboardViewHelper
staskus Oct 23, 2025
ff44181
Integrate POSCatalogLoadingView in PointOfSaleDashboardView
staskus Oct 23, 2025
cde1ba7
Update PointOfSaleDashboardViewHelperTests
staskus Oct 23, 2025
5b3b8ae
Add a TODO for an error state handling
staskus Oct 23, 2025
acb40e7
Update POSCatalogSyncCoordinator.swift
staskus Oct 23, 2025
90f2949
Check error in syncFailed
staskus Oct 23, 2025
0232f01
Observe catalog sync state using Observation model
staskus Oct 24, 2025
3f3b654
Move catalogSyncState checking logic to ObservableItemsController
staskus Oct 24, 2025
644c9ee
Merge PointOfSaleLoading view and POSCatalogLoadingView
staskus Oct 24, 2025
e900ecb
Merge branch 'trunk' into woomob-1101-woo-poslocal-catalog-loading-sc…
joshheald Oct 30, 2025
8c1fe2c
Prevent saving first sync date if it doesn’t happen
joshheald Oct 30, 2025
6f0230e
Start syncs after updating catalog eligibility
joshheald Oct 30, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ protocol PointOfSaleSearchingItemsControllerProtocol: PointOfSaleItemsController

init(itemProvider: PointOfSaleItemServiceProtocol,
itemFetchStrategyFactory: PointOfSaleItemFetchStrategyFactoryProtocol,
initialState: ItemsViewState = ItemsViewState(containerState: .loading,
initialState: ItemsViewState = ItemsViewState(containerState: .loading(),
itemsStack: ItemsStackState(root: .initial,
itemStates: [:])),
analyticsProvider: POSAnalyticsProviding) {
Expand Down Expand Up @@ -213,7 +213,7 @@ private extension PointOfSaleItemsController {
func setRootLoadingState() {
let items = itemsViewState.itemsStack.root.items

let isInitialState = itemsViewState.containerState == .loading && itemsViewState.itemsStack.root == .initial
let isInitialState = itemsViewState.containerState == .loading() && itemsViewState.itemsStack.root == .initial
if isInitialState {
// Transition from initial to loading on first load
itemsViewState.itemsStack.root = .loading([])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ final class PointOfSaleObservableItemsController: PointOfSaleItemsControllerProt
currencySettings: currencySettings
)
self.catalogSyncCoordinator = catalogSyncCoordinator

preloadloadLastFullSyncState()
}

// periphery:ignore - used by tests
Expand All @@ -54,6 +56,8 @@ final class PointOfSaleObservableItemsController: PointOfSaleItemsControllerProt
self.siteID = siteID
self.dataSource = dataSource
self.catalogSyncCoordinator = catalogSyncCoordinator

preloadloadLastFullSyncState()
}

func loadItems(base: ItemListBaseItem) async {
Expand Down Expand Up @@ -117,13 +121,29 @@ final class PointOfSaleObservableItemsController: PointOfSaleItemsControllerProt
// MARK: - State Computation
private extension PointOfSaleObservableItemsController {
var containerState: ItemsContainerState {
// Use .loading during initial load, .content otherwise
if isInitialCatalogSync {
return .loading(isCatalogSyncing: true)
}

if !loadingState.productsLoaded && dataSource.isLoadingProducts {
return .loading
return .loading()
}
return .content
}

var isInitialCatalogSync: Bool {
guard let syncState = catalogSyncCoordinator.fullSyncStateModel.state[siteID] else {
return false
}

switch syncState {
case .syncStarted(_, true), .syncNeverDone:
return true
default:
return false
}
}

var rootState: ItemListState {
computeItemListState(
items: dataSource.productItems,
Expand Down Expand Up @@ -244,3 +264,12 @@ private extension PointOfSaleObservableItemsController {
var variationsLoaded = false
}
}

private extension PointOfSaleObservableItemsController {
func preloadloadLastFullSyncState() {
Task { @MainActor in
/// Ensure last full sync state is loaded with initial value
_ = await catalogSyncCoordinator.loadLastFullSyncState(for: siteID)
}
}
}
11 changes: 10 additions & 1 deletion Modules/Sources/PointOfSale/Models/ItemsContainerState.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import Foundation

enum ItemsContainerState {
case loading
case loading(isCatalogSyncing: Bool = false)
case error(PointOfSaleErrorState)
case content

var isCatalogSyncing: Bool {
switch self {
case .loading(let isCatalogSyncing):
return isCatalogSyncing
default:
return false
}
}
}

extension ItemsContainerState: Equatable {}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import enum Yosemite.POSItemType
import protocol Yosemite.PointOfSaleBarcodeScanServiceProtocol
import enum Yosemite.PointOfSaleBarcodeScanError
import protocol Yosemite.POSCatalogSyncCoordinatorProtocol
import class Yosemite.POSCatalogSyncCoordinator

protocol PointOfSaleAggregateModelProtocol {
var cart: Cart { get }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,44 @@
import SwiftUI

struct PointOfSaleLoadingView: View {
private let isCatalogSyncing: Bool
private let onExit: (() -> Void)?

init(isCatalogSyncing: Bool = false, onExit: (() -> Void)? = nil) {
self.isCatalogSyncing = isCatalogSyncing
self.onExit = onExit
}

var body: some View {
HStack(alignment: .center) {
Spacer()
VStack(alignment: .center) {
Spacer()
ProgressView()
.progressViewStyle(POSProgressViewStyle())
Spacer()

if isCatalogSyncing {
Spacer().frame(height: POSSpacing.large * 2)
Text(Localization.syncingTitle)
.font(.posHeadingBold)
Spacer()
VStack(spacing: POSSpacing.medium) {
Button {
onExit?()
} label: {
Text(Localization.exitButtonTitle)
.font(.posBodySmallBold(underline: true))
.foregroundStyle(Color.posOnSurface)
}

Text(Localization.exitButtonDescription)
.font(.posCaptionRegular)
.foregroundStyle(Color.posOnSurfaceVariantLowest)
}
.padding(.bottom, POSPadding.large)
} else {
Spacer()
}
}
.multilineTextAlignment(.center)
Spacer()
Expand All @@ -20,3 +50,29 @@ struct PointOfSaleLoadingView: View {
#Preview {
PointOfSaleLoadingView()
}

#Preview("Catalog Syncing") {
PointOfSaleLoadingView(isCatalogSyncing: true) {}
}

private extension PointOfSaleLoadingView {
struct Localization {
static let syncingTitle = NSLocalizedString(
"pointOfSale.catalogLoadingView.title",
value: "Syncing catalog",
comment: "A title of a full screen view that is displayed while the POS catalog is being synced."
)

static let exitButtonTitle = NSLocalizedString(
"pointOfSale.catalogLoadingView.exitButton.title",
value: "Exit POS",
comment: "A button that exits POS."
)

static let exitButtonDescription = NSLocalizedString(
"pointOfSale.catalogLoadingView.exitButton.description",
value: "Syncing will continue in the background.",
comment: "A description within a full screen loading view for POS catalog."
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ private extension CouponRowView {
static let horizontalPadding: CGFloat = POSPadding.medium
static let horizontalElementSpacing: CGFloat = POSSpacing.medium
static let cardContentHorizontalPadding: CGFloat = POSPadding.medium
static let titleFont: POSFontStyle = .posBodySmallBold
static let titleFont: POSFontStyle = .posBodySmallBold()
static let titleSummarySpacing: CGFloat = POSSpacing.xSmall
static let summaryFont: POSFontStyle = .posBodySmallRegular()
}
Expand Down
2 changes: 1 addition & 1 deletion Modules/Sources/PointOfSale/Presentation/ItemRowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ private extension ItemRowView {
static let horizontalElementSpacing: CGFloat = POSSpacing.medium
static let cardContentHorizontalPadding: CGFloat = POSPadding.medium
static let itemTitleAndPriceSpacing: CGFloat = POSSpacing.xSmall
static let itemTitleFont: POSFontStyle = .posBodySmallBold
static let itemTitleFont: POSFontStyle = .posBodySmallBold()
static let itemSubtitleFont: POSFontStyle = .posBodySmallRegular()
static let itemPriceFont: POSFontStyle = .posBodySmallRegular()
static let titleSubtitleLineLimit: Int = 4
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ private struct POSOrderRowView: View {
private var orderHeaderRow: some View {
HStack(alignment: .center) {
Text(POSOrderListView.Localization.orderTitle(order.number))
.font(.posBodySmallBold)
.font(.posBodySmallBold())
.foregroundStyle(Color.posOnSurface)
.fixedSize(horizontal: false, vertical: true)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ struct PointOfSaleDashboardView: View {
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@Environment(\.posAnalytics) private var analytics
@Environment(\.posExternalViews) private var externalViews
@Environment(\.dismiss) private var dismiss

@State private var showExitPOSModal: Bool = false
@State private var showSupport: Bool = false
Expand Down Expand Up @@ -39,7 +40,7 @@ struct PointOfSaleDashboardView: View {
// MARK: View State

enum ViewState: Equatable {
case loading
case loading(isCatalogSyncing: Bool = false)
case ineligible(reason: POSIneligibleReason)
case error(PointOfSaleErrorState)
case content
Expand All @@ -58,8 +59,11 @@ struct PointOfSaleDashboardView: View {
@Bindable var posModel = posModel
ZStack(alignment: .bottomLeading) {
switch viewState {
case .loading:
PointOfSaleLoadingView()
case .loading(let isCatalogSyncing):
PointOfSaleLoadingView(
isCatalogSyncing: isCatalogSyncing,
onExit: { dismiss() }
)
.transition(.opacity)
.ignoresSafeArea()
case .ineligible(let reason):
Expand Down Expand Up @@ -107,7 +111,7 @@ struct PointOfSaleDashboardView: View {
CGSizeMake(floatingSize.width + Constants.floatingControlHorizontalOffset,
floatingSize.height + Constants.floatingControlVerticalOffset))
.environment(\.posBackgroundAppearance, backgroundAppearance)
.animation(.easeInOut, value: viewState == .loading)
.animation(.easeInOut, value: viewState == .loading())
.background(Color.posSurface)
.navigationBarBackButtonHidden(true)
.posModal(item: $posModel.cardPresentPaymentOnboardingViewContainer, onDismiss: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ struct POSConnectivityView: View {
HStack(spacing: Constants.spacing) {
Image(systemName: "wifi.exclamationmark")
.foregroundColor(Color.posOnSecondaryContainer)
.font(.posBodySmallBold)
.font(.posBodySmallBold())

Text(Localization.title)
.foregroundColor(Color.posOnSecondaryContainer)
.font(.posBodySmallBold)
.font(.posBodySmallBold())
}
.padding(.vertical, Constants.verticalPadding)
.padding(.horizontal, Constants.horizontalPadding)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ private enum Localization {
VStack(alignment: .leading, spacing: Constants.textSpacing) {
Text("This is a subtitle that explains more about the notice.")
Text("Here's a hint about what to do next. Learn More")
.font(.posBodySmallBold)
.font(.posBodySmallBold())
.foregroundColor(Color.posPrimary)
}
}
Expand All @@ -126,7 +126,7 @@ private enum Localization {
VStack(alignment: .leading, spacing: Constants.textSpacing) {
Text("This is a subtitle that explains more about the notice.")
Text("Here's a hint about what to do next. Learn More")
.font(.posBodySmallBold)
.font(.posBodySmallBold())
.foregroundColor(Color.posPrimary)
}
}
Expand Down
9 changes: 6 additions & 3 deletions Modules/Sources/PointOfSale/Utils/POSFontStyle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ enum POSFontStyle {
case posBodyLargeRegular(underline: Bool = false)
case posBodyMediumBold
case posBodyMediumRegular(underline: Bool = false)
case posBodySmallBold
case posBodySmallBold(underline: Bool = false)
case posBodySmallRegular(underline: Bool = false)
case posCaptionBold
case posCaptionRegular
Expand Down Expand Up @@ -105,7 +105,8 @@ private struct POSScaledFont: ViewModifier {
switch style {
case .posBodyLargeRegular(let underline),
.posBodyMediumRegular(let underline),
.posBodySmallRegular(let underline):
.posBodySmallRegular(let underline),
.posBodySmallBold(let underline):
return underline
default:
return false
Expand Down Expand Up @@ -167,7 +168,9 @@ extension UIContentSizeCategory {
Text("Body Medium Regular Underline")
.font(.posBodyMediumRegular(underline: true))
Text("Body Small Bold")
.font(.posBodySmallBold)
.font(.posBodySmallBold())
Text("Body Small Bold Underline")
.font(.posBodySmallBold(underline: true))
Text("Body Small Regular")
.font(.posBodySmallRegular())
Text("Body Small Regular Underline")
Expand Down
12 changes: 10 additions & 2 deletions Modules/Sources/PointOfSale/Utils/PreviewHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import typealias Yosemite.OrderItemAttribute
import class Yosemite.POSOrderListService
import class Yosemite.POSOrderListFetchStrategyFactory
import protocol Yosemite.POSCatalogSyncCoordinatorProtocol
import enum Yosemite.POSCatalogSyncState
import class Yosemite.POSCatalogSyncStateModel
import protocol Yosemite.POSCatalogSettingsServiceProtocol
import struct Yosemite.POSCatalogInfo
import struct Yosemite.Site
Expand Down Expand Up @@ -100,7 +102,7 @@ struct PointOfSalePreviewPurchasableItemFetchStrategy: PointOfSalePurchasableIte
}

final class PointOfSalePreviewCouponsController: PointOfSaleCouponsControllerProtocol {
@Published var itemsViewState: ItemsViewState = ItemsViewState(containerState: .loading,
@Published var itemsViewState: ItemsViewState = ItemsViewState(containerState: .loading(),
itemsStack: ItemsStackState(root: .loading([]),
itemStates: [:]))
func enableCoupons() async { }
Expand All @@ -112,7 +114,7 @@ final class PointOfSalePreviewCouponsController: PointOfSaleCouponsControllerPro
}

final class PointOfSalePreviewItemsController: PointOfSaleSearchingItemsControllerProtocol {
@Published var itemsViewState: ItemsViewState = ItemsViewState(containerState: .loading,
@Published var itemsViewState: ItemsViewState = ItemsViewState(containerState: .loading(),
itemsStack: ItemsStackState(root: .loading([]),
itemStates: [:]))

Expand Down Expand Up @@ -627,6 +629,12 @@ final class POSPreviewCatalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol
// Simulates a smart sync operation with a 1 second delay.
try await Task.sleep(nanoseconds: 1_000_000_000)
}

let fullSyncStateModel = POSCatalogSyncStateModel()

func loadLastFullSyncState(for siteID: Int64) async -> POSCatalogSyncState {
return fullSyncStateModel.state[siteID] ?? .syncCompleted(siteID: siteID)
}
}

#endif
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,21 @@ struct PointOfSaleDashboardViewHelper {
itemsContainerState: ItemsContainerState,
horizontalSizeClass: UserInterfaceSizeClass?
) -> PointOfSaleDashboardView.ViewState {

guard case .regular = horizontalSizeClass else {
return .unsupportedWidth
}

guard let eligibilityState else {
return .loading
return .loading(isCatalogSyncing: itemsContainerState.isCatalogSyncing)
}

switch eligibilityState {
case .eligible:
// Check items container state
switch itemsContainerState {
case .loading:
return .loading
case let .loading(isCatalogSyncing):
return .loading(isCatalogSyncing: isCatalogSyncing)
case .error(let error):
return .error(error)
case .content:
Expand Down
Loading