From 418e512fa1bc3ff6c27021e35baea01833325021 Mon Sep 17 00:00:00 2001 From: John Rosendahl Date: Thu, 29 Oct 2026 15:10:09 -0600 Subject: [PATCH] Read GPP consent from the IAB's UserDefaults keys The SDK read the GPP string from IABGPP_2_TCString, which is not an IAB key, so a spec-compliant CMP's string was never found. It never read the section IDs either, so gpp went out without gpp_sid unless the app set it. Read IABGPP_HDR_GppString (keeping IABGPP_2_TCString as a fallback) and IABGPP_GppSID, converting the CMP's "2_7" to the Edge API's "2,7". The CMP's section IDs are only used with the CMP's GPP string; an explicit config.gppSid still wins. Co-Authored-By: Claude Opus 5.5 --- Source/Core/EdgeAPI.swift | 6 ++++- Source/Misc/IABConsent.swift | 12 ++++++++- Source/Public/OptableConfig.swift | 4 ++- Tests/Unit/EdgeAPITests.swift | 41 ++++++++++++++++++++++++++++++- 4 files changed, 59 insertions(+), 4 deletions(-) diff --git a/Source/Core/EdgeAPI.swift b/Source/Core/EdgeAPI.swift index cc1b406..5e61c85 100644 --- a/Source/Core/EdgeAPI.swift +++ b/Source/Core/EdgeAPI.swift @@ -261,6 +261,9 @@ extension EdgeAPI { ]) } + // The CMP's section IDs are only used with the CMP's GPP string, never with one set in config. + var gppSid = config.gppSid + if let gpp = config.gpp { components.queryItems?.append( .init(name: "gpp", value: gpp) @@ -269,9 +272,10 @@ extension EdgeAPI { components.queryItems?.append( .init(name: "gpp", value: globalGPP) ) + gppSid = gppSid ?? IABConsent.gppSid } - if let gppSid = config.gppSid { + if let gppSid { components.queryItems?.append( .init(name: "gpp_sid", value: gppSid) ) diff --git a/Source/Misc/IABConsent.swift b/Source/Misc/IABConsent.swift index b341ffa..e608bb8 100644 --- a/Source/Misc/IABConsent.swift +++ b/Source/Misc/IABConsent.swift @@ -16,6 +16,9 @@ enum IABConsent { enum Keys { static let IABTCF_TCString = "IABTCF_TCString" static let IABTCF_gdprApplies = "IABTCF_gdprApplies" + static let IABGPP_HDR_GppString = "IABGPP_HDR_GppString" + static let IABGPP_GppSID = "IABGPP_GppSID" + /// Not an IAB key, read before `IABGPP_HDR_GppString` was. Kept as a fallback for apps that write it. static let IABGPP_2_TCString = "IABGPP_2_TCString" } @@ -31,6 +34,13 @@ enum IABConsent { } static var gppTC: String? { - UserDefaults.standard.string(forKey: Keys.IABGPP_2_TCString) + UserDefaults.standard.string(forKey: Keys.IABGPP_HDR_GppString) + ?? UserDefaults.standard.string(forKey: Keys.IABGPP_2_TCString) + } + + /// The CMP stores section IDs separated by underscores ("2_7"); the Edge API takes them comma-separated ("2,7"). + static var gppSid: String? { + UserDefaults.standard.string(forKey: Keys.IABGPP_GppSID)? + .replacingOccurrences(of: "_", with: ",") } } diff --git a/Source/Public/OptableConfig.swift b/Source/Public/OptableConfig.swift index 9585bc8..b199c4a 100644 --- a/Source/Public/OptableConfig.swift +++ b/Source/Public/OptableConfig.swift @@ -148,7 +148,7 @@ public final class OptableConfig: NSObject, Sendable { /** GPP privacy string. - > If not set, SDK will try to fetch data from UserDefaults => `IABGPP_2_TCString`, as stated in [](https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/TCFv2/IAB%20Tech%20Lab%20-%20CMP%20API%20v2.md#in-app-details) + > If not set, SDK will try to fetch data from UserDefaults => `IABGPP_HDR_GppString`, as stated in [](https://github.com/InteractiveAdvertisingBureau/Global-Privacy-Platform/blob/main/Core/CMP%20API%20Specification.md) */ @objc public var gpp: String? { @@ -159,6 +159,8 @@ public final class OptableConfig: NSObject, Sendable { /** A comma-separated list of up to two sections applicable in a given GPP privacy string. This value is required when gpp is present. + + > If not set and `gpp` is read from UserDefaults, SDK will also read `IABGPP_GppSID` from UserDefaults, as stated in [](https://github.com/InteractiveAdvertisingBureau/Global-Privacy-Platform/blob/main/Core/CMP%20API%20Specification.md) */ @objc public var gppSid: String? { diff --git a/Tests/Unit/EdgeAPITests.swift b/Tests/Unit/EdgeAPITests.swift index afe040c..0894270 100644 --- a/Tests/Unit/EdgeAPITests.swift +++ b/Tests/Unit/EdgeAPITests.swift @@ -82,6 +82,8 @@ class EdgeAPITests: XCTestCase { UserDefaults.standard.set(nil, forKey: IABConsent.Keys.IABTCF_gdprApplies) UserDefaults.standard.set(nil, forKey: IABConsent.Keys.IABTCF_TCString) UserDefaults.standard.set(nil, forKey: IABConsent.Keys.IABGPP_2_TCString) + UserDefaults.standard.set(nil, forKey: IABConsent.Keys.IABGPP_HDR_GppString) + UserDefaults.standard.set(nil, forKey: IABConsent.Keys.IABGPP_GppSID) let config = OptableConfig(tenant: T.api.tenant.prebidtest, originSlug: T.api.slug.iosSDK) let generatedURL = OptableSDK(config: config).api.buildEdgeAPIURL(endpoint: T.api.endpoint.identify) @@ -100,7 +102,9 @@ class EdgeAPITests: XCTestCase { func test_url_generation_privacy_regulations_global() throws { UserDefaults.standard.set("0", forKey: IABConsent.Keys.IABTCF_gdprApplies) UserDefaults.standard.set("globalGDPRConsent", forKey: IABConsent.Keys.IABTCF_TCString) - UserDefaults.standard.set("globalGPP", forKey: IABConsent.Keys.IABGPP_2_TCString) + UserDefaults.standard.set(nil, forKey: IABConsent.Keys.IABGPP_2_TCString) + UserDefaults.standard.set("globalGPP", forKey: IABConsent.Keys.IABGPP_HDR_GppString) + UserDefaults.standard.set("2_7", forKey: IABConsent.Keys.IABGPP_GppSID) let config = OptableConfig(tenant: T.api.tenant.prebidtest, originSlug: T.api.slug.iosSDK) let generatedURL = OptableSDK(config: config).api.buildEdgeAPIURL(endpoint: T.api.endpoint.identify) @@ -113,7 +117,40 @@ class EdgeAPITests: XCTestCase { XCTAssertEqual(generatedURLComponents.queryItems!.first(where: { $0.name == "gdpr" })!.value, "0") XCTAssertNotNil(generatedURLComponents.queryItems?.first(where: { $0.name == "gpp" })) XCTAssertEqual(generatedURLComponents.queryItems!.first(where: { $0.name == "gpp" })!.value, "globalGPP") + XCTAssertNotNil(generatedURLComponents.queryItems?.first(where: { $0.name == "gpp_sid" })) + XCTAssertEqual(generatedURLComponents.queryItems!.first(where: { $0.name == "gpp_sid" })!.value, "2,7") + } + + func test_url_generation_privacy_regulations_global_legacyGPPKey() throws { + UserDefaults.standard.set(nil, forKey: IABConsent.Keys.IABGPP_HDR_GppString) + UserDefaults.standard.set(nil, forKey: IABConsent.Keys.IABGPP_GppSID) + UserDefaults.standard.set("legacyGPP", forKey: IABConsent.Keys.IABGPP_2_TCString) + + let config = OptableConfig(tenant: T.api.tenant.prebidtest, originSlug: T.api.slug.iosSDK) + let generatedURL = OptableSDK(config: config).api.buildEdgeAPIURL(endpoint: T.api.endpoint.identify) + let generatedURLComponents = URLComponents(url: generatedURL!, resolvingAgainstBaseURL: false)! + + XCTAssertEqual(generatedURLComponents.queryItems?.first(where: { $0.name == "gpp" })?.value, "legacyGPP") + XCTAssertNil(generatedURLComponents.queryItems?.first(where: { $0.name == "gpp_sid" })) + + UserDefaults.standard.set(nil, forKey: IABConsent.Keys.IABGPP_2_TCString) + } + + func test_url_generation_privacy_regulations_explicitGPP_ignoresGlobalGPPSid() throws { + UserDefaults.standard.set("globalGPP", forKey: IABConsent.Keys.IABGPP_HDR_GppString) + UserDefaults.standard.set("2_7", forKey: IABConsent.Keys.IABGPP_GppSID) + + let config = OptableConfig(tenant: T.api.tenant.prebidtest, originSlug: T.api.slug.iosSDK) + config.gpp = "gpp" + + let generatedURL = OptableSDK(config: config).api.buildEdgeAPIURL(endpoint: T.api.endpoint.identify) + let generatedURLComponents = URLComponents(url: generatedURL!, resolvingAgainstBaseURL: false)! + + XCTAssertEqual(generatedURLComponents.queryItems?.first(where: { $0.name == "gpp" })?.value, "gpp") XCTAssertNil(generatedURLComponents.queryItems?.first(where: { $0.name == "gpp_sid" })) + + UserDefaults.standard.set(nil, forKey: IABConsent.Keys.IABGPP_HDR_GppString) + UserDefaults.standard.set(nil, forKey: IABConsent.Keys.IABGPP_GppSID) } /** @@ -123,6 +160,8 @@ class EdgeAPITests: XCTestCase { UserDefaults.standard.set("0", forKey: IABConsent.Keys.IABTCF_gdprApplies) UserDefaults.standard.set("globalGDPRConsent", forKey: IABConsent.Keys.IABTCF_TCString) UserDefaults.standard.set(nil, forKey: IABConsent.Keys.IABGPP_2_TCString) + UserDefaults.standard.set(nil, forKey: IABConsent.Keys.IABGPP_HDR_GppString) + UserDefaults.standard.set(nil, forKey: IABConsent.Keys.IABGPP_GppSID) let config = OptableConfig(tenant: T.api.tenant.prebidtest, originSlug: T.api.slug.iosSDK) config.reg = "reg"