From 82881c12dd95fdfeedd782eae66509680942c678 Mon Sep 17 00:00:00 2001 From: John Rosendahl Date: Mon, 9 Nov 2026 15:27:23 -0700 Subject: [PATCH 1/2] Ignore blank GPP values; only send IABGPP_GppSID with IABGPP_HDR_GppString A blank (empty or whitespace-only) IABGPP_HDR_GppString, IABGPP_2_TCString or IABGPP_GppSID now counts as unset, so gpp falls back to the legacy key and the SDK never sends an empty gpp= or gpp_sid=. IABGPP_GppSID describes the CMP's own string, so it's no longer sent alongside the legacy IABGPP_2_TCString value. Mirrors Optable/optable-android-sdk#49. Co-Authored-By: Claude Opus 5.5 --- Source/Misc/IABConsent.swift | 15 ++++++-- Source/Public/OptableConfig.swift | 2 +- Tests/Unit/EdgeAPITests.swift | 63 +++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 4 deletions(-) diff --git a/Source/Misc/IABConsent.swift b/Source/Misc/IABConsent.swift index e608bb8..b028bd1 100644 --- a/Source/Misc/IABConsent.swift +++ b/Source/Misc/IABConsent.swift @@ -34,13 +34,22 @@ enum IABConsent { } static var gppTC: String? { - UserDefaults.standard.string(forKey: Keys.IABGPP_HDR_GppString) - ?? UserDefaults.standard.string(forKey: Keys.IABGPP_2_TCString) + nonBlankString(forKey: Keys.IABGPP_HDR_GppString) + ?? nonBlankString(forKey: Keys.IABGPP_2_TCString) } /// The CMP stores section IDs separated by underscores ("2_7"); the Edge API takes them comma-separated ("2,7"). + /// Only read when `IABGPP_HDR_GppString` is set: the section IDs describe the CMP's own string and must never be sent with `IABGPP_2_TCString`. static var gppSid: String? { - UserDefaults.standard.string(forKey: Keys.IABGPP_GppSID)? + guard nonBlankString(forKey: Keys.IABGPP_HDR_GppString) != nil else { return nil } + return nonBlankString(forKey: Keys.IABGPP_GppSID)? .replacingOccurrences(of: "_", with: ",") } + + /// A blank value (empty or whitespace only) counts as unset. + private static func nonBlankString(forKey key: String) -> String? { + guard let value = UserDefaults.standard.string(forKey: key), + !value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return nil } + return value + } } diff --git a/Source/Public/OptableConfig.swift b/Source/Public/OptableConfig.swift index b199c4a..705c86b 100644 --- a/Source/Public/OptableConfig.swift +++ b/Source/Public/OptableConfig.swift @@ -160,7 +160,7 @@ 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) + > If not set and `gpp` is read from UserDefaults `IABGPP_HDR_GppString`, 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 0894270..7b60d18 100644 --- a/Tests/Unit/EdgeAPITests.swift +++ b/Tests/Unit/EdgeAPITests.swift @@ -136,6 +136,69 @@ class EdgeAPITests: XCTestCase { UserDefaults.standard.set(nil, forKey: IABConsent.Keys.IABGPP_2_TCString) } + func test_url_generation_privacy_regulations_global_legacyGPPKey_ignoresGlobalGPPSid() throws { + UserDefaults.standard.set(nil, forKey: IABConsent.Keys.IABGPP_HDR_GppString) + UserDefaults.standard.set("2_7", 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_GppSID) + UserDefaults.standard.set(nil, forKey: IABConsent.Keys.IABGPP_2_TCString) + } + + func test_url_generation_privacy_regulations_global_blankGPP_fallsBackToLegacyGPPKey() throws { + UserDefaults.standard.set("", 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") + + UserDefaults.standard.set(nil, forKey: IABConsent.Keys.IABGPP_HDR_GppString) + UserDefaults.standard.set(nil, forKey: IABConsent.Keys.IABGPP_2_TCString) + } + + func test_url_generation_privacy_regulations_global_blankGPPSid() throws { + UserDefaults.standard.set("globalGPP", forKey: IABConsent.Keys.IABGPP_HDR_GppString) + UserDefaults.standard.set("", forKey: IABConsent.Keys.IABGPP_GppSID) + UserDefaults.standard.set(nil, 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, "globalGPP") + 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) + } + + func test_url_generation_privacy_regulations_global_blankGPPKeys() throws { + UserDefaults.standard.set("", forKey: IABConsent.Keys.IABGPP_HDR_GppString) + UserDefaults.standard.set(nil, forKey: IABConsent.Keys.IABGPP_GppSID) + UserDefaults.standard.set(" ", 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)! + + XCTAssertNil(generatedURLComponents.queryItems?.first(where: { $0.name == "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_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) From 400246984d83c338ae8ad9a13a3d80e0cb4309fd Mon Sep 17 00:00:00 2001 From: John Rosendahl Date: Mon, 9 Nov 2026 15:29:52 -0700 Subject: [PATCH 2/2] Document the narrower gppSid fallback and blank GPP values in both guides Co-Authored-By: Claude Opus 5.5 --- docs/usage-objc.md | 4 ++-- docs/usage-swift.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/usage-objc.md b/docs/usage-objc.md index 1a29546..04ba282 100644 --- a/docs/usage-objc.md +++ b/docs/usage-objc.md @@ -118,9 +118,9 @@ If you don't set them, the SDK reads the values that an IAB-compliant consent ma | `gppSid` | `IABGPP_GppSID` | `gpp_sid` | | `reg` | none | `reg` | -`gdpr` and `gdprConsent` are used together: if either one is missing from the config, the SDK ignores both and uses the `NSUserDefaults` values instead. `gppSid` falls back to `IABGPP_GppSID` only when `gpp` also comes from `NSUserDefaults`, so the section IDs always match the string they describe: if you set `gpp` in the config, set `gppSid` too. The CMP stores section IDs separated by underscores (`2_7`); the SDK sends them comma-separated (`2,7`). +`gdpr` and `gdprConsent` are used together: if either one is missing from the config, the SDK ignores both and uses the `NSUserDefaults` values instead. `gppSid` falls back to `IABGPP_GppSID` only when `gpp` comes from `NSUserDefaults` `IABGPP_HDR_GppString` (not the legacy `IABGPP_2_TCString`), so the section IDs always match the string they describe: if you set `gpp` in the config, set `gppSid` too. The CMP stores section IDs separated by underscores (`2_7`); the SDK sends them comma-separated (`2,7`). -If `IABGPP_HDR_GppString` is not set, the SDK also reads `gpp` from `IABGPP_2_TCString`, a key that earlier versions of the SDK used. +If `IABGPP_HDR_GppString` is not set, the SDK also reads `gpp` from `IABGPP_2_TCString`, a key that earlier versions of the SDK used. Empty or whitespace-only values count as not set. ### Identify API diff --git a/docs/usage-swift.md b/docs/usage-swift.md index e318bb9..d1c7160 100644 --- a/docs/usage-swift.md +++ b/docs/usage-swift.md @@ -96,9 +96,9 @@ If you don't set them, the SDK reads the values that an IAB-compliant consent ma | `gppSid` | `IABGPP_GppSID` | `gpp_sid` | | `reg` | none | `reg` | -`gdpr` and `gdprConsent` are used together: if either one is missing from the config, the SDK ignores both and uses the `UserDefaults` values instead. `gppSid` falls back to `IABGPP_GppSID` only when `gpp` also comes from `UserDefaults`, so the section IDs always match the string they describe: if you set `gpp` in the config, set `gppSid` too. The CMP stores section IDs separated by underscores (`2_7`); the SDK sends them comma-separated (`2,7`). +`gdpr` and `gdprConsent` are used together: if either one is missing from the config, the SDK ignores both and uses the `UserDefaults` values instead. `gppSid` falls back to `IABGPP_GppSID` only when `gpp` comes from `UserDefaults` `IABGPP_HDR_GppString` (not the legacy `IABGPP_2_TCString`), so the section IDs always match the string they describe: if you set `gpp` in the config, set `gppSid` too. The CMP stores section IDs separated by underscores (`2_7`); the SDK sends them comma-separated (`2,7`). -If `IABGPP_HDR_GppString` is not set, the SDK also reads `gpp` from `IABGPP_2_TCString`, a key that earlier versions of the SDK used. +If `IABGPP_HDR_GppString` is not set, the SDK also reads `gpp` from `IABGPP_2_TCString`, a key that earlier versions of the SDK used. Empty or whitespace-only values count as not set. ### Identify API