Skip to content

Commit 07b5fa9

Browse files
authored
[Local Catalog] POS Settings: integrate with catalog data and full sync functionality (#16144)
2 parents 83cfd68 + 6293042 commit 07b5fa9

File tree

12 files changed

+486
-22
lines changed

12 files changed

+486
-22
lines changed

Modules/Sources/Yosemite/Tools/POS/POSCatalogSettingsService.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// periphery:ignore:all
21
import Foundation
32
import GRDB
43
import protocol Storage.GRDBManagerProtocol

WooCommerce/Classes/POS/Models/PointOfSaleSettingsController.swift

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,45 @@ import protocol Yosemite.PluginsServiceProtocol
77
import protocol Yosemite.PointOfSaleSettingsServiceProtocol
88
import struct Yosemite.POSReceiptInformation
99
import Observation
10+
import protocol Storage.GRDBManagerProtocol
11+
import protocol Yosemite.POSCatalogSyncCoordinatorProtocol
12+
import class Yosemite.POSCatalogSettingsService
1013

1114
protocol PointOfSaleSettingsControllerProtocol {
1215
var connectedCardReader: CardPresentPaymentCardReader? { get }
1316
var storeViewModel: POSSettingsStoreViewModel { get }
17+
var localCatalogViewModel: POSSettingsLocalCatalogViewModel? { get }
1418
}
1519

1620
@Observable final class PointOfSaleSettingsController: PointOfSaleSettingsControllerProtocol {
1721
private(set) var connectedCardReader: CardPresentPaymentCardReader?
1822
private var cancellables: AnyCancellable?
1923

2024
let storeViewModel: POSSettingsStoreViewModel
25+
let localCatalogViewModel: POSSettingsLocalCatalogViewModel?
2126

2227
init(siteID: Int64,
2328
settingsService: PointOfSaleSettingsServiceProtocol,
2429
cardPresentPaymentService: CardPresentPaymentFacade,
2530
pluginsService: PluginsServiceProtocol,
2631
defaultSiteName: String?,
27-
siteSettings: [SiteSetting]) {
32+
siteSettings: [SiteSetting],
33+
grdbManager: GRDBManagerProtocol?,
34+
catalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol?) {
2835
self.storeViewModel = POSSettingsStoreViewModel(siteID: siteID,
2936
settingsService: settingsService,
3037
pluginsService: pluginsService,
3138
defaultSiteName: defaultSiteName,
3239
siteSettings: siteSettings)
40+
if let catalogSyncCoordinator, let grdbManager {
41+
self.localCatalogViewModel = POSSettingsLocalCatalogViewModel(
42+
siteID: siteID,
43+
catalogSettingsService: POSCatalogSettingsService(grdbManager: grdbManager),
44+
catalogSyncCoordinator: catalogSyncCoordinator
45+
)
46+
} else {
47+
self.localCatalogViewModel = nil
48+
}
3349

3450
observeCardReader(from: cardPresentPaymentService)
3551
}
@@ -62,6 +78,8 @@ final class PointOfSaleSettingsPreviewController: PointOfSaleSettingsControllerP
6278
pluginsService: PluginsServicePreview(),
6379
defaultSiteName: "Sample Store",
6480
siteSettings: [])
81+
82+
var localCatalogViewModel: POSSettingsLocalCatalogViewModel?
6583
}
6684

6785
final class MockPointOfSaleSettingsService: PointOfSaleSettingsServiceProtocol {

WooCommerce/Classes/POS/Presentation/Settings/POSSettingsLocalCatalogDetailView.swift

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ import SwiftUI
33
struct POSSettingsLocalCatalogDetailView: View {
44
// TODO: WOOMOB-1335 - implement full sync cellular data setting functionality
55
@State private var allowFullSyncOnCellular: Bool = true
6+
private let viewModel: POSSettingsLocalCatalogViewModel
7+
8+
init(viewModel: POSSettingsLocalCatalogViewModel) {
9+
self.viewModel = viewModel
10+
}
611

712
var body: some View {
813
NavigationStack {
@@ -21,6 +26,9 @@ struct POSSettingsLocalCatalogDetailView: View {
2126
.background(Style.backgroundColor)
2227
}
2328
}
29+
.task {
30+
await viewModel.loadCatalogData()
31+
}
2432
}
2533
}
2634

@@ -31,12 +39,13 @@ private extension POSSettingsLocalCatalogDetailView {
3139
sectionHeaderView(title: Localization.catalogStatus)
3240

3341
VStack(spacing: POSSpacing.medium) {
34-
// TODO: WOOMOB-1100 - replace with catalog data
35-
fieldRowView(label: Localization.catalogSize, value: "1,250 products, 3,420 variations")
36-
fieldRowView(label: Localization.lastIncrementalUpdate, value: "5 minutes ago")
37-
fieldRowView(label: Localization.lastFullSync, value: "Today at 2:34 PM")
42+
fieldRowView(label: Localization.catalogSize, value: viewModel.catalogSize)
43+
fieldRowView(label: Localization.lastIncrementalUpdate, value: viewModel.lastIncrementalSyncDate)
44+
fieldRowView(label: Localization.lastFullSync, value: viewModel.lastFullSyncDate)
3845
}
3946
.padding(.bottom, POSPadding.medium)
47+
.redacted(reason: viewModel.isLoading ? .placeholder : [])
48+
.shimmering(active: viewModel.isLoading)
4049
}
4150
}
4251

