Skip to content
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
e387d4e
Add actions for POS sync eligibility date tracking
joshheald Oct 24, 2025
fbbf8c0
Track POS last opened date when tab is selected
joshheald Oct 24, 2025
50c50fa
Add eligibility parameters to POSCatalogSyncCoordinator
joshheald Oct 24, 2025
6886c35
Implement 30-day sync eligibility logic in coordinator
joshheald Oct 24, 2025
c86b6fd
Wire up catalog eligibility checker to sync coordinator
joshheald Oct 24, 2025
a083191
Fix actor isolation for setCatalogEligibilityChecker
joshheald Oct 24, 2025
044cef4
Add comprehensive tests for POS catalog sync eligibility
joshheald Oct 24, 2025
f22ea9e
Apply eligibility checks to all sync types and remove catalog size ch…
joshheald Oct 24, 2025
6d8663e
Lint fixes
joshheald Oct 27, 2025
35fabd9
Disable line length checks in generated code
joshheald Oct 27, 2025
a3d887f
Assume site is ineligible when no checker found
joshheald Oct 27, 2025
0ab1fb5
Fix warning
joshheald Oct 27, 2025
e5023bc
Update to make the catalog eligibility checker
joshheald Oct 27, 2025
d64f3f5
Refactor POS catalog eligibility checker to eliminate late binding
joshheald Oct 27, 2025
c28cb1f
Remove unused eligibility checking protocol
joshheald Oct 28, 2025
c75ce3d
Use an actor without any nonisolated functions
joshheald Oct 28, 2025
0645921
Make eligibility service required
joshheald Oct 28, 2025
ebe90a2
Pass siteID when checking catalog eligibility
joshheald Oct 28, 2025
654f89f
Remove unused mocks
joshheald Oct 28, 2025
f6df1ce
Use POS tab eligibility, not visibility
joshheald Oct 28, 2025
a4e6207
Assume ineligible to avoid bugs in race condition
joshheald Oct 28, 2025
80d776a
Remove errors that are never returned from action
joshheald Oct 29, 2025
8394d94
Pass feature flag value directly in tests
joshheald Oct 29, 2025
1aae672
Set POS eligibility in tests to allow them to run
joshheald Oct 29, 2025
0e2e22b
Move catalog eligibility tests to Yosemite
joshheald Oct 29, 2025
1eae7e0
Remove unused mock
joshheald Oct 29, 2025
abadef0
Apply suggestions from code review
joshheald Oct 29, 2025
5594f7a
Fix lint and variable name
joshheald Oct 29, 2025
a6de570
Make stores a `let` instead of `private(set) var`
joshheald Oct 30, 2025
bc226bc
Fix line spacing in sync eligibility function
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice to edit this directly from the template 💯

Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ let specsToGenerate: [CopiableSpec] = matchingTypes.map { type in
import <%= module %>
<% } -%>

// swiftlint:disable line_length
<% for copiableSpec in specsToGenerate { -%>

extension <%= copiableSpec.name %> {
Expand All @@ -231,3 +232,5 @@ extension <%= copiableSpec.name %> {
}
}
<% } -%>

// swiftlint:enable line_length
4 changes: 4 additions & 0 deletions Modules/Sources/Codegen/Sourcery/Fakes/Fakes.swifttemplate
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ import Networking
import Hardware
import WooFoundation

// swiftlint:disable line_length

<% for spec in specsToGenerate { -%>
extension <%= spec.name -%> {
/// Returns a "ready to use" type filled with fake values.
Expand All @@ -115,3 +117,5 @@ extension <%= spec.name -%> {
}
<% } -%>
<% } -%>

// swiftlint:enable line_length
4 changes: 4 additions & 0 deletions Modules/Sources/Fakes/Hardware.generated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import Networking
import Hardware
import WooFoundation

// swiftlint:disable line_length

extension Hardware.CardBrand {
/// Returns a "ready to use" type filled with fake values.
///
Expand Down Expand Up @@ -97,3 +99,5 @@ extension Hardware.PaymentMethod {
.card
}
}

// swiftlint:enable line_length
4 changes: 4 additions & 0 deletions Modules/Sources/Fakes/Networking.generated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import Networking
import Hardware
import WooFoundation

// swiftlint:disable line_length

extension Networking.AIProduct {
/// Returns a "ready to use" type filled with fake values.
///
Expand Down Expand Up @@ -2611,3 +2613,5 @@ extension Networking.WordPressTheme {
)
}
}

// swiftlint:enable line_length
4 changes: 4 additions & 0 deletions Modules/Sources/Fakes/NetworkingCore.generated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import Networking
import Hardware
import WooFoundation

// swiftlint:disable line_length

