Skip to content

Commit 44f5d2a

Browse files
committed
Merge branch 'trunk' into feat/WOOMOB-1326-flaky-concurrent-tests-AlamofireNetworkTests
# Conflicts: # Modules/Tests/NetworkingTests/Network/AlamofireNetworkTests.swift
2 parents c5624e9 + 10ec6fc commit 44f5d2a

File tree

322 files changed

+7029
-1654
lines changed

Some content is hidden

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

322 files changed

+7029
-1654
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
<!--
22
Contains editorialized release notes. Raw release notes should go into `RELEASE-NOTES.txt`.
33
-->
4+
## 23.3
5+
Enjoy a smoother store management experience with clearer shipping labels for physical items, friendlier product code error messages, more reliable custom fields, and better app performance for users authenticated with WordPress.com. All designed to streamline your daily workflow.
6+
47
## 23.2
58
This update smooths your WooCommerce experience with improved cash payments, easier access to POS settings, and accurate HAZMAT details on shipping labels. Plus, we fixed a Blaze flow issue so campaigns behave as expected. Faster, clearer, and more reliable — just how you need it!
69

Modules/Package.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,10 @@ let package = Package(
179179
),
180180
.target(
181181
name: "WooFoundation",
182-
dependencies: ["WooFoundationCore"]
182+
dependencies: [
183+
"WooFoundationCore",
184+
.product(name: "Kingfisher", package: "Kingfisher")
185+
]
183186
),
184187
.target(
185188
name: "WooFoundationCore",

Modules/Sources/Experiments/DefaultFeatureFlagService.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,10 @@ public struct DefaultFeatureFlagService: FeatureFlagService {
100100
return true
101101
case .pointOfSaleHistoricalOrdersi1:
102102
return buildConfig == .localDeveloper || buildConfig == .alpha
103-
case .applicationPasswordExperiment:
104-
return buildConfig == .localDeveloper || buildConfig == .alpha
105103
case .pointOfSaleLocalCatalogi1:
106104
return buildConfig == .localDeveloper || buildConfig == .alpha
105+
case .ciabBookings:
106+
return buildConfig == .localDeveloper || buildConfig == .alpha
107107
default:
108108
return true
109109
}

Modules/Sources/Experiments/FeatureFlag.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -207,12 +207,12 @@ public enum FeatureFlag: Int {
207207
///
208208
case pointOfSaleHistoricalOrdersi1
209209

210-
/// Enables switching Jetpack requests to use application password
211-
///
212-
case applicationPasswordExperiment
213-
214210
/// Enables Local Catalog i1 in Point of Sale.
215211
/// It syncs products and variations to local storage and display them in POS for quick access.
216212
///
217213
case pointOfSaleLocalCatalogi1
214+
215+
/// Enables a new Bookings tab for CIAB sites
216+
///
217+
case ciabBookings
218218
}

Modules/Sources/Networking/Model/Product/Product.swift

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -457,7 +457,7 @@ public struct Product: Codable, GeneratedCopiable, Equatable, GeneratedFakeable
457457
// `true`
458458
value.lowercased() == Values.manageStockParent ? true : false
459459
})
460-
]) ?? false
460+
]) ?? false
461461

462462
// Even though WooCommerce Core returns Int or null values,
463463
// some plugins alter the field value from Int to Decimal or String.
@@ -535,15 +535,17 @@ public struct Product: Codable, GeneratedCopiable, Equatable, GeneratedFakeable
535535
let menuOrder = try container.decode(Int.self, forKey: .menuOrder)
536536

537537
// Filter out metadata if the key is prefixed with an underscore (internal meta keys)
538-
let customFields = (try? container.decode([MetaData].self, forKey: .metadata).filter({ !$0.key.hasPrefix("_")})) ?? []
538+
// Support both array format and object keyed by index strings
539+
let allMetaData = [MetaData].decodeFlexibly(from: container, forKey: .metadata)
540+
let customFields = allMetaData.filter { !$0.key.hasPrefix("_") }
539541

540542
// In some isolated cases, it appears to be some malformed meta-data that causes this line to throw hence the whole product decoding to throw.
541543
// Since add-ons are optional, `try?` will be used to prevent the whole decoding to stop.
542544
// https://github.com/woocommerce/woocommerce-ios/issues/4205
543545
let addOns = (try? container.decodeIfPresent(ProductAddOnEnvelope.self, forKey: .metadata)?.revolve()) ?? []
544546

