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>
This commit is contained in:
136
public/js/tcf-bridge.js
Normal file
136
public/js/tcf-bridge.js
Normal file
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* 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();
|
||||
})();
|
||||
Reference in New Issue
Block a user