Files
consentos/sdks/ios/ConsentOS/Sources/ConsentOSCore/ConsentState.swift
James Cottrill fbf26453f2 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.
2026-04-14 09:18:18 +00:00

119 lines
4.0 KiB
Swift

import Foundation
/// Represents the complete consent state for a visitor.
///
/// This model mirrors the web consent cookie structure for cross-platform consistency.
/// It is persisted locally via ``ConsentStorage`` and synced to the server.
public struct ConsentState: Codable, Equatable, Sendable {
// MARK: - Properties
/// A stable, anonymous identifier for this device/visitor.
/// Generated once and persisted across sessions.
public let visitorId: String
/// The set of categories the visitor has explicitly accepted.
public var accepted: Set<ConsentCategory>
/// The set of categories the visitor has explicitly rejected.
public var rejected: Set<ConsentCategory>
/// The timestamp at which consent was last recorded.
public var consentedAt: Date?
/// The banner configuration version active when consent was collected.
/// Used to detect when consent must be re-collected after a config change.
public var bannerVersion: String?
/// Whether the user has interacted with the banner (accepted or rejected).
///
/// Returns `false` when the state represents the pre-consent default.
public var hasInteracted: Bool {
consentedAt != nil
}
// MARK: - Derived State
/// Returns `true` if the user has granted consent for the given category.
///
/// `necessary` is always considered granted regardless of the stored state.
public func isGranted(_ category: ConsentCategory) -> Bool {
guard category != .necessary else { return true }
return accepted.contains(category)
}
/// Returns `true` if the user has explicitly denied consent for the given category.
public func isDenied(_ category: ConsentCategory) -> Bool {
guard category != .necessary else { return false }
return rejected.contains(category)
}
// MARK: - Initialisers
/// Creates a new, blank consent state for the given visitor.
///
/// No categories are accepted or rejected; `consentedAt` is `nil`.
public init(visitorId: String) {
self.visitorId = visitorId
self.accepted = []
self.rejected = []
self.consentedAt = nil
self.bannerVersion = nil
}
/// Creates a fully populated consent state.
public init(
visitorId: String,
accepted: Set<ConsentCategory>,
rejected: Set<ConsentCategory>,
consentedAt: Date?,
bannerVersion: String?
) {
self.visitorId = visitorId
self.accepted = accepted
self.rejected = rejected
self.consentedAt = consentedAt
self.bannerVersion = bannerVersion
}
// MARK: - Mutations
/// Returns a new state with all non-necessary categories accepted.
public func acceptingAll() -> ConsentState {
let allOptional = ConsentCategory.allCases.filter { $0.requiresConsent }
return ConsentState(
visitorId: visitorId,
accepted: Set(allOptional),
rejected: [],
consentedAt: Date(),
bannerVersion: bannerVersion
)
}
/// Returns a new state with all non-necessary categories rejected.
public func rejectingAll() -> ConsentState {
let allOptional = ConsentCategory.allCases.filter { $0.requiresConsent }
return ConsentState(
visitorId: visitorId,
accepted: [],
rejected: Set(allOptional),
consentedAt: Date(),
bannerVersion: bannerVersion
)
}
/// Returns a new state accepting only the specified categories (and rejecting the rest).
public func accepting(categories: Set<ConsentCategory>) -> ConsentState {
let allOptional = Set(ConsentCategory.allCases.filter { $0.requiresConsent })
let toAccept = categories.filter { $0.requiresConsent }
let toReject = allOptional.subtracting(toAccept)
return ConsentState(
visitorId: visitorId,
accepted: toAccept,
rejected: toReject,
consentedAt: Date(),
bannerVersion: bannerVersion
)
}
}