545-
let metaDataExtractor = try? container.decodeIfPresent(ProductMetadataExtractor.self, forKey: .metadata)
546-
let isSampleItem = (metaDataExtractor?.extractStringValue(forKey: MetadataKeys.headStartPost) == Values.headStartValue)
547+
let metaDataExtractor = ProductMetadataExtractor(metadata: allMetaData)
548+
let isSampleItem = (metaDataExtractor.extractStringValue(forKey: MetadataKeys.headStartPost) == Values.headStartValue)
547549

548550
// Product Bundle properties
549551
// Uses failsafe decoding because non-bundle product types can return unexpected value types.
@@ -561,7 +563,7 @@ public struct Product: Codable, GeneratedCopiable, Equatable, GeneratedFakeable
561563
let compositeComponents = try container.decodeIfPresent([ProductCompositeComponent].self, forKey: .compositeComponents) ?? []
562564

563565
// Subscription properties
564-
let subscription = try? metaDataExtractor?.extractProductSubscription()
566+
let subscription = try? metaDataExtractor.extractProductSubscription()
565567

566568
// Min/Max Quantities properties
567569
let minAllowedQuantity = container.failsafeDecodeIfPresent(stringForKey: .minAllowedQuantity)

Modules/Sources/Networking/Model/Product/ProductMetadataExtractor.swift

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Foundation
22
import WordPressShared
3+
import NetworkingCore
34

