Files
att-consent/public/js/tcf-bridge.js
Steve Hanlon 5de5b3ddb7 Reduce TCF bridge poll timeout from 10s to 3s (v1.3.4)
10s was unnecessarily long — any EEA user with Funding Choices properly
loaded will have __tcfapi within 3s. Reducing the timeout means non-TCF
visitors (Americas etc.) receive the granted fallback 7 seconds sooner.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-09 08:20:36 +01:00

146 lines
4.8 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 = 3000;
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) {
// __tcfapi never appeared: visitor is outside TCF jurisdiction (e.g. Americas)
// or the CMP script was blocked. GDPR opt-in doesn't apply; grant full consent
// so these visitors aren't silently tracked as denied.
try {
window.AttConsent.update({ functional: true, analytics: true, marketing: true });
} catch (e) {}
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. Google Funding Choices: use its own re-prompt API.
// FC accepts but silently ignores the IAB 'displayConsentUi'
// command, so we must try the FC path FIRST when FC is detected.
if (window.googlefc && typeof window.googlefc.showRevocationMessage === 'function') {
try { window.googlefc.showRevocationMessage(); return; } catch (err) {}
}
if (window.googlefc && window.googlefc.callbackQueue) {
window.googlefc.callbackQueue.push({
CONSENT_DATA_READY: function () {
if (typeof window.googlefc.showRevocationMessage === 'function') {
window.googlefc.showRevocationMessage();
}
}
});
return;
}
// 2. Generic IAB TCF v2.2 re-prompt — for non-FC CMPs that
// actually implement this (Sourcepoint, Didomi, etc.).
if (typeof window.__tcfapi === 'function') {
try { window.__tcfapi('displayConsentUi', 2, function () {}); return; } catch (err) {}
}
// 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();
})();