extension NetworkingCore.Account {
/// Returns a "ready to use" type filled with fake values.
///
Expand Down Expand Up @@ -593,3 +595,5 @@ extension NetworkingCore.User {
)
}
}

// swiftlint:enable line_length
2 changes: 2 additions & 0 deletions Modules/Sources/Fakes/WooFoundation.generated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
// DO NOT EDIT

// Currently empty because none of the given sources conforms to GeneratedFakeable

// swiftlint:enable line_length
4 changes: 4 additions & 0 deletions Modules/Sources/Fakes/WooFoundationCore.generated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@ import Networking
import Hardware
import WooFoundation

// swiftlint:disable line_length

extension WooFoundationCore.CurrencyCode {
/// Returns a "ready to use" type filled with fake values.
///
public static func fake() -> WooFoundationCore.CurrencyCode {
.AED
}
}

// swiftlint:enable line_length
4 changes: 4 additions & 0 deletions Modules/Sources/Fakes/Yosemite.generated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import Networking
import Hardware
import WooFoundation

// swiftlint:disable line_length

extension Yosemite.JustInTimeMessage {
/// Returns a "ready to use" type filled with fake values.
///
Expand Down Expand Up @@ -99,3 +101,5 @@ extension Yosemite.WooPaymentsPayoutsOverviewByCurrency {
)
}
}

// swiftlint:enable line_length
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Codegen
import Foundation
import UIKit

// swiftlint:disable line_length

extension Hardware.CardPresentReceiptParameters {
public func copy(
Expand Down Expand Up @@ -94,3 +95,5 @@ extension Hardware.PaymentIntent {
)
}
}

// swiftlint:enable line_length
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import WooFoundation
import struct Alamofire.JSONEncoding
import struct NetworkingCore.JetpackSite

// swiftlint:disable line_length

extension Networking.AIProduct {
public func copy(
Expand Down Expand Up @@ -3970,3 +3971,5 @@ extension Networking.WordPressTheme {
)
}
}

// swiftlint:enable line_length
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import Codegen
import Foundation

// swiftlint:disable line_length

extension NetworkingCore.Address {
public func copy(
Expand Down Expand Up @@ -927,3 +928,5 @@ extension NetworkingCore.User {
)
}
}

// swiftlint:enable line_length
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import Codegen
import Foundation

// swiftlint:disable line_length

