import { AbstractConsentHandler, BeslistConsentType, ConsentStatus, ConsentStatusData, ConsentTypeMapping, } from './abstract.consent-handler'; export type GoogleConsentModeConsentType = | 'functionality_storage' | 'analytics_storage' | 'ad_storage' | 'ad_user_data' | 'ad_personalization' | 'personalization_storage' | 'security_storage'; type ConsentCommandValue = 'granted' | 'denied'; type ConsentCommandData = Partial>; type ConsentCommand = { type: 'default' | 'update'; data: ConsentCommandData; }; type ConsentMirrorEntry = { default?: ConsentCommandValue, update?: ConsentCommandValue }; type ConsentMirror = Record; type GoogleTagDataIcsEntry = { default?: unknown; update?: unknown; }; type GoogleTagData = { ics?: { entries?: Record; }; }; declare const window: { dataLayer?: any[]; google_tag_data?: GoogleTagData; }; export class GoogleConsentModeConsentHandler extends AbstractConsentHandler { public static consentHandlerName = 'google-consent-mode'; public static consentTypeMapping: ConsentTypeMapping = { necessary: 'security_storage', functional: 'functionality_storage', analytics: 'analytics_storage', performance: 'personalization_storage', marketing: 'ad_storage', }; public static cookieNameMapping: ConsentTypeMapping = { necessary: undefined, functional: undefined, analytics: undefined, performance: undefined, marketing: undefined, }; private mirror: ConsentMirror = {}; private gtagDataMirror: ConsentMirror = {}; private hooked: boolean = false; private sawAnyConsentSignal: boolean = false; protected getConsentHandlerName(): string { return GoogleConsentModeConsentHandler.consentHandlerName; } protected getConsentTypeMapping(): ConsentTypeMapping { return GoogleConsentModeConsentHandler.consentTypeMapping; } protected getCookieNameMapping(): ConsentTypeMapping { return GoogleConsentModeConsentHandler.cookieNameMapping; } protected parseConsentCookieValue(consentType: BeslistConsentType, cookieValue: string): boolean | undefined { return undefined; } protected getInitialConsent(): ConsentStatusData { const commandMirror: ConsentMirror = {}; const dataLayer = (typeof window !== 'undefined' && Array.isArray(window.dataLayer)) ? window.dataLayer : []; for (const item of dataLayer) { this.foldCommand(commandMirror, this.extractConsentCommand(item)); } return this.buildConsentFrom(commandMirror, this.readGoogleTagData()); } public initializeConsentUpdateListener(): void { window.dataLayer = window.dataLayer || []; for (const item of window.dataLayer) { if (this.foldCommand(this.mirror, this.extractConsentCommand(item))) { this.sawAnyConsentSignal = true; } } this.refreshGoogleTagData(); this.hookDataLayerPush(); if (!this.sawAnyConsentSignal) { console.warn('Beslist Tracking: Google Consent Mode is selected, but no consent signals were found. If this site does not use Google Consent Mode, tracking events will stay queued and will not be sent.'); } this.updateConsent(this.buildConsent()); } private hookDataLayerPush(): void { if (this.hooked) { return; } const dataLayer = window.dataLayer!; const originalPush = dataLayer.push.bind(dataLayer); dataLayer.push = (...args: any[]): number => { const result = originalPush(...args); for (const item of args) { if (this.foldCommand(this.mirror, this.extractConsentCommand(item))) { this.sawAnyConsentSignal = true; } } this.refreshGoogleTagData(); this.updateConsent(this.buildConsent()); return result; }; this.hooked = true; } private foldCommand(mirror: ConsentMirror, command: ConsentCommand | null): boolean { if (!command) { return false; } for (const key of Object.keys(command.data)) { const value = command.data[key as GoogleConsentModeConsentType]; if (value === undefined) { continue; } if (!mirror[key]) { mirror[key] = {}; } mirror[key][command.type] = value; } return true; } private extractConsentCommand(item: any): ConsentCommand | null { if (!item) { return null; } if (item[0] === 'consent' && (item[1] === 'default' || item[1] === 'update') && item[2] && typeof item[2] === 'object') { const raw = item[2]; const data: ConsentCommandData = {}; for (const key of Object.getOwnPropertyNames(raw)) { if (typeof raw[key] === 'string') { data[key as GoogleConsentModeConsentType] = raw[key] as ConsentCommandValue; } } return { type: item[1], data }; } return null; } private refreshGoogleTagData(): void { const snapshot = this.readGoogleTagData(); this.gtagDataMirror = snapshot; if (Object.keys(snapshot).length > 0) { this.sawAnyConsentSignal = true; } } private readGoogleTagData(): ConsentMirror { const entries = window.google_tag_data && window.google_tag_data.ics ? window.google_tag_data.ics.entries : undefined; const snapshot: ConsentMirror = {}; if (entries) { for (const key of Object.keys(entries)) { const entry = entries[key]; if (!entry) { continue; } const defaultValue = this.normalizeConsentValue(entry.default); const updateValue = this.normalizeConsentValue(entry.update); if (defaultValue === undefined && updateValue === undefined) { continue; } snapshot[key] = {}; if (defaultValue !== undefined) { snapshot[key].default = defaultValue; } if (updateValue !== undefined) { snapshot[key].update = updateValue; } } } return snapshot; } private buildConsent(): ConsentStatusData { return this.buildConsentFrom(this.mirror, this.gtagDataMirror); } private buildConsentFrom(commandMirror: ConsentMirror, gtagMirror: ConsentMirror): ConsentStatusData { return { necessary: 'granted', functional: this.resolveFrom('functional', commandMirror, gtagMirror), analytics: this.resolveFrom('analytics', commandMirror, gtagMirror), performance: this.resolveFrom('performance', commandMirror, gtagMirror), marketing: this.resolveFrom('marketing', commandMirror, gtagMirror), }; } private resolveFrom(consentType: BeslistConsentType, commandMirror: ConsentMirror, gtagMirror: ConsentMirror): ConsentStatus { const key = this.mapConsentType(consentType); const command = commandMirror[key]; const gtag = gtagMirror[key]; const update = command && command.update !== undefined ? command.update : (gtag ? gtag.update : undefined); const fallback = command && command.default !== undefined ? command.default : (gtag ? gtag.default : undefined); const effective = update !== undefined ? update : fallback; return effective === 'granted' ? 'granted' : 'denied'; } private normalizeConsentValue(value: unknown): ConsentCommandValue | undefined { if (value === 'granted' || value === true || value === 1) { return 'granted'; } if (value === 'denied' || value === false || value === 0) { return 'denied'; } return undefined; } }