Skip to content

Commit c9541cd

Browse files
authored
Merge branch 'trunk' into woomob-699-woo-posbarcodes-set-up-flow-low-end-scanner-tera-1200-2d
2 parents 23e63b8 + 2e322ea commit c9541cd

File tree

13 files changed

+496
-115
lines changed

13 files changed

+496
-115
lines changed

Modules/Sources/Experiments/DefaultFeatureFlagService.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ public struct DefaultFeatureFlagService: FeatureFlagService {
100100
case .pointOfSaleOrdersi1:
101101
return true
102102
case .pointOfSaleOrdersi2:
103-
return buildConfig == .localDeveloper || buildConfig == .alpha
103+
return true
104104
case .pointOfSaleBarcodeScanningi2:
105105
return buildConfig == .localDeveloper || buildConfig == .alpha
106106
default:

Modules/Sources/Networking/Remote/ProductsRemote.swift

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ public final class ProductsRemote: Remote, ProductsRemoteProtocol {
259259
productsPerPage: String = POSConstants.productsPerPage,
260260
productTypes: [ProductType],
261261
orderBy: OrderKey = .name,
262-
order: Order = .ascending) -> [String: String] {
262+
order: Order = .ascending) -> [String: any Hashable] {
263263
[
264264
ParameterKey.page: String(pageNumber),
265265
ParameterKey.perPage: productsPerPage,
@@ -276,7 +276,7 @@ public final class ProductsRemote: Remote, ProductsRemoteProtocol {
276276

277277
private func makePagedPointOfSaleProductsRequest(for siteID: Int64,
278278
pageNumber: Int,
279-
parameters: [String: String]) async throws -> PagedItems<POSProduct> {
279+
parameters: [String: any Hashable]) async throws -> PagedItems<POSProduct> {
280280
let request = JetpackRequest(wooApiVersion: .mark3,
281281
method: .get,
282282
siteID: siteID,
@@ -318,8 +318,13 @@ public final class ProductsRemote: Remote, ProductsRemoteProtocol {
318318
productTypes: productTypes)
319319

320320
parameters.updateValue(query, forKey: ParameterKey.search)
321+
322+
// Takes precedence over `search` from WC 9.9 to 10.1
321323
parameters.updateValue(query, forKey: ParameterKey.searchNameOrSKU)
322324

325+
// Takes precedence over `search_name_or_sku` from WC 10.1+ and is combined with `search` value
326+
parameters.updateValue([SearchField.name, SearchField.sku, SearchField.globalUniqueID], forKey: ParameterKey.searchFields)
327+
323328
return try await makePagedPointOfSaleProductsRequest(
324329
for: siteID,
325330
pageNumber: pageNumber,
@@ -749,6 +754,7 @@ public extension ProductsRemote {
749754
static let include: String = "include"
750755
static let search: String = "search"
751756
static let searchNameOrSKU: String = "search_name_or_sku"
757+
static let searchFields: String = "search_fields"
752758
static let orderBy: String = "orderby"
753759
static let order: String = "order"
754760
static let sku: String = "sku"
@@ -776,6 +782,12 @@ public extension ProductsRemote {
776782
static let productSegment = "product"
777783
static let itemsSold = "items_sold"
778784
}
785+
786+
private enum SearchField {
787+
static let name = "name"
788+
static let sku = "sku"
789+
static let globalUniqueID = "global_unique_id"
790+
}
779791
}
780792

781793
private extension ProductsRemote {

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -195,11 +195,11 @@ public extension StorageType {
195195
}
196196
}
197197

198-
/// Deletes all of the stored SystemPlugins for the provided siteID whose name is not included in `currentSystemPlugins` array
198+
/// Deletes stored system plugins for the provided siteID that are not in the currentSystemPluginPaths list.
199199
///
200-
func deleteStaleSystemPlugins(siteID: Int64, currentSystemPlugins: [String]) {
200+
func deleteStaleSystemPlugins(siteID: Int64, currentSystemPluginPaths: [String]) {
201201
let systemPlugins = loadSystemPlugins(siteID: siteID).filter {
202-
!currentSystemPlugins.contains($0.name)
202+
!currentSystemPluginPaths.contains($0.plugin)
203203
}
204204
systemPlugins.forEach {
205205
deleteObject($0)

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

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -841,11 +841,28 @@ public extension StorageType {
841841

842842
/// Returns a system plugin with a specified `siteID` and `path`.
843843
///
844-
func loadSystemPlugin(siteID: Int64, path: String) -> SystemPlugin? {
845-
let predicate = \SystemPlugin.siteID == siteID && \SystemPlugin.plugin == path
844+
/// - Parameters:
845+
/// - siteID: The site ID to filter by.
846+
/// - path: The plugin path to match (e.g., "woocommerce/woocommerce.php").
847+
/// - active: Optional active state filter. If provided, only plugins with matching active state are returned. If nil, active state is ignored.
848+
/// - Returns: The matching system plugin, or nil if not found.
849+
func loadSystemPlugin(siteID: Int64, path: String, active: Bool? = nil) -> SystemPlugin? {
850+
let predicate = if let active {
851+
\SystemPlugin.siteID == siteID && \SystemPlugin.plugin == path && \SystemPlugin.active == active
852+
} else {
853+
\SystemPlugin.siteID == siteID && \SystemPlugin.plugin == path
854+
}
846855
return firstObject(ofType: SystemPlugin.self, matching: predicate)
847856
}
848857

858+
/// Returns stored system plugins for a provided `siteID` matching the given plugin `paths`.
859+
///
860+
func loadSystemPlugins(siteID: Int64, matchingPaths paths: [String]) -> [SystemPlugin] {
861+
let predicate = NSPredicate(format: "siteID == %lld && plugin in %@", siteID, paths)
862+
let descriptor = NSSortDescriptor(keyPath: \SystemPlugin.plugin, ascending: true)
863+
return allObjects(ofType: SystemPlugin.self, matching: predicate, sortedBy: [descriptor])
864+
}
865+
849866
// MARK: - Inbox Notes
850867

851868
/// Returns a single Inbox Note given a `siteID` and `id`

Modules/Sources/Yosemite/PointOfSale/Eligibility/POSSystemStatusService.swift

Lines changed: 4 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,13 @@ public final class POSSystemStatusService: POSSystemStatusServiceProtocol {
4848
)
4949

5050
// Upserts all plugins in storage.
51-
await storageManager.performAndSaveAsync({ [weak self] storage in
52-
self?.upsertSystemPlugins(siteID: siteID, systemStatus: systemStatus, in: storage)
51+
await storageManager.performAndSaveAsync({ storage in
52+
let useCase = SystemPluginsUpsertUseCase(storage: storage)
53+
useCase.upsert(siteID: siteID, activePlugins: systemStatus.activePlugins, inactivePlugins: systemStatus.inactivePlugins)
5354
})
5455

5556
// Loads WooCommerce plugin from storage.
56-
guard let wcPlugin = storageManager.viewStorage.loadSystemPlugin(siteID: siteID, path: Constants.wcPluginPath)?.toReadOnly() else {
57+
guard let wcPlugin = storageManager.viewStorage.loadSystemPlugin(siteID: siteID, path: Constants.wcPluginPath, active: true)?.toReadOnly() else {
5758
return POSPluginAndFeatureInfo(wcPlugin: nil, featureValue: nil)
5859
}
5960

@@ -63,42 +64,6 @@ public final class POSSystemStatusService: POSSystemStatusServiceProtocol {
6364
}
6465
}
6566

66-
private extension POSSystemStatusService {
67-
/// Updates or inserts system plugins in storage.
68-
func upsertSystemPlugins(siteID: Int64, systemStatus: POSPluginEligibilitySystemStatus, in storage: StorageType) {
69-
// Active and inactive plugins share identical structure, but are stored in separate parts of the remote response
70-
// (and without an active attribute in the response). So we apply the correct value for active (or not)
71-
let readonlySystemPlugins: [SystemPlugin] = {
72-
let activePlugins = systemStatus.activePlugins.map {
73-
$0.copy(active: true)
74-
}
75-
76-
let inactivePlugins = systemStatus.inactivePlugins.map {
77-
$0.copy(active: false)
78-
}
79-
80-
return activePlugins + inactivePlugins
81-
}()
82-
83-
let storedPlugins = storage.loadSystemPlugins(siteID: siteID, matching: readonlySystemPlugins.map { $0.name })
84-
readonlySystemPlugins.forEach { readonlySystemPlugin in
85-
// Loads or creates new StorageSystemPlugin matching the readonly one.
86-
let storageSystemPlugin: StorageSystemPlugin = {
87-
if let systemPlugin = storedPlugins.first(where: { $0.name == readonlySystemPlugin.name }) {
88-
return systemPlugin
89-
}
90-
return storage.insertNewObject(ofType: StorageSystemPlugin.self)
91-
}()
92-
93-
storageSystemPlugin.update(with: readonlySystemPlugin)
94-
}
95-
96-
// Removes stale system plugins.
97-
let currentSystemPlugins = readonlySystemPlugins.map(\.name)
98-
storage.deleteStaleSystemPlugins(siteID: siteID, currentSystemPlugins: currentSystemPlugins)
99-
}
100-
}
101-
10267
private extension POSSystemStatusService {
10368
enum Constants {
10469
static let wcPluginPath = "woocommerce/woocommerce.php"
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import Foundation
2+
import Storage
3+
import Networking
4+
5+
/// Upserts `Networking.SystemPlugin` objects into Storage.
6+
///
7+
/// This UseCase encapsulates the business logic for inserting or updating system plugins,
8+
/// including handling active/inactive state and removing stale plugins.
9+
struct SystemPluginsUpsertUseCase {
10+
private let storage: StorageType
11+
12+
/// Initializes a new UseCase.
13+
///
14+
/// - Parameter storage: A derived `StorageType`.
15+
init(storage: StorageType) {
16+
self.storage = storage
17+
}
18+
19+
/// Updates or inserts the given system plugins for a site.
20+
///
21+
/// - Parameters:
22+
/// - siteID: The site ID these plugins belong to.
23+
/// - activePlugins: Array of active system plugins.
24+
/// - inactivePlugins: Array of inactive system plugins.
25+
///
26+
func upsert(siteID: Int64, activePlugins: [SystemPlugin], inactivePlugins: [SystemPlugin]) {
27+
// Active and inactive plugins share identical structure, but are stored in separate parts of the remote response
28+
// (and without an active attribute in the response). So we apply the correct value for active (or not).
29+
let readonlySystemPlugins: [SystemPlugin] = {
30+
let activePluginsWithState = activePlugins.map {
31+
$0.copy(active: true)
32+
}
33+
34+
let inactivePluginsWithState = inactivePlugins.map {
35+
$0.copy(active: false)
36+
}
37+
38+
return activePluginsWithState + inactivePluginsWithState
39+
}()
40+
41+
let storedPlugins = storage.loadSystemPlugins(siteID: siteID, matchingPaths: readonlySystemPlugins.map { $0.plugin })
42+
readonlySystemPlugins.forEach { readonlySystemPlugin in
43+
// Loads or creates new StorageSystemPlugin matching the readonly one.
44+
let storageSystemPlugin: StorageSystemPlugin = {
45+
if let systemPlugin = storedPlugins.first(where: { $0.plugin == readonlySystemPlugin.plugin }) {
46+
return systemPlugin
47+
}
48+
return storage.insertNewObject(ofType: StorageSystemPlugin.self)
49+
}()
50+
51+
storageSystemPlugin.update(with: readonlySystemPlugin)
52+
}
53+
54+
// Removes stale system plugins.
55+
let currentSystemPluginPaths = readonlySystemPlugins.map(\.plugin)
56+
storage.deleteStaleSystemPlugins(siteID: siteID, currentSystemPluginPaths: currentSystemPluginPaths)
57+
}
58+
}

Modules/Sources/Yosemite/Stores/SystemStatusStore.swift

Lines changed: 3 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,9 @@ private extension SystemStatusStore {
7878
func upsertSystemPluginsInBackground(siteID: Int64,
7979
readonlySystemInformation: SystemStatus,
8080
completionHandler: @escaping () -> Void) {
81-
storageManager.performAndSave({ [weak self] storage in
82-
self?.upsertSystemPlugins(siteID: siteID, readonlySystemInformation: readonlySystemInformation, in: storage)
81+
storageManager.performAndSave({ storage in
82+
let useCase = SystemPluginsUpsertUseCase(storage: storage)
83+
useCase.upsert(siteID: siteID, activePlugins: readonlySystemInformation.activePlugins, inactivePlugins: readonlySystemInformation.inactivePlugins)
8384
}, completion: completionHandler, on: .main)
8485
}
8586

@@ -90,43 +91,6 @@ private extension SystemStatusStore {
9091
dispatcher.dispatch(action)
9192
}
9293

93-
/// Updates or inserts Readonly sistem plugins from the read only system information in specified storage.
94-
/// Also removes stale plugins that no longer exist in remote plugin list.
95-
///
96-
func upsertSystemPlugins(siteID: Int64, readonlySystemInformation: SystemStatus, in storage: StorageType) {
97-
/// Active and in-active plugins share identical structure, but are stored in separate parts of the remote response
98-
/// (and without an active attribute in the response). So... we use the same decoder for active and in-active plugins
99-
/// and here we apply the correct value for active (or not)
100-
///
101-
let readonlySystemPlugins: [SystemPlugin] = {
102-
let activePlugins = readonlySystemInformation.activePlugins.map {
103-
$0.copy(active: true)
104-
}
105-
106-
let inactivePlugins = readonlySystemInformation.inactivePlugins.map {
107-
$0.copy(active: false)
108-
}
109-
110-
return activePlugins + inactivePlugins
111-
}()
112-
113-
let storedPlugins = storage.loadSystemPlugins(siteID: siteID, matching: readonlySystemPlugins.map { $0.name })
114-
readonlySystemPlugins.forEach { readonlySystemPlugin in
115-
// load or create new StorageSystemPlugin matching the readonly one
116-
let storageSystemPlugin: StorageSystemPlugin = {
117-
if let systemPlugin = storedPlugins.first(where: { $0.name == readonlySystemPlugin.name }) {
118-
return systemPlugin
119-
}
120-
return storage.insertNewObject(ofType: StorageSystemPlugin.self)
121-
}()
122-
123-
storageSystemPlugin.update(with: readonlySystemPlugin)
124-
}
125-
126-
// remove stale system plugins
127-
let currentSystemPlugins = readonlySystemPlugins.map(\.name)
128-
storage.deleteStaleSystemPlugins(siteID: siteID, currentSystemPlugins: currentSystemPlugins)
129-
}
13094

13195
/// Retrieve a `SystemPlugin` entity from storage whose name matches any name from the provided name list.
13296
/// Useful when a plugin has had multiple names.

Modules/Tests/NetworkingTests/Remote/POSProductsNetworkingTests.swift

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,9 @@ struct POSProductsNetworkingTests {
6464
_ = try? await remote.loadProductsForPointOfSale(for: sampleSiteID, productTypes: [.simple, .variable], pageNumber: 1)
6565

6666
// Then
67-
let queryParametersDictionary = try #require(network.queryParametersDictionary as? [String: String])
68-
#expect(queryParametersDictionary["downloadable"] == "false")
69-
#expect(queryParametersDictionary["include_types"] == "simple,variable")
67+
let queryParametersDictionary = try #require(network.queryParametersDictionary as? [String: any Hashable])
68+
#expect(queryParametersDictionary["downloadable"] as? String == "false")
69+
#expect(queryParametersDictionary["include_types"] as? String == "simple,variable")
7070
}
7171

7272
@Test(arguments: 1...4) func loadProductsForPointOfSale_returns_hasMorePages_based_on_header_with_case_insensitive_name(pageNumber: Int) async throws {
@@ -126,13 +126,13 @@ struct POSProductsNetworkingTests {
126126
pageNumber: 1)
127127

128128
// Then
129-
let queryParametersDictionary = try #require(network.queryParametersDictionary as? [String: String])
130-
#expect(queryParametersDictionary["downloadable"] == "false")
131-
#expect(queryParametersDictionary["include_types"] == "simple,variable")
132-
#expect(queryParametersDictionary["search"] == "search terms")
129+
let queryParametersDictionary = try #require(network.queryParametersDictionary as? [String: any Hashable])
130+
#expect(queryParametersDictionary["downloadable"] as? String == "false")
131+
#expect(queryParametersDictionary["include_types"] as? String == "simple,variable")
132+
#expect(queryParametersDictionary["search"] as? String == "search terms")
133133
}
134134

135-
@Test func searchProductsForPointOfSale_sets_both_search_parameters() async throws {
135+
@Test func searchProductsForPointOfSale_sets_all_search_parameters() async throws {
136136
// Given
137137
let remote = ProductsRemote(network: network)
138138
let searchQuery = "search terms"
@@ -145,9 +145,10 @@ struct POSProductsNetworkingTests {
145145
pageNumber: 1)
146146

147147
// Then
148-
let queryParametersDictionary = try #require(network.queryParametersDictionary as? [String: String])
149-
#expect(queryParametersDictionary["search"] == searchQuery)
150-
#expect(queryParametersDictionary["search_name_or_sku"] == searchQuery)
148+
let queryParametersDictionary = try #require(network.queryParametersDictionary as? [String: any Hashable])
149+
#expect(queryParametersDictionary["search"] as? String == searchQuery)
150+
#expect(queryParametersDictionary["search_name_or_sku"] as? String == searchQuery)
151+
#expect(queryParametersDictionary["search_fields"] as? [String] == ["name", "sku", "global_unique_id"])
151152
}
152153

153154
@Test func searchProductsForPointOfSale_returns_parsed_products() async throws {
@@ -205,8 +206,8 @@ struct POSProductsNetworkingTests {
205206
_ = try? await remote.loadProductsForPointOfSale(for: sampleSiteID)
206207

207208
// Then
208-
let queryParametersDictionary = try #require(network.queryParametersDictionary as? [String: String])
209-
#expect(queryParametersDictionary["_fields"] == POSProduct.requestFields.joined(separator: ","))
209+
let queryParametersDictionary = try #require(network.queryParametersDictionary as? [String: any Hashable])
210+
#expect(queryParametersDictionary["_fields"] as? String == POSProduct.requestFields.joined(separator: ","))
210211
}
211212

212213
@Test func searchProductsForPointOfSale_requests_only_required_fields() async throws {
@@ -217,8 +218,8 @@ struct POSProductsNetworkingTests {
217218
_ = try? await remote.searchProductsForPointOfSale(for: sampleSiteID, query: "search")
218219

219220
// Then
220-
let queryParametersDictionary = try #require(network.queryParametersDictionary as? [String: String])
221-
#expect(queryParametersDictionary["_fields"] == POSProduct.requestFields.joined(separator: ","))
221+
let queryParametersDictionary = try #require(network.queryParametersDictionary as? [String: any Hashable])
222+
#expect(queryParametersDictionary["_fields"] as? String == POSProduct.requestFields.joined(separator: ","))
222223
}
223224

224225
@Test func loadProductsForPointOfSale_returns_total_items_from_header() async throws {

0 commit comments

Comments
 (0)