@@ -64,11 +73,13 @@ private extension POSSettingsLocalCatalogDetailView {
6473
.frame(maxWidth: .infinity, alignment: .leading)
6574

6675
Button(action: {
67-
// Handle refresh catalog action
76+
Task {
77+
await viewModel.refreshCatalog()
78+
}
6879
}) {
6980
Text(Localization.refreshCatalog)
7081
}
71-
.buttonStyle(POSFilledButtonStyle(size: .normal))
82+
.buttonStyle(POSFilledButtonStyle(size: .normal, isLoading: viewModel.isRefreshingCatalog))
7283
}
7384
.padding(.horizontal, POSPadding.medium)
7485
.padding(.bottom, POSPadding.medium)
@@ -134,20 +145,20 @@ private extension POSSettingsLocalCatalogDetailView {
134145
)
135146

136147
static let managingDataUsage = NSLocalizedString(
137-
"posSettingsLocalCatalogDetailView.managingDataUsage",
138-
value: "Managing data usage",
148+
"posSettingsLocalCatalogDetailView.managingDataUsage.1",
149+
value: "Managing Data Usage",
139150
comment: "Section title for managing data usage in Point of Sale settings."
140151
)
141152

142153
static let lastIncrementalUpdate = NSLocalizedString(
143-
"posSettingsLocalCatalogDetailView.lastIncrementalUpdate",
144-
value: "Last incremental update",
154+
"posSettingsLocalCatalogDetailView.lastIncrementalSync",
155+
value: "Last update",
145156
comment: "Label for last incremental update field in Point of Sale settings."
146157
)
147158

148159
static let lastFullSync = NSLocalizedString(
149-
"posSettingsLocalCatalogDetailView.lastFullSync",
150-
value: "Last full sync",
160+
"posSettingsLocalCatalogDetailView.lastFullSync.1",
161+
value: "Last full update",
151162
comment: "Label for last full sync field in Point of Sale settings."
152163
)
153164

