Skip to content

Commit 5eb23ae

Browse files
authored
Merge branch 'trunk' into woomob-1124-remote-feature-flag
2 parents 7bbc33d + 3b620a9 commit 5eb23ae

File tree

303 files changed

+6276
-1799
lines changed

Some content is hidden

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

303 files changed

+6276
-1799
lines changed

Modules/Sources/Fakes/Networking.generated.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1774,7 +1774,10 @@ extension Networking.Site {
17741774
isAdmin: .fake(),
17751775
wasEcommerceTrial: .fake(),
17761776
hasSSOEnabled: .fake(),
1777-
applicationPasswordAvailable: .fake()
1777+
applicationPasswordAvailable: .fake(),
1778+
isGarden: false,
1779+
gardenName: nil,
1780+
gardenPartner: nil
17781781
)
17791782
}
17801783
}

Modules/Sources/Networking/Model/Copiable/Models+Copiable.generated.swift

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2747,7 +2747,10 @@ extension Networking.Site {
27472747
isAdmin: CopiableProp<Bool> = .copy,
27482748
wasEcommerceTrial: CopiableProp<Bool> = .copy,
27492749
hasSSOEnabled: CopiableProp<Bool> = .copy,
2750-
applicationPasswordAvailable: CopiableProp<Bool> = .copy
2750+
applicationPasswordAvailable: CopiableProp<Bool> = .copy,
2751+
isGarden: CopiableProp<Bool> = .copy,
2752+
gardenName: CopiableProp<String?> = .copy,
2753+
gardenPartner: CopiableProp<String?> = .copy
27512754
) -> Networking.Site {
27522755
let siteID = siteID ?? self.siteID
27532756
let name = name ?? self.name
@@ -2772,6 +2775,9 @@ extension Networking.Site {
27722775
let wasEcommerceTrial = wasEcommerceTrial ?? self.wasEcommerceTrial
27732776
let hasSSOEnabled = hasSSOEnabled ?? self.hasSSOEnabled
27742777
let applicationPasswordAvailable = applicationPasswordAvailable ?? self.applicationPasswordAvailable
2778+
let isGarden = isGarden ?? self.isGarden
2779+
let gardenName = gardenName ?? self.gardenName
2780+
let gardenPartner = gardenPartner ?? self.gardenPartner
27752781

27762782
return Networking.Site(
27772783
siteID: siteID,
@@ -2796,7 +2802,10 @@ extension Networking.Site {
27962802
isAdmin: isAdmin,
27972803
wasEcommerceTrial: wasEcommerceTrial,
27982804
hasSSOEnabled: hasSSOEnabled,
2799-
applicationPasswordAvailable: applicationPasswordAvailable
2805+
applicationPasswordAvailable: applicationPasswordAvailable,
2806+
isGarden: isGarden,
2807+
gardenName: gardenName,
2808+
gardenPartner: gardenPartner
28002809
)
28012810
}
28022811
}

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/Model/Site.swift

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,18 @@ public struct Site: Decodable, Equatable, Hashable, GeneratedFakeable, Generated
9797
///
9898
public let applicationPasswordAvailable: Bool
9999

100+
/// Whether the site is running on Garden architecture
101+
///
102+
public let isGarden: Bool
103+
104+
/// The site Garden name is present
105+
///
106+
public let gardenName: String?
107+
108+
/// The site Garden partner if present
109+
///
110+
public let gardenPartner: String?
111+
100112
/// Decodable Conformance.
101113
///
102114
public init(from decoder: Decoder) throws {
@@ -140,6 +152,10 @@ public struct Site: Decodable, Equatable, Hashable, GeneratedFakeable, Generated
140152
return jetpackModules.contains(OptionKeys.sso.rawValue) == true
141153
}()
142154

155+
let isGarden = try siteContainer.decodeIfPresent(Bool.self, forKey: .isGarden) ?? false
156+
let gardenName = try siteContainer.decodeIfPresent(String.self, forKey: .gardenName)
157+
let gardenPartner = try siteContainer.decodeIfPresent(String.self, forKey: .gardenPartner)
158+
143159
self.init(siteID: siteID,
144160
name: name,
145161
description: description,
@@ -162,7 +178,10 @@ public struct Site: Decodable, Equatable, Hashable, GeneratedFakeable, Generated
162178
isAdmin: isAdmin,
163179
wasEcommerceTrial: wasEcommerceTrial,
164180
hasSSOEnabled: hasSSOEnabled,
165-
applicationPasswordAvailable: false) // to be updated by fetching SiteAPI
181+
applicationPasswordAvailable: false, // to be updated by fetching SiteAPI
182+
isGarden: isGarden,
183+
gardenName: gardenName,
184+
gardenPartner: gardenPartner)
166185
}
167186

