Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// AcceptedCategoriesBuilder.swift
// ConsentViewController
//
// Created by Claude on 27/08/2026.
//

import Didomi
import Foundation

enum AcceptedCategoriesBuilder {
static func buildAcceptedCategories(
from purposes: [String: CurrentUserStatus.PurposeStatus],
purposeMap: [String: Purpose]
) -> [String] {
purposes
.filter { $0.value.enabled }
.compactMap { purposeMap[$0.key]?.namespaces?.sp }
}

static func buildAcceptedLegIntCategories(
from purposes: [String: CurrentUserStatus.PurposeStatus],
purposeMap: [String: Purpose],
legIntPurposeIds: Set<String>
) -> [String] {
purposes
.filter { $0.value.enabled && legIntPurposeIds.contains($0.key) }
.compactMap { purposeMap[$0.key]?.namespaces?.sp }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// AcceptedVendorsBuilder.swift
// ConsentViewController
//
// Created by Claude on 27/08/2026.
//

import Didomi
import Foundation

enum AcceptedVendorsBuilder {
static func buildAcceptedVendors(
from vendors: [String: CurrentUserStatus.VendorStatus],
vendorMap: [String: Vendor]
) -> [String] {
build(from: vendors, vendorMap: vendorMap) { !$0.purposeIDs.isEmpty }
}

static func buildAcceptedLegIntVendors(
from vendors: [String: CurrentUserStatus.VendorStatus],
vendorMap: [String: Vendor]
) -> [String] {
build(from: vendors, vendorMap: vendorMap) { !$0.legIntPurposeIDs.isEmpty }
}

private static func build(
from vendors: [String: CurrentUserStatus.VendorStatus],
vendorMap: [String: Vendor],
filteringBy predicate: (Vendor) -> Bool
) -> [String] {
vendors.compactMap { didomiVendorId, vendorStatus in
guard vendorStatus.enabled,
let vendor = vendorMap[didomiVendorId],
predicate(vendor),
let spVendorId = vendor.namespaces?.sp else { return nil }
return spVendorId
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
//
// ConsentStatusBuilder.swift
// ConsentViewController
//
// Created by Claude on 27/08/2026.
//

import Didomi
import Foundation

enum ConsentStatusBuilder {
static func buildGDPR(
from status: CurrentUserStatus,
vendorMap: [String: Vendor],
purposeMap: [String: Purpose],
acceptedLegIntVendors: [String]
) -> ConsentStatus {
let totalVendorsCount = status.vendors.count
let totalPurposesCount = status.purposes.count

let enabledVendorsCount = status.vendors.values.filter { $0.enabled }.count
let enabledPurposesCount = status.purposes.values.filter { $0.enabled }.count

let consentedAll = (totalVendorsCount > 0 && enabledVendorsCount == totalVendorsCount) &&
(totalPurposesCount > 0 && enabledPurposesCount == totalPurposesCount)
let rejectedAll = (totalVendorsCount > 0 && enabledVendorsCount == 0) &&
(totalPurposesCount > 0 && enabledPurposesCount == 0)
let consentedToAny = enabledVendorsCount > 0 || enabledPurposesCount > 0
let rejectedAny = enabledVendorsCount < totalVendorsCount || enabledPurposesCount < totalPurposesCount

let totalLegIntVendorsCount = vendorMap.values.filter { !$0.legIntPurposeIDs.isEmpty }.count
let rejectedLI = acceptedLegIntVendors.count < totalLegIntVendorsCount

let rejectedVendors = status.vendors
.filter { !$0.value.enabled }
.compactMap { vendorMap[$0.key]?.namespaces?.sp }

let rejectedCategories = status.purposes
.filter { !$0.value.enabled }
.compactMap { purposeMap[$0.key]?.namespaces?.sp }

return ConsentStatus(
rejectedAny: rejectedAny,
rejectedLI: rejectedLI,
consentedAll: consentedAll,
consentedToAll: consentedAll,
consentedToAny: consentedToAny,
rejectedAll: rejectedAll,
vendorListAdditions: nil,
legalBasisChanges: nil,
hasConsentData: !status.vendors.isEmpty || !status.purposes.isEmpty,
rejectedVendors: rejectedVendors,
rejectedCategories: rejectedCategories
)
}

static func buildCCPA(
rejectedVendorsCount: Int,
rejectedCategoriesCount: Int,
totalVendorsCount: Int,
totalPurposesCount: Int
) -> CCPAConsentStatus {
if rejectedVendorsCount == 0 && rejectedCategoriesCount == 0 {
return .RejectedNone
} else if rejectedVendorsCount == totalVendorsCount && rejectedCategoriesCount == totalPurposesCount {
return .RejectedAll
} else {
return .RejectedSome
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// IABDataBuilder.swift
// ConsentViewController
//
// Created by Claude on 27/08/2026.
//

import Foundation

enum IABDataBuilder {
static func buildTCFData() -> SPJson {
buildIABData(prefix: "IABTCF_")
}

static func buildGPPData() -> SPJson {
buildIABData(prefix: "IABGPP_")
}

private static func buildIABData(prefix: String) -> SPJson {
let filteredData = UserDefaults.standard.dictionaryRepresentation()
.filter { $0.key.hasPrefix(prefix) }
.compactMap { key, value -> (SPJson.Key, SPJson)? in
guard let json = try? SPJson(value) else { return nil }
return (SPJson.Key(key), json)
}

return .object(Dictionary(uniqueKeysWithValues: filteredData))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//
// VendorGrantsBuilder.swift
// ConsentViewController
//
// Created by Claude on 27/08/2026.
//

import Didomi
import Foundation

enum VendorGrantsBuilder {
static func build(
from vendors: [String: CurrentUserStatus.VendorStatus],
purposes: [String: CurrentUserStatus.PurposeStatus],
vendorMap: [String: Vendor],
purposeMap: [String: Purpose]
) -> SPGDPRVendorGrants {
vendors.reduce(into: [:]) { result, entry in
let (didomiVendorId, vendorStatus) = entry

guard let vendor = vendorMap[didomiVendorId],
let spVendorId = vendor.namespaces?.sp else { return }

let purposeGrants = buildPurposeGrants(
for: vendor,
purposes: purposes,
purposeMap: purposeMap
)

result[spVendorId] = SPGDPRVendorGrant(
granted: vendorStatus.enabled,
purposeGrants: purposeGrants
)
}
}

private static func buildPurposeGrants(
for vendor: Vendor,
purposes: [String: CurrentUserStatus.PurposeStatus],
purposeMap: [String: Purpose]
) -> SPGDPRPurposeGrants {
let allPurposeIds = Array(vendor.purposeIDs) + Array(vendor.legIntPurposeIDs)

return allPurposeIds
.compactMap { purposeId -> (String, Bool)? in
guard let purpose = purposeMap[purposeId],
let spPurposeId = purpose.namespaces?.sp,
let purposeStatus = purposes[purposeId] else { return nil }
return (spPurposeId, purposeStatus.enabled)
}
.reduce(into: [:]) { $0[$1.0] = $1.1 }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//
// CCPAConsentAdapter.swift
// ConsentViewController
//
// Created by Claude on 27/08/2026.
//

import Didomi
import Foundation

final class CCPAConsentAdapter: ConsentAdapter {
private let vendorProvider: VendorProvider
private let purposeProvider: PurposeProvider

init(
vendorProvider: VendorProvider = DidomiVendorProvider(),
purposeProvider: PurposeProvider = DidomiPurposeProvider()
) {
self.vendorProvider = vendorProvider
self.purposeProvider = purposeProvider
}

func adapt(from status: CurrentUserStatus) -> SPCCPAConsent {
let vendors = vendorProvider.getVendors()
let purposes = purposeProvider.getPurposes()

let vendorMap = vendors.keyedById()
let purposeMap = purposes.keyedById()

let rejectedVendors = status.vendors
.filter { !$0.value.enabled }
.compactMap { vendorMap[$0.key]?.namespaces?.sp }

let rejectedCategories = status.purposes
.filter { !$0.value.enabled }
.compactMap { purposeMap[$0.key]?.namespaces?.sp }

let consentStatus = ConsentStatusBuilder.buildCCPA(
rejectedVendorsCount: rejectedVendors.count,
rejectedCategoriesCount: rejectedCategories.count,
totalVendorsCount: status.vendors.count,
totalPurposesCount: status.purposes.count
)

let dateCreated = SPDate(string: status.created)

return SPCCPAConsent(
uuid: status.userID.isEmpty ? nil : status.userID,
status: consentStatus,
rejectedVendors: rejectedVendors,
rejectedCategories: rejectedCategories,
signedLspa: false,
applies: true,
dateCreated: dateCreated,
expirationDate: SPDate.anYearFrom(dateCreated),
GPPData: IABDataBuilder.buildGPPData()
)
}
}

// MARK: - Helper Extensions

private extension Array where Element == Vendor {
func keyedById() -> [String: Vendor] {
Dictionary(uniqueKeysWithValues: map { ($0.id, $0) })
}
}

private extension Array where Element == Purpose {
func keyedById() -> [String: Purpose] {
Dictionary(uniqueKeysWithValues: map { ($0.id, $0) })
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// ConsentAdapter.swift
// ConsentViewController
//
// Created by Claude on 27/08/2026.
//

import Didomi
import Foundation

/// Protocol for adapting CurrentUserStatus to Sourcepoint consent types
protocol ConsentAdapter<Output> {
associatedtype Output
func adapt(from status: CurrentUserStatus) -> Output
}
Loading