diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml
index 7954998f..ad5649e1 100644
--- a/.github/workflows/phpunit.yml
+++ b/.github/workflows/phpunit.yml
@@ -60,7 +60,7 @@ jobs:
- name: Run JavaScript tests
run: |
- npm run test:cookie-notice
+ npm run test:column-link
npm run test:table-of-contents
npm run test:accessibility-utils
npm run test:carousel-accessibility
diff --git a/.wordpress-playground/blueprint.json b/.wordpress-playground/blueprint.json
index 82404f6b..f9177dc3 100644
--- a/.wordpress-playground/blueprint.json
+++ b/.wordpress-playground/blueprint.json
@@ -22,6 +22,16 @@
"activate": true
}
},
+ {
+ "step": "installPlugin",
+ "pluginZipFile": {
+ "resource": "wordpress.org/plugins",
+ "slug": "frontconsent"
+ },
+ "options": {
+ "activate": true
+ }
+ },
{
"step": "installPlugin",
"pluginZipFile": {
diff --git a/AGENTS.md b/AGENTS.md
index 0aab82d0..5b00eb93 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -4,7 +4,9 @@ Guidelines for AI coding agents working on this WordPress plugin.
## Project Overview
-**FrontBlocks Site Tools** is a free, all-in-one WordPress plugin covering the everyday needs of a site — not just a block-extension add-on. It bundles Gutenberg/GeneratePress block enhancements alongside general-purpose site utilities: cookie consent, maintenance mode, image management (size control + WebP/AVIF delivery), social login, and more. New features do not need to be block-related to belong here — evaluate additions against "does a typical WordPress site commonly need this," not "is this a block." The PRO companion lives at `wp-content/plugins/frontblocks-pro/`.
+**FrontBlocks Site Tools** is a free, all-in-one WordPress plugin covering the everyday needs of a site — not just a block-extension add-on. It bundles Gutenberg/GeneratePress block enhancements alongside general-purpose site utilities: maintenance mode, image management (size control + WebP/AVIF delivery), social login, and more. New features do not need to be block-related to belong here — evaluate additions against "does a typical WordPress site commonly need this," not "is this a block." The PRO companion lives at `wp-content/plugins/frontblocks-pro/`.
+
+**Cookie Notice is deprecated here and moving to FrontConsent** (`wp-content/plugins/frontconsent/`), a dedicated standalone plugin. It still ships and works in FrontBlocks during the transition (see `includes/Frontend/CookieNotice.php` and `includes/Admin/CookieNoticeDeprecationNotice.php`), but do not extend it further — new cookie-consent work belongs in FrontConsent's own `AGENTS.md`. FrontBlocks' module disables itself automatically once FrontConsent is active (`FRCN_VERSION` defined), and FrontConsent migrates the site's settings/stats on activation.
- **PHP minimum:** 7.0
- **WordPress minimum:** 5.8 (required for the image-format-output filters used by Image Management)
@@ -148,7 +150,7 @@ Every feature is a self-contained class following the same shape:
Actions for making a release:
- Update the Stable Tag in `readme.txt` and the plugin version in its header and `CCRMRE_VERSION` constant.
-- Finalize the `= Unreleased =` changelog entries under the released version and create a new empty `= Unreleased =` section.
+- Finalize the `= Unreleased =` changelog entries under the released version.
- Create the matching GitHub release and tag.
## Test Enforcement
diff --git a/assets/admin/settings.css b/assets/admin/settings.css
index 9fd73c3a..b446db1a 100644
--- a/assets/admin/settings.css
+++ b/assets/admin/settings.css
@@ -12,11 +12,7 @@
--tw-color-red-200: oklch(88.5% 0.062 18.334);
--tw-color-red-600: oklch(57.7% 0.245 27.325);
--tw-color-red-700: oklch(50.5% 0.213 27.518);
- --tw-color-amber-50: oklch(98.7% 0.022 95.277);
- --tw-color-amber-200: oklch(92.4% 0.12 95.746);
--tw-color-amber-600: oklch(66.6% 0.179 58.318);
- --tw-color-amber-800: oklch(47.3% 0.137 46.201);
- --tw-color-amber-900: oklch(41.4% 0.112 45.904);
--tw-color-yellow-50: oklch(98.7% 0.026 102.212);
--tw-color-yellow-200: oklch(94.5% 0.129 101.54);
--tw-color-yellow-400: oklch(85.2% 0.199 91.936);
@@ -24,9 +20,13 @@
--tw-color-green-50: oklch(98.2% 0.018 155.826);
--tw-color-green-200: oklch(92.5% 0.084 155.995);
--tw-color-green-700: oklch(52.7% 0.154 150.069);
+ --tw-color-green-800: oklch(44.8% 0.119 151.328);
--tw-color-blue-50: oklch(97% 0.014 254.604);
+ --tw-color-blue-200: oklch(88.2% 0.059 254.128);
--tw-color-blue-400: oklch(70.7% 0.165 254.624);
--tw-color-blue-700: oklch(48.8% 0.243 264.376);
+ --tw-color-blue-800: oklch(42.4% 0.199 265.638);
+ --tw-color-blue-900: oklch(37.9% 0.146 265.522);
--tw-color-gray-50: oklch(98.5% 0.002 247.839);
--tw-color-gray-200: oklch(92.8% 0.006 264.531);
--tw-color-gray-300: oklch(87.2% 0.01 258.338);
@@ -36,6 +36,7 @@
--tw-color-gray-900: oklch(21% 0.034 264.665);
--tw-color-white: #fff;
--tw-spacing: 0.25rem;
+ --tw-container-xl: 36rem;
--tw-text-xs: 0.75rem;
--tw-text-xs--line-height: calc(1 / 0.75);
--tw-text-sm: 0.875rem;
@@ -218,9 +219,6 @@
}
}
@layer utilities {
- .frbl-settings-wrapper .tw\:m-0 {
- margin: 0px;
- }
.frbl-settings-wrapper .tw\:mt-0 {
margin-top: 0px;
}
@@ -278,18 +276,15 @@
.frbl-settings-wrapper .tw\:h-5 {
height: calc(var(--tw-spacing) * 5);
}
- .frbl-settings-wrapper .tw\:h-10 {
- height: calc(var(--tw-spacing) * 10);
- }
.frbl-settings-wrapper .tw\:w-5 {
width: calc(var(--tw-spacing) * 5);
}
- .frbl-settings-wrapper .tw\:w-32 {
- width: calc(var(--tw-spacing) * 32);
- }
.frbl-settings-wrapper .tw\:w-full {
width: 100%;
}
+ .frbl-settings-wrapper .tw\:max-w-xl {
+ max-width: var(--tw-container-xl);
+ }
.frbl-settings-wrapper .tw\:flex-1 {
flex: 1;
}
@@ -302,9 +297,6 @@
.frbl-settings-wrapper .tw\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
- .frbl-settings-wrapper .tw\:grid-cols-3 {
- grid-template-columns: repeat(3, minmax(0, 1fr));
- }
.frbl-settings-wrapper .tw\:items-center {
align-items: center;
}
@@ -326,11 +318,6 @@
.frbl-settings-wrapper .tw\:gap-6 {
gap: calc(var(--tw-spacing) * 6);
}
- :where(.frbl-settings-wrapper .tw\:space-y-2 > :not(:last-child)) {
- --tw-space-y-reverse: 0;
- margin-block-start: calc(calc(var(--tw-spacing) * 2) * var(--tw-space-y-reverse));
- margin-block-end: calc(calc(var(--tw-spacing) * 2) * calc(1 - var(--tw-space-y-reverse)));
- }
:where(.frbl-settings-wrapper .tw\:space-y-6 > :not(:last-child)) {
--tw-space-y-reverse: 0;
margin-block-start: calc(calc(var(--tw-spacing) * 6) * var(--tw-space-y-reverse));
@@ -360,8 +347,8 @@
border-left-style: var(--tw-border-style);
border-left-width: 4px;
}
- .frbl-settings-wrapper .tw\:border-amber-200 {
- border-color: var(--tw-color-amber-200);
+ .frbl-settings-wrapper .tw\:border-blue-200 {
+ border-color: var(--tw-color-blue-200);
}
.frbl-settings-wrapper .tw\:border-blue-400 {
border-color: var(--tw-color-blue-400);
@@ -387,9 +374,6 @@
.frbl-settings-wrapper .tw\:border-yellow-400 {
border-color: var(--tw-color-yellow-400);
}
- .frbl-settings-wrapper .tw\:bg-amber-50 {
- background-color: var(--tw-color-amber-50);
- }
.frbl-settings-wrapper .tw\:bg-blue-50 {
background-color: var(--tw-color-blue-50);
}
@@ -453,9 +437,6 @@
.frbl-settings-wrapper .tw\:text-center {
text-align: center;
}
- .frbl-settings-wrapper .tw\:font-mono {
- font-family: var(--tw-font-mono);
- }
.frbl-settings-wrapper .tw\:text-2xl {
font-size: var(--tw-text-2xl);
line-height: var(--tw-leading, var(--tw-text-2xl--line-height));
@@ -492,24 +473,21 @@
--tw-font-weight: var(--tw-font-weight-semibold);
font-weight: var(--tw-font-weight-semibold);
}
- .frbl-settings-wrapper .tw\:whitespace-nowrap {
- white-space: nowrap;
- }
.frbl-settings-wrapper .tw\:text-amber-600 {
color: var(--tw-color-amber-600);
}
- .frbl-settings-wrapper .tw\:text-amber-800 {
- color: var(--tw-color-amber-800);
- }
- .frbl-settings-wrapper .tw\:text-amber-900 {
- color: var(--tw-color-amber-900);
- }
.frbl-settings-wrapper .tw\:text-blue-400 {
color: var(--tw-color-blue-400);
}
.frbl-settings-wrapper .tw\:text-blue-700 {
color: var(--tw-color-blue-700);
}
+ .frbl-settings-wrapper .tw\:text-blue-800 {
+ color: var(--tw-color-blue-800);
+ }
+ .frbl-settings-wrapper .tw\:text-blue-900 {
+ color: var(--tw-color-blue-900);
+ }
.frbl-settings-wrapper .tw\:text-gray-500 {
color: var(--tw-color-gray-500);
}
@@ -525,6 +503,9 @@
.frbl-settings-wrapper .tw\:text-green-700 {
color: var(--tw-color-green-700);
}
+ .frbl-settings-wrapper .tw\:text-green-800 {
+ color: var(--tw-color-green-800);
+ }
.frbl-settings-wrapper .tw\:text-red-600 {
color: var(--tw-color-red-600);
}
diff --git a/docs/COOKIE-NOTICE.md b/docs/COOKIE-NOTICE.md
index 80d864bd..968cd893 100644
--- a/docs/COOKIE-NOTICE.md
+++ b/docs/COOKIE-NOTICE.md
@@ -1,222 +1,23 @@
-# FrontBlocks: Cookie Notice
+# Cookie Notice migration
-**Plugin:** FrontBlocks for Gutenberg/GeneratePress
-**Feature:** Cookie consent banner + consent-gated Google Tag Manager / GA4
-**Since:** 1.5.0
+Cookie consent configuration has moved from FrontBlocks to the dedicated free
+[FrontConsent plugin](https://wordpress.org/plugins/frontconsent/). New sites
+should install and configure FrontConsent directly.
----
+## Existing FrontBlocks sites
-## Overview
+The legacy Cookie Notice module remains active during the transition, so an
+existing banner and its consent-gated integrations keep working. Its settings
+are no longer shown in **Appearance → FrontBlocks → Cookie Notice**.
-Cookie Notice is an optional module that shows a lightweight, configurable cookie
-consent banner on the frontend. Visitors can **Accept** or **Reject**; Google Tag
-Manager and/or GA4 are only ever loaded after a visitor accepts. The module also
-keeps a simple aggregate acceptance-rate stat (accepted vs. rejected counts) in
-the settings page.
+Install and activate FrontConsent from that tab or from the FrontBlocks admin
+notice. On its first run, FrontConsent automatically:
-Enable it from **Appearance → FrontBlocks → Cookie Notice**.
+- Copies the existing Cookie Notice settings and acceptance statistics.
+- Converts retired dedicated Google Tag Manager and GA4 IDs into its shared
+ tracking integrations.
+- Disables the legacy FrontBlocks banner, preventing duplicate notices.
-## Settings
-
-| Field | Description |
-| --- | --- |
-| Message | The banner copy. Falls back to a default English sentence when left empty. |
-| Accept / Reject button label | Text for the two decision buttons. |
-| Cookie policy page | Optional page picker (dropdown of the site's own pages), shown next to the message as a "Learn more" link resolved to that page's permalink. The banner is suppressed on that exact page so visitors can read the policy before deciding. |
-| Layout | `Full-width bar` (floating bottom bar), `Boxed panel` (bottom-left/bottom-right), or `Centered popup` (modal with a dimmed backdrop). |
-| Position | Only shown for the boxed panel layout — bottom-left or bottom-right. |
-| Accent color | Used for the Accept button and the policy link. A contrasting text color is computed automatically so the button and link stay legible even with a very light accent. |
-| Background color | The notice panel's background. A contrasting text color is computed automatically for the message and Reject button, the same way the accent color's own contrast is handled. |
-| Corner rounding | `None`, `Slightly rounded`, or `Very rounded` — applies to the notice panel across all three layouts. |
-| Cookie expiration (days) | How long the visitor's decision is remembered. |
-| Add a tracking ID or code integration | A single add-only field for every supported tool, including Google Tag Manager and GA4: paste a bare `GTM-XXXXXXX` / `G-XXXXXXXXXX` id, GTM/gtag's own install snippet, or the full `
+ 403 ) );
+ }
+
+ if ( ! current_user_can( 'install_plugins' ) ) {
+ wp_die( esc_html__( 'You do not have permission to do this.', 'frontblocks' ), '', array( 'response' => 403 ) );
+ }
+
+ update_user_meta( get_current_user_id(), self::DISMISSED_META_KEY, true );
+ wp_die();
+ }
+}
diff --git a/includes/Admin/Settings.php b/includes/Admin/Settings.php
index a3d72dd8..f9a61765 100644
--- a/includes/Admin/Settings.php
+++ b/includes/Admin/Settings.php
@@ -96,110 +96,6 @@ class Settings {
*/
private $option_maintenance_image = 'maintenance_image';
- /**
- * Option key for the cookie notice feature.
- *
- * @var string
- */
- private $option_enable_cookie_notice = 'enable_cookie_notice';
-
- /**
- * Option key for the cookie notice message.
- *
- * @var string
- */
- private $option_cookie_notice_message = 'cookie_notice_message';
-
- /**
- * Option key for the cookie notice accept button label.
- *
- * @var string
- */
- private $option_cookie_notice_accept_label = 'cookie_notice_accept_label';
-
- /**
- * Option key for the cookie notice reject button label.
- *
- * @var string
- */
- private $option_cookie_notice_reject_label = 'cookie_notice_reject_label';
-
- /**
- * Option key for the cookie policy page ID.
- *
- * @var string
- */
- private $option_cookie_notice_policy_page_id = 'cookie_notice_policy_page_id';
-
- /**
- * Option key for the cookie notice layout variant.
- *
- * @var string
- */
- private $option_cookie_notice_layout = 'cookie_notice_layout';
-
- /**
- * Option key for the cookie notice boxed panel position.
- *
- * @var string
- */
- private $option_cookie_notice_position = 'cookie_notice_position';
-
- /**
- * Option key for the cookie notice accent color.
- *
- * @var string
- */
- private $option_cookie_notice_color = 'cookie_notice_color';
-
- /**
- * Option key for the cookie notice panel background color.
- *
- * @var string
- */
- private $option_cookie_notice_bg_color = 'cookie_notice_bg_color';
-
- /**
- * Option key for the cookie notice panel corner rounding.
- *
- * @var string
- */
- private $option_cookie_notice_radius = 'cookie_notice_radius';
-
- /**
- * Option key for the cookie notice expiration (in days).
- *
- * @var string
- */
- private $option_cookie_notice_expiration_days = 'cookie_notice_expiration_days';
-
- /**
- * Legacy option key that used to store the Google Tag Manager container ID
- * as a dedicated field. Superseded by 'gtm' records in the shared
- * cookie_notice_tracking_integrations list; kept only so
- * migrate_legacy_gtm_ga4_tracking_ids() can read and clear old values.
- *
- * @var string
- */
- private $option_cookie_notice_gtm_id = 'cookie_notice_gtm_id';
-
- /**
- * Legacy option key that used to store the GA4 Measurement ID as a
- * dedicated field. Superseded by 'ga4' records in the shared
- * cookie_notice_tracking_integrations list; kept only so
- * migrate_legacy_gtm_ga4_tracking_ids() can read and clear old values.
- *
- * @var string
- */
- private $option_cookie_notice_ga4_id = 'cookie_notice_ga4_id';
-
- /**
- * Option key for the additional tracking integrations.
- *
- * @var string
- */
- private $option_cookie_notice_tracking_integrations = 'cookie_notice_tracking_integrations';
-
/**
* Option key for popups feature.
*
@@ -370,7 +266,6 @@ public function __construct() {
add_action( 'admin_menu', array( $this, 'register_menu' ) );
add_action( 'admin_init', array( $this, 'register_settings' ) );
- add_action( 'admin_init', array( $this, 'migrate_legacy_gtm_ga4_tracking_ids' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_styles' ) );
add_action( 'admin_head', array( $this, 'add_menu_icon_styles' ) );
}
@@ -412,15 +307,6 @@ public function enqueue_admin_styles( $hook ) {
FRBL_VERSION
);
- // Reuse the real frontend banner styles so the settings-page preview
- // renders pixel-identical to what visitors will actually see.
- wp_enqueue_style(
- 'frontblocks-cookie-notice',
- FRBL_PLUGIN_URL . 'assets/cookie-notice/frontblocks-cookie-notice.css',
- array(),
- FRBL_VERSION
- );
-
wp_enqueue_media();
wp_add_inline_script(
@@ -750,68 +636,6 @@ function updateScrollTopVisibility() {
// Enqueue the WordPress media uploader for the maintenance background image field.
wp_enqueue_media();
- // Separate, isolated inline script for the cookie notice fields, kept apart so a
- // failure in another script can never prevent this one from running.
- wp_add_inline_script(
- 'jquery',
- "
- document.addEventListener('DOMContentLoaded', function() {
- const cookieCheckbox = document.getElementById('enable_cookie_notice');
- const cookieWrapper = document.getElementById('cookie-notice-fields-wrapper');
-
- if (cookieCheckbox && cookieWrapper) {
- cookieCheckbox.addEventListener('change', function () {
- cookieWrapper.style.display = cookieCheckbox.checked ? 'block' : 'none';
- });
- }
-
- const layoutSelect = document.getElementById('cookie_notice_layout');
- const positionSelect = document.getElementById('cookie_notice_position');
- const radiusSelect = document.getElementById('cookie_notice_radius');
- const positionWrapper = document.getElementById('cookie-notice-position-wrapper');
- const preview = document.getElementById('frbl-cookie-notice-preview');
-
- if (layoutSelect && positionWrapper) {
- layoutSelect.addEventListener('change', function () {
- positionWrapper.style.display = layoutSelect.value === 'box' ? 'block' : 'none';
- });
- }
-
- function updatePreviewLayout() {
- if (!preview) {
- return;
- }
-
- var layout = layoutSelect ? layoutSelect.value : 'bar';
- var position = positionSelect ? positionSelect.value : 'bottom-right';
-
- preview.className = 'frbl-cookie-notice frbl-cookie-notice-preview frbl-cookie-notice--' + layout;
-
- if (layout === 'box') {
- preview.className += ' frbl-cookie-notice--' + (position === 'bottom-left' ? 'left' : 'right');
- }
-
- if (radiusSelect) {
- var radii = { none: '0', small: '12px', large: '24px' };
- preview.style.setProperty('--frbl-cookie-radius', radii[radiusSelect.value] || radii.small);
- }
- }
-
- if (layoutSelect) {
- layoutSelect.addEventListener('change', updatePreviewLayout);
- }
-
- if (positionSelect) {
- positionSelect.addEventListener('change', updatePreviewLayout);
- }
-
- if (radiusSelect) {
- radiusSelect.addEventListener('change', updatePreviewLayout);
- }
- });
- "
- );
-
// Isolated inline script for the tabbed admin shell: tab switching, bulk
// enable/disable, discard, and "unsaved changes" tracking on the save bar.
wp_add_inline_script(
@@ -1118,21 +942,9 @@ function () use ( $frblp_license ) {
'frontblocks_section_maintenance'
);
- // Cookie Notice section (own full-width section: banner copy, layout, colors, GTM/GA4 and stats).
- add_settings_section(
- 'frontblocks_section_cookie_notice',
- __( 'Cookie Notice', 'frontblocks' ),
- array( $this, 'section_cookie_notice_callback' ),
- $this->page_slug
- );
-
- add_settings_field(
- $this->option_enable_cookie_notice,
- __( 'Enable Cookie Notice', 'frontblocks' ),
- array( $this, 'field_enable_cookie_notice' ),
- $this->page_slug,
- 'frontblocks_section_cookie_notice'
- );
+ // Cookie Notice is no longer a configurable section here — it moved to
+ // FrontConsent. Its settings tab (see render_cookie_notice_promo_tab())
+ // is now a static promo panel, not backed by the Settings API.
// PRO Features section.
add_settings_section(
@@ -1372,7 +1184,7 @@ public function render_page() {
),
array(
'id' => 'cookies',
- 'label' => __( 'Cookies', 'frontblocks' ),
+ 'label' => __( 'Cookie Notice', 'frontblocks' ),
),
array(
'id' => 'google-signin',
@@ -1537,8 +1349,6 @@ public function render_page() {
render_cookie_notice_cache_notice();
?>
@@ -1594,7 +1404,7 @@ public function render_page() {
- render_section_if_exists( $sections, 'frontblocks_section_cookie_notice' ); ?>
+ render_cookie_notice_promo_tab(); ?>
@@ -2053,56 +1863,50 @@ private function section_maintenance_callback() {
}
/**
- * Cookie Notice section callback.
+ * Render the Cookie Notice tab as a promo panel for FrontConsent.
+ *
+ * Cookie Notice configuration moved out of FrontBlocks entirely (see
+ * `includes/Frontend/CookieNotice.php` and
+ * `includes/Admin/CookieNoticeDeprecationNotice.php`) — this tab no
+ * longer renders any settings fields, only an explanation and an
+ * install/manage call to action, so site owners who click the tab
+ * looking for cookie settings find where they actually live now.
*
* @return void
*/
- private function section_cookie_notice_callback() {
+ private function render_cookie_notice_promo_tab() {
+ $is_frontconsent_active = CookieNoticeDeprecationNotice::is_frontconsent_active();
?>
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
render_advanced_cookies_upsell();
- }
-
- /**
- * Show what FrontBlocks PRO's Advanced Cookie Management adds on top of
- * this banner. The feature's own settings (and the fields to configure
- * it) live entirely in the PRO plugin — this is only a plain message,
- * shown when PRO isn't active/licensed yet.
- *
- * @return void
- */
- private function render_advanced_cookies_upsell() {
- if ( frbl_is_pro_active() && $this->is_license_valid ) {
- // Already unlocked: the PRO plugin renders its own settings section
- // right below this one.
- return;
- }
-
- if ( ! frbl_is_pro_active() ) {
- echo '';
- echo '
' . esc_html__( 'Want per-category consent?', 'frontblocks' ) . '
';
- echo '
';
- printf(
- /* translators: %s: FrontBlocks PRO link */
- esc_html__( '%s adds a "Customize" option so visitors can accept Analytics and Marketing cookies separately, with native Google Ads, Meta Pixel and Microsoft Clarity integrations that only load once accepted.', 'frontblocks' ),
- 'FrontBlocks PRO '
- );
- echo '
';
- echo '
';
- } else {
- echo '';
- echo '
';
- printf(
- /* translators: %s: License section link */
- esc_html__( 'Advanced Cookie Management (per-category consent, Google Ads / Meta Pixel / Microsoft Clarity) is included with FrontBlocks PRO. Activate your license in the %s section below to unlock it.', 'frontblocks' ),
- '' . esc_html__( 'License', 'frontblocks' ) . ' '
- );
- echo '
';
- echo '
';
- }
}
/**
@@ -2125,7 +1929,7 @@ private function render_settings_section( $section ) {
$is_callback_only = ! $has_fields && $section['callback'];
// Check if this is a section that needs full width (rich fields, not a simple toggle grid).
- $is_cpt_section = in_array( $section['id'], array( 'frontblocks_section_custom_post_types', 'frontblocks_section_maintenance', 'frontblocks_section_cookie_notice', 'frontblocks_section_image_management' ), true );
+ $is_cpt_section = in_array( $section['id'], array( 'frontblocks_section_custom_post_types', 'frontblocks_section_maintenance', 'frontblocks_section_image_management' ), true );
// Show PRO CTA button before the Optional Features section.
if ( 'frontblocks_section_features' === $section['id'] && ! $this->is_license_valid ) {
@@ -2737,589 +2541,6 @@ class="tw:block tw:w-full tw:px-3 tw:py-2 tw:border tw:border-gray-300 tw:rounde
option_enable_cookie_notice ] ?? false );
- $message = (string) ( $options[ $this->option_cookie_notice_message ] ?? '' );
- $accept_label = (string) ( $options[ $this->option_cookie_notice_accept_label ] ?? '' );
- $reject_label = (string) ( $options[ $this->option_cookie_notice_reject_label ] ?? '' );
- $policy_page_id = (int) ( $options[ $this->option_cookie_notice_policy_page_id ] ?? 0 );
- $layout = (string) ( $options[ $this->option_cookie_notice_layout ] ?? 'bar' );
- $position = (string) ( $options[ $this->option_cookie_notice_position ] ?? 'bottom-right' );
- $color = (string) ( $options[ $this->option_cookie_notice_color ] ?? '#687df9' );
- $bg_color = (string) ( $options[ $this->option_cookie_notice_bg_color ] ?? '#ffffff' );
- $radius = (string) ( $options[ $this->option_cookie_notice_radius ] ?? 'small' );
- $expiration = (int) ( $options[ $this->option_cookie_notice_expiration_days ] ?? 365 );
- $tracking_integrations = \FrontBlocks\Frontend\CookieNotice::get_tracking_integrations( $options );
- $site_kit_tags = $this->get_google_site_kit_managed_tags();
- $accepted_count = (int) get_option( \FrontBlocks\Frontend\CookieNotice::STATS_OPTION_ACCEPTED, 0 );
- $rejected_count = (int) get_option( \FrontBlocks\Frontend\CookieNotice::STATS_OPTION_REJECTED, 0 );
- $total_count = $accepted_count + $rejected_count;
- $acceptance_pct = $total_count > 0 ? round( ( $accepted_count / $total_count ) * 100, 1 ) : 0;
- ?>
-
-
-
-
-
-
-
- />
-
-
-
-
-
-
- 0 ) : ?>
-
-
-
-
- $site_kit_tags['gtm'],
- 'ga4' => $site_kit_tags['ga4'],
- )
- )
- );
- $visible_integrations = array_values(
- array_filter(
- $tracking_integrations,
- static function ( $integration ) use ( $site_kit_managed_types ) {
- return ! in_array( $integration['type'], $site_kit_managed_types, true );
- }
- )
- );
- $gtm_integration = null;
- foreach ( $tracking_integrations as $integration ) {
- if ( 'gtm' === $integration['type'] ) {
- $gtm_integration = $integration['id'];
- break;
- }
- }
- ?>
-
-
-
-
-
-
-
-
-
-
- is_gtm4wp_container_loading( (string) $gtm_integration ) ) : ?>
-
-
-
-
-
- its settings so FrontBlocks can load it only after consent. Its data layer can remain enabled.', 'frontblocks' ),
- array( 'a' => array( 'href' => array() ) )
- ),
- esc_url( admin_url( 'options-general.php?page=gtm4wp-settings' ) )
- );
- ?>
-
-
-
-
-
- __( 'Google Tag Manager', 'frontblocks' ),
- 'ga4' => __( 'GA4 (Google Analytics)', 'frontblocks' ),
- 'clientify_analytics_plus' => __( 'Clientify Analytics Plus', 'frontblocks' ),
- 'clientify_analytics_classic' => __( 'Clientify Analytics (classic)', 'frontblocks' ),
- 'brevo' => __( 'Brevo', 'frontblocks' ),
- 'openai_chatgpt_ads' => __( 'ChatGPT Ads', 'frontblocks' ),
- );
- ?>
-
-
-
-
-
-
-
-
-
-
-
-
-
- Contact us.', 'frontblocks' ),
- array(
- 'a' => array(
- 'href' => array(),
- 'target' => array(),
- 'rel' => array(),
- ),
- )
- ),
- esc_url( 'https://close.technology/contacto' )
- );
- ?>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 'post_title',
- 'number' => $pages_limit,
- )
- );
-
- // The saved page may fall outside the limited list above (e.g. sorted
- // past it alphabetically on a large site) — make sure it still shows up
- // and stays selected instead of silently disappearing from the dropdown.
- $selected_page_listed = 0 === $policy_page_id;
- foreach ( $pages as $page ) {
- if ( $policy_page_id === $page->ID ) {
- $selected_page_listed = true;
- break;
- }
- }
-
- if ( ! $selected_page_listed ) {
- $selected_page = get_post( $policy_page_id );
- if ( $selected_page instanceof \WP_Post ) {
- array_unshift( $pages, $selected_page );
- }
- }
-
- foreach ( $pages as $page ) {
- printf(
- '%3$s ',
- (int) $page->ID,
- selected( $policy_page_id, $page->ID, false ),
- esc_html( $page->post_title )
- );
- }
- ?>
-
- = $pages_limit ) : ?>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- >
- >
-
-
-
-
-
-
-
- >
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- >
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- false,
- 'ga4' => false,
- );
-
- if ( ! defined( 'GOOGLESITEKIT_VERSION' ) && ! class_exists( '\\Google\\Site_Kit\\Plugin' ) ) {
- return $tags;
- }
-
- $tag_manager_settings = get_option( 'googlesitekit_tagmanager_settings', array() );
- if ( is_array( $tag_manager_settings ) && ! empty( $tag_manager_settings['containerID'] ) && ( ! isset( $tag_manager_settings['useSnippet'] ) || $tag_manager_settings['useSnippet'] ) ) {
- $tags['gtm'] = true;
- }
-
- $analytics_settings = get_option( 'googlesitekit_analytics-4_settings', array() );
- if ( is_array( $analytics_settings ) && ! empty( $analytics_settings['measurementID'] ) && ( ! isset( $analytics_settings['useSnippet'] ) || $analytics_settings['useSnippet'] ) ) {
- $tags['ga4'] = true;
- }
-
- return $tags;
- }
-
- /**
- * One-time migration: move the retired dedicated GTM/GA4 ID fields into the
- * shared cookie_notice_tracking_integrations list as {type, id} records,
- * then clear the legacy option keys.
- *
- * Guarded by the legacy values themselves being non-empty, so this is a
- * no-op on every run after the first: the legacy keys are unset as soon as
- * they are migrated, and never written to again.
- *
- * @return void
- */
- public function migrate_legacy_gtm_ga4_tracking_ids() {
- $options = get_option( 'frontblocks_settings', array() );
- if ( ! is_array( $options ) ) {
- return;
- }
-
- $legacy = array(
- $this->option_cookie_notice_gtm_id => 'gtm',
- $this->option_cookie_notice_ga4_id => 'ga4',
- );
-
- $has_legacy_value = false;
- foreach ( $legacy as $option_key => $type ) {
- if ( '' !== (string) ( $options[ $option_key ] ?? '' ) ) {
- $has_legacy_value = true;
- break;
- }
- }
-
- if ( ! $has_legacy_value ) {
- return;
- }
-
- $integrations = \FrontBlocks\Frontend\CookieNotice::get_tracking_integrations( $options, true );
-
- foreach ( $legacy as $option_key => $type ) {
- $legacy_id = sanitize_text_field( (string) ( $options[ $option_key ] ?? '' ) );
- unset( $options[ $option_key ] );
-
- if ( '' === $legacy_id ) {
- continue;
- }
-
- // A stored 'gtm'/'ga4' record already wins over the legacy value,
- // which the settings page would have stopped displaying once the
- // admin added an equivalent entry to the generic list.
- $already_present = false;
- foreach ( $integrations as $integration ) {
- if ( $type === $integration['type'] ) {
- $already_present = true;
- break;
- }
- }
-
- if ( ! $already_present ) {
- $integrations[] = array(
- 'type' => $type,
- 'id' => $legacy_id,
- );
- }
- }
-
- $options[ $this->option_cookie_notice_tracking_integrations ] = array_values( $integrations );
-
- update_option( 'frontblocks_settings', $options );
- }
-
- /**
- * Render a one-time cache notice after Cookie Notice settings are saved.
- *
- * @return void
- */
- private function render_cookie_notice_cache_notice() {
- $user_id = get_current_user_id();
- if ( ! $user_id ) {
- return;
- }
-
- $notice = get_transient( 'frbl_cookie_notice_cache_notice_' . $user_id );
- if ( ! $notice ) {
- return;
- }
-
- delete_transient( 'frbl_cookie_notice_cache_notice_' . $user_id );
- ?>
-
- option_enable_events,
$this->option_enable_fluid_typography,
$this->option_enable_maintenance,
- $this->option_enable_cookie_notice,
$this->option_enable_gutenberg,
$this->option_enable_simple_prices_variable_products,
$this->option_enable_after_add_to_cart,
@@ -4052,97 +3272,7 @@ public function sanitize_settings( $value ) {
$sanitized[ $key ] = sanitize_text_field( $val );
} elseif ( $this->option_maintenance_image === $key ) {
$sanitized[ $key ] = absint( $val );
- } elseif ( $this->option_cookie_notice_message === $key ) {
- $sanitized[ $key ] = sanitize_textarea_field( $val );
- } elseif ( in_array( $key, array( $this->option_cookie_notice_accept_label, $this->option_cookie_notice_reject_label ), true ) ) {
- $sanitized[ $key ] = sanitize_text_field( $val );
- } elseif ( $this->option_cookie_notice_policy_page_id === $key ) {
- $sanitized[ $key ] = absint( $val );
- } elseif ( $this->option_cookie_notice_layout === $key ) {
- $sanitized[ $key ] = in_array( $val, array( 'bar', 'box', 'popup' ), true ) ? $val : 'bar';
- } elseif ( $this->option_cookie_notice_position === $key ) {
- $sanitized[ $key ] = in_array( $val, array( 'bottom-right', 'bottom-left' ), true ) ? $val : 'bottom-right';
- } elseif ( $this->option_cookie_notice_color === $key ) {
- $hex_color = sanitize_hex_color( $val );
- $sanitized[ $key ] = $hex_color ? $hex_color : '#687df9';
- } elseif ( $this->option_cookie_notice_bg_color === $key ) {
- $hex_color = sanitize_hex_color( $val );
- $sanitized[ $key ] = $hex_color ? $hex_color : '#ffffff';
- } elseif ( $this->option_cookie_notice_radius === $key ) {
- $sanitized[ $key ] = in_array( $val, array( 'none', 'small', 'large' ), true ) ? $val : 'small';
- } elseif ( $this->option_cookie_notice_expiration_days === $key ) {
- $days = absint( $val );
- $sanitized[ $key ] = $days > 0 ? min( $days, 730 ) : 365;
- }
- }
-
- if ( array_key_exists( 'cookie_notice_tracking_integration_code', $value ) || array_key_exists( 'cookie_notice_tracking_remove', $value ) ) {
- $tracking_integrations = \FrontBlocks\Frontend\CookieNotice::get_tracking_integrations( $current_options, true );
- $remove_types = isset( $value['cookie_notice_tracking_remove'] ) && is_array( $value['cookie_notice_tracking_remove'] ) ? array_map( 'sanitize_key', $value['cookie_notice_tracking_remove'] ) : array();
- $tracking_integrations = array_values(
- array_filter(
- $tracking_integrations,
- static function ( $integration ) use ( $remove_types ) {
- return ! in_array( $integration['type'], $remove_types, true );
- }
- )
- );
-
- $raw_code = (string) ( $value['cookie_notice_tracking_integration_code'] ?? '' );
- $detected = \FrontBlocks\Frontend\CookieNotice::detect_tracking_snippet( $raw_code );
-
- if ( null === $detected && '' !== trim( $raw_code ) ) {
- add_settings_error(
- 'frontblocks_settings',
- 'frbl_cookie_notice_tracking_unrecognized',
- sprintf(
- /* translators: %s: contact page URL. */
- esc_html__( 'The tracking code was not recognized and was not saved. Need support for this tool? Contact us at %s.', 'frontblocks' ),
- 'close.technology/contacto'
- ),
- 'error'
- );
}
-
- if ( $detected ) {
- $tracking_integrations = array_values(
- array_filter(
- $tracking_integrations,
- static function ( $integration ) use ( $detected ) {
- return $integration['type'] !== $detected['type'];
- }
- )
- );
- $tracking_integrations[] = array(
- 'type' => $detected['type'],
- 'id' => sanitize_text_field( $detected['id'] ),
- );
- }
-
- // Defensive re-validation for the native gtm/ga4 types: guards against a
- // malformed record ever reaching the stored array outside the normal
- // detect_tracking_snippet() path (e.g. a hand-edited option value).
- $tracking_integrations = array_values(
- array_filter(
- array_map(
- function ( $integration ) {
- if ( 'gtm' === $integration['type'] ) {
- $integration['id'] = preg_match( '/^GTM-[A-Z0-9]+$/', $integration['id'] ) ? $integration['id'] : '';
- } elseif ( 'ga4' === $integration['type'] ) {
- $integration['id'] = preg_match( '/^G-[A-Z0-9]+$/', $integration['id'] ) ? $integration['id'] : '';
- }
- return $integration;
- },
- $tracking_integrations
- ),
- static function ( $integration ) {
- return '' !== $integration['id'];
- }
- )
- );
-
- $sanitized[ $this->option_cookie_notice_tracking_integrations ] = $tracking_integrations;
- unset( $sanitized['cookie_notice_tracking_type'], $sanitized['cookie_notice_tracking_id'] );
}
// Ensure mutual exclusion: if both description options are enabled, keep only the last one changed.
diff --git a/includes/Frontend/CookieNotice.php b/includes/Frontend/CookieNotice.php
index 2eb613a2..60825164 100644
--- a/includes/Frontend/CookieNotice.php
+++ b/includes/Frontend/CookieNotice.php
@@ -192,9 +192,21 @@ private function settings_changed( $old_options, $new_options ) {
/**
* Check if the Cookie Notice module is enabled.
*
+ * Always disabled once FrontConsent is active: FrontConsent is the
+ * dedicated cookie-consent plugin this module is being retired in favor
+ * of, and its own Migration class already flips
+ * frontblocks_settings['enable_cookie_notice'] to false on activation —
+ * this check is what keeps both banners from ever rendering together in
+ * the narrow window before that migration has run (or if a site's admin
+ * hand-edits the option back afterward).
+ *
* @return bool
*/
private function is_enabled() {
+ if ( defined( 'FRCN_VERSION' ) ) {
+ return false;
+ }
+
$options = get_option( 'frontblocks_settings', array() );
return (bool) ( $options['enable_cookie_notice'] ?? false );
}
@@ -1305,6 +1317,23 @@ public static function get_tracking_integrations( $options, $include_unknown = f
}
}
+ // Keep sites upgrading directly from the retired dedicated fields working
+ // until FrontConsent migrates them into its shared integrations list.
+ $legacy_ids = array(
+ 'cookie_notice_gtm_id' => 'gtm',
+ 'cookie_notice_ga4_id' => 'ga4',
+ );
+
+ foreach ( $legacy_ids as $option_key => $type ) {
+ $legacy_id = sanitize_text_field( $options[ $option_key ] ?? '' );
+ if ( '' !== $legacy_id && ! isset( $integrations[ $type ] ) ) {
+ $integrations[ $type ] = array(
+ 'type' => $type,
+ 'id' => $legacy_id,
+ );
+ }
+ }
+
return array_values( $integrations );
}
diff --git a/includes/Plugin_Main.php b/includes/Plugin_Main.php
index 40597ecf..094dfdb9 100644
--- a/includes/Plugin_Main.php
+++ b/includes/Plugin_Main.php
@@ -84,6 +84,11 @@ private function load_modules() {
require_once FRBL_PLUGIN_PATH . 'includes/Admin/RedundantPlugins.php';
}
new Admin\RedundantPlugins();
+
+ if ( ! class_exists( 'FrontBlocks\Admin\CookieNoticeDeprecationNotice' ) ) {
+ require_once FRBL_PLUGIN_PATH . 'includes/Admin/CookieNoticeDeprecationNotice.php';
+ }
+ new Admin\CookieNoticeDeprecationNotice();
}
// Image Management admin UI (settings section + bulk actions). Loaded
diff --git a/package.json b/package.json
index 7a2d8bdf..b62cf563 100644
--- a/package.json
+++ b/package.json
@@ -56,7 +56,6 @@
"build:columns-same-height": "babel assets/columns-same-height/frontblocks-columns-same-height-option.jsx --out-file assets/columns-same-height/frontblocks-columns-same-height-option.js",
"build:column-link": "babel assets/column-link/frontblocks-column-link-option.jsx --out-file assets/column-link/frontblocks-column-link-option.js",
"test:column-link": "node --test tests/js/column-link-frontend.test.js",
- "test:cookie-notice": "node --test tests/js/cookie-notice-injection.test.js",
"test:table-of-contents": "node --test tests/js/table-of-contents-frontend.test.js",
"test:accessibility-utils": "node --test tests/js/accessibility-utils.test.js",
"test:carousel-accessibility": "node --test tests/js/carousel-accessibility.test.js",
diff --git a/readme.md b/readme.md
index 8658d489..2cccb940 100644
--- a/readme.md
+++ b/readme.md
@@ -27,7 +27,7 @@ Repository for FrontBlocks Site Tools, a WordPress plugin covering the everyday
- Custom SVG Animations
### Site Tools
-- **Cookie Notice** - Configurable cookie consent banner (bar/box/popup) that gates Google Tag Manager, GA4, and supported tracking integrations behind consent, with Google Consent Mode v2 support for compatibility with Google Site Kit and other analytics plugins
+- **Cookie Notice (moved to FrontConsent)** - Cookie consent configuration now lives in the dedicated free [FrontConsent](https://wordpress.org/plugins/frontconsent/) plugin. The legacy FrontBlocks module remains active for existing sites until FrontConsent migrates its settings and stats.
- **Image Management** - Enable/disable/override registered image sizes, define custom sizes, bulk-regenerate thumbnails, and automatically serve WebP/AVIF variants on the frontend
- **Maintenance Mode** - Show a customizable maintenance page to visitors while keeping the site accessible to logged-in administrators
- **Google Sign-In** - "Sign in / Sign up with Google" button for wp-login.php, WooCommerce My Account and Checkout, plus a shortcode and block, with server-side ID token verification
@@ -72,7 +72,7 @@ Repository for FrontBlocks Site Tools, a WordPress plugin covering the everyday
- [Full Page Implementation](./docs/FULLPAGE-IMPLEMENTATION-SUMMARY.md) - Technical implementation details
- [Changelog Full Page](./docs/CHANGELOG-FULLPAGE.md) - Full Page feature changelog
- [Carousel Pattern](./docs/CAROUSEL-PATTERN.md) - Hero carousel pattern with native WordPress blocks
-- [Cookie Notice](./docs/COOKIE-NOTICE.md) - Consent banner and consent-gated tracking integrations
+- [Cookie Notice migration](./docs/COOKIE-NOTICE.md) - Moving legacy Cookie Notice settings to FrontConsent
- [Image Management](./docs/IMAGE-MANAGEMENT.md) - Image size control and automatic WebP/AVIF delivery
- [Review Notice](./docs/REVIEW-NOTICE.md) - Dismissible WordPress.org review prompt
- [Redundant Plugins Notice](./docs/REDUNDANT-PLUGINS.md) - Detects plugins made unnecessary by FrontBlocks features and how to register new pairings
diff --git a/readme.txt b/readme.txt
index 95a73b1e..1d5c258e 100644
--- a/readme.txt
+++ b/readme.txt
@@ -181,11 +181,8 @@ Register and manage events directly from WordPress. Enable the Events feature fr
**Language Recommendation Banner:**
Display a smart banner that detects the visitor's browser language and suggests switching to the matching version of the site. Enable the banner from the FrontBlocks settings page. The banner only appears when the site has a translated version matching the user's browser language, and respects user dismissal via a cookie.
-**Cookie Notice:**
-Show a lightweight, configurable cookie consent banner with Accept and Reject actions. Choose between a full-width bottom bar, a boxed panel (bottom-left or bottom-right) or a centered popup, and customize the message, button labels, cookie policy page, accent color and cookie expiration. Google Tag Manager and/or GA4 are only requested and loaded after a visitor accepts — never before — and returning visitors who already accepted get the scripts on normal page load. The settings page also shows a simple accepted/rejected acceptance-rate stat (admins are excluded from the count). Implements Google Consent Mode v2, so it also holds back tracking from other analytics/ads plugins that respect it — including **Google Site Kit** — until the visitor decides.
-
-**Advanced Cookie Management (FrontBlocks PRO):**
-Extend Cookie Notice with separate Necessary, Analytics and Marketing preferences, a customizable preferences dialog and a reusable trigger that lets visitors update their choices later. Google Ads, Meta Pixel and Microsoft Clarity only run after the relevant category is accepted. It also supports the official Meta Pixel for WordPress plugin by holding its Pixel and Conversions API signals until Marketing consent.
+**Cookie Notice (moved to FrontConsent):**
+Cookie consent — including the Advanced Cookie Management upsell (per-category consent, Google Ads/Meta Pixel/Microsoft Clarity) — moved out of FrontBlocks entirely into its own dedicated free plugin, **FrontConsent** (https://wordpress.org/plugins/frontconsent/), so it's easier to find and keeps improving on its own. The Cookie Notice module keeps working here during the transition, but if you have it enabled you'll see a notice offering a one-click install of FrontConsent, which automatically migrates your existing settings and stats and turns this module off. New sites should install FrontConsent directly instead of enabling Cookie Notice here.
**WooCommerce Features:**
Included features for WooCommerce FrontBlocks PRO.
@@ -225,6 +222,7 @@ More information in the [FrontBlocks PRO](https://close.technology/en/wordpress-
== Changelog ==
= Unreleased =
+* Deprecated: Cookie Notice is being extracted into its own dedicated plugin, FrontConsent — including the Advanced Cookie Management PRO upsell, which now lives in FrontConsent's own readme/settings. This module keeps working for now; sites with it enabled see an admin notice offering a one-click install of FrontConsent, which migrates settings/stats automatically and disables this module. The Cookie Notice settings tab no longer shows configuration fields — only an install/manage link.
= 1.5.4 =
* Added: Column Link controls for native WordPress Column blocks — make an entire column clickable, optionally open the destination in a new tab, and preserve the behaviour of links and buttons placed inside it.
diff --git a/tests/Unit/CookieNoticeConfigCallbackTest.php b/tests/Unit/CookieNoticeConfigCallbackTest.php
deleted file mode 100644
index 1df5fdaa..00000000
--- a/tests/Unit/CookieNoticeConfigCallbackTest.php
+++ /dev/null
@@ -1,142 +0,0 @@
-cookie_notice = new CookieNotice();
-
- update_option( 'frontblocks_settings', array( 'enable_cookie_notice' => true ) );
- $_COOKIE['frbl_cookie_consent'] = 'accepted';
-
- // wp_send_json_success() only routes through the interceptable
- // wp_die() below when the request is treated as an Ajax one; filtered
- // rather than defining the DOING_AJAX constant so it cannot leak into
- // any other test running later in the same process.
- add_filter( 'wp_doing_ajax', '__return_true' );
- add_filter( 'wp_die_ajax_handler', array( $this, 'get_die_handler' ) );
- }
-
- public function tear_down() {
- remove_filter( 'wp_die_ajax_handler', array( $this, 'get_die_handler' ) );
- remove_filter( 'wp_doing_ajax', '__return_true' );
- unset( $_COOKIE['frbl_cookie_consent'] );
- delete_option( 'frontblocks_settings' );
- parent::tear_down();
- }
-
- /**
- * A wp_die() handler that throws instead of terminating the process, the
- * same technique WP core's own WP_Ajax_UnitTestCase uses to make
- * wp_send_json_success()/wp_die() testable.
- *
- * @return callable
- */
- public function get_die_handler() {
- return static function ( $message ) {
- throw new Exception( is_scalar( $message ) ? (string) $message : 'die' );
- };
- }
-
- /**
- * Invoke get_config_callback() and decode its JSON response.
- *
- * @return array
- */
- private function get_config() {
- ob_start();
-
- try {
- $this->cookie_notice->get_config_callback();
- } catch ( Exception $e ) {
- unset( $e );
- }
-
- $output = ob_get_clean();
-
- return json_decode( $output, true );
- }
-
- public function test_gtm_and_ga4_entries_are_surfaced_as_dedicated_response_keys() {
- update_option(
- 'frontblocks_settings',
- array(
- 'enable_cookie_notice' => true,
- 'cookie_notice_tracking_integrations' => array(
- array( 'type' => 'gtm', 'id' => 'GTM-ABC1234' ),
- array( 'type' => 'ga4', 'id' => 'G-ABC1234567' ),
- ),
- )
- );
-
- $response = $this->get_config();
-
- $this->assertSame( 'GTM-ABC1234', $response['data']['gtmId'] );
- $this->assertSame( 'G-ABC1234567', $response['data']['ga4Id'] );
- }
-
- public function test_gtm_and_ga4_are_excluded_from_the_generic_tracking_integrations_list() {
- update_option(
- 'frontblocks_settings',
- array(
- 'enable_cookie_notice' => true,
- 'cookie_notice_tracking_integrations' => array(
- array( 'type' => 'gtm', 'id' => 'GTM-ABC1234' ),
- array( 'type' => 'ga4', 'id' => 'G-ABC1234567' ),
- array( 'type' => 'brevo', 'id' => 'brevo-key' ),
- ),
- )
- );
-
- $response = $this->get_config();
- $types = wp_list_pluck( $response['data']['trackingIntegrations'], 'type' );
-
- $this->assertNotContains( 'gtm', $types );
- $this->assertNotContains( 'ga4', $types );
- $this->assertContains( 'brevo', $types );
- }
-
- public function test_gtm_id_is_suppressed_when_google_site_kit_manages_the_tag() {
- if ( ! defined( 'GOOGLESITEKIT_VERSION' ) ) {
- define( 'GOOGLESITEKIT_VERSION', '1.0.0' );
- }
-
- update_option(
- 'frontblocks_settings',
- array(
- 'enable_cookie_notice' => true,
- 'cookie_notice_tracking_integrations' => array(
- array( 'type' => 'gtm', 'id' => 'GTM-ABC1234' ),
- ),
- )
- );
- update_option(
- 'googlesitekit_tagmanager_settings',
- array(
- 'containerID' => 'GTM-ABC1234',
- 'useSnippet' => true,
- )
- );
-
- $response = $this->get_config();
-
- $this->assertSame( '', $response['data']['gtmId'] );
-
- delete_option( 'googlesitekit_tagmanager_settings' );
- }
-}
diff --git a/tests/Unit/CookieNoticeContrastTest.php b/tests/Unit/CookieNoticeContrastTest.php
deleted file mode 100644
index fbcaa8bf..00000000
--- a/tests/Unit/CookieNoticeContrastTest.php
+++ /dev/null
@@ -1,97 +0,0 @@
-assertSame( '#000000', CookieNotice::get_readable_text_color( '#687df9' ) );
- }
-
- /**
- * A near-black accent should get white text — the opposite case from the
- * default accent, confirming the helper picks per-color, not a fixed answer.
- */
- public function test_very_dark_accent_resolves_to_white_text() {
- $this->assertSame( '#ffffff', CookieNotice::get_readable_text_color( '#111827' ) );
- }
-
- /**
- * A mid-gray accent (#767676) is a near-tie borderline case (black wins
- * against it at 4.623:1 vs white's 4.542:1) — a good regression guard
- * against off-by-one comparisons between "tested" and "returned" colors,
- * or between >= and > in the white_contrast/black_contrast comparison.
- */
- public function test_borderline_gray_accent_resolves_to_black_text() {
- $this->assertSame( '#000000', CookieNotice::get_readable_text_color( '#767676' ) );
- }
-
- /**
- * The default accent (#687df9) only reaches ~3.57:1 contrast against
- * white — below the 4.5:1 threshold — so it must fall back to the dark
- * neutral for text/links on the white panel, not be used verbatim.
- */
- public function test_default_accent_falls_back_to_dark_neutral_on_white() {
- $this->assertSame( '#111827', CookieNotice::get_readable_on_white_color( '#687df9' ) );
- }
-
- /**
- * A very light accent (near-white) can't reach 4.5:1 against a white
- * panel no matter what, so it must fall back to the dark neutral instead
- * of being used verbatim as illegible link text.
- */
- public function test_light_accent_falls_back_to_dark_neutral_on_white() {
- $this->assertSame( '#111827', CookieNotice::get_readable_on_white_color( '#f5f5f5' ) );
- }
-
- /**
- * Malformed input (not a valid hex color) must not throw or warn — it
- * should degrade to treating the color as black, same as hex_to_rgb()'s
- * own documented fallback.
- */
- public function test_malformed_color_does_not_throw() {
- $this->assertSame( '#ffffff', CookieNotice::get_readable_text_color( 'not-a-color' ) );
- }
-
- /**
- * 3-digit shorthand hex colors must expand correctly, not be misread as
- * 6-digit ones.
- */
- public function test_shorthand_hex_color_is_expanded_correctly() {
- // #fff (white) should behave identically to #ffffff.
- $this->assertSame(
- CookieNotice::get_readable_text_color( '#ffffff' ),
- CookieNotice::get_readable_text_color( '#fff' )
- );
- }
-
- /**
- * Each corner-rounding preset must map to its own distinct CSS length.
- */
- public function test_radius_presets_resolve_to_their_own_css_value() {
- $this->assertSame( '0px', CookieNotice::get_radius_value( 'none' ) );
- $this->assertSame( '12px', CookieNotice::get_radius_value( 'small' ) );
- $this->assertSame( '24px', CookieNotice::get_radius_value( 'large' ) );
- }
-
- /**
- * An unknown preset (e.g. stale data from before an option was renamed)
- * must degrade to the 'small' default rather than emitting an invalid or
- * empty CSS value.
- */
- public function test_unknown_radius_preset_falls_back_to_small() {
- $this->assertSame( CookieNotice::get_radius_value( 'small' ), CookieNotice::get_radius_value( 'not-a-real-preset' ) );
- }
-}
diff --git a/tests/Unit/CookieNoticeIntegrationsTest.php b/tests/Unit/CookieNoticeIntegrationsTest.php
deleted file mode 100644
index 9a396e78..00000000
--- a/tests/Unit/CookieNoticeIntegrationsTest.php
+++ /dev/null
@@ -1,227 +0,0 @@
-settings = new CookieNotice();
- }
-
- /**
- * Verify that cache integrations are notified after a Cookie Notice change.
- *
- * @return void
- */
- public function test_cookie_notice_update_fires_extension_action() {
- $received = array();
- $callback = static function ( $old_options, $new_options ) use ( &$received ) {
- $received = array( $old_options, $new_options );
- };
-
- add_action( 'frbl_cookie_notice_settings_updated', $callback, 10, 2 );
-
- $old_options = array( 'enable_cookie_notice' => false );
- $new_options = array( 'enable_cookie_notice' => true );
- update_option( 'frontblocks_settings', $old_options );
- update_option( 'frontblocks_settings', $new_options );
-
- remove_action( 'frbl_cookie_notice_settings_updated', $callback, 10 );
-
- $this->assertSame( $old_options, $received[0] );
- $this->assertSame( $new_options, $received[1] );
- }
-
- /**
- * Verify that unrelated settings do not trigger cache invalidation hooks.
- *
- * @return void
- */
- public function test_unrelated_settings_update_does_not_fire_cookie_notice_action() {
- $was_called = false;
- $callback = static function () use ( &$was_called ) {
- $was_called = true;
- };
-
- add_action( 'frbl_cookie_notice_settings_updated', $callback );
-
- $this->settings->handle_frontblocks_settings_updated(
- array( 'enable_events' => false ),
- array( 'enable_events' => true ),
- 'frontblocks_settings'
- );
-
- remove_action( 'frbl_cookie_notice_settings_updated', $callback );
-
- $this->assertFalse( $was_called );
- }
-
- /**
- * Verify that a matching active GTM4WP container is detected.
- *
- * @return void
- */
- public function test_gtm4wp_conflict_is_detected_when_matching_container_is_enabled() {
- if ( ! defined( 'GTM4WP_OPTIONS' ) ) {
- define( 'GTM4WP_OPTIONS', 'gtm4wp-options' );
- }
-
- update_option(
- 'gtm4wp-options',
- array(
- 'gtm-code' => 'GTM-ABC123',
- 'gtm-code-placement' => 0,
- )
- );
-
- $method = new ReflectionMethod( Settings::class, 'is_gtm4wp_container_loading' );
- $method->setAccessible( true );
- $settings = new Settings();
-
- $this->assertTrue( $method->invoke( $settings, 'GTM-ABC123' ) );
- $this->assertFalse( $method->invoke( $settings, 'GTM-OTHER' ) );
- }
-
- /**
- * Verify that GTM4WP placement off does not produce a conflict.
- *
- * @return void
- */
- public function test_gtm4wp_conflict_is_not_reported_when_container_code_is_off() {
- if ( ! defined( 'GTM4WP_OPTIONS' ) ) {
- define( 'GTM4WP_OPTIONS', 'gtm4wp-options' );
- }
-
- update_option(
- 'gtm4wp-options',
- array(
- 'gtm-code' => 'GTM-ABC123',
- 'gtm-code-placement' => 3,
- )
- );
-
- $method = new ReflectionMethod( Settings::class, 'is_gtm4wp_container_loading' );
- $method->setAccessible( true );
- $settings = new Settings();
-
- $this->assertFalse( $method->invoke( $settings, 'GTM-ABC123' ) );
- }
-
- /**
- * Verify that legacy dedicated GTM/GA4 ids are moved into the shared
- * tracking-integrations list, and the legacy keys are cleared.
- *
- * @return void
- */
- public function test_migration_moves_legacy_gtm_and_ga4_ids_into_the_shared_list() {
- update_option(
- 'frontblocks_settings',
- array(
- 'cookie_notice_gtm_id' => 'GTM-LEGACY1',
- 'cookie_notice_ga4_id' => 'G-LEGACY123',
- )
- );
-
- ( new Settings() )->migrate_legacy_gtm_ga4_tracking_ids();
-
- $options = get_option( 'frontblocks_settings' );
-
- $this->assertArrayNotHasKey( 'cookie_notice_gtm_id', $options );
- $this->assertArrayNotHasKey( 'cookie_notice_ga4_id', $options );
- $this->assertSame(
- array(
- array( 'type' => 'gtm', 'id' => 'GTM-LEGACY1' ),
- array( 'type' => 'ga4', 'id' => 'G-LEGACY123' ),
- ),
- $options['cookie_notice_tracking_integrations']
- );
- }
-
- /**
- * A site with no legacy values must not gain an empty integrations key
- * out of nowhere, since the migration is meant to be a no-op there.
- *
- * @return void
- */
- public function test_migration_is_a_no_op_when_no_legacy_value_is_present() {
- update_option( 'frontblocks_settings', array( 'enable_cookie_notice' => true ) );
-
- ( new Settings() )->migrate_legacy_gtm_ga4_tracking_ids();
-
- $options = get_option( 'frontblocks_settings' );
-
- $this->assertArrayNotHasKey( 'cookie_notice_tracking_integrations', $options );
- }
-
- /**
- * If the admin already added an equivalent entry to the generic list
- * before the migration runs, the legacy value must not override it.
- *
- * @return void
- */
- public function test_migration_does_not_override_an_existing_gtm_entry() {
- update_option(
- 'frontblocks_settings',
- array(
- 'cookie_notice_gtm_id' => 'GTM-LEGACY1',
- 'cookie_notice_tracking_integrations' => array(
- array( 'type' => 'gtm', 'id' => 'GTM-CURRENT' ),
- ),
- )
- );
-
- ( new Settings() )->migrate_legacy_gtm_ga4_tracking_ids();
-
- $options = get_option( 'frontblocks_settings' );
-
- $this->assertArrayNotHasKey( 'cookie_notice_gtm_id', $options );
- $this->assertSame(
- array( array( 'type' => 'gtm', 'id' => 'GTM-CURRENT' ) ),
- $options['cookie_notice_tracking_integrations']
- );
- }
-
- /**
- * Running the migration a second time (e.g. on a later admin_init) must
- * not error or duplicate anything now that the legacy keys are gone.
- *
- * @return void
- */
- public function test_migration_only_runs_once() {
- update_option( 'frontblocks_settings', array( 'cookie_notice_gtm_id' => 'GTM-LEGACY1' ) );
-
- $settings = new Settings();
- $settings->migrate_legacy_gtm_ga4_tracking_ids();
- $settings->migrate_legacy_gtm_ga4_tracking_ids();
-
- $options = get_option( 'frontblocks_settings' );
-
- $this->assertSame(
- array( array( 'type' => 'gtm', 'id' => 'GTM-LEGACY1' ) ),
- $options['cookie_notice_tracking_integrations']
- );
- }
-}
diff --git a/tests/Unit/CookieNoticePolicyPageTest.php b/tests/Unit/CookieNoticePolicyPageTest.php
deleted file mode 100644
index f8d59693..00000000
--- a/tests/Unit/CookieNoticePolicyPageTest.php
+++ /dev/null
@@ -1,137 +0,0 @@
-policy_page_id = self::factory()->post->create(
- array(
- 'post_type' => 'page',
- 'post_title' => 'Cookie Policy',
- 'post_status' => 'publish',
- )
- );
-
- update_option(
- 'frontblocks_settings',
- array(
- 'enable_cookie_notice' => true,
- 'cookie_notice_policy_page_id' => $this->policy_page_id,
- )
- );
-
- $this->cookie_notice = new CookieNotice();
- }
-
- private function render_banner_html() {
- ob_start();
- $this->cookie_notice->render_banner();
- return ob_get_clean();
- }
-
- /**
- * The banner must never render on the page the admin configured as the
- * cookie policy page — otherwise a popup layout would immediately cover
- * the very content the visitor is trying to read before deciding.
- */
- public function test_banner_is_suppressed_on_the_configured_policy_page() {
- $this->go_to( get_permalink( $this->policy_page_id ) );
-
- $this->assertSame( '', $this->render_banner_html() );
- }
-
- /**
- * The banner must still render on every other page — suppression is
- * scoped to exactly the configured page, not a global kill switch.
- */
- public function test_banner_still_renders_on_other_pages() {
- $other_page_id = self::factory()->post->create( array( 'post_type' => 'page' ) );
- $this->go_to( get_permalink( $other_page_id ) );
-
- $this->assertStringContainsString( 'id="frbl-cookie-notice"', $this->render_banner_html() );
- }
-
- /**
- * With no policy page configured at all, nothing should ever be suppressed.
- */
- public function test_no_policy_page_configured_means_no_suppression_anywhere() {
- update_option(
- 'frontblocks_settings',
- array( 'enable_cookie_notice' => true )
- );
- $cookie_notice = new CookieNotice();
-
- $this->go_to( get_permalink( $this->policy_page_id ) );
-
- ob_start();
- $cookie_notice->render_banner();
- $html = ob_get_clean();
-
- $this->assertStringContainsString( 'id="frbl-cookie-notice"', $html );
- }
-
- /**
- * The banner must render already invisible/off-screen by default — the
- * 'frbl-cookie-notice--init' class is what a returning, already-decided
- * visitor never sees removed, avoiding the flash frontblocks-cookie-notice.js
- * used to cause by hiding a banner that started out visible.
- */
- public function test_banner_renders_with_the_init_class_by_default() {
- $other_page_id = self::factory()->post->create( array( 'post_type' => 'page' ) );
- $this->go_to( get_permalink( $other_page_id ) );
-
- $html = $this->render_banner_html();
-
- $this->assertMatchesRegularExpression( '/class="[^"]*\bfrbl-cookie-notice--init\b[^"]*"/', $html );
- }
-
- /**
- * A no-JS visitor must still see the banner: the printed style
- * resets '--init' back to visible, since nothing would otherwise ever
- * remove that class for them.
- */
- public function test_banner_includes_a_noscript_fallback_for_the_init_state() {
- $other_page_id = self::factory()->post->create( array( 'post_type' => 'page' ) );
- $this->go_to( get_permalink( $other_page_id ) );
-
- $html = $this->render_banner_html();
-
- $this->assertStringContainsString( '', $html );
- $this->assertStringContainsString( 'frbl-cookie-notice--init', $html );
- }
-
- /**
- * The "Learn more" link in the banner message must resolve the saved
- * page ID to its actual current permalink, not embed a stale/raw URL.
- */
- public function test_learn_more_link_resolves_to_the_policy_page_permalink() {
- $other_page_id = self::factory()->post->create( array( 'post_type' => 'page' ) );
- $this->go_to( get_permalink( $other_page_id ) );
-
- $html = $this->render_banner_html();
-
- $this->assertStringContainsString(
- esc_url( get_permalink( $this->policy_page_id ) ),
- $html
- );
- }
-}
diff --git a/tests/Unit/CookieNoticeSettingsSanitizationTest.php b/tests/Unit/CookieNoticeSettingsSanitizationTest.php
deleted file mode 100644
index de5b88f5..00000000
--- a/tests/Unit/CookieNoticeSettingsSanitizationTest.php
+++ /dev/null
@@ -1,260 +0,0 @@
-settings = new Settings();
-
- // sanitize_settings() rejects the call outright (returning the existing
- // option untouched) without a valid nonce — every test below needs one
- // present to actually exercise the sanitization logic under test.
- $_POST['_wpnonce'] = wp_create_nonce( 'frontblocks_settings-options' );
- }
-
- public function tear_down() {
- unset( $_POST['_wpnonce'] );
- parent::tear_down();
- }
-
- public function test_enable_cookie_notice_is_cast_to_bool() {
- $sanitized = $this->settings->sanitize_settings( array( 'enable_cookie_notice' => '1' ) );
- $this->assertTrue( $sanitized['enable_cookie_notice'] );
- }
-
- public function test_enable_cookie_notice_defaults_to_false_when_absent() {
- // Simulates an unchecked checkbox: the key is simply missing from $_POST.
- $sanitized = $this->settings->sanitize_settings( array() );
- $this->assertFalse( $sanitized['enable_cookie_notice'] );
- }
-
- public function test_message_is_sanitized_as_textarea_field() {
- $sanitized = $this->settings->sanitize_settings(
- array( 'cookie_notice_message' => "Line one\nLine two " )
- );
-
- $this->assertStringNotContainsString( '';
- $sanitized = $this->settings->sanitize_settings( array( 'cookie_notice_tracking_integration_code' => $snippet ) );
-
- $this->assertSame(
- array(
- array(
- 'type' => 'clientify_analytics_plus',
- 'id' => 'TestPixel1',
- ),
- ),
- $sanitized['cookie_notice_tracking_integrations']
- );
- $this->assertArrayNotHasKey( 'cookie_notice_tracking_integration_code', $sanitized );
- }
-
- public function test_unrecognized_tracking_code_is_rejected_and_reports_an_error() {
- $before_count = $this->count_tracking_notice_errors();
-
- $sanitized = $this->settings->sanitize_settings( array( 'cookie_notice_tracking_integration_code' => '' ) );
-
- $this->assertSame( array(), $sanitized['cookie_notice_tracking_integrations'] );
- $this->assertSame( $before_count + 1, $this->count_tracking_notice_errors(), 'Expected exactly one new admin notice for the unrecognized tracking snippet.' );
- }
-
- public function test_blank_tracking_code_leaves_the_integration_list_empty_without_an_error() {
- $before_count = $this->count_tracking_notice_errors();
-
- $sanitized = $this->settings->sanitize_settings( array( 'cookie_notice_tracking_integration_code' => '' ) );
-
- $this->assertSame( array(), $sanitized['cookie_notice_tracking_integrations'] );
- $this->assertSame( $before_count, $this->count_tracking_notice_errors(), 'A blank snippet must not add an admin notice.' );
- }
-
- public function test_tracking_integration_can_be_removed() {
- update_option(
- 'frontblocks_settings',
- array(
- 'cookie_notice_tracking_integrations' => array(
- array( 'type' => 'brevo', 'id' => 'brevo-key' ),
- ),
- )
- );
-
- $sanitized = $this->settings->sanitize_settings(
- array(
- 'cookie_notice_tracking_integration_code' => '',
- 'cookie_notice_tracking_remove' => array( 'brevo' ),
- )
- );
-
- $this->assertSame( array(), $sanitized['cookie_notice_tracking_integrations'] );
- }
-
- /**
- * Count how many "unrecognized tracking snippet" admin notices are
- * currently queued, so tests can assert a delta instead of an absolute
- * count — WordPress's settings-errors list is a process-wide global that
- * earlier tests (in this file or elsewhere) may have already added to.
- *
- * @return int
- */
- private function count_tracking_notice_errors() {
- $count = 0;
-
- foreach ( get_settings_errors( 'frontblocks_settings' ) as $error ) {
- if ( 'frbl_cookie_notice_tracking_unrecognized' === $error['code'] ) {
- ++$count;
- }
- }
-
- return $count;
- }
-
- public function test_expiration_days_is_capped_at_730() {
- $sanitized = $this->settings->sanitize_settings( array( 'cookie_notice_expiration_days' => '99999' ) );
- $this->assertSame( 730, $sanitized['cookie_notice_expiration_days'] );
- }
-
- public function test_expiration_days_of_zero_falls_back_to_365() {
- $sanitized = $this->settings->sanitize_settings( array( 'cookie_notice_expiration_days' => '0' ) );
- $this->assertSame( 365, $sanitized['cookie_notice_expiration_days'] );
- }
-
- public function test_valid_gtm_id_pasted_through_the_generic_field_is_preserved_uppercased() {
- $sanitized = $this->settings->sanitize_settings( array( 'cookie_notice_tracking_integration_code' => 'gtm-abc123' ) );
-
- $this->assertSame(
- array( array( 'type' => 'gtm', 'id' => 'GTM-ABC123' ) ),
- $sanitized['cookie_notice_tracking_integrations']
- );
- }
-
- public function test_malformed_gtm_id_is_rejected() {
- $before_count = $this->count_tracking_notice_errors();
-
- $sanitized = $this->settings->sanitize_settings( array( 'cookie_notice_tracking_integration_code' => 'not-a-gtm-id' ) );
-
- $this->assertSame( array(), $sanitized['cookie_notice_tracking_integrations'] );
- $this->assertSame( $before_count + 1, $this->count_tracking_notice_errors() );
- }
-
- public function test_gtm_snippet_replaces_a_previously_saved_gtm_entry() {
- update_option(
- 'frontblocks_settings',
- array(
- 'cookie_notice_tracking_integrations' => array(
- array( 'type' => 'gtm', 'id' => 'GTM-OLD1234' ),
- ),
- )
- );
-
- $sanitized = $this->settings->sanitize_settings( array( 'cookie_notice_tracking_integration_code' => 'GTM-NEW5678' ) );
-
- $this->assertSame(
- array( array( 'type' => 'gtm', 'id' => 'GTM-NEW5678' ) ),
- $sanitized['cookie_notice_tracking_integrations']
- );
- }
-
- public function test_gtm_entry_can_be_removed_through_the_generic_list() {
- update_option(
- 'frontblocks_settings',
- array(
- 'cookie_notice_tracking_integrations' => array(
- array( 'type' => 'gtm', 'id' => 'GTM-ABC123' ),
- ),
- )
- );
-
- $sanitized = $this->settings->sanitize_settings(
- array(
- 'cookie_notice_tracking_integration_code' => '',
- 'cookie_notice_tracking_remove' => array( 'gtm' ),
- )
- );
-
- $this->assertSame( array(), $sanitized['cookie_notice_tracking_integrations'] );
- }
-}
diff --git a/tests/Unit/CookieNoticeTrackingSnippetTest.php b/tests/Unit/CookieNoticeTrackingSnippetTest.php
deleted file mode 100644
index a7c8cf59..00000000
--- a/tests/Unit/CookieNoticeTrackingSnippetTest.php
+++ /dev/null
@@ -1,341 +0,0 @@
-assertSame( 'gtm', $detected['type'] );
- $this->assertSame( 'GTM-ABC1234', $detected['id'] );
- }
-
- /**
- * A full GTM loader snippet (e.g. copied from Google Tag Manager's own
- * install instructions) also carries the container ID in its src URL.
- */
- public function test_detects_gtm_container_id_from_a_full_snippet() {
- $snippet = <<<'HTML'
-
-HTML;
-
- $detected = CookieNotice::detect_tracking_snippet( $snippet );
-
- $this->assertSame( 'gtm', $detected['type'] );
- $this->assertSame( 'GTM-ABC1234', $detected['id'] );
- }
-
- /**
- * GA4's measurement ID can also be pasted on its own.
- */
- public function test_detects_bare_ga4_measurement_id() {
- $detected = CookieNotice::detect_tracking_snippet( 'g-abc1234567' );
-
- $this->assertSame( 'ga4', $detected['type'] );
- $this->assertSame( 'G-ABC1234567', $detected['id'] );
- }
-
- /**
- * A full gtag.js loader snippet also carries the measurement ID in its
- * src URL.
- */
- public function test_detects_ga4_measurement_id_from_a_full_snippet() {
- $snippet = <<<'HTML'
-
-
-HTML;
-
- $detected = CookieNotice::detect_tracking_snippet( $snippet );
-
- $this->assertSame( 'ga4', $detected['type'] );
- $this->assertSame( 'G-ABC1234567', $detected['id'] );
- }
-
- /**
- * The Clientify Analytics Plus snippet is just a single pixel
-
-HTML;
-
- $detected = CookieNotice::detect_tracking_snippet( $snippet );
-
- $this->assertSame( 'clientify_analytics_plus', $detected['type'] );
- $this->assertSame( 'TestPixelId123', $detected['id'] );
- }
-
- /**
- * The classic Clientify Analytics snippet loads a different domain
- * (tracker.js, not the Analytics Plus pixel host) and carries its code
- * inside a setTrackingCode(...) call rather than in a URL.
- */
- public function test_detects_clientify_analytics_classic_tracking_code() {
- $snippet = <<<'HTML'
-
-
-
-HTML;
-
- $detected = CookieNotice::detect_tracking_snippet( $snippet );
-
- $this->assertSame( 'clientify_analytics_classic', $detected['type'] );
- $this->assertSame( 'CF-00000-00000-TEST', $detected['id'] );
- }
-
- /**
- * Brevo's SDK loader + init() snippet carries its id as a client_key.
- */
- public function test_detects_brevo_client_key() {
- $snippet = <<<'HTML'
-
-
-HTML;
-
- $detected = CookieNotice::detect_tracking_snippet( $snippet );
-
- $this->assertSame( 'brevo', $detected['type'] );
- $this->assertSame( 'testclientkey1234567890', $detected['id'] );
- }
-
- /**
- * ChatGPT Ads can be configured from the vendor-provided oaiq snippet.
- */
- public function test_detects_chatgpt_ads_snippet() {
- $snippet = <<<'HTML'
-
-HTML;
-
- $detected = CookieNotice::detect_tracking_snippet( $snippet );
-
- $this->assertSame( 'openai_chatgpt_ads', $detected['type'] );
- $this->assertSame( 'TestChatGPTPixelId1234', $detected['id'] );
- }
-
- /**
- * ChatGPT Ads can also be configured by pasting only its Pixel ID.
- */
- public function test_detects_chatgpt_ads_pixel_id() {
- $detected = CookieNotice::detect_tracking_snippet( 'TestChatGPTPixelId1234' );
-
- $this->assertSame( 'openai_chatgpt_ads', $detected['type'] );
- $this->assertSame( 'TestChatGPTPixelId1234', $detected['id'] );
- }
-
- /**
- * A generic provider code must not be treated as a ChatGPT Ads Pixel ID.
- */
- public function test_does_not_mistake_a_hyphenated_tracking_code_for_a_chatgpt_ads_pixel_id() {
- $this->assertNull( CookieNotice::detect_tracking_snippet( 'CF-00000-00000-TEST' ) );
- $this->assertNull( CookieNotice::detect_tracking_snippet( 'testclientkey1234567890' ) );
- }
-
- /**
- * A snippet from an unsupported tool (or plain garbage) must not be
- * mistaken for one of the three supported patterns.
- */
- public function test_unrecognized_snippet_returns_null() {
- $this->assertNull( CookieNotice::detect_tracking_snippet( '' ) );
- }
-
- /**
- * Add-ons can register their own detector and type without adding a
- * provider-specific pattern to the free plugin.
- */
- public function test_add_on_can_register_a_tracking_detector() {
- $types_callback = function ( $types ) {
- $types[] = 'example_add_on';
- return $types;
- };
- $detector_callback = function ( $detected, $raw ) {
- return 'example-id' === $raw ? array( 'type' => 'example_add_on', 'id' => 'ExampleId123' ) : $detected;
- };
- add_filter( 'frbl_cookie_notice_tracking_types', $types_callback );
- add_filter( 'frbl_cookie_notice_detect_tracking_snippet', $detector_callback, 10, 2 );
-
- $this->assertSame(
- array( 'type' => 'example_add_on', 'id' => 'ExampleId123' ),
- CookieNotice::detect_tracking_snippet( 'example-id' )
- );
-
- remove_filter( 'frbl_cookie_notice_tracking_types', $types_callback );
- remove_filter( 'frbl_cookie_notice_detect_tracking_snippet', $detector_callback, 10 );
- }
-
- /**
- * An empty/blank field is "no snippet configured", not an invalid one.
- */
- public function test_blank_snippet_returns_null() {
- $this->assertNull( CookieNotice::detect_tracking_snippet( '' ) );
- $this->assertNull( CookieNotice::detect_tracking_snippet( " \n\t" ) );
- }
-
- /**
- * Stored integration records are normalized and invalid records are ignored.
- */
- public function test_tracking_integrations_are_normalized_without_raw_code() {
- $integrations = CookieNotice::get_tracking_integrations(
- array(
- 'cookie_notice_tracking_integrations' => array(
- array( 'type' => 'brevo', 'id' => 'first-key' ),
- array( 'type' => 'invalid', 'id' => 'discard-me' ),
- array( 'type' => 'brevo', 'id' => 'latest-key' ),
- ),
- )
- );
-
- $this->assertSame( array( array( 'type' => 'brevo', 'id' => 'latest-key' ) ), $integrations );
- }
-
- /**
- * Settings saves preserve integrations from an inactive companion plugin.
- */
- public function test_tracking_integrations_can_preserve_unknown_add_on_records() {
- $this->assertSame(
- array( array( 'type' => 'inactive_add_on', 'id' => 'saved-id' ) ),
- CookieNotice::get_tracking_integrations(
- array(
- 'cookie_notice_tracking_integrations' => array(
- array( 'type' => 'inactive_add_on', 'id' => 'saved-id' ),
- ),
- ),
- true
- )
- );
- }
-
- /**
- * Existing sites using the former single type/id settings retain tracking
- * until their next settings save migrates them to the integrations list.
- */
- public function test_legacy_tracking_options_are_read_as_an_integration() {
- $this->assertSame(
- array( array( 'type' => 'brevo', 'id' => 'legacy-key' ) ),
- CookieNotice::get_tracking_integrations(
- array(
- 'cookie_notice_tracking_type' => 'brevo',
- 'cookie_notice_tracking_id' => 'legacy-key',
- )
- )
- );
- }
-
- /**
- * 'gtm' and 'ga4' are FrontBlocks' own native types, registered by default
- * alongside the additional tools detectable from a pasted snippet.
- */
- public function test_gtm_and_ga4_are_registered_tracking_types() {
- $types = CookieNotice::get_tracking_types();
-
- $this->assertContains( 'gtm', $types );
- $this->assertContains( 'ga4', $types );
- }
-
- /**
- * A stored 'gtm'/'ga4' record round-trips through get_tracking_integrations()
- * the same way any other supported type does.
- */
- public function test_gtm_and_ga4_records_are_normalized_like_other_integrations() {
- $integrations = CookieNotice::get_tracking_integrations(
- array(
- 'cookie_notice_tracking_integrations' => array(
- array( 'type' => 'gtm', 'id' => 'GTM-ABC1234' ),
- array( 'type' => 'ga4', 'id' => 'G-ABC1234567' ),
- ),
- )
- );
-
- $this->assertSame(
- array(
- array( 'type' => 'gtm', 'id' => 'GTM-ABC1234' ),
- array( 'type' => 'ga4', 'id' => 'G-ABC1234567' ),
- ),
- $integrations
- );
- }
-
- /**
- * GTM/GA4 default to the "analytics" category, while every Clientify/Brevo
- * variant defaults to "marketing" — this is what FrontBlocks PRO's
- * per-category consent gating keys off of.
- */
- public function test_integration_default_categories() {
- $this->assertSame( 'analytics', CookieNotice::get_integration_default_category( 'gtm' ) );
- $this->assertSame( 'analytics', CookieNotice::get_integration_default_category( 'ga4' ) );
- $this->assertSame( 'marketing', CookieNotice::get_integration_default_category( 'clientify_analytics_plus' ) );
- $this->assertSame( 'marketing', CookieNotice::get_integration_default_category( 'clientify_analytics_classic' ) );
- $this->assertSame( 'marketing', CookieNotice::get_integration_default_category( 'brevo' ) );
- $this->assertSame( 'marketing', CookieNotice::get_integration_default_category( 'openai_chatgpt_ads' ) );
- }
-
- /**
- * An unknown integration type must still resolve to a sane default
- * ("marketing") instead of an empty/invalid category.
- */
- public function test_unknown_integration_type_defaults_to_marketing_category() {
- $this->assertSame( 'marketing', CookieNotice::get_integration_default_category( 'some-future-tool' ) );
- }
-
- /**
- * The category default must be filterable, so FrontBlocks PRO (or any
- * add-on) can override it without forking this method.
- */
- public function test_integration_default_category_is_filterable() {
- $callback = static function ( $category, $type ) {
- return 'brevo' === $type ? 'custom-category' : $category;
- };
-
- add_filter( 'frbl_cookie_notice_integration_category', $callback, 10, 2 );
-
- $this->assertSame( 'custom-category', CookieNotice::get_integration_default_category( 'brevo' ) );
- $this->assertSame( 'analytics', CookieNotice::get_integration_default_category( 'ga4' ) );
-
- remove_filter( 'frbl_cookie_notice_integration_category', $callback, 10 );
- }
-}
diff --git a/tests/Unit/CookieNoticeTransitionTest.php b/tests/Unit/CookieNoticeTransitionTest.php
new file mode 100644
index 00000000..7f81c7fe
--- /dev/null
+++ b/tests/Unit/CookieNoticeTransitionTest.php
@@ -0,0 +1,63 @@
+ 'GTM-LEGACY1',
+ 'cookie_notice_ga4_id' => 'G-LEGACY123',
+ )
+ );
+
+ $this->assertSame(
+ array(
+ array( 'type' => 'gtm', 'id' => 'GTM-LEGACY1' ),
+ array( 'type' => 'ga4', 'id' => 'G-LEGACY123' ),
+ ),
+ $integrations
+ );
+ }
+
+ public function test_existing_shared_integration_wins_over_the_retired_dedicated_id() {
+ $integrations = CookieNotice::get_tracking_integrations(
+ array(
+ 'cookie_notice_tracking_integrations' => array(
+ array( 'type' => 'gtm', 'id' => 'GTM-CURRENT' ),
+ ),
+ 'cookie_notice_gtm_id' => 'GTM-LEGACY1',
+ )
+ );
+
+ $this->assertSame( array( array( 'type' => 'gtm', 'id' => 'GTM-CURRENT' ) ), $integrations );
+ }
+
+ public function test_frontconsent_install_url_uses_the_published_wordpress_org_slug() {
+ $url = \FrontBlocks\Admin\CookieNoticeDeprecationNotice::get_frontconsent_action_url();
+
+ $this->assertStringContainsString( 'plugin=frontconsent', html_entity_decode( $url, ENT_QUOTES, 'UTF-8' ) );
+ }
+
+ public function test_frontconsent_disables_an_enabled_legacy_module() {
+ define( 'FRCN_VERSION', '1.0.1' );
+ update_option( 'frontblocks_settings', array( 'enable_cookie_notice' => true ) );
+
+ $method = new ReflectionMethod( CookieNotice::class, 'is_enabled' );
+ $method->setAccessible( true );
+
+ $this->assertFalse( $method->invoke( new CookieNotice() ) );
+ }
+}
diff --git a/tests/Unit/SettingsTest.php b/tests/Unit/SettingsTest.php
index a1df6057..b32743ac 100644
--- a/tests/Unit/SettingsTest.php
+++ b/tests/Unit/SettingsTest.php
@@ -121,21 +121,6 @@ public function test_maintenance_image_is_cast_to_a_non_negative_integer() {
$this->assertSame( 5, $sanitized['maintenance_image'] );
}
- public function test_ga4_id_is_preserved_when_valid_and_uppercased() {
- $sanitized = $this->settings->sanitize_settings( array( 'cookie_notice_tracking_integration_code' => 'g-abc123' ) );
-
- $this->assertSame(
- array( array( 'type' => 'ga4', 'id' => 'G-ABC123' ) ),
- $sanitized['cookie_notice_tracking_integrations']
- );
- }
-
- public function test_ga4_id_is_rejected_when_malformed() {
- $sanitized = $this->settings->sanitize_settings( array( 'cookie_notice_tracking_integration_code' => 'not-a-valid-id' ) );
-
- $this->assertSame( array(), $sanitized['cookie_notice_tracking_integrations'] );
- }
-
public function test_deactivate_short_description_and_move_content_are_mutually_exclusive() {
// Both were already true in storage (should not normally happen, but
// the sanitizer must still resolve to a single winner deterministically):
@@ -271,9 +256,7 @@ public function test_enqueue_admin_styles_enqueues_assets_on_the_settings_page_h
$this->settings->enqueue_admin_styles( 'appearance_page_frontblocks-settings' );
$this->assertTrue( wp_style_is( 'frontblocks-admin-settings', 'enqueued' ) );
- $this->assertTrue( wp_style_is( 'frontblocks-cookie-notice', 'enqueued' ) );
wp_dequeue_style( 'frontblocks-admin-settings' );
- wp_dequeue_style( 'frontblocks-cookie-notice' );
}
}
diff --git a/tests/js/cookie-notice-injection.test.js b/tests/js/cookie-notice-injection.test.js
deleted file mode 100644
index d7719df0..00000000
--- a/tests/js/cookie-notice-injection.test.js
+++ /dev/null
@@ -1,207 +0,0 @@
-const assert = require('node:assert/strict');
-const fs = require('node:fs');
-const path = require('node:path');
-const test = require('node:test');
-const vm = require('node:vm');
-
-const scriptPath = path.resolve(__dirname, '../../assets/cookie-notice/frontblocks-cookie-notice.js');
-const cookieNoticeScript = fs.readFileSync(scriptPath, 'utf8');
-const cookieNoticePhpPath = path.resolve(__dirname, '../../includes/Frontend/CookieNotice.php');
-const cookieNoticePhp = fs.readFileSync(cookieNoticePhpPath, 'utf8');
-
-function createEnvironment(options = {}) {
- const scripts = [];
- let fetchCalls = 0;
- const fetchActions = [];
- const listeners = {};
- const actionListeners = {};
- const banner = {
- classList: {
- add() {},
- contains() { return false; },
- remove() {}
- },
- parentNode: { removeChild() {} },
- querySelector(selector) {
- const actionMatch = selector.match(/^\[data-frbl-cookie-action="(accept|reject)"\]$/);
- if (actionMatch) {
- return {
- addEventListener(event, callback) {
- actionListeners[actionMatch[1]] = actionListeners[actionMatch[1]] || {};
- actionListeners[actionMatch[1]][event] = callback;
- }
- };
- }
- return null;
- },
- querySelectorAll() { return []; },
- style: {}
- };
- const document = {
- activeElement: null,
- body: { classList: { add() {}, remove() {} } },
- cookie: options.cookie || '',
- head: { appendChild(script) { scripts.push(script); } },
- readyState: 'loading',
- addEventListener(event, callback) { listeners[event] = callback; },
- createElement() { return {}; },
- dispatchEvent() {},
- getElementById() { return banner; },
- getElementsByTagName() { return []; },
- removeEventListener() {}
- };
- const window = {
- location: { protocol: 'https:' },
- setTimeout(callback) { callback(); }
- };
- if (options.existingOaiq) {
- window.oaiq = options.existingOaiq;
- }
- const context = {
- Array,
- CustomEvent: function () {},
- Date,
- FormData: function () {
- this.values = {};
- this.append = function (key, value) { this.values[key] = value; };
- },
- decodeURIComponent,
- document,
- encodeURIComponent,
- fetch(url, request) {
- fetchCalls += 1;
- fetchActions.push(request.body.values.action);
- return Promise.resolve({
- json() {
- return Promise.resolve({
- success: true,
- data: {
- gtmId: '',
- ga4Id: '',
- trackingIntegrations: options.trackingIntegrations || [ { type: 'openai_chatgpt_ads', id: 'TestChatGPTPixelId1234' } ],
- allowedCategories: options.allowedCategories
- }
- });
- }
- });
- },
- frblCookieNotice: {
- ajaxUrl: 'https://example.test/wp-admin/admin-ajax.php',
- cookieName: 'frbl_cookie_consent',
- cookiePath: '/',
- expirationDays: 365
- },
- window
- };
- vm.runInNewContext(cookieNoticeScript, context);
- listeners.DOMContentLoaded();
-
- return { actionListeners, fetchActions, fetchCalls: () => fetchCalls, scripts, window };
-}
-
-test('does not load ChatGPT Ads before consent and loads it after acceptance', async () => {
- const environment = createEnvironment();
-
- await new Promise((resolve) => setImmediate(resolve));
- assert.equal(environment.scripts.length, 0);
- assert.equal(environment.fetchCalls(), 0);
- environment.actionListeners.accept.click();
- await new Promise((resolve) => setImmediate(resolve));
- await new Promise((resolve) => setImmediate(resolve));
-
- assert.equal(environment.scripts.length, 1);
- assert.equal(environment.scripts[0].src, 'https://bzrcdn.openai.com/sdk/oaiq.min.js');
- assert.equal(environment.window.oaiq.q[0][0], 'init');
- assert.equal(environment.window.oaiq.q[0][1].pixelId, 'TestChatGPTPixelId1234');
- assert.equal(environment.window.oaiq.q[0][1].debug, true);
-});
-
-test('the complete inline bootstrap initializes ChatGPT Ads for an accepted returning visitor', async () => {
- const inlineBootstrapMatch = cookieNoticePhp.match(/public function render_consent_bootstrap_script\(\) \{[\s\S]*?