Skip to content

Commit 1d26152

Browse files
committed
Merge branch 'trunk' into WOOMOB-1553-update-attendance-status-remotely
# Conflicts: # Modules/Tests/NetworkingTests/Remote/BookingsRemoteTests.swift # Modules/Tests/YosemiteTests/Mocks/MockBookingsRemote.swift # Modules/Tests/YosemiteTests/Stores/BookingStoreTests.swift
2 parents 8ed229e + 8977b5b commit 1d26152

File tree

57 files changed

+2127
-199
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+2127
-199
lines changed

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,10 @@
66
## Description
77
<!-- Take the time to write a good summary. Why is it needed? What does it do? When fixing bugs try to avoid just writing “See original issue” – clarify what the problem was and how you’ve fixed it. -->
88

9-
## Steps to reproduce
10-
<!-- Step-by-step testing instructions. For new user flows, consider instead stating the goal of the workflow and see if your PR reviewer can accomplish the workflow without specific steps! -->
11-
12-
## Testing information
13-
<!-- This is your opportunity to break out individual scenarios that need testing (when necessary) and/or include a checklist for the reviewer to go through. Consider documenting the following from your own completed testing: devices used, alternate workflows, edge cases, affected areas, critical flows, areas not tested, and any remaining unknowns. Provide feedback on this new section of the PR template through Sept 30, 2024 to Apps Quality; additional context here: https://woomobilep2.wordpress.com/2024/05/06/woocommerce-mobile-quality-report-march-april/#comment-12036 -->
9+
## Test Steps
10+
<!-- Describe how to test your changes. Include only what’s needed for the reviewer to validate the behavior.
11+
For new features, outline the main user flow or goal rather than every tap or click — the reviewer should be able to complete the flow naturally.
12+
If applicable, mention key devices, scenarios, or edge cases to verify. -->
1413

1514
## Screenshots
1615
<!-- Include before and after images or gifs when appropriate. -->

