Files
att-consent/public/js/tcf-bridge.js
Steve Hanlon eb35fdabb6 Add TCF v2.2 bridge mode for external CMP integration (v1.3.0)
New cmp_mode setting toggles between the built-in banner (default,
unchanged) and a bridge mode that defers banner UX to an external
IAB TCF v2.2 certified CMP — e.g. Google's free Privacy & messaging
(Funding Choices) or Tarteaucitron.js — and listens to __tcfapi to
keep custom-script gating, attribution preservation, the att_cc_consent
cookie cache and the WP Consent API bridge in sync. Lets sites that
need a Google-certified CMP for EEA/UK AdSense/AdMob serving keep
using this plugin for everything except the consent UI itself.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-30 14:10:58 +01:00

137 lines
4.1 KiB
JavaScript

/**
* ATT Consent — TCF v2.2 Bridge
*
* Used when cmp_mode = 'tcf_bridge'. A separate, IAB TCF v2.2 certified CMP
* (e.g. Google's "Privacy & messaging" / Funding Choices, Tarteaucitron.js)
* is expected to render the banner and emit gtag('consent', ...) calls.
*
* This script:
* 1. Waits for window.__tcfapi to appear.
* 2. Subscribes to the TCF event stream.
* 3. Maps TCF v2.2 purposes back to this plugin's three categories
* (functional / analytics / marketing).
* 4. Calls AttConsent.update() so custom scripts, the att_cc_consent
* cookie cache and the WP Consent API bridge all stay in sync.
* 5. Wires the floating widget click to the CMP's "re-open" entry point.
*
* If __tcfapi never appears (CMP not installed, or visitor is outside the
* TCF jurisdiction and the CMP loaded no stub) the plugin's default-denied
* state simply persists — no fail-open here.
*
* TCF purpose → category mapping:
* 1, 5, 6 → functional (device storage, personalised content)
* 7, 8, 9, 10 → analytics (ad/content measurement, market research, product dev)
* 2, 3, 4 → marketing (basic ads, ad profiling, personalised ads)
*/
(function () {
'use strict';
if (typeof window.AttConsent === 'undefined') {
return;
}
var MAX_WAIT_MS = 10000;
var POLL_INTERVAL = 100;
var elapsed = 0;
var lastSignature = '';
function mapTcDataToCategories(tcData) {
// Outside the TCF scope (typically non-EEA/UK/CH): the CMP isn't
// asserting any restriction, so treat as full consent. The site
// operator remains responsible for any other applicable law (GPP/CCPA).
if (tcData && tcData.gdprApplies === false) {
return { functional: true, analytics: true, marketing: true };
}
var purposes = (tcData && tcData.purpose && tcData.purpose.consents) || {};
return {
functional: !!(purposes[1] || purposes[5] || purposes[6]),
analytics: !!(purposes[7] || purposes[8] || purposes[9] || purposes[10]),
marketing: !!(purposes[2] || purposes[3] || purposes[4])
};
}
function handleTcData(tcData, success) {
if (!success || !tcData) {
return;
}
// Only act on terminal states.
if (tcData.eventStatus !== 'tcloaded' && tcData.eventStatus !== 'useractioncomplete') {
return;
}
var cats = mapTcDataToCategories(tcData);
// Suppress repeat updates with identical state — TCF can fire
// multiple times per page (e.g. tcloaded then useractioncomplete).
var sig = cats.functional + '|' + cats.analytics + '|' + cats.marketing;
if (sig === lastSignature) {
return;
}
lastSignature = sig;
try {
window.AttConsent.update(cats);
} catch (e) {
// Swallow — don't break the page if consent-manager errored.
}
}
function attach() {
try {
window.__tcfapi('addEventListener', 2, handleTcData);
} catch (e) {
// Bad CMP stub. Nothing else to do.
}
}
function poll() {
if (typeof window.__tcfapi === 'function') {
attach();
return;
}
elapsed += POLL_INTERVAL;
if (elapsed >= MAX_WAIT_MS) {
return;
}
setTimeout(poll, POLL_INTERVAL);
}
// --- Floating widget: re-open the external CMP UI ---
var widget = document.getElementById('att-cc-widget');
if (widget) {
widget.addEventListener('click', function (e) {
e.preventDefault();
// 1. Standard IAB way (works for any compliant TCF v2.2 CMP).
if (typeof window.__tcfapi === 'function') {
try {
window.__tcfapi('displayConsentUi', 2, function () {});
return;
} catch (err) {}
}
// 2. Google Funding Choices specific re-prompt.
if (window.googlefc && window.googlefc.callbackQueue) {
window.googlefc.callbackQueue.push({
CONSENT_DATA_READY: function () {
if (typeof window.googlefc.showRevocationMessage === 'function') {
window.googlefc.showRevocationMessage();
}
}
});
return;
}
// 3. Last-ditch: scroll to a #privacy / #cookies anchor if the
// site has one in its footer. Fail silently otherwise.
var fallback = document.querySelector('a[href*="#cookie"], a[href*="#privacy"]');
if (fallback) {
fallback.click();
}
});
}
poll();
})();