From 37b7cc205204b458bf3dd2eeaa028f7ee34c964e Mon Sep 17 00:00:00 2001 From: Huong Do Date: Wed, 10 Sep 2025 10:55:44 +0700 Subject: [PATCH 1/5] Support parsing dictionary for SiteAPI --- Modules/Sources/Networking/Model/SiteAPI.swift | 11 ++++++++++- .../Extensions/KeyedDecodingContainer+Woo.swift | 5 +++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Modules/Sources/Networking/Model/SiteAPI.swift b/Modules/Sources/Networking/Model/SiteAPI.swift index cc8acbb75f9..b9dfef2a711 100644 --- a/Modules/Sources/Networking/Model/SiteAPI.swift +++ b/Modules/Sources/Networking/Model/SiteAPI.swift @@ -45,7 +45,16 @@ public struct SiteAPI: Decodable, Equatable, GeneratedFakeable { } let siteAPIContainer = try decoder.container(keyedBy: SiteAPIKeys.self) - let namespaces = siteAPIContainer.failsafeDecodeIfPresent([String].self, forKey: .namespaces) ?? [] + + /// Some third-party plugins (like CoCart API) alter the response of `namespaces` field into a dictionary instead of array. + /// This workaround transforms the unexpected dictionary to extract the values in the dictionary. + let namespaces = siteAPIContainer.failsafeDecodeIfPresent( + targetType: [String].self, + forKey: .namespaces, alternativeTypes: [ + .dictionary(transform: { Array($0.values) }) + ] + ) ?? [] + let authentication = try? siteAPIContainer.decode(Authentication.self, forKey: .authentication) let applicationPasswordAvailable = authentication?.applicationPasswords?.endpoints?.authorization != nil diff --git a/Modules/Sources/NetworkingCore/Extensions/KeyedDecodingContainer+Woo.swift b/Modules/Sources/NetworkingCore/Extensions/KeyedDecodingContainer+Woo.swift index 7bce142c4dc..093b2c4467d 100644 --- a/Modules/Sources/NetworkingCore/Extensions/KeyedDecodingContainer+Woo.swift +++ b/Modules/Sources/NetworkingCore/Extensions/KeyedDecodingContainer+Woo.swift @@ -6,6 +6,7 @@ public enum AlternativeDecodingType { case string(transform: (String) -> T) case bool(transform: (Bool) -> T) case integer(transform: (Int) -> T) + case dictionary(transform: ([String: String]) -> T) } // MARK: - KeyedDecodingContainer: Bulletproof JSON Decoding. @@ -60,6 +61,10 @@ public extension KeyedDecodingContainer { if let result = failsafeDecodeIfPresent(integerForKey: key) { return transform(result) } + case .dictionary(let transform): + if let result = failsafeDecodeIfPresent([String: String].self, forKey: key) { + return transform(result) + } } } return nil From 1c4ca3d5a7f106838f5a4a94c3de6513e4212855 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Wed, 10 Sep 2025 11:07:51 +0700 Subject: [PATCH 2/5] Add new test for SiteAPI --- .../Mapper/SiteAPIMapperTests.swift | 20 +++++++++++++++- .../Responses/site-api-malformed.json | 23 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 Modules/Tests/NetworkingTests/Responses/site-api-malformed.json diff --git a/Modules/Tests/NetworkingTests/Mapper/SiteAPIMapperTests.swift b/Modules/Tests/NetworkingTests/Mapper/SiteAPIMapperTests.swift index c7373302d1d..4d3ce308b5a 100644 --- a/Modules/Tests/NetworkingTests/Mapper/SiteAPIMapperTests.swift +++ b/Modules/Tests/NetworkingTests/Mapper/SiteAPIMapperTests.swift @@ -5,7 +5,7 @@ import XCTest /// SiteAPIMapperTests Unit Tests /// -class SiteAPIMapperTests: XCTestCase { +final class SiteAPIMapperTests: XCTestCase { /// Dummy Site ID. /// @@ -54,6 +54,18 @@ class SiteAPIMapperTests: XCTestCase { XCTAssertEqual(apiSettings?.namespaces, dummyBrokenNamespaces) XCTAssertEqual(apiSettings?.highestWooVersion, WooAPIVersion.none) } + + /// Verifies the SiteSetting fields are parsed correctly. + /// + func test_SiteSetting_with_malformed_namespaces_fields_are_properly_parsed() { + let apiSettings = mapLoadSiteAPIResponseWithMalformedNamespaces() + + XCTAssertNotNil(apiSettings) + XCTAssertEqual(apiSettings?.siteID, dummySiteID) + XCTAssertNotNil(apiSettings?.namespaces) + XCTAssertEqual(apiSettings?.namespaces.sorted(), dummyNamespaces.sorted()) + XCTAssertEqual(apiSettings?.highestWooVersion, WooAPIVersion.mark3) + } } @@ -88,4 +100,10 @@ private extension SiteAPIMapperTests { func mapLoadBrokenSiteAPIResponse() -> SiteAPI? { return mapSiteAPIData(from: "site-api-no-woo") } + + /// Returns the SiteAPIMapper output with malformed namespaces + /// + func mapLoadSiteAPIResponseWithMalformedNamespaces() -> SiteAPI? { + return mapSiteAPIData(from: "site-api-malformed") + } } diff --git a/Modules/Tests/NetworkingTests/Responses/site-api-malformed.json b/Modules/Tests/NetworkingTests/Responses/site-api-malformed.json new file mode 100644 index 00000000000..c6b4c2d4ab4 --- /dev/null +++ b/Modules/Tests/NetworkingTests/Responses/site-api-malformed.json @@ -0,0 +1,23 @@ +{ + "data": { + "namespaces": { + "0": "oembed\/1.0", + "1": "akismet\/v1", + "2": "jetpack\/v4", + "3": "wpcom\/v2", + "4": "wc\/v1", + "5": "wc\/v2", + "6": "wc\/v3", + "7": "wc-pb\/v3", + "8": "wp\/v2" + }, + "authentication": [], + "_links": { + "help": [ + { + "href": "http:\/\/v2.wp-api.org\/" + } + ] + } + } +} From 12a092c30fd6a4c1750998b51ebcc3a63033e988 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Wed, 10 Sep 2025 11:09:04 +0700 Subject: [PATCH 3/5] Remove unnecessary observers upon authenticating --- WooCommerce/Classes/Yosemite/DefaultStoresManager.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/WooCommerce/Classes/Yosemite/DefaultStoresManager.swift b/WooCommerce/Classes/Yosemite/DefaultStoresManager.swift index 2f5eb8c814f..1d05c18d990 100644 --- a/WooCommerce/Classes/Yosemite/DefaultStoresManager.swift +++ b/WooCommerce/Classes/Yosemite/DefaultStoresManager.swift @@ -166,8 +166,10 @@ class DefaultStoresManager: StoresManager { if case .wpcom = credentials { listenToWPCOMInvalidWPCOMTokenNotification() + applicationPasswordGenerationFailureObserver = nil } else { listenToApplicationPasswordGenerationFailureNotification() + invalidWPCOMTokenNotificationObserver = nil } return self From 87d3b02ecbf5fd5ebf85d1b08ac45f3cbe1e4b91 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Wed, 10 Sep 2025 11:23:33 +0700 Subject: [PATCH 4/5] Remove invalidWPCOMTokenNotificationObserver upon deauthenticate --- WooCommerce/Classes/Yosemite/DefaultStoresManager.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/WooCommerce/Classes/Yosemite/DefaultStoresManager.swift b/WooCommerce/Classes/Yosemite/DefaultStoresManager.swift index 1d05c18d990..a552090ee2a 100644 --- a/WooCommerce/Classes/Yosemite/DefaultStoresManager.swift +++ b/WooCommerce/Classes/Yosemite/DefaultStoresManager.swift @@ -252,6 +252,7 @@ class DefaultStoresManager: StoresManager { @discardableResult func deauthenticate() -> StoresManager { applicationPasswordGenerationFailureObserver = nil + invalidWPCOMTokenNotificationObserver = nil if isAuthenticated { let resetAction = CardPresentPaymentAction.reset From c7889b0d867a9cc8281799ab40796606c67f0970 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Thu, 11 Sep 2025 09:49:12 +0700 Subject: [PATCH 5/5] Update indentation --- Modules/Sources/Networking/Model/SiteAPI.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/Sources/Networking/Model/SiteAPI.swift b/Modules/Sources/Networking/Model/SiteAPI.swift index b9dfef2a711..f8d59dc755a 100644 --- a/Modules/Sources/Networking/Model/SiteAPI.swift +++ b/Modules/Sources/Networking/Model/SiteAPI.swift @@ -50,7 +50,8 @@ public struct SiteAPI: Decodable, Equatable, GeneratedFakeable { /// This workaround transforms the unexpected dictionary to extract the values in the dictionary. let namespaces = siteAPIContainer.failsafeDecodeIfPresent( targetType: [String].self, - forKey: .namespaces, alternativeTypes: [ + forKey: .namespaces, + alternativeTypes: [ .dictionary(transform: { Array($0.values) }) ] ) ?? []