Modules/Sources/Networking/Model/Site.swift

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,19 @@ public enum SiteVisibility: Int, Codable, GeneratedFakeable {
333333
///
334334
public extension Site {
335335

336+
private var jetpackCanonicalURL: String {
337+
guard let originalURL = URL(string: url),
338+
originalURL.scheme?.lowercased() == "http"
339+
else {
340+
return url
341+
}
342+
343+
var components = URLComponents(url: originalURL, resolvingAgainstBaseURL: false)
344+
components?.scheme = "https"
345+
346+
return components?.string ?? url
347+
}
348+
336349
/// Returns the TimeZone using the gmtOffset
337350
///
338351
var siteTimezone: TimeZone {
@@ -347,7 +360,7 @@ public extension Site {
347360
}
348361

349362
func toJetpackSite() -> JetpackSite {
350-
JetpackSite(siteID: siteID, siteAddress: url, applicationPasswordAvailable: applicationPasswordAvailable)
363+
JetpackSite(siteID: siteID, siteAddress: jetpackCanonicalURL, applicationPasswordAvailable: applicationPasswordAvailable)
351364
}
352365
}
353366

Modules/Sources/Networking/Remote/BookingsRemote.swift

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ public protocol BookingsRemoteProtocol {
2525

2626
func fetchResource(resourceID: Int64,
2727
siteID: Int64) async throws -> BookingResource?
28+
29+
func fetchResources(for siteID: Int64,
30+
pageNumber: Int,
31+
pageSize: Int) async throws -> [BookingResource]
2832
}
2933

3034
/// Booking: Remote Endpoints
@@ -133,6 +137,37 @@ public final class BookingsRemote: Remote, BookingsRemoteProtocol {
133137

134138
return try await enqueue(request, mapper: mapper)
135139
}
140+
141+
/// Retrieves all of the `BookingResources` available.
142+
///
143+
/// - Parameters:
144+
/// - siteID: Site for which we'll fetch remote booking resources.
145+
/// - pageNumber: Number of page that should be retrieved.
146+
/// - pageSize: Number of resources to be retrieved per page.
147+
///
148+
public func fetchResources(
149+
for siteID: Int64,
150+
pageNumber: Int = Default.pageNumber,
151+
pageSize: Int = Default.pageSize
152+
) async throws -> [BookingResource] {
153+
let parameters = [
154+
ParameterKey.page: String(pageNumber),
155+
ParameterKey.perPage: String(pageSize)
156+
]
157+
158+
let path = Path.resources
159+
let request = JetpackRequest(
160+
wooApiVersion: .wcBookings,
161+
method: .get,
162+
siteID: siteID,
163+
path: path,
164+
parameters: parameters,
165+
availableAsRESTRequest: true
166+
)
167+
let mapper = ListMapper<BookingResource>(siteID: siteID)
168+
169+
return try await enqueue(request, mapper: mapper)
170+
}
136171
}
137172

138173
// MARK: - Constants

Modules/Sources/PointOfSale/Models/PointOfSaleSettingsController.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ protocol PointOfSaleSettingsControllerProtocol {
1515
var connectedCardReader: CardPresentPaymentCardReader? { get }
1616
var storeViewModel: POSSettingsStoreViewModel { get }
1717
var localCatalogViewModel: POSSettingsLocalCatalogViewModel? { get }
18+
var isLocalCatalogEligible: Bool { get }
1819
}
1920

2021
@Observable final class PointOfSaleSettingsController: PointOfSaleSettingsControllerProtocol {
@@ -23,6 +24,7 @@ protocol PointOfSaleSettingsControllerProtocol {
2324

2425
let storeViewModel: POSSettingsStoreViewModel
2526
let localCatalogViewModel: POSSettingsLocalCatalogViewModel?
27+
let isLocalCatalogEligible: Bool
2628

2729
init(siteID: Int64,
2830
settingsService: PointOfSaleSettingsServiceProtocol,
@@ -31,12 +33,15 @@ protocol PointOfSaleSettingsControllerProtocol {
3133
defaultSiteName: String?,
3234
siteSettings: [SiteSetting],
3335
grdbManager: GRDBManagerProtocol?,
34-
catalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol?) {
36+
catalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol?,
37+
isLocalCatalogEligible: Bool) {
3538
self.storeViewModel = POSSettingsStoreViewModel(siteID: siteID,
3639
settingsService: settingsService,
3740
pluginsService: pluginsService,
3841
defaultSiteName: defaultSiteName,
3942
siteSettings: siteSettings)
43+
self.isLocalCatalogEligible = isLocalCatalogEligible
44+
4045
if let catalogSyncCoordinator, let grdbManager {
4146
self.localCatalogViewModel = POSSettingsLocalCatalogViewModel(
4247
siteID: siteID,
@@ -80,6 +85,10 @@ final class PointOfSaleSettingsPreviewController: PointOfSaleSettingsControllerP
8085
siteSettings: [])
8186

8287
var localCatalogViewModel: POSSettingsLocalCatalogViewModel?
88+
89+
var isLocalCatalogEligible: Bool {
90+
localCatalogViewModel != nil
91+
}
8392
}
8493

8594
final class MockPointOfSaleSettingsService: PointOfSaleSettingsServiceProtocol {

Modules/Sources/PointOfSale/Presentation/PointOfSaleEntryPointView.swift

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,13 @@ public struct PointOfSaleEntryPointView: View {
6363
siteSettings: [SiteSetting],
6464
grdbManager: GRDBManagerProtocol?,
6565
catalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol?,
66+
isLocalCatalogEligible: Bool,
6667
services: POSDependencyProviding) {
6768
self.onPointOfSaleModeActiveStateChange = onPointOfSaleModeActiveStateChange
6869

69-
// Use observable controller with GRDB if available and feature flag is enabled, otherwise fall back to standard controller
70-
// Note: We check feature flag here for eligibility. Once eligibility checking is
71-
// refactored to be more centralized, this check can be simplified.
72-
let isGRDBEnabled = services.featureFlags.isFeatureFlagEnabled(.pointOfSaleLocalCatalogi1)
73-
if let grdbManager = grdbManager, catalogSyncCoordinator != nil, isGRDBEnabled {
70+
// Use observable controller with GRDB if local catalog is eligible,
71+
// otherwise fall back to standard controller.
72+
if isLocalCatalogEligible, let grdbManager = grdbManager {
7473
self.itemsController = PointOfSaleObservableItemsController(
7574
siteID: siteID,
7675
grdbManager: grdbManager,
@@ -113,7 +112,8 @@ public struct PointOfSaleEntryPointView: View {
113112
defaultSiteName: defaultSiteName,
114113
siteSettings: siteSettings,
115114
grdbManager: grdbManager,
116-
catalogSyncCoordinator: catalogSyncCoordinator)
115+
catalogSyncCoordinator: catalogSyncCoordinator,
116+
isLocalCatalogEligible: isLocalCatalogEligible)
117117
self.collectOrderPaymentAnalyticsTracker = collectOrderPaymentAnalyticsTracker
118118
self.searchHistoryService = searchHistoryService
119119
self.popularPurchasableItemsController = PointOfSaleItemsController(
@@ -203,6 +203,7 @@ public struct PointOfSaleEntryPointView: View {
203203
siteSettings: [],
204204
grdbManager: nil,
205205
catalogSyncCoordinator: nil,
206+
isLocalCatalogEligible: false,
206207
services: POSPreviewServices()
207208
)
208209
}

Modules/Sources/PointOfSale/Presentation/Settings/PointOfSaleSettingsView.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import SwiftUI
33
struct PointOfSaleSettingsView: View {
44
@Environment(\.dismiss) private var dismiss
55
@Environment(\.posAnalytics) private var analytics
6-
@Environment(\.posFeatureFlags) private var featureFlags
76
@State private var selection: SidebarNavigation? = .store
87

98
let settingsController: PointOfSaleSettingsControllerProtocol
@@ -55,8 +54,7 @@ extension PointOfSaleSettingsView {
5554
}
5655
)
5756

58-
// TODO: WOOMOB-1287 - integrate with local catalog feature eligibility
59-
if featureFlags.isFeatureFlagEnabled(.pointOfSaleLocalCatalogi1) && settingsController.localCatalogViewModel != nil {
57+
if settingsController.isLocalCatalogEligible {
6058
PointOfSaleSettingsCard(
6159
item: .localCatalog,
6260
isSelected: selection == .localCatalog,
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import Foundation
2+
3+
/// Eligibility state for local catalog feature
4+
/// Provides diagnostic information for UI display and decision-making
5+
public enum POSLocalCatalogEligibilityState: Equatable {
6+
/// Local catalog is eligible for use
7+
case eligible
8+
9+
/// Local catalog is not eligible
10+
case ineligible(reason: POSLocalCatalogIneligibleReason)
11+
}
12+
13+
/// Reasons why local catalog is ineligible
14+
public enum POSLocalCatalogIneligibleReason: Equatable {
15+
case posTabNotVisible
16+
case featureFlagDisabled
17+
case catalogSizeTooLarge(totalCount: Int, limit: Int)
18+
case catalogSizeCheckFailed(underlyingError: String)
19+
}
20+
21+
/// Service that provides eligibility information for local catalog feature
22+
///
23+
/// Other services can query this for eligibility state and reasons:
24+
/// - Sync coordinator can check if catalog is eligible
25+
/// - Settings UI can display eligibility status and reasons
26+
/// - Analytics can track why stores are ineligible
27+
///
28+
/// NOTE: This service checks catalog-related eligibility (size limits) and feature flag state.
29+
/// The service performs an initial eligibility check during initialization.
30+
public protocol POSLocalCatalogEligibilityServiceProtocol {
31+
/// Current eligibility state (synchronously accessible on main thread)
32+
var eligibilityState: POSLocalCatalogEligibilityState { get }
33+
34+
/// Update the POS tab visibility state and refresh eligibility
35+
/// - Parameter isPOSTabVisible: Whether the POS tab is visible
36+
func updateVisibility(isPOSTabVisible: Bool) async
37+
38+
/// Force refresh eligibility (bypasses cache and updates eligibilityState)
39+
/// - Returns: Fresh eligibility state with reason if ineligible
40+
@discardableResult func refreshEligibilityState() async -> POSLocalCatalogEligibilityState
41+
}

Modules/Sources/Storage/Tools/StorageType+Extensions.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -967,6 +967,13 @@ public extension StorageType {
967967
return objects.isEmpty ? nil : objects
968968
}
969969

970+
/// Retrieves the store booking resources
971+
func loadBookingResources(siteID: Int64, resourceIDs: [Int64]) -> [BookingResource] {
972+
let predicate = NSPredicate(format: "siteID == %lld && resourceID in %@", siteID, resourceIDs)
973+
let descriptor = NSSortDescriptor(keyPath: \BookingResource.resourceID, ascending: false)
974+
return allObjects(ofType: BookingResource.self, matching: predicate, sortedBy: [descriptor])
975+
}
976+
970977
/// Retrieves the store booking resource
971978
func loadBookingResource(siteID: Int64, resourceID: Int64) -> BookingResource? {
972979
let predicate = \BookingResource.resourceID == resourceID && \BookingResource.siteID == siteID

Modules/Sources/Yosemite/Actions/BookingAction.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,15 @@ public enum BookingAction: Action {
5353
resourceID: Int64,
5454
onCompletion: (Result<BookingResource, Error>) -> Void)
5555

56+
/// Synchronizes booking resources matching the specified criteria.
57+
///
58+
/// - Parameter onCompletion: called when sync completes, returns an error or a boolean that indicates whether there might be more resources to sync.
59+
///
60+
case synchronizeResources(siteID: Int64,
61+
pageNumber: Int,
62+
pageSize: Int = BookingsRemote.Default.pageSize,
63+
onCompletion: (Result<Bool, Error>) -> Void)
64+
5665
/// Updates a booking attendance status.
5766
///
5867
/// - Parameter siteID: The site ID of the booking.
Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
1-
// periphery:ignore:all - will be used for booking filters
21
import Foundation
32

43
/// Used to filter bookings by product
54
///
65
public struct BookingProductFilter: Codable, Hashable {
76
/// ID of the product
7+
/// periphery:ignore - to be used later when applying filter
88
///
9-
public let id: Int64
9+
public let productID: Int64
1010

1111
/// Name of the product
1212
///
1313
public let name: String
1414

15-
public init(id: Int64,
16-
name: String) {
17-
self.id = id
15+
public init(productID: Int64, name: String) {
16+
self.productID = productID
1817
self.name = name
1918
}
2019
}

0 commit comments

Comments
 (0)