Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// periphery:ignore:all
import Foundation
import GRDB
import protocol Storage.GRDBManagerProtocol
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,45 @@ import protocol Yosemite.PluginsServiceProtocol
import protocol Yosemite.PointOfSaleSettingsServiceProtocol
import struct Yosemite.POSReceiptInformation
import Observation
import protocol Storage.GRDBManagerProtocol
import protocol Yosemite.POSCatalogSyncCoordinatorProtocol
import class Yosemite.POSCatalogSettingsService

protocol PointOfSaleSettingsControllerProtocol {
var connectedCardReader: CardPresentPaymentCardReader? { get }
var storeViewModel: POSSettingsStoreViewModel { get }
var localCatalogViewModel: POSSettingsLocalCatalogViewModel? { get }
}

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

let storeViewModel: POSSettingsStoreViewModel
let localCatalogViewModel: POSSettingsLocalCatalogViewModel?

init(siteID: Int64,
settingsService: PointOfSaleSettingsServiceProtocol,
cardPresentPaymentService: CardPresentPaymentFacade,
pluginsService: PluginsServiceProtocol,
defaultSiteName: String?,
siteSettings: [SiteSetting]) {
siteSettings: [SiteSetting],
grdbManager: GRDBManagerProtocol?,
catalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol?) {
self.storeViewModel = POSSettingsStoreViewModel(siteID: siteID,
settingsService: settingsService,
pluginsService: pluginsService,
defaultSiteName: defaultSiteName,
siteSettings: siteSettings)
if let catalogSyncCoordinator, let grdbManager {
self.localCatalogViewModel = POSSettingsLocalCatalogViewModel(
siteID: siteID,
catalogSettingsService: POSCatalogSettingsService(grdbManager: grdbManager),
catalogSyncCoordinator: catalogSyncCoordinator
)
} else {
self.localCatalogViewModel = nil
}

observeCardReader(from: cardPresentPaymentService)
}
Expand Down Expand Up @@ -62,6 +78,8 @@ final class PointOfSaleSettingsPreviewController: PointOfSaleSettingsControllerP
pluginsService: PluginsServicePreview(),
defaultSiteName: "Sample Store",
siteSettings: [])

var localCatalogViewModel: POSSettingsLocalCatalogViewModel?
}

final class MockPointOfSaleSettingsService: PointOfSaleSettingsServiceProtocol {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import SwiftUI
struct POSSettingsLocalCatalogDetailView: View {
// TODO: WOOMOB-1335 - implement full sync cellular data setting functionality
@State private var allowFullSyncOnCellular: Bool = true
private let viewModel: POSSettingsLocalCatalogViewModel

init(viewModel: POSSettingsLocalCatalogViewModel) {
self.viewModel = viewModel
}

var body: some View {
NavigationStack {
Expand All @@ -21,6 +26,9 @@ struct POSSettingsLocalCatalogDetailView: View {
.background(Style.backgroundColor)
}
}
.task {
await viewModel.loadCatalogData()
}
}
}

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

VStack(spacing: POSSpacing.medium) {
// TODO: WOOMOB-1100 - replace with catalog data
fieldRowView(label: Localization.catalogSize, value: "1,250 products, 3,420 variations")
fieldRowView(label: Localization.lastIncrementalUpdate, value: "5 minutes ago")
fieldRowView(label: Localization.lastFullSync, value: "Today at 2:34 PM")
fieldRowView(label: Localization.catalogSize, value: viewModel.catalogSize)
fieldRowView(label: Localization.lastIncrementalUpdate, value: viewModel.lastIncrementalSyncDate)
fieldRowView(label: Localization.lastFullSync, value: viewModel.lastFullSyncDate)
}
.padding(.bottom, POSPadding.medium)
.redacted(reason: viewModel.isLoading ? .placeholder : [])
.shimmering(active: viewModel.isLoading)
}
}

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

