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() { @@ -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; - ?> - - 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