extension Storage.AnalyticsCard {
public func copy(
Expand Down Expand Up @@ -80,10 +81,8 @@ extension Storage.GeneralAppSettings {
let sitesWithAtLeastOneIPPTransactionFinished = sitesWithAtLeastOneIPPTransactionFinished ?? self.sitesWithAtLeastOneIPPTransactionFinished
let isEUShippingNoticeDismissed = isEUShippingNoticeDismissed ?? self.isEUShippingNoticeDismissed
let isCustomFieldsTopBannerDismissed = isCustomFieldsTopBannerDismissed ?? self.isCustomFieldsTopBannerDismissed
let isPOSSurveyPotentialMerchantNotificationScheduled = isPOSSurveyPotentialMerchantNotificationScheduled ??
self.isPOSSurveyPotentialMerchantNotificationScheduled
let isPOSSurveyCurrentMerchantNotificationScheduled = isPOSSurveyCurrentMerchantNotificationScheduled ??
self.isPOSSurveyCurrentMerchantNotificationScheduled
let isPOSSurveyPotentialMerchantNotificationScheduled = isPOSSurveyPotentialMerchantNotificationScheduled ?? self.isPOSSurveyPotentialMerchantNotificationScheduled
let isPOSSurveyCurrentMerchantNotificationScheduled = isPOSSurveyCurrentMerchantNotificationScheduled ?? self.isPOSSurveyCurrentMerchantNotificationScheduled
let hasPOSBeenOpenedAtLeastOnce = hasPOSBeenOpenedAtLeastOnce ?? self.hasPOSBeenOpenedAtLeastOnce

return Storage.GeneralAppSettings(
Expand Down Expand Up @@ -126,7 +125,9 @@ extension Storage.GeneralStoreSettings {
lastSelectedOrderStatus: NullableCopiableProp<String> = .copy,
favoriteProductIDs: CopiableProp<[Int64]> = .copy,
searchTermsByKey: CopiableProp<[String: [String]]> = .copy,
isPOSTabVisible: NullableCopiableProp<Bool> = .copy
isPOSTabVisible: NullableCopiableProp<Bool> = .copy,
lastPOSOpenedDate: NullableCopiableProp<Date> = .copy,
firstPOSCatalogSyncDate: NullableCopiableProp<Date> = .copy
) -> Storage.GeneralStoreSettings {
let storeID = storeID ?? self.storeID
let isTelemetryAvailable = isTelemetryAvailable ?? self.isTelemetryAvailable
Expand All @@ -148,6 +149,8 @@ extension Storage.GeneralStoreSettings {
let favoriteProductIDs = favoriteProductIDs ?? self.favoriteProductIDs
let searchTermsByKey = searchTermsByKey ?? self.searchTermsByKey
let isPOSTabVisible = isPOSTabVisible ?? self.isPOSTabVisible
let lastPOSOpenedDate = lastPOSOpenedDate ?? self.lastPOSOpenedDate
let firstPOSCatalogSyncDate = firstPOSCatalogSyncDate ?? self.firstPOSCatalogSyncDate

return Storage.GeneralStoreSettings(
storeID: storeID,
Expand All @@ -169,7 +172,11 @@ extension Storage.GeneralStoreSettings {
lastSelectedOrderStatus: lastSelectedOrderStatus,
favoriteProductIDs: favoriteProductIDs,
searchTermsByKey: searchTermsByKey,
isPOSTabVisible: isPOSTabVisible
isPOSTabVisible: isPOSTabVisible,
lastPOSOpenedDate: lastPOSOpenedDate,
firstPOSCatalogSyncDate: firstPOSCatalogSyncDate
)
}
}

// swiftlint:enable line_length
19 changes: 17 additions & 2 deletions Modules/Sources/Storage/Model/GeneralStoreSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,13 @@ public struct GeneralStoreSettings: Codable, Equatable, GeneratedCopiable {
///
public var isPOSTabVisible: Bool?

/// The last time POS was opened for this store.
///
public var lastPOSOpenedDate: Date?

/// The date of the first POS catalog sync for this store.
///
public var firstPOSCatalogSyncDate: Date?

public init(storeID: String? = nil,
isTelemetryAvailable: Bool = false,
Expand All @@ -106,7 +113,9 @@ public struct GeneralStoreSettings: Codable, Equatable, GeneratedCopiable {
lastSelectedOrderStatus: String? = nil,
favoriteProductIDs: [Int64] = [],
searchTermsByKey: [String: [String]] = [:],
isPOSTabVisible: Bool? = nil) {
isPOSTabVisible: Bool? = nil,
lastPOSOpenedDate: Date? = nil,
firstPOSCatalogSyncDate: Date? = nil) {
self.storeID = storeID
self.isTelemetryAvailable = isTelemetryAvailable
self.telemetryLastReportedTime = telemetryLastReportedTime
Expand All @@ -127,6 +136,8 @@ public struct GeneralStoreSettings: Codable, Equatable, GeneratedCopiable {
self.favoriteProductIDs = favoriteProductIDs
self.searchTermsByKey = searchTermsByKey
self.isPOSTabVisible = isPOSTabVisible
self.lastPOSOpenedDate = lastPOSOpenedDate
self.firstPOSCatalogSyncDate = firstPOSCatalogSyncDate
}

public func erasingSelectedTaxRateID() -> GeneralStoreSettings {
Expand All @@ -148,7 +159,9 @@ public struct GeneralStoreSettings: Codable, Equatable, GeneratedCopiable {
lastSelectedOrderStatus: lastSelectedOrderStatus,
favoriteProductIDs: favoriteProductIDs,
searchTermsByKey: searchTermsByKey,
isPOSTabVisible: isPOSTabVisible)
isPOSTabVisible: isPOSTabVisible,
lastPOSOpenedDate: lastPOSOpenedDate,
firstPOSCatalogSyncDate: firstPOSCatalogSyncDate)
}
}

Expand Down Expand Up @@ -183,6 +196,8 @@ extension GeneralStoreSettings {
self.searchTermsByKey = try container.decodeIfPresent([String: [String]].self, forKey: .searchTermsByKey) ?? [:]

self.isPOSTabVisible = try container.decodeIfPresent(Bool.self, forKey: .isPOSTabVisible)
self.lastPOSOpenedDate = try container.decodeIfPresent(Date.self, forKey: .lastPOSOpenedDate)
self.firstPOSCatalogSyncDate = try container.decodeIfPresent(Date.self, forKey: .firstPOSCatalogSyncDate)

// Decode new properties with `decodeIfPresent` and provide a default value if necessary.
}
Expand Down
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see this one and Models+Copiable.generated.swift are generated as artifacts from rake generate after editing the template 👍

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Generated using Sourcery 2.2.6 — https://github.com/krzysztofzablocki/Sourcery
// DO NOT EDIT

// swiftlint:disable line_length

// swiftlint:enable line_length
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Generated using Sourcery 2.2.6 — https://github.com/krzysztofzablocki/Sourcery
// DO NOT EDIT

// swiftlint:disable line_length

// swiftlint:enable line_length
18 changes: 18 additions & 0 deletions Modules/Sources/Yosemite/Actions/AppSettingsAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -396,4 +396,22 @@ public enum AppSettingsAction: Action {
/// Resets all POS survey notification scheduled states
/// At the moment this one is used for testing only. To remove in WOOMOB-1480
case resetPOSSurveyNotificationScheduled(onCompletion: (Result<Void, Error>) -> Void)

// MARK: - POS Sync Eligibility Tracking

/// Sets the last time POS was opened for a specific site
///
case setPOSLastOpenedDate(siteID: Int64, date: Date, onCompletion: () -> Void)

/// Gets the last time POS was opened for a specific site
///
case getPOSLastOpenedDate(siteID: Int64, onCompletion: (Date?) -> Void)

/// Sets the date of the first POS catalog sync for a specific site
///
case setFirstPOSCatalogSyncDate(siteID: Int64, date: Date, onCompletion: () -> Void)

/// Gets the date of the first POS catalog sync for a specific site
///
case getFirstPOSCatalogSyncDate(siteID: Int64, onCompletion: (Date?) -> Void)
}
4 changes: 4 additions & 0 deletions Modules/Sources/Yosemite/Base/StoresManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ public protocol StoresManager {
///
var posCatalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol? { get }

/// Provides access to the session-scoped POS catalog eligibility service
///
var posCatalogEligibilityChecker: POSLocalCatalogEligibilityServiceProtocol? { get set }

/// Indicates if we need a Default StoreID, or there's one already set.
///
var needsDefaultStore: Bool { get }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import struct NetworkingCore.Order
import struct NetworkingCore.OrderItem
import struct NetworkingCore.OrderRefundCondensed

// swiftlint:disable line_length

extension Yosemite.JustInTimeMessage {
public func copy(
Expand Down Expand Up @@ -230,3 +231,5 @@ extension Yosemite.WooPaymentsPayoutsOverviewByCurrency {
)
}
}

// swiftlint:enable line_length
5 changes: 5 additions & 0 deletions Modules/Sources/Yosemite/Model/Mocks/MockStoresManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,11 @@ public class MockStoresManager: StoresManager {
public var posCatalogSyncCoordinator: (any POSCatalogSyncCoordinatorProtocol)? {
nil
}

public var posCatalogEligibilityChecker: POSLocalCatalogEligibilityServiceProtocol? {
get { nil }
set { }
}
}

private extension MockStoresManager {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public enum POSLocalCatalogEligibilityState: Equatable {

/// Reasons why local catalog is ineligible
public enum POSLocalCatalogIneligibleReason: Equatable {
case posTabNotVisible
case posTabNotEligible
case featureFlagDisabled
case catalogSizeTooLarge(totalCount: Int, limit: Int)
case catalogSizeCheckFailed(underlyingError: String)
Expand All @@ -28,14 +28,19 @@ public enum POSLocalCatalogIneligibleReason: Equatable {
/// NOTE: This service checks catalog-related eligibility (size limits) and feature flag state.
/// The service performs an initial eligibility check during initialization.
public protocol POSLocalCatalogEligibilityServiceProtocol {
/// Current eligibility state (synchronously accessible on main thread)
var eligibilityState: POSLocalCatalogEligibilityState { get }
/// Get catalog eligibility for a specific site
/// - Parameter siteID: The site ID to check eligibility for
/// - Returns: Cached eligibility state, or eligible if not yet checked
func catalogEligibility(for siteID: Int64) async -> POSLocalCatalogEligibilityState
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ Just for context: When we presentPOSView and check for eligibility state, if we land in POSLocalCatalogIneligibleReason.catalogSizeTooLarge or others, we'd just fallback to fetching from remote and not even initializing the GRDB infrastructure?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, that's right


/// Update the POS tab visibility state and refresh eligibility
/// - Parameter isPOSTabVisible: Whether the POS tab is visible
func updateVisibility(isPOSTabVisible: Bool) async
/// Update POS eligibility and refresh catalog eligibility for the specified site
/// - Parameters:
/// - isEligible: Whether POS is eligible for the site
/// - siteID: The site ID to refresh eligibility for
func updatePOSEligibility(isEligible: Bool, for siteID: Int64) async

/// Force refresh eligibility (bypasses cache and updates eligibilityState)
/// Refresh eligibility state for a specific site
/// - Parameter siteID: The site ID to check eligibility for
/// - Returns: Fresh eligibility state with reason if ineligible
@discardableResult func refreshEligibilityState() async -> POSLocalCatalogEligibilityState
@discardableResult func refreshEligibilityState(for siteID: Int64) async -> POSLocalCatalogEligibilityState
}
Loading