45
/// Helper to extract specific data from inside `Product` metadata.
56
/// Sample Json:
@@ -21,25 +22,23 @@ import WordPressShared
2122
/// }
2223
/// ]
2324
///
24-
internal struct ProductMetadataExtractor: Decodable {
25+
struct ProductMetadataExtractor {
2526

26-
private typealias DecodableDictionary = [String: AnyDecodable]
2727
private typealias AnyDictionary = [String: Any?]
2828

2929
/// Internal metadata representation
3030
///
31-
private let metadata: [DecodableDictionary]
31+
private let metadata: [MetaData]
3232

33-
/// Decode main metadata array as an untyped dictionary.
33+
/// Initialize with already-decoded metadata array
3434
///
35-
init(from decoder: Decoder) throws {
36-
let container = try decoder.singleValueContainer()
37-
self.metadata = try container.decode([DecodableDictionary].self)
35+
init(metadata: [MetaData]) {
36+
self.metadata = metadata
3837
}
3938

4039
/// Searches product metadata for subscription data and converts it to a `ProductSubscription` if possible.
4140
///
42-
internal func extractProductSubscription() throws -> ProductSubscription? {
41+
func extractProductSubscription() throws -> ProductSubscription? {
4342
let subscriptionMetadata = filterMetadata(with: Constants.subscriptionPrefix)
4443

4544
guard !subscriptionMetadata.isEmpty else {
@@ -54,30 +53,33 @@ internal struct ProductMetadataExtractor: Decodable {
5453

5554
/// Extracts a `String` metadata value for the provided key.
5655
///
57-
internal func extractStringValue(forKey key: String) -> String? {
56+
func extractStringValue(forKey key: String) -> String? {
5857
let metaData = filterMetadata(with: key)
5958
let keyValueMetadata = getKeyValueDictionary(from: metaData)
6059
return keyValueMetadata.valueAsString(forKey: key)
6160
}
6261

6362
/// Filters product metadata using the provided prefix.
6463
///
65-
private func filterMetadata(with prefix: String) -> [DecodableDictionary] {
66-
metadata.filter { object in
67-
let objectKey = object["key"]?.value as? String ?? ""
68-
return objectKey.hasPrefix(prefix)
69-
}
64+
private func filterMetadata(with prefix: String) -> [MetaData] {
65+
metadata.filter { $0.key.hasPrefix(prefix) }
7066
}
7167

7268
/// Parses provided metadata to return a dictionary with each metadata object's key and value.
7369
///
74-
private func getKeyValueDictionary(from metadata: [DecodableDictionary]) -> AnyDictionary {
75-
metadata.reduce(AnyDictionary()) { (dict, object) in
76-
var newDict = dict
77-
let objectKey = object["key"]?.value as? String ?? ""
78-
let objectValue = object["value"]?.value
79-
newDict.updateValue(objectValue, forKey: objectKey)
80-
return newDict
70+
private func getKeyValueDictionary(from metadata: [MetaData]) -> AnyDictionary {
71+
metadata.reduce(into: AnyDictionary()) { dict, object in
72+
// For JSON values, decode them to get the actual object
73+
if object.value.isJson {
74+
if let data = object.value.stringValue.data(using: .utf8),
75+
let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []) {
76+
dict[object.key] = jsonObject
77+
} else {
78+
dict[object.key] = object.value.stringValue
79+
}
80+
} else {
81+
dict[object.key] = object.value.stringValue
82+
}
8183
}
8284
}
8385

Modules/Sources/Networking/Model/Product/ProductVariation.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ public struct ProductVariation: Codable, GeneratedCopiable, Equatable, Generated
279279
}
280280
return false
281281
})
282-
]) ?? false
282+
]) ?? false
283283

284284
// Even though WooCommerce Core returns Int or null values,
285285
// some plugins alter the field value from Int to Decimal or String.
@@ -309,7 +309,9 @@ public struct ProductVariation: Codable, GeneratedCopiable, Equatable, Generated
309309
let menuOrder = try container.decode(Int64.self, forKey: .menuOrder)
310310

311311
// Subscription settings for subscription variations
312-
let subscription = try? container.decodeIfPresent(ProductMetadataExtractor.self, forKey: .metadata)?.extractProductSubscription()
312+
let allMetaData = [MetaData].decodeFlexibly(from: container, forKey: .metadata)
313+
let metaDataExtractor = ProductMetadataExtractor(metadata: allMetaData)
314+
let subscription = try? metaDataExtractor.extractProductSubscription()
313315

314316
// Min/Max Quantities properties
315317
let minAllowedQuantity = container.failsafeDecodeIfPresent(stringForKey: .minAllowedQuantity)

Modules/Sources/Networking/Remote/FeatureFlagRemote.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public enum RemoteFeatureFlag: Decodable {
3030
case storeCreationCompleteNotification
3131
case hardcodedPlanUpgradeDetailsMilestone1AreAccurate
3232
case pointOfSale
33+
case appPasswordsForJetpackSites
3334

3435
init?(rawValue: String) {
3536
switch rawValue {
@@ -39,6 +40,8 @@ public enum RemoteFeatureFlag: Decodable {
3940
self = .hardcodedPlanUpgradeDetailsMilestone1AreAccurate
4041
case "woo_pos":
4142
self = .pointOfSale
43+
case "woo_app_passwords_for_jetpack_sites":
44+
self = .appPasswordsForJetpackSites
4245
default:
4346
return nil
4447
}

Modules/Sources/Networking/Remote/ProductsRemote.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public protocol ProductsRemoteProtocol {
4848
pageNumber: Int,
4949
pageSize: Int,
5050
productStatus: ProductStatus?,
51+
productType: ProductType?,
5152
completion: @escaping (Result<[Int64], Error>) -> Void)
5253
func loadNumberOfProducts(siteID: Int64) async throws -> Int64
5354

@@ -593,12 +594,14 @@ public final class ProductsRemote: Remote, ProductsRemoteProtocol {
593594
pageNumber: Int = Default.pageNumber,
594595
pageSize: Int = Default.pageSize,
595596
productStatus: ProductStatus? = nil,
597+
productType: ProductType? = nil,
596598
completion: @escaping (Result<[Int64], Error>) -> Void) {
597599
let parameters = [
598600
ParameterKey.page: String(pageNumber),
599601
ParameterKey.perPage: String(pageSize),
600602
ParameterKey.fields: ParameterKey.id,
601-
ParameterKey.productStatus: productStatus?.rawValue ?? ""
603+
ParameterKey.productStatus: productStatus?.rawValue ?? "",
604+
ParameterKey.productType: productType?.rawValue ?? ""
602605
].filter({ $0.value.isEmpty == false })
603606

604607
let path = Path.products
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import Foundation
2+
3+
/// Extension to support flexible decoding of MetaData arrays
4+
/// Handles both standard array format and object format keyed by index strings
5+
extension Array where Element == MetaData {
6+
7+
/// Custom decoding from container that supports both array and dictionary formats
8+
public static func decodeFlexibly<K>(from container: KeyedDecodingContainer<K>,
9+
forKey key: KeyedDecodingContainer<K>.Key) -> [MetaData] {
10+
// Try to decode as array first (standard format)
11+
if let metaDataArray = try? container.decode([MetaData].self, forKey: key) {
12+
return metaDataArray
13+
}
14+
15+
// Try to decode as object keyed by index strings
16+
if let metaDataDict = try? container.decode([String: MetaData].self, forKey: key) {
17+
return Array(metaDataDict.values)
18+
}
19+
20+
// Fallback to empty array
21+
DDLogWarn("⚠️ Could not decode metadata as either an array or object keyed by index strings. Falling back to empty array.")
22+
return []
23+
}
24+
}

0 commit comments

Comments
 (0)