v1.24.0 — editable opt-out text, FlyingPress/Redis cache integration, per-gateway payment opt-in
[1.24.0] — 2026-07-21
Added
- Editable opt-out (Do Not Sell) modal text — issue #187. The "Opt-out Preferences" popup a visitor sees after clicking the "Do Not Sell or Share My Personal Information" link previously always used the bundled default copy — there was no admin field for it. A new Opt-out (Do Not Sell) Text card on the Cookie Banner → Preference Center tab now edits its Title, Description (rich-text) and toggle label, per language, exactly like the Preference Center text. The card appears when the banner law is CCPA / US State Laws or Both. Translated into every bundled locale.
- Cookie Policy generator routed through the WordPress gettext pipeline. The generated cookie-policy sections now flow through the standard translation pipeline (
_x()with per-section context), so the policy honours the site locale and any.mooverrides, falling back to the bundled Markdown when a translation is missing.
Changed
-
Payment gateway scripts: per-gateway opt-in instead of an automatic allow-list (reported on the #125 thread). A Forminator PayPal form was throwing
paypal is not definedbecause the consent blocker was blockingpaypal.com/sdk/js; the gateway auto-whitelist only covered WooCommerce checkout/cart pages, so it never applied on a non-WooCommerce payment form. The fix does not auto-allow the SDK — a payment SDK can set cookies and fingerprint, so loading it before consent is a GDPR/ePrivacy decision, not something the plugin should do silently. Instead, Settings → Script Blocking → Payment gateways now offers a clearly-labelled, per-gateway toggle (PayPal, Stripe, Square, Braintree, Klarna, Mollie, Amazon Pay). When a store owner enables a gateway, its payment scripts (never its marketing pixel — PayPal'spptm.jsstays blocked) are allowed before consent site-wide, for payment forms that live outside a WooCommerce checkout (Forminator, Paid Memberships Pro, Easy Digital Downloads, Give, …). All off by default; a genuine WooCommerce checkout/cart page is still exempt automatically as strictly necessary. The cookie shredder follows the same rule, so a gateway's cookies are exempted exactly when its scripts are. Migration: the Stripe SDK was previously loaded before consent on every page; if you use Stripe elements outside a WooCommerce checkout (express buttons on product/account pages), enable Stripe under Settings → Script Blocking → Payment gateways after updating. -
Cookie shredder now runs on
template_redirect. The server-side non-consented-cookie shredder moved fromsend_headerstotemplate_redirect(priority 1), where the main query is resolved so the WooCommerce checkout/cart conditionals and page exclusions are reliable, while still running before any template output so the deletion headers stay valid. An explicit per-service / per-cookie denial now also wins over the admin cookie whitelist on both the server and the client, so a deliberately-withdrawn cookie is always removed.
Fixed
- Category toggles rendering as editable text when another plugin filters
wp_kses_allowed_html— issue #188.faz_allowed_html()merged its own<input>allow-list as the first argument toarray_merge(), so any active plugin that hookswp_kses_allowed_htmland declares its owninputentry overwrote that key and droppedtype—wp_kses()then strippedtype="checkbox"from the "Customize consent preferences" toggles, which defaulted totype="text"and rendered as editable fields. The allow-list is now built the other way round, merging the plugin's required<input>attributes into whatever the filter returns, so both sides survive regardless of filter order. - Per-service toggle invisible for JS-injected embeds on block-first sites (#134/#146). When an embed's
iframe.srcwas assigned from JavaScript, the iframe src-gate parked the URL and blocked the embed before it entered the DOM, so the block-time MutationObserver — the only place the per-service reveal fired — saw an emptysrcand skipped it. The embed was blocked but the visitor never got its granular consent toggle. The src-gate now surfaces the per-service toggle when it parks a blocked embed. - Consent banner double-initialising under Cloudflare Rocket Loader — issue #185. Rocket Loader can execute the plugin's init script twice;
_fazInit()is now guarded against double-execution so the banner does not bind its handlers or render twice. - Notice dismiss link unlabelled for screen readers. The custom icon-only dismiss link on the payment-gateway and WooCommerce admin notices had no accessible name; both now carry a translatable
aria-labeland mark the decorative glypharia-hidden. - WPML multilingual banner showing only the default language under Cache Compatibility Mode. Cache Compatibility Mode renders the banner visitor-invariant so aggressive page caches can serve it, which means it deliberately does not vary the language by request state. WPML was gated out entirely — but WPML's directory (
/it/,/en/) and per-domain negotiation modes encode the language in the URL, so a URL-keyed cache already stores one entry per language, exactly like Polylang (which was never gated). The plugin now detects WPML's negotiation mode: in directory/domain mode it resolves the per-URL WPML language even under Cache Compatibility Mode, so a WPML site keeps both the cache-friendly render and a correctly-translated banner. Only WPML's "language as a URL parameter" mode stays gated to the site default (query strings are not a reliable cache key). - TranslatePress and Weglot multilingual banners showing only the default language under Cache Compatibility Mode. Same root cause as the WPML fix above. Both plugins were gated out of the language resolution on the assumption that they resolve the language from cookie/session state — they do not. TranslatePress always encodes the language in a URL subdirectory (
/it/) and derives$TRP_LANGUAGEfrom that URL; Weglot resolves the language from the request URL (subdirectory, or subdomain on paid plans) through its Request_Url_Service. Neither has a cookie-based mode, so both are URL-keyed and a URL-keyed page cache already stores one entry per language, exactly like Polylang. Both branches now resolve the visitor's language even with Cache Compatibility Mode on, so a TranslatePress or Weglot site keeps both the cache-friendly render and a correctly-translated banner. - Banner/cookie saves not sticking with a persistent object cache (Redis Object Cache, Memcached) — issue #125. The plugin's internal cache invalidation deleted its transient copies by scanning
wp_options, but with an external object-cache drop-in transients never touchwp_options— the scan found nothing, the stale payload survived in Redis under the unchanged prefix, and every read re-promoted it into the object cache. A banner save wrote the new row to the database, yet the editor (and the frontend) kept serving the pre-save data.Cache::delete_transient()now rotates the transient prefix seed (the same epoch-bump strategy already used for the object cache), making previously written entries unreachable on both backends, and data transients now carry a 7-day TTL so rotated-away epochs self-expire (on plain-DB installs this also keeps them out of the autoload set).
Added
- FlyingPress cache purge integration — issue #125. FlyingPress joins the supported cache services (WP Rocket, LiteSpeed, W3TC, …): saving a banner, cookie, category or setting now automatically purges the FlyingPress cached HTML pages via its documented API (
FlyingPress\Purge::purge_pages(), withpurge_everything()as a fallback for older builds), so a stale cached page can no longer keep serving the old banner markup after a save. The purge is HTML-only — a consent change only alters the rendered page, so FlyingPress's site-wide preload crawl is deliberately not triggered — and is wrapped fail-closed so an unexpected FlyingPress state degrades to a no-op instead of aborting the other cache adapters on the same save hook. - FlyingPress country-dependent cache bypass — issue #125. FlyingPress honours neither the
DONOTCACHEPAGEconstant nor theCache-Control: no-storeheader the plugin emits when the rendered output varies by visitor country (IAB TCFgdprApplies, geo-targeted banners, runtime geo-routing, country language fallback) — a FlyingPress-cached page would freeze one visitor's country variant for everyone. The plugin now hooks FlyingPress's documentedflying_press_is_cacheablefilter with the same gating as the existing header/constant bypass; under Cache Compatibility Mode the page stays fully cacheable, unchanged. - FlyingPress JS delay/defer/minify exclusion for the consent scripts — issue #125. FlyingPress ignores the
data-cfasync/data-no-optimize/data-no-minifyattributes the plugin already prints on its own script tags, so "Delay all JavaScript" could hold the consent banner back until the first user interaction — a late/absent banner defeats pre-consent blocking. The exclusion is now registered from the normal frontend bootstrap (not the admin-only cache adapter): FlyingPress 4 receives its delay/defer filters, both versions receive the minify filter, and FlyingPress 5 receivesfaz-cookie-manager/faz-fwin its in-memoryjs_delay_excludesruntime config. Both plugin load orders and FlyingPress settings updates are covered, existing exclusions are preserved, and the stored FlyingPress option is never modified. No manual keyword exclusion is required.