feat: initial public release
ConsentOS — a privacy-first cookie consent management platform. Self-hosted, source-available alternative to OneTrust, Cookiebot, and CookieYes. Full standards coverage (IAB TCF v2.2, GPP v1, Google Consent Mode v2, GPC, Shopify Customer Privacy API), multi-tenant architecture with role-based access, configuration cascade (system → org → group → site → region), dark-pattern detection in the scanner, and a tamper-evident consent record audit trail. This is the initial public release. Prior development history is retained internally. See README.md for the feature list, architecture overview, and quick-start instructions. Licensed under the Elastic Licence 2.0 — self-host freely; do not resell as a managed service.
This commit is contained in:
128
sdks/ios/ConsentOS/Sources/ConsentOSCore/GCMBridge.swift
Normal file
128
sdks/ios/ConsentOS/Sources/ConsentOSCore/GCMBridge.swift
Normal file
@@ -0,0 +1,128 @@
|
||||
import Foundation
|
||||
|
||||
// MARK: - GCM Consent Types
|
||||
|
||||
/// The Google Consent Mode v2 consent type strings.
|
||||
public enum GCMConsentType: String, CaseIterable, Sendable {
|
||||
case analyticsStorage = "analytics_storage"
|
||||
case adStorage = "ad_storage"
|
||||
case adUserData = "ad_user_data"
|
||||
case adPersonalisation = "ad_personalization"
|
||||
case functionalityStorage = "functionality_storage"
|
||||
case personalisationStorage = "personalization_storage"
|
||||
case securityStorage = "security_storage"
|
||||
}
|
||||
|
||||
/// The granted/denied status for a GCM consent type.
|
||||
public enum GCMConsentStatus: String, Sendable {
|
||||
case granted = "granted"
|
||||
case denied = "denied"
|
||||
}
|
||||
|
||||
// MARK: - GCM Analytics Provider Protocol
|
||||
|
||||
/// Abstracts the Firebase Analytics / Google Tag Manager call surface.
|
||||
///
|
||||
/// Conforming types should call the underlying GCM API. The default no-op
|
||||
/// implementation is used when Firebase is not present.
|
||||
public protocol GCMAnalyticsProvider: AnyObject, Sendable {
|
||||
/// Sets the default consent state before any interaction.
|
||||
func setConsentDefaults(_ defaults: [String: String])
|
||||
|
||||
/// Updates consent state after the user interacts with the banner.
|
||||
func updateConsent(_ updates: [String: String])
|
||||
}
|
||||
|
||||
// MARK: - No-Op Provider
|
||||
|
||||
/// A no-op ``GCMAnalyticsProvider`` used when Firebase Analytics is not linked.
|
||||
public final class NoOpGCMAnalyticsProvider: GCMAnalyticsProvider, @unchecked Sendable {
|
||||
public init() {}
|
||||
public func setConsentDefaults(_ defaults: [String: String]) {}
|
||||
public func updateConsent(_ updates: [String: String]) {}
|
||||
}
|
||||
|
||||
// MARK: - GCM Bridge
|
||||
|
||||
/// Maps CMP consent state to Google Consent Mode v2 signals.
|
||||
///
|
||||
/// On app launch, call ``applyDefaults(config:)`` before the user interacts.
|
||||
/// After consent is collected, call ``applyConsent(state:)`` to update GCM.
|
||||
///
|
||||
/// To integrate with Firebase Analytics, implement ``GCMAnalyticsProvider`` and
|
||||
/// call the `Firebase.Analytics.setConsent(_:)` API inside it.
|
||||
public final class GCMBridge: @unchecked Sendable {
|
||||
|
||||
// MARK: - Dependencies
|
||||
|
||||
private let provider: any GCMAnalyticsProvider
|
||||
|
||||
// MARK: - Initialiser
|
||||
|
||||
/// - Parameter provider: The analytics provider to forward consent signals to.
|
||||
/// Defaults to a no-op implementation.
|
||||
public init(provider: any GCMAnalyticsProvider = NoOpGCMAnalyticsProvider()) {
|
||||
self.provider = provider
|
||||
}
|
||||
|
||||
// MARK: - Public API
|
||||
|
||||
/// Sends default (pre-consent) consent signals to GCM based on the site's blocking mode.
|
||||
///
|
||||
/// Call this as early as possible — ideally before any analytics events are sent.
|
||||
///
|
||||
/// - Parameter config: The effective site configuration.
|
||||
public func applyDefaults(config: ConsentConfig) {
|
||||
let defaults = buildDefaults(for: config.blockingMode)
|
||||
provider.setConsentDefaults(defaults)
|
||||
}
|
||||
|
||||
/// Updates GCM consent signals to reflect the user's explicit choices.
|
||||
///
|
||||
/// - Parameter state: The resolved consent state after user interaction.
|
||||
public func applyConsent(state: ConsentState) {
|
||||
let updates = buildConsentMap(from: state)
|
||||
provider.updateConsent(updates)
|
||||
}
|
||||
|
||||
// MARK: - Private Helpers
|
||||
|
||||
/// Builds the default GCM consent map based on blocking mode.
|
||||
///
|
||||
/// - `opt_in`: all types denied by default (GDPR).
|
||||
/// - `opt_out`: all types granted by default (CCPA).
|
||||
/// - `informational`: all types granted.
|
||||
private func buildDefaults(for mode: ConsentConfig.BlockingMode) -> [String: String] {
|
||||
let status: GCMConsentStatus = mode == .optIn ? .denied : .granted
|
||||
return Dictionary(
|
||||
uniqueKeysWithValues: GCMConsentType.allCases.map {
|
||||
($0.rawValue, status.rawValue)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/// Maps the consent state's accepted/rejected categories to GCM consent type values.
|
||||
private func buildConsentMap(from state: ConsentState) -> [String: String] {
|
||||
var map: [String: String] = [:]
|
||||
|
||||
// security_storage is always granted — it is necessary for security.
|
||||
map[GCMConsentType.securityStorage.rawValue] = GCMConsentStatus.granted.rawValue
|
||||
|
||||
for category in ConsentCategory.allCases {
|
||||
guard let gcmType = category.gcmConsentType else { continue }
|
||||
let status: GCMConsentStatus = state.isGranted(category) ? .granted : .denied
|
||||
map[gcmType] = status.rawValue
|
||||
}
|
||||
|
||||
// ad_user_data and ad_personalization follow the marketing category.
|
||||
let marketingGranted = state.isGranted(.marketing)
|
||||
map[GCMConsentType.adUserData.rawValue] = marketingGranted
|
||||
? GCMConsentStatus.granted.rawValue
|
||||
: GCMConsentStatus.denied.rawValue
|
||||
map[GCMConsentType.adPersonalisation.rawValue] = marketingGranted
|
||||
? GCMConsentStatus.granted.rawValue
|
||||
: GCMConsentStatus.denied.rawValue
|
||||
|
||||
return map
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user