@@ -159,8 +170,8 @@ private extension POSSettingsLocalCatalogDetailView {
159170

160171

161172
static let allowFullSyncOnCellular = NSLocalizedString(
162-
"posSettingsLocalCatalogDetailView.allowFullSyncOnCellular",
163-
value: "Allow full sync on cellular data",
173+
"posSettingsLocalCatalogDetailView.allowFullSyncOnCellular.1",
174+
value: "Allow full update on cellular data",
164175
comment: "Label for allow full sync on cellular data toggle in Point of Sale settings."
165176
)
166177

@@ -187,6 +198,11 @@ private extension POSSettingsLocalCatalogDetailView {
187198

188199
#if DEBUG
189200
#Preview {
190-
POSSettingsLocalCatalogDetailView()
201+
let viewModel = POSSettingsLocalCatalogViewModel(
202+
siteID: 123,
203+
catalogSettingsService: POSPreviewCatalogSettingsService(),
204+
catalogSyncCoordinator: POSPreviewCatalogSyncCoordinator()
205+
)
206+
POSSettingsLocalCatalogDetailView(viewModel: viewModel)
191207
}
192208
#endif
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import Yosemite
2+
import Foundation
3+
4+
@Observable
5+
final class POSSettingsLocalCatalogViewModel {
6+
private(set) var catalogSize: String = ""
7+
private(set) var lastFullSyncDate: String = ""
8+
private(set) var lastIncrementalSyncDate: String = ""
9+
10+
private(set) var isLoading: Bool = false
11+
private(set) var isRefreshingCatalog: Bool = false
12+
13+
private let siteID: Int64
14+
private let catalogSettingsService: POSCatalogSettingsServiceProtocol
15+
private let catalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol
16+
private let dateFormatter: RelativeDateTimeFormatter = {
17+
let formatter = RelativeDateTimeFormatter()
18+
formatter.dateTimeStyle = .named
19+
formatter.unitsStyle = .full
20+
return formatter
21+
}()
22+
23+
init(siteID: Int64,
24+
catalogSettingsService: POSCatalogSettingsServiceProtocol,
25+
catalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol) {
26+
self.siteID = siteID
27+
self.catalogSettingsService = catalogSettingsService
28+
self.catalogSyncCoordinator = catalogSyncCoordinator
29+
}
30+
31+
@MainActor
32+
func loadCatalogData() async {
33+
isLoading = true
34+
defer { isLoading = false }
35+
36+
do {
37+
let catalogInfo = try await catalogSettingsService.loadCatalogInfo(for: siteID)
38+
catalogSize = String(format: Localization.catalogSizeFormat, catalogInfo.productCount, catalogInfo.variationCount)
39+
lastFullSyncDate = formatSyncDate(catalogInfo.lastFullSyncDate)
40+
lastIncrementalSyncDate = formatSyncDate(catalogInfo.lastIncrementalSyncDate)
41+
} catch {
42+
DDLogError("⛔️ POSSettingsLocalCatalog: Error loading catalog data: \(error)")
43+
catalogSize = Localization.catalogSizeUnavailable
44+
lastFullSyncDate = Localization.syncDateUnavailable
45+
lastIncrementalSyncDate = Localization.syncDateUnavailable
46+
}
47+
}
48+
49+
@MainActor
50+
func refreshCatalog() async {
51+
isRefreshingCatalog = true
52+
defer { isRefreshingCatalog = false }
53+
54+
do {
55+
try await catalogSyncCoordinator.performFullSync(for: siteID)
56+
await loadCatalogData()
57+
} catch {
58+
DDLogError("⛔️ POSSettingsLocalCatalog: Failed to refresh catalog: \(error)")
59+
}
60+
}
61+
}
62+
63+
private extension POSSettingsLocalCatalogViewModel {
64+
func formatSyncDate(_ date: Date?) -> String {
65+
guard let date else { return Localization.neverSynced }
66+
return dateFormatter.localizedString(for: date, relativeTo: Date())
67+
}
68+
}
69+
70+
private extension POSSettingsLocalCatalogViewModel {
71+
enum Localization {
72+
static let catalogSizeFormat = NSLocalizedString(
73+
"posSettingsLocalCatalogViewModel.catalogSizeFormat",
74+
value: "%1$d products, %2$ld variations",
75+
comment: "Format string for catalog size showing product count and variation count. " +
76+
"%1$d will be replaced by the product count, and %2$ld will be replaced by the variation count."
77+
)
78+
79+
static let catalogSizeUnavailable = NSLocalizedString(
80+
"posSettingsLocalCatalogViewModel.catalogSizeUnavailable",
81+
value: "Catalog size unavailable",
82+
comment: "Text shown when catalog size cannot be determined."
83+
)
84+
85+
static let neverSynced = NSLocalizedString(
86+
"posSettingsLocalCatalogViewModel.neverSynced",
87+
value: "Not updated",
88+
comment: "Text shown when no update has been performed yet."
89+
)
90+
91+
static let syncDateUnavailable = NSLocalizedString(
92+
"posSettingsLocalCatalogViewModel.syncDateUnavailable",
93+
value: "Update date unavailable",
94+
comment: "Text shown when update date cannot be determined."
95+
)
96+
}
97+
}

WooCommerce/Classes/POS/Presentation/Settings/PointOfSaleSettingsView.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ extension PointOfSaleSettingsView {
5555
)
5656

5757
// TODO: WOOMOB-1287 - integrate with local catalog feature eligibility
58-
if ServiceLocator.featureFlagService.isFeatureFlagEnabled(.pointOfSaleLocalCatalogi1) {
58+
if ServiceLocator.featureFlagService.isFeatureFlagEnabled(.pointOfSaleLocalCatalogi1) && settingsController.localCatalogViewModel != nil {
5959
PointOfSaleSettingsCard(
6060
item: .localCatalog,
6161
isSelected: selection == .localCatalog,
@@ -89,7 +89,11 @@ extension PointOfSaleSettingsView {
8989
case .hardware:
9090
PointOfSaleSettingsHardwareDetailView(settingsController: settingsController)
9191
case .localCatalog:
92-
POSSettingsLocalCatalogDetailView()
92+
if let viewModel = settingsController.localCatalogViewModel {
93+
POSSettingsLocalCatalogDetailView(viewModel: viewModel)
94+
} else {
95+
EmptyView()
96+
}
9397
case .help:
9498
PointOfSaleSettingsHelpDetailView()
9599
default:

WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import UIKit
33
import SwiftUI
44
import Yosemite
55
import class WooFoundation.CurrencySettings
6+
import protocol Storage.GRDBManagerProtocol
67
import protocol Storage.StorageManagerType
78
import class WooFoundationCore.CurrencyFormatter
89
import struct NetworkingCore.JetpackSite
@@ -171,6 +172,8 @@ private extension POSTabCoordinator {
171172
let pluginsService = PluginsService(storageManager: storageManager)
172173
let siteTimezone = storesManager.sessionManager.defaultSite?.siteTimezone ?? .current
173174

175+
let grdbManager: GRDBManagerProtocol? = serviceAdaptor.featureFlags.isFeatureFlagEnabled(.pointOfSaleLocalCatalogi1) ? ServiceLocator.grdbManager : nil
176+
let catalogSyncCoordinator = ServiceLocator.posCatalogSyncCoordinator
174177

175178
if let receiptService = POSReceiptService(siteID: siteID,
176179
credentials: credentials,
@@ -230,7 +233,9 @@ private extension POSTabCoordinator {
230233
cardPresentPaymentService: cardPresentPaymentService,
231234
pluginsService: pluginsService,
232235
defaultSiteName: storesManager.sessionManager.defaultSite?.name,
233-
siteSettings: ServiceLocator.selectedSiteSettings.siteSettings),
236+
siteSettings: ServiceLocator.selectedSiteSettings.siteSettings,
237+
grdbManager: grdbManager,
238+
catalogSyncCoordinator: catalogSyncCoordinator),
234239
collectOrderPaymentAnalyticsTracker: collectPaymentAnalyticsAdaptor,
235240
searchHistoryService: POSSearchHistoryService(siteID: siteID),
236241
popularPurchasableItemsController: PointOfSaleItemsController(

WooCommerce/Classes/POS/Utils/PreviewHelpers.swift

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ import struct Yosemite.POSOrderRefund
2828
import typealias Yosemite.OrderItemAttribute
2929
import class Yosemite.POSOrderListService
3030
import class Yosemite.POSOrderListFetchStrategyFactory
31+
import protocol Yosemite.POSCatalogSyncCoordinatorProtocol
32+
import protocol Yosemite.POSCatalogSettingsServiceProtocol
33+
import struct Yosemite.POSCatalogInfo
3134
import struct Yosemite.Site
3235

3336
// MARK: - PreviewProvider helpers
@@ -453,4 +456,36 @@ final class POSPreviewServices: POSDependencyProviding {
453456
var externalViews: POSExternalViewProviding = EmptyPOSExternalView()
454457
}
455458

459+
// MARK: - Preview Catalog Services
460+
461+
final class POSPreviewCatalogSettingsService: POSCatalogSettingsServiceProtocol {
462+
func loadCatalogInfo(for siteID: Int64) async throws -> POSCatalogInfo {
463+
let now = Date()
464+
let lastFullSync = now.addingTimeInterval(-2 * 60 * 60) // 2 hours ago
465+
let lastIncrementalSync = now.addingTimeInterval(-15 * 60) // 15 minutes ago
466+
return POSCatalogInfo(
467+
productCount: 247,
468+
variationCount: 89,
469+
lastFullSyncDate: lastFullSync,
470+
lastIncrementalSyncDate: lastIncrementalSync
471+
)
472+
}
473+
}
474+
475+
final class POSPreviewCatalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol {
476+
func performFullSync(for siteID: Int64) async throws {
477+
// Simulates a full sync operation with a 1 second delay.
478+
try await Task.sleep(nanoseconds: 1_000_000_000)
479+
}
480+
481+
func shouldPerformFullSync(for siteID: Int64, maxAge: TimeInterval) async -> Bool {
482+
true
483+
}
484+
485+
func performIncrementalSyncIfApplicable(for siteID: Int64, forceSync: Bool) async throws {
486+
// Simulates an incremental sync operation with a 0.5 second delay.
487+
try await Task.sleep(nanoseconds: 500_000_000)
488+
}
489+
}
490+
456491
#endif

0 commit comments

Comments
 (0)