Skip to content

Commit 504e7b3

Browse files
authored
Merge branch 'trunk' into woomob-754-ui-updates
2 parents 6fa79d4 + 2329236 commit 504e7b3

File tree

33 files changed

+882
-325
lines changed

33 files changed

+882
-325
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/Protocols/StorageManagerType.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,17 @@ public extension StorageManagerType {
5757
func reset() {
5858
reset(onCompletion: nil)
5959
}
60+
61+
/// Async/await version of `performAndSave`.
62+
///
63+
/// - Parameters:
64+
/// - operation: A closure which uses the given `StorageType` to make data changes in background.
65+
/// - queue: A queue on which to execute the completion closure.
66+
func performAndSaveAsync(_ operation: @escaping (StorageType) -> Void, on queue: DispatchQueue = .main) async {
67+
await withCheckedContinuation { continuation in
68+
performAndSave(operation, completion: {
69+
continuation.resume()
70+
}, on: queue)
71+
}
72+
}
6073
}

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

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Foundation
22
import Networking
3+
import Storage
34

45
public protocol POSSystemStatusServiceProtocol {
56
/// Loads WooCommerce plugin and POS feature switch value remotely for eligibility checks.
@@ -23,27 +24,36 @@ public struct POSPluginAndFeatureInfo {
2324
/// Service for fetching POS-related system status information.
2425
public final class POSSystemStatusService: POSSystemStatusServiceProtocol {
2526
private let remote: SystemStatusRemote
27+
private let storageManager: StorageManagerType
2628

27-
public init(credentials: Credentials?) {
29+
public init(credentials: Credentials?, storageManager: StorageManagerType) {
2830
let network = AlamofireNetwork(credentials: credentials)
29-
remote = SystemStatusRemote(network: network)
31+
self.remote = SystemStatusRemote(network: network)
32+
self.storageManager = storageManager
3033
}
3134

3235
/// Test-friendly initializer that accepts a network implementation.
33-
init(network: Network) {
34-
remote = SystemStatusRemote(network: network)
36+
init(network: Network, storageManager: StorageManagerType) {
37+
self.remote = SystemStatusRemote(network: network)
38+
self.storageManager = storageManager
3539
}
3640

41+
@MainActor
3742
public func loadWooCommercePluginAndPOSFeatureSwitch(siteID: Int64) async throws -> POSPluginAndFeatureInfo {
3843
let mapper = SingleItemMapper<POSPluginEligibilitySystemStatus>(siteID: siteID)
3944
let systemStatus: POSPluginEligibilitySystemStatus = try await remote.loadSystemStatus(
4045
for: siteID,
41-
fields: [.activePlugins, .settings],
46+
fields: [.activePlugins, .inactivePlugins, .settings],
4247
mapper: mapper
4348
)
4449

45-
// Finds WooCommerce plugin from active plugins response.
46-
guard let wcPlugin = systemStatus.activePlugins.first(where: { $0.plugin == Constants.wcPluginPath }) else {
50+
// Upserts all plugins in storage.
51+
await storageManager.performAndSaveAsync({ [weak self] storage in
52+
self?.upsertSystemPlugins(siteID: siteID, systemStatus: systemStatus, in: storage)
53+
})
54+
55+
// Loads WooCommerce plugin from storage.
56+
guard let wcPlugin = storageManager.viewStorage.loadSystemPlugin(siteID: siteID, path: Constants.wcPluginPath)?.toReadOnly() else {
4757
return POSPluginAndFeatureInfo(wcPlugin: nil, featureValue: nil)
4858
}
4959

@@ -53,6 +63,42 @@ public final class POSSystemStatusService: POSSystemStatusServiceProtocol {
5363
}
5464
}
5565

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+
56102
private extension POSSystemStatusService {
57103
enum Constants {
58104
static let wcPluginPath = "woocommerce/woocommerce.php"
@@ -63,10 +109,12 @@ private extension POSSystemStatusService {
63109

64110
private struct POSPluginEligibilitySystemStatus: Decodable {
65111
let activePlugins: [SystemPlugin]
112+
let inactivePlugins: [SystemPlugin]
66113
let settings: POSEligibilitySystemStatusSettings
67114

68115
enum CodingKeys: String, CodingKey {
69116
case activePlugins = "active_plugins"
117+
case inactivePlugins = "inactive_plugins"
70118
case settings
71119
}
72120
}

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 {

Modules/Tests/NetworkingTests/Responses/system-status-wc-plugin-and-pos-feature-disabled.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"data": {
3+
"inactive_plugins": [],
34
"active_plugins":[
45
{
56
"plugin": "woocommerce/woocommerce.php",

Modules/Tests/NetworkingTests/Responses/system-status-wc-plugin-and-pos-feature-enabled.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"data": {
3+
"inactive_plugins": [],
34
"active_plugins":[
45
{
56
"plugin": "woocommerce/woocommerce.php",

Modules/Tests/NetworkingTests/Responses/system-status-wc-plugin-missing.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"data": {
3+
"inactive_plugins": [],
34
"active_plugins":[
45
],
56
"settings": {

Modules/Tests/YosemiteTests/PointOfSale/POSSystemStatusServiceTests.swift

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,24 @@ import Foundation
22
import Testing
33
import TestKit
44
@testable import Networking
5+
@testable import Storage
56
@testable import Yosemite
67

78
@MainActor
89
struct POSSystemStatusServiceTests {
910
private let network = MockNetwork()
11+
private let storageManager = MockStorageManager()
1012
private let sampleSiteID: Int64 = 134
1113
private let sut: POSSystemStatusService
1214

1315
init() async throws {
1416
network.removeAllSimulatedResponses()
15-
sut = POSSystemStatusService(network: network)
17+
sut = POSSystemStatusService(network: network, storageManager: storageManager)
1618
}
1719

1820
// MARK: - loadWooCommercePluginAndPOSFeatureSwitch Tests
1921

20-
@Test func loadWooCommercePluginAndPOSFeatureSwitch_returns_plugin_and_nil_feature_when_settings_response_does_not_include_enabled_featuers() async throws {
22+
@Test func loadWooCommercePluginAndPOSFeatureSwitch_returns_plugin_and_nil_feature_when_settings_response_does_not_include_enabled_features() async throws {
2123
// Given
2224
network.simulateResponse(requestUrlSuffix: "system_status", filename: "systemStatus")
2325

@@ -37,6 +39,10 @@ struct POSSystemStatusServiceTests {
3739
// Given
3840
network.simulateResponse(requestUrlSuffix: "system_status", filename: "system-status-wc-plugin-and-pos-feature-enabled")
3941

42+
// Inserts WooCommerce plugin into storage with an older version and inactive.
43+
let storageWCPlugin = createWCPlugin(version: "9.5.2", active: false)
44+
storageManager.insertSampleSystemPlugin(readOnlySystemPlugin: storageWCPlugin)
45+
4046
// When
4147
let result = try await sut.loadWooCommercePluginAndPOSFeatureSwitch(siteID: sampleSiteID)
4248

@@ -53,6 +59,10 @@ struct POSSystemStatusServiceTests {
5359
// Given
5460
network.simulateResponse(requestUrlSuffix: "system_status", filename: "system-status-wc-plugin-and-pos-feature-disabled")
5561

62+
// Inserts WooCommerce plugin into storage with an older version and inactive.
63+
let storageWCPlugin = createWCPlugin(version: "9.5.2", active: false)
64+
storageManager.insertSampleSystemPlugin(readOnlySystemPlugin: storageWCPlugin)
65+
5666
// When
5767
let result = try await sut.loadWooCommercePluginAndPOSFeatureSwitch(siteID: sampleSiteID)
5868

@@ -69,6 +79,10 @@ struct POSSystemStatusServiceTests {
6979
// Given
7080
network.simulateResponse(requestUrlSuffix: "system_status", filename: "system-status-wc-plugin-missing")
7181

82+
// Inserts WooCommerce plugin eligible for POS into storage.
83+
let storageWCPlugin = createWCPlugin(version: "9.9.0", active: true)
84+
storageManager.insertSampleSystemPlugin(readOnlySystemPlugin: storageWCPlugin)
85+
7286
// When
7387
let result = try await sut.loadWooCommercePluginAndPOSFeatureSwitch(siteID: sampleSiteID)
7488

@@ -87,3 +101,15 @@ struct POSSystemStatusServiceTests {
87101
}
88102
}
89103
}
104+
105+
private extension POSSystemStatusServiceTests {
106+
func createWCPlugin(version: String = "5.8.0", active: Bool = true) -> Yosemite.SystemPlugin {
107+
.fake().copy(
108+
siteID: sampleSiteID,
109+
plugin: "woocommerce/woocommerce.php",
110+
version: version,
111+
versionLatest: version,
112+
active: active
113+
)
114+
}
115+
}

RELEASE-NOTES.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
22.9
55
-----
66
- [**] Order Details: Update Shipping Labels section for stores with Woo Shipping extension [https://github.com/woocommerce/woocommerce-ios/pull/15889]
7-
7+
- [*] Order List: New orders made through Point of Sale are now filterable via the Order List filters menu [https://github.com/woocommerce/woocommerce-ios/pull/15910]
88

99
22.8
1010
-----

0 commit comments

Comments
 (0)