168187
/// Designated Initializer.
@@ -189,7 +208,10 @@ public struct Site: Decodable, Equatable, Hashable, GeneratedFakeable, Generated
189208
isAdmin: Bool,
190209
wasEcommerceTrial: Bool,
191210
hasSSOEnabled: Bool,
192-
applicationPasswordAvailable: Bool) {
211+
applicationPasswordAvailable: Bool,
212+
isGarden: Bool,
213+
gardenName: String?,
214+
gardenPartner: String?) {
193215
self.siteID = siteID
194216
self.name = name
195217
self.description = description
@@ -213,6 +235,9 @@ public struct Site: Decodable, Equatable, Hashable, GeneratedFakeable, Generated
213235
self.wasEcommerceTrial = wasEcommerceTrial
214236
self.hasSSOEnabled = hasSSOEnabled
215237
self.applicationPasswordAvailable = applicationPasswordAvailable
238+
self.isGarden = isGarden
239+
self.gardenName = gardenName
240+
self.gardenPartner = gardenPartner
216241
}
217242
}
218243

@@ -264,6 +289,9 @@ private extension Site {
264289
case isJetpackConnected = "jetpack_connection"
265290
case wasEcommerceTrial = "was_ecommerce_trial"
266291
case jetpackModules = "jetpack_modules"
292+
case isGarden = "is_garden"
293+
case gardenName = "garden_name"
294+
case gardenPartner = "garden_partner"
267295
}
268296

269297
enum PlanInfo: String, CodingKey {

Modules/Sources/Networking/Model/WordPressSite.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,10 @@ public extension WordPressSite {
110110
isAdmin: false,
111111
wasEcommerceTrial: false,
112112
hasSSOEnabled: false,
113-
applicationPasswordAvailable: false)
113+
applicationPasswordAvailable: false,
114+
isGarden: false,
115+
gardenName: nil,
116+
gardenPartner: nil)
114117
}
115118

116119
struct Authentication: Decodable {

Modules/Sources/Networking/Remote/SiteRemote.swift

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -338,12 +338,37 @@ extension SiteRemote {
338338
enum SiteParameter {
339339
enum Fields {
340340
static let key = "fields"
341-
static let value = "ID,name,description,URL,options,jetpack,jetpack_connection,capabilities,was_ecommerce_trial,plan,jetpack_modules"
341+
static let value = [
342+
"ID",
343+
"name",
344+
"description",
345+
"URL",
346+
"options",
347+
"jetpack",
348+
"jetpack_connection",
349+
"capabilities",
350+
"was_ecommerce_trial",
351+
"plan",
352+
"jetpack_modules",
353+
"is_garden",
354+
"garden_name",
355+
"garden_partner"
356+
].joined(separator: ",")
342357
}
343358
enum Options {
344359
static let key = "options"
345-
static let value =
346-
"timezone,is_wpcom_store,woocommerce_is_active,gmt_offset,jetpack_connection_active_plugins,admin_url,login_url,frame_nonce,blog_public,can_blaze"
360+
static let value = [
361+
"timezone",
362+
"is_wpcom_store",
363+
"woocommerce_is_active",
364+
"gmt_offset",
365+
"jetpack_connection_active_plugins",
366+
"admin_url",
367+
"login_url",
368+
"frame_nonce",
369+
"blog_public",
370+
"can_blaze"
371+
].joined(separator: ",")
347372
}
348373
}
349374
}
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)