Button(action: {
// Handle refresh catalog action
Task {
await viewModel.refreshCatalog()
}
}) {
Text(Localization.refreshCatalog)
}
.buttonStyle(POSFilledButtonStyle(size: .normal))
.buttonStyle(POSFilledButtonStyle(size: .normal, isLoading: viewModel.isRefreshingCatalog))
}
.padding(.horizontal, POSPadding.medium)
.padding(.bottom, POSPadding.medium)
Expand Down Expand Up @@ -187,6 +198,11 @@ private extension POSSettingsLocalCatalogDetailView {

#if DEBUG
#Preview {
POSSettingsLocalCatalogDetailView()
let viewModel = POSSettingsLocalCatalogViewModel(
siteID: 123,
catalogSettingsService: POSPreviewCatalogSettingsService(),
catalogSyncCoordinator: POSPreviewCatalogSyncCoordinator()
)
POSSettingsLocalCatalogDetailView(viewModel: viewModel)
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import Yosemite
import Foundation

@Observable
final class POSSettingsLocalCatalogViewModel {
private(set) var catalogSize: String = ""
private(set) var lastFullSyncDate: String = ""
private(set) var lastIncrementalSyncDate: String = ""

private(set) var isLoading: Bool = false
private(set) var isRefreshingCatalog: Bool = false

private let siteID: Int64
private let catalogSettingsService: POSCatalogSettingsServiceProtocol
private let catalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol
private let dateFormatter: RelativeDateTimeFormatter = {
let formatter = RelativeDateTimeFormatter()
formatter.dateTimeStyle = .named
formatter.unitsStyle = .full
return formatter
}()

init(siteID: Int64,
catalogSettingsService: POSCatalogSettingsServiceProtocol,
catalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol) {
self.siteID = siteID
self.catalogSettingsService = catalogSettingsService
self.catalogSyncCoordinator = catalogSyncCoordinator
}

@MainActor
func loadCatalogData() async {
isLoading = true
defer { isLoading = false }

do {
let catalogInfo = try await catalogSettingsService.loadCatalogInfo(for: siteID)
catalogSize = String(format: Localization.catalogSizeFormat, catalogInfo.productCount, catalogInfo.variationCount)
lastFullSyncDate = formatSyncDate(catalogInfo.lastFullSyncDate)
lastIncrementalSyncDate = formatSyncDate(catalogInfo.lastIncrementalSyncDate)
} catch {
DDLogError("⛔️ POSSettingsLocalCatalog: Error loading catalog data: \(error)")
catalogSize = Localization.catalogSizeUnavailable
lastFullSyncDate = Localization.syncDateUnavailable
lastIncrementalSyncDate = Localization.syncDateUnavailable
}
}

@MainActor
func refreshCatalog() async {
isRefreshingCatalog = true
defer { isRefreshingCatalog = false }

do {
try await catalogSyncCoordinator.performFullSync(for: siteID)
await loadCatalogData()
} catch {
DDLogError("⛔️ POSSettingsLocalCatalog: Failed to refresh catalog: \(error)")
}
}
}

private extension POSSettingsLocalCatalogViewModel {
func formatSyncDate(_ date: Date?) -> String {
guard let date else { return Localization.neverSynced }
return dateFormatter.localizedString(for: date, relativeTo: Date())
}
}

private extension POSSettingsLocalCatalogViewModel {
enum Localization {
static let catalogSizeFormat = NSLocalizedString(
"posSettingsLocalCatalogViewModel.catalogSizeFormat",
value: "%1$d products, %2$ld variations",
comment: "Format string for catalog size showing product count and variation count. " +
"%1$d will be replaced by the product count, and %2$ld will be replaced by the variation count."
)

static let catalogSizeUnavailable = NSLocalizedString(
"posSettingsLocalCatalogViewModel.catalogSizeUnavailable",
value: "Catalog size unavailable",
comment: "Text shown when catalog size cannot be determined."
)

static let neverSynced = NSLocalizedString(
"posSettingsLocalCatalogViewModel.neverSynced",
value: "Not synced",
comment: "Text shown when no sync has been performed yet."
)

static let syncDateUnavailable = NSLocalizedString(
"posSettingsLocalCatalogViewModel.syncDateUnavailable",
value: "Sync date unavailable",
comment: "Text shown when sync date cannot be determined."
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ extension PointOfSaleSettingsView {
)

// TODO: WOOMOB-1287 - integrate with local catalog feature eligibility
if ServiceLocator.featureFlagService.isFeatureFlagEnabled(.pointOfSaleLocalCatalogi1) {
if ServiceLocator.featureFlagService.isFeatureFlagEnabled(.pointOfSaleLocalCatalogi1) && settingsController.localCatalogViewModel != nil {
PointOfSaleSettingsCard(
item: .localCatalog,
isSelected: selection == .localCatalog,
Expand Down Expand Up @@ -89,7 +89,11 @@ extension PointOfSaleSettingsView {
case .hardware:
PointOfSaleSettingsHardwareDetailView(settingsController: settingsController)
case .localCatalog:
POSSettingsLocalCatalogDetailView()
if let viewModel = settingsController.localCatalogViewModel {
POSSettingsLocalCatalogDetailView(viewModel: viewModel)
} else {
EmptyView()
}
case .help:
PointOfSaleSettingsHelpDetailView()
default:
Expand Down
7 changes: 6 additions & 1 deletion WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import UIKit
import SwiftUI
import Yosemite
import class WooFoundation.CurrencySettings
import protocol Storage.GRDBManagerProtocol
import protocol Storage.StorageManagerType
import class WooFoundationCore.CurrencyFormatter
import struct NetworkingCore.JetpackSite
Expand Down Expand Up @@ -128,6 +129,8 @@ private extension POSTabCoordinator {
let pluginsService = PluginsService(storageManager: storageManager)
let siteTimezone = storesManager.sessionManager.defaultSite?.siteTimezone ?? .current

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

if let receiptService = POSReceiptService(siteID: siteID,
credentials: credentials,
Expand Down Expand Up @@ -188,7 +191,9 @@ private extension POSTabCoordinator {
cardPresentPaymentService: cardPresentPaymentService,
pluginsService: pluginsService,
defaultSiteName: storesManager.sessionManager.defaultSite?.name,
siteSettings: ServiceLocator.selectedSiteSettings.siteSettings),
siteSettings: ServiceLocator.selectedSiteSettings.siteSettings,
grdbManager: grdbManager,
catalogSyncCoordinator: catalogSyncCoordinator),
collectOrderPaymentAnalyticsTracker: collectPaymentAnalyticsAdaptor,
searchHistoryService: POSSearchHistoryService(siteID: siteID),
popularPurchasableItemsController: PointOfSaleItemsController(
Expand Down
35 changes: 35 additions & 0 deletions WooCommerce/Classes/POS/Utils/PreviewHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ import struct Yosemite.POSOrderRefund
import typealias Yosemite.OrderItemAttribute
import class Yosemite.POSOrderListService
import class Yosemite.POSOrderListFetchStrategyFactory
import protocol Yosemite.POSCatalogSyncCoordinatorProtocol
import protocol Yosemite.POSCatalogSettingsServiceProtocol
import struct Yosemite.POSCatalogInfo
import struct Yosemite.Site

// MARK: - PreviewProvider helpers
Expand Down Expand Up @@ -453,4 +456,36 @@ final class POSPreviewServices: POSDependencyProviding {
var externalViews: POSExternalViewProviding = EmptyPOSExternalView()
}

// MARK: - Preview Catalog Services

final class POSPreviewCatalogSettingsService: POSCatalogSettingsServiceProtocol {
func loadCatalogInfo(for siteID: Int64) async throws -> POSCatalogInfo {
let now = Date()
let lastFullSync = now.addingTimeInterval(-2 * 60 * 60) // 2 hours ago
let lastIncrementalSync = now.addingTimeInterval(-15 * 60) // 15 minutes ago
return POSCatalogInfo(
productCount: 247,
variationCount: 89,
lastFullSyncDate: lastFullSync,
lastIncrementalSyncDate: lastIncrementalSync
)
}
}

final class POSPreviewCatalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol {
func performFullSync(for siteID: Int64) async throws {
// Simulates a full sync operation with a 1 second delay.
try await Task.sleep(nanoseconds: 1_000_000_000)
}

func shouldPerformFullSync(for siteID: Int64, maxAge: TimeInterval) async -> Bool {
true
}

func performIncrementalSyncIfApplicable(for siteID: Int64, forceSync: Bool) async throws {
// Simulates an incremental sync operation with a 0.5 second delay.
try await Task.sleep(nanoseconds: 500_000_000)
}
}

#endif
Loading