Skip to content

Commit 3b7a05d

Browse files
authored
Merge branch 'trunk' into woomob-1293-wcios17-store-charts-address-plotareaframe-deprecation
2 parents a62143b + 959c8ac commit 3b7a05d

File tree

85 files changed

+1960
-377
lines changed

Some content is hidden

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

85 files changed

+1960
-377
lines changed

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+
}

Modules/Sources/NetworkingCore/Model/Order.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,10 @@ public struct Order: Decodable, Sendable, GeneratedCopiable, GeneratedFakeable {
201201
// "payment_url" is only available on stores with version >= 6.4
202202
let paymentURL = try container.decodeIfPresent(URL.self, forKey: .paymentURL)
203203

204-
let allOrderMetaData = try? container.decode([MetaData].self, forKey: .metadata)
204+
let allOrderMetaData: [MetaData]? = {
205+
let metadata = [MetaData].decodeFlexibly(from: container, forKey: .metadata)
206+
return metadata.isEmpty ? nil : metadata
207+
}()
205208
var chargeID: String? = nil
206209
chargeID = allOrderMetaData?.first(where: { $0.key == "_charge_id" })?.value.stringValue
207210

0 commit comments

Comments
 (0)