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:
2026-06-30 14:10:58 +01:00
parent 7059c45e20
commit eb35fdabb6
7 changed files with 238 additions and 7 deletions

View File

@@ -50,6 +50,7 @@ class ATT_Consent_Frontend {
$config = array(
'consent_mode' => $s['consent_mode'],
'cmp_mode' => $s['cmp_mode'],
'consent_expiry' => (int) $s['consent_expiry'],
'wait_for_update' => (int) $s['wait_for_update'],
'url_passthrough' => (bool) $s['url_passthrough'],
@@ -66,11 +67,13 @@ class ATT_Consent_Frontend {
}
// In basic mode, pass the tracking snippet for deferred injection.
if ( 'basic' === $s['consent_mode'] ) {
// Basic mode only applies when this plugin owns consent (self mode).
if ( 'basic' === $s['consent_mode'] && 'self' === $s['cmp_mode'] ) {
$config['tracking_snippet'] = $this->get_tracking_snippet();
}
$config_json = wp_json_encode( $config );
$is_tcf = ( 'tcf_bridge' === $s['cmp_mode'] );
?>
<script>
@@ -87,6 +90,24 @@ window.gtag=g;
g('js',new Date());
if(c.ads_data_redaction){g('set','ads_data_redaction',true);}
if(c.url_passthrough){g('set','url_passthrough',true);}
<?php if ( $is_tcf ) : ?>
// TCF bridge mode: an external CMP will emit gtag('consent','update', ...)
// once it has read the user's TC string. We still emit a default-denied
// here so gtag.js holds events until the CMP responds — without this,
// gtag falls back to its implicit "granted" default and would fire a
// fully-tracked hit before the CMP has had a chance to apply consent.
g('consent','default',{
ad_storage:'denied',
analytics_storage:'denied',
ad_user_data:'denied',
ad_personalization:'denied',
functionality_storage:'denied',
personalization_storage:'denied',
security_storage:'granted',
wait_for_update:c.wait_for_update||500
});
window.attCCHasConsent=false;
<?php else : ?>
var ck=document.cookie.match(/(?:^|; )att_cc_consent=([^;]*)/);
var st=null;
if(ck){try{st=JSON.parse(decodeURIComponent(ck[1]));}catch(e){}}
@@ -115,6 +136,7 @@ wait_for_update:c.wait_for_update||500
});
window.attCCHasConsent=false;
}
<?php endif; ?>
if(!sessionStorage.getItem('att_cc_attr')){
var p=new URLSearchParams(window.location.search);
var a={
@@ -134,6 +156,22 @@ if(a.r||a.us||a.gc||a.gl){
sessionStorage.setItem('att_cc_attr',JSON.stringify(a));
}
}
<?php if ( $is_tcf ) : ?>
// Pre-stage attribution params on dataLayer (no-op if nothing captured).
try{
var sa=sessionStorage.getItem('att_cc_attr');
if(sa){
var aa=JSON.parse(sa);
var sp={};
if(aa.us){sp.campaign_source=aa.us;}
if(aa.um){sp.campaign_medium=aa.um;}
if(aa.uc){sp.campaign_name=aa.uc;}
if(aa.ut){sp.campaign_term=aa.ut;}
if(aa.uo){sp.campaign_content=aa.uo;}
for(var k in sp){g('set',sp);break;}
}
}catch(e){}
<?php endif; ?>
})();
</script>
<?php
@@ -200,6 +238,9 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
* Enqueue frontend assets.
*/
public function enqueue_assets() {
$is_tcf = ( 'tcf_bridge' === $this->settings['cmp_mode'] );
// Banner CSS is needed in both modes — the floating widget uses it.
wp_enqueue_style(
'att-consent-banner',
ATT_CC_PLUGIN_URL . 'public/css/banner.css',
@@ -207,6 +248,7 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
ATT_CC_VERSION
);
// consent-manager.js runs in both modes (script gating, cookie I/O, WP Consent API).
wp_enqueue_script(
'att-consent-manager',
ATT_CC_PLUGIN_URL . 'public/js/consent-manager.js',
@@ -215,6 +257,18 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
true
);
if ( $is_tcf ) {
// In TCF bridge mode the banner.js UI is replaced by the TCF listener.
wp_enqueue_script(
'att-consent-tcf-bridge',
ATT_CC_PLUGIN_URL . 'public/js/tcf-bridge.js',
array( 'att-consent-manager' ),
ATT_CC_VERSION,
true
);
return;
}
wp_enqueue_script(
'att-consent-banner',
ATT_CC_PLUGIN_URL . 'public/js/banner.js',
@@ -226,9 +280,13 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
/**
* Output the banner and preferences modal HTML.
* In TCF bridge mode the banner and modal are suppressed (an external CMP
* renders its own UI); only the optional floating widget is emitted, and
* its click handler is wired to the external CMP via tcf-bridge.js.
*/
public function output_banner_html() {
$s = $this->settings;
$s = $this->settings;
$is_tcf = ( 'tcf_bridge' === $s['cmp_mode'] );
do_action( 'att_consent_before_banner' );
@@ -246,7 +304,11 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
esc_attr( $s['btn_preferences_text'] )
);
$banner_html = '<div id="att-cc-banner" class="' . esc_attr( $position_class ) . '" role="dialog" aria-labelledby="att-cc-banner-heading" aria-hidden="false" tabindex="-1" style="' . $style_vars . '">
$banner_html = '';
$modal_html = '';
if ( ! $is_tcf ) {
$banner_html = '<div id="att-cc-banner" class="' . esc_attr( $position_class ) . '" role="dialog" aria-labelledby="att-cc-banner-heading" aria-hidden="false" tabindex="-1" style="' . $style_vars . '">
<div class="att-cc-banner__inner">
<div class="att-cc-banner__content">
<h2 id="att-cc-banner-heading" class="att-cc-banner__heading">' . esc_html( $s['banner_heading'] ) . '</h2>
@@ -260,7 +322,7 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
</div>
</div>';
$modal_html = '<div id="att-cc-modal" role="dialog" aria-labelledby="att-cc-modal-heading" aria-hidden="true" aria-modal="true" style="' . $style_vars . '">
$modal_html = '<div id="att-cc-modal" role="dialog" aria-labelledby="att-cc-modal-heading" aria-hidden="true" aria-modal="true" style="' . $style_vars . '">
<div class="att-cc-modal__overlay"></div>
<div class="att-cc-modal__dialog">
<h2 id="att-cc-modal-heading" class="att-cc-modal__heading">' . esc_html__( 'Manage Cookie Preferences', 'att-consent' ) . '</h2>
@@ -312,12 +374,15 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
</div>
</div>
</div>';
}
$widget_html = '';
if ( 'none' !== $s['floating_widget'] ) {
$widget_position = 'right' === $s['floating_widget'] ? 'att-cc-widget--right' : 'att-cc-widget--bottom-right';
$widget_label = ! empty( $s['floating_widget_label'] ) ? $s['floating_widget_label'] : 'Cookie Settings';
$widget_html = '<button type="button" id="att-cc-widget" class="att-cc-widget ' . esc_attr( $widget_position ) . '" aria-label="' . esc_attr( $widget_label ) . '" style="display:none;' . $style_vars . '"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm0 18a8 8 0 1 1 8-8 8 8 0 0 1-8 8zm-1-13a1.5 1.5 0 1 1-1.5-1.5A1.5 1.5 0 0 1 11 7zm4 2a1 1 0 1 1-1-1 1 1 0 0 1 1 1zm-7 3a1 1 0 1 1-1-1 1 1 0 0 1 1 1zm3 4a1.5 1.5 0 1 1-1.5-1.5A1.5 1.5 0 0 1 11 16zm5-1a1 1 0 1 1-1-1 1 1 0 0 1 1 1z"/></svg> ' . esc_html( $widget_label ) . '</button>';
// In TCF bridge mode the widget is shown immediately (no banner to wait for).
$widget_display = $is_tcf ? '' : 'display:none;';
$widget_html = '<button type="button" id="att-cc-widget" class="att-cc-widget ' . esc_attr( $widget_position ) . '" aria-label="' . esc_attr( $widget_label ) . '" style="' . $widget_display . $style_vars . '"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm0 18a8 8 0 1 1 8-8 8 8 0 0 1-8 8zm-1-13a1.5 1.5 0 1 1-1.5-1.5A1.5 1.5 0 0 1 11 7zm4 2a1 1 0 1 1-1-1 1 1 0 0 1 1 1zm-7 3a1 1 0 1 1-1-1 1 1 0 0 1 1 1zm3 4a1.5 1.5 0 1 1-1.5-1.5A1.5 1.5 0 0 1 11 16zm5-1a1 1 0 1 1-1-1 1 1 0 0 1 1 1z"/></svg> ' . esc_html( $widget_label ) . '</button>';
}
$html = apply_filters( 'att_consent_banner_html', $banner_html . $modal_html . $widget_html, $s );