PluginProbe ʕ •ᴥ•ʔ
FrontBlocks for Gutenberg/GeneratePress / 1.5.2
FrontBlocks for Gutenberg/GeneratePress v1.5.2
1.5.2 1.5.1 1.4.0 1.5.0 trunk 0.2.0 0.2.1 0.2.2 0.2.3 0.2.4 0.2.5 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.1.0 1.2.0 1.2.1 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 ci-artifacts
frontblocks / includes / Admin / Settings.php
frontblocks / includes / Admin Last commit date
RedundantPlugins.php 3 days ago ReviewNotice.php 5 days ago Settings.php 3 days ago UI.php 2 months ago
Settings.php
3538 lines
1 <?php
2 /**
3 * Settings page
4 *
5 * @package FrontBlocks
6 * @author Closemarketing
7 * @copyright 2025 Closemarketing
8 * @version 1.0.0
9 */
10
11 namespace FrontBlocks\Admin;
12
13 defined( 'ABSPATH' ) || exit;
14
15 use FrontBlocks\Admin\UI;
16
17 /**
18 * Settings class
19 */
20 class Settings {
21
22 /**
23 * Option key for testimonials feature.
24 *
25 * @var string
26 */
27 private $option_enable_testimonials = 'enable_testimonials';
28
29 /**
30 * Option key for reading progress bar feature.
31 *
32 * @var string
33 */
34 private $option_enable_reading_progress = 'enable_reading_progress';
35
36 /**
37 * Option key for back button feature.
38 *
39 * @var string
40 */
41 private $option_enable_back_button = 'enable_back_button';
42
43 /**
44 * Option key for scroll to top feature.
45 *
46 * @var string
47 */
48 private $option_enable_scroll_top = 'enable_scroll_top';
49
50 /**
51 * Option key for scroll to top button position.
52 *
53 * @var string
54 */
55 private $option_scroll_top_position = 'scroll_top_position';
56
57 /**
58 * Option key for scroll to top custom icon URL.
59 *
60 * @var string
61 */
62 private $option_scroll_top_icon_url = 'scroll_top_icon_url';
63
64 /**
65 * Option key for events CPT feature.
66 *
67 * @var string
68 */
69 private $option_enable_events = 'enable_events';
70
71 /**
72 * Option key for events type (cpt or posts).
73 *
74 * @var string
75 */
76 private $option_events_type = 'events_type';
77
78 /**
79 * Option key for maintenance mode feature.
80 *
81 * @var string
82 */
83 private $option_enable_maintenance = 'enable_maintenance';
84
85 /**
86 * Option key for maintenance page title.
87 *
88 * @var string
89 */
90 private $option_maintenance_title = 'maintenance_title';
91
92 /**
93 * Option key for maintenance page background image (attachment ID).
94 *
95 * @var string
96 */
97 private $option_maintenance_image = 'maintenance_image';
98
99 /**
100 * Option key for the cookie notice feature.
101 *
102 * @var string
103 */
104 private $option_enable_cookie_notice = 'enable_cookie_notice';
105
106 /**
107 * Option key for the cookie notice message.
108 *
109 * @var string
110 */
111 private $option_cookie_notice_message = 'cookie_notice_message';
112
113 /**
114 * Option key for the cookie notice accept button label.
115 *
116 * @var string
117 */
118 private $option_cookie_notice_accept_label = 'cookie_notice_accept_label';
119
120 /**
121 * Option key for the cookie notice reject button label.
122 *
123 * @var string
124 */
125 private $option_cookie_notice_reject_label = 'cookie_notice_reject_label';
126
127 /**
128 * Option key for the cookie policy page ID.
129 *
130 * @var string
131 */
132 private $option_cookie_notice_policy_page_id = 'cookie_notice_policy_page_id';
133
134 /**
135 * Option key for the cookie notice layout variant.
136 *
137 * @var string
138 */
139 private $option_cookie_notice_layout = 'cookie_notice_layout';
140
141 /**
142 * Option key for the cookie notice boxed panel position.
143 *
144 * @var string
145 */
146 private $option_cookie_notice_position = 'cookie_notice_position';
147
148 /**
149 * Option key for the cookie notice accent color.
150 *
151 * @var string
152 */
153 private $option_cookie_notice_color = 'cookie_notice_color';
154
155 /**
156 * Option key for the cookie notice panel background color.
157 *
158 * @var string
159 */
160 private $option_cookie_notice_bg_color = 'cookie_notice_bg_color';
161
162 /**
163 * Option key for the cookie notice panel corner rounding.
164 *
165 * @var string
166 */
167 private $option_cookie_notice_radius = 'cookie_notice_radius';
168
169 /**
170 * Option key for the cookie notice expiration (in days).
171 *
172 * @var string
173 */
174 private $option_cookie_notice_expiration_days = 'cookie_notice_expiration_days';
175
176 /**
177 * Option key for the Google Tag Manager container ID.
178 *
179 * @var string
180 */
181 private $option_cookie_notice_gtm_id = 'cookie_notice_gtm_id';
182
183 /**
184 * Option key for the GA4 Measurement ID.
185 *
186 * @var string
187 */
188 private $option_cookie_notice_ga4_id = 'cookie_notice_ga4_id';
189
190 /**
191 * Option key for the additional tracking integrations.
192 *
193 * @var string
194 */
195 private $option_cookie_notice_tracking_integrations = 'cookie_notice_tracking_integrations';
196
197 /**
198 * Option key for popups feature.
199 *
200 * @var string
201 */
202 private $option_enable_popups = 'enable_popups';
203
204 /**
205 * Option key for fluid typography feature.
206 *
207 * @var string
208 */
209 private $option_enable_fluid_typography = 'enable_fluid_typography';
210
211 /**
212 * Option key for Gutenberg in products (PRO).
213 *
214 * @var string
215 */
216 private $option_enable_gutenberg = 'enable_gutenberg';
217
218 /**
219 * Option key for Simple Prices Variable Products (PRO).
220 *
221 * @var string
222 */
223 private $option_enable_simple_prices_variable_products = 'enable_simple_prices_variable_products';
224
225 /**
226 * Option key for After Add to Cart Block (PRO).
227 *
228 * @var string
229 */
230 private $option_enable_after_add_to_cart = 'enable_after_add_to_cart';
231
232 /**
233 * Option key for deactivate short description (PRO).
234 *
235 * @var string
236 */
237 private $option_deactivate_short_description = 'deactivate_short_description';
238
239 /**
240 * Option key for move content to short description (PRO).
241 *
242 * @var string
243 */
244 private $option_move_content_to_short_description = 'move_content_to_short_description';
245
246 /**
247 * Option key for disable zoom in WooCommerce images (PRO).
248 *
249 * @var string
250 */
251 private $option_disable_zoom_images = 'disable_zoom_images';
252
253 /**
254 * Option key for add share buttons in product page (PRO).
255 *
256 * @var string
257 */
258 private $option_add_share_buttons = 'add_share_buttons';
259
260 /**
261 * Option key for deactivate product tabs (PRO).
262 *
263 * @var string
264 */
265 private $option_deactivate_product_tabs = 'deactivate_product_tabs';
266
267 /**
268 * Option key for horizontal product form layout (PRO).
269 *
270 * @var string
271 */
272 private $option_horizontal_product_form = 'horizontal_product_form';
273
274 /**
275 * Option key for enabling variant display mode per-attribute selector (PRO).
276 *
277 * @var string
278 */
279 private $option_enable_variant_display_mode = 'enable_variant_display_mode';
280
281 /**
282 * Option key for disabling coupon field in cart (PRO).
283 *
284 * @var string
285 */
286 private $option_disable_cart_coupon = 'disable_cart_coupon';
287
288 /**
289 * Option key for disabling cross-sells in cart (PRO).
290 *
291 * @var string
292 */
293 private $option_disable_cart_cross_sells = 'disable_cart_cross_sells';
294
295 /**
296 * Option key for disabling coupon form in checkout (PRO).
297 *
298 * @var string
299 */
300 private $option_disable_checkout_coupon = 'disable_checkout_coupon';
301
302 /**
303 * Option key for disabling order notes in checkout (PRO).
304 *
305 * @var string
306 */
307 private $option_disable_checkout_order_notes = 'disable_checkout_order_notes';
308
309 /**
310 * Option key for disabling login prompt in checkout (PRO).
311 *
312 * @var string
313 */
314 private $option_disable_checkout_login_prompt = 'disable_checkout_login_prompt';
315
316 /**
317 * Option key for custom post types builder (PRO).
318 *
319 * @var string
320 */
321 private $option_enable_custom_post_types = 'enable_custom_post_types';
322
323 /**
324 * Option key for full page scroll feature (PRO).
325 *
326 * @var string
327 */
328 private $option_enable_fullpage_scroll = 'enable_fullpage_scroll';
329
330 /**
331 * Option key for language banner feature (PRO).
332 *
333 * @var string
334 */
335 private $option_enable_language_banner = 'enable_language_banner';
336
337 /**
338 * Option key for checkout inline fields (PRO).
339 *
340 * @var string
341 */
342 private $option_checkout_inline = 'checkout_inline';
343
344 /**
345 * Page slug.
346 *
347 * @var string
348 */
349 private $page_slug = 'frontblocks-settings';
350
351 /**
352 * Is license valid.
353 *
354 * @var bool
355 */
356 private $is_license_valid = false;
357
358 /**
359 * Constructor.
360 */
361 public function __construct() {
362 // Check license via FrontBlocks PRO helper function.
363 $this->is_license_valid = function_exists( 'frblp_is_license_valid' ) && frblp_is_license_valid();
364
365 add_action( 'admin_menu', array( $this, 'register_menu' ) );
366 add_action( 'admin_init', array( $this, 'register_settings' ) );
367 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_styles' ) );
368 add_action( 'admin_head', array( $this, 'add_menu_icon_styles' ) );
369 }
370
371 /**
372 * Add menu icon styles.
373 *
374 * @return void
375 */
376 public function add_menu_icon_styles() {
377 ?>
378 <style>
379 #toplevel_page_frontblocks-settings .wp-menu-image img,
380 #toplevel_page_frontblocks-settings .wp-menu-image svg {
381 width: 20px !important;
382 height: 20px !important;
383 max-width: 20px !important;
384 max-height: 20px !important;
385 }
386 </style>
387 <?php
388 }
389
390 /**
391 * Enqueue admin styles for settings page.
392 *
393 * @param string $hook Current admin page hook.
394 * @return void
395 */
396 public function enqueue_admin_styles( $hook ) {
397 if ( 'appearance_page_' . $this->page_slug !== $hook ) {
398 return;
399 }
400
401 wp_enqueue_style(
402 'frontblocks-admin-settings',
403 FRBL_PLUGIN_URL . 'assets/admin/settings.css',
404 array(),
405 FRBL_VERSION
406 );
407
408 // Reuse the real frontend banner styles so the settings-page preview
409 // renders pixel-identical to what visitors will actually see.
410 wp_enqueue_style(
411 'frontblocks-cookie-notice',
412 FRBL_PLUGIN_URL . 'assets/cookie-notice/frontblocks-cookie-notice.css',
413 array(),
414 FRBL_VERSION
415 );
416
417 wp_enqueue_media();
418
419 wp_add_inline_script(
420 'jquery',
421 "
422 document.addEventListener('DOMContentLoaded', function() {
423 const deactivateCheckbox = document.getElementById('deactivate_short_description');
424 const moveContentCheckbox = document.getElementById('move_content_to_short_description');
425
426 if (deactivateCheckbox && moveContentCheckbox) {
427 function updateMutualExclusion() {
428 const deactivateWrapper = deactivateCheckbox.closest('.tw:flex');
429 const moveContentWrapper = moveContentCheckbox.closest('.tw:flex');
430
431 // Check if license is valid (not just PRO active).
432 const isLicenseValid = " . ( $this->is_license_valid ? 'true' : 'false' ) . ";
433
434 if (deactivateCheckbox.checked) {
435 moveContentCheckbox.disabled = true;
436 if (moveContentWrapper) {
437 moveContentWrapper.style.opacity = '0.5';
438 moveContentWrapper.style.filter = 'grayscale(100%)';
439 const toggle = moveContentWrapper.querySelector('.frbl-toggle');
440 if (toggle) {
441 toggle.style.borderColor = '#ef4444';
442 toggle.style.opacity = '0.7';
443 }
444 }
445 } else {
446 moveContentCheckbox.disabled = !isLicenseValid;
447 if (moveContentWrapper) {
448 moveContentWrapper.style.opacity = isLicenseValid ? '1' : '0.5';
449 moveContentWrapper.style.filter = '';
450 const toggle = moveContentWrapper.querySelector('.frbl-toggle');
451 if (toggle) {
452 toggle.style.borderColor = '';
453 toggle.style.opacity = '';
454 }
455 }
456 }
457
458 if (moveContentCheckbox.checked) {
459 deactivateCheckbox.disabled = true;
460 if (deactivateWrapper) {
461 deactivateWrapper.style.opacity = '0.5';
462 deactivateWrapper.style.filter = 'grayscale(100%)';
463 const toggle = deactivateWrapper.querySelector('.frbl-toggle');
464 if (toggle) {
465 toggle.style.borderColor = '#ef4444';
466 toggle.style.opacity = '0.7';
467 }
468 }
469 } else {
470 deactivateCheckbox.disabled = !isLicenseValid;
471 if (deactivateWrapper) {
472 deactivateWrapper.style.opacity = isLicenseValid ? '1' : '0.5';
473 deactivateWrapper.style.filter = '';
474 const toggle = deactivateWrapper.querySelector('.frbl-toggle');
475 if (toggle) {
476 toggle.style.borderColor = '';
477 toggle.style.opacity = '';
478 }
479 }
480 }
481 }
482
483 deactivateCheckbox.addEventListener('change', updateMutualExclusion);
484 moveContentCheckbox.addEventListener('change', updateMutualExclusion);
485
486 updateMutualExclusion();
487 }
488
489 // Show/hide events type select based on toggle state.
490 const eventsCheckbox = document.getElementById('enable_events');
491 const eventsTypeWrapper = document.getElementById('events-type-wrapper');
492
493 if (eventsCheckbox && eventsTypeWrapper) {
494 // Find the parent feature card and feature content.
495 const featureCard = eventsCheckbox.closest('.frbl-feature-card');
496 const featureContent = featureCard ? featureCard.querySelector('.frbl-feature-content') : null;
497
498 // Move the wrapper outside of frbl-feature-content but inside frbl-feature-card.
499 // This ensures it appears below the entire horizontal row (icon, text, toggle).
500 if (featureCard && featureContent) {
501 // Check if wrapper is still inside feature-content and move it.
502 if (featureContent.contains(eventsTypeWrapper)) {
503 // Move it to be a direct child of feature-card, after feature-content.
504 featureCard.appendChild(eventsTypeWrapper);
505 }
506 }
507
508 function updateEventsTypeVisibility() {
509 if (eventsCheckbox.checked) {
510 eventsTypeWrapper.style.display = 'block';
511 eventsTypeWrapper.style.width = '100%';
512 eventsTypeWrapper.style.minWidth = '100%';
513 eventsTypeWrapper.style.marginTop = '1rem';
514 eventsTypeWrapper.style.paddingTop = '1rem';
515 eventsTypeWrapper.style.paddingLeft = '1rem';
516 eventsTypeWrapper.style.paddingRight = '1rem';
517 eventsTypeWrapper.style.paddingBottom = '1rem';
518 eventsTypeWrapper.style.borderTop = '1px solid #e5e7eb';
519 eventsTypeWrapper.style.backgroundColor = '#f9fafb';
520 // Set feature card to column layout.
521 if (featureCard) {
522 featureCard.style.display = 'flex';
523 featureCard.style.flexDirection = 'column';
524 }
525 // Keep the feature content horizontal - never change it.
526 if (featureContent) {
527 featureContent.style.flexDirection = 'row';
528 featureContent.style.alignItems = 'center';
529 featureContent.style.justifyContent = 'space-between';
530 }
531 } else {
532 eventsTypeWrapper.style.display = 'none';
533 // Reset feature card layout.
534 if (featureCard) {
535 featureCard.style.display = '';
536 featureCard.style.flexDirection = '';
537 }
538 // Keep the feature content horizontal.
539 if (featureContent) {
540 featureContent.style.flexDirection = 'row';
541 featureContent.style.alignItems = 'center';
542 featureContent.style.justifyContent = 'space-between';
543 }
544 }
545 }
546
547 // Run immediately to move the element on page load.
548 if (featureCard && featureContent && featureContent.contains(eventsTypeWrapper)) {
549 featureCard.appendChild(eventsTypeWrapper);
550 }
551
552 eventsCheckbox.addEventListener('change', updateEventsTypeVisibility);
553 updateEventsTypeVisibility();
554 }
555
556 // Show/hide scroll-top sub-settings based on toggle state.
557 const scrollTopCheckbox = document.getElementById('enable_scroll_top');
558 const scrollTopWrapper = document.getElementById('scroll-top-settings-wrapper');
559
560 if (scrollTopCheckbox && scrollTopWrapper) {
561 const scrollTopCard = scrollTopCheckbox.closest('.frbl-feature-card');
562 const scrollTopContent = scrollTopCard ? scrollTopCard.querySelector('.frbl-feature-content') : null;
563
564 if (scrollTopCard && scrollTopContent && scrollTopContent.contains(scrollTopWrapper)) {
565 scrollTopCard.appendChild(scrollTopWrapper);
566 }
567
568 function updateScrollTopVisibility() {
569 if (scrollTopCheckbox.checked) {
570 scrollTopWrapper.style.display = 'block';
571 scrollTopWrapper.style.width = '100%';
572 scrollTopWrapper.style.minWidth = '100%';
573 scrollTopWrapper.style.marginTop = '1rem';
574 scrollTopWrapper.style.paddingTop = '1rem';
575 scrollTopWrapper.style.paddingLeft = '1rem';
576 scrollTopWrapper.style.paddingRight = '1rem';
577 scrollTopWrapper.style.paddingBottom = '1rem';
578 scrollTopWrapper.style.borderTop = '1px solid #e5e7eb';
579 scrollTopWrapper.style.backgroundColor = '#f9fafb';
580 if (scrollTopCard) {
581 scrollTopCard.style.display = 'flex';
582 scrollTopCard.style.flexDirection = 'column';
583 }
584 if (scrollTopContent) {
585 scrollTopContent.style.flexDirection = 'row';
586 scrollTopContent.style.alignItems = 'center';
587 scrollTopContent.style.justifyContent = 'space-between';
588 }
589 } else {
590 scrollTopWrapper.style.display = 'none';
591 if (scrollTopCard) {
592 scrollTopCard.style.display = '';
593 scrollTopCard.style.flexDirection = '';
594 }
595 if (scrollTopContent) {
596 scrollTopContent.style.flexDirection = 'row';
597 scrollTopContent.style.alignItems = 'center';
598 scrollTopContent.style.justifyContent = 'space-between';
599 }
600 }
601 }
602
603 scrollTopCheckbox.addEventListener('change', updateScrollTopVisibility);
604 updateScrollTopVisibility();
605
606 // Media uploader for custom icon.
607 const uploadBtn = document.getElementById('scroll-top-icon-upload');
608 const removeBtn = document.getElementById('scroll-top-icon-remove');
609 const iconInput = document.getElementById('scroll_top_icon_url');
610 const iconPreview = document.getElementById('scroll-top-icon-preview');
611 const iconImg = document.getElementById('scroll-top-icon-img');
612
613 if (uploadBtn && iconInput && typeof wp !== 'undefined' && wp.media) {
614 var mediaFrame;
615
616 uploadBtn.addEventListener('click', function(e) {
617 e.preventDefault();
618
619 if (mediaFrame) {
620 mediaFrame.open();
621 return;
622 }
623
624 mediaFrame = wp.media({
625 title: '" . esc_js( __( 'Select Icon', 'frontblocks' ) ) . "',
626 button: { text: '" . esc_js( __( 'Use this image', 'frontblocks' ) ) . "' },
627 multiple: false
628 });
629
630 mediaFrame.on('select', function() {
631 const attachment = mediaFrame.state().get('selection').first().toJSON();
632 iconInput.value = attachment.url;
633 if (iconImg) { iconImg.src = attachment.url; }
634 if (iconPreview) { iconPreview.style.display = ''; }
635 if (removeBtn) { removeBtn.style.display = ''; }
636 });
637
638 mediaFrame.open();
639 });
640 }
641
642 if (removeBtn && iconInput) {
643 removeBtn.addEventListener('click', function(e) {
644 e.preventDefault();
645 iconInput.value = '';
646 if (iconImg) { iconImg.src = ''; }
647 if (iconPreview) { iconPreview.style.display = 'none'; }
648 removeBtn.style.display = 'none';
649 });
650 }
651 }
652
653 });
654 "
655 );
656
657 // Separate, isolated inline script for the maintenance mode fields: kept apart from the
658 // script above so that an error there can never prevent this one from running.
659 wp_add_inline_script(
660 'jquery',
661 "
662 document.addEventListener('DOMContentLoaded', function() {
663 const maintenanceCheckbox = document.getElementById('enable_maintenance');
664 const maintenanceWrapper = document.getElementById('maintenance-fields-wrapper');
665
666 if (maintenanceCheckbox && maintenanceWrapper) {
667 maintenanceCheckbox.addEventListener('change', function () {
668 maintenanceWrapper.style.display = maintenanceCheckbox.checked ? 'block' : 'none';
669 });
670 }
671
672 // Event delegation: works even if the button is added/replaced later, and does not
673 // depend on any other inline script having run successfully first.
674 let mediaFrame;
675
676 document.addEventListener('click', function (event) {
677 const selectButton = event.target.closest('.frbl-maintenance-select-image');
678 const removeButton = event.target.closest('.frbl-maintenance-remove-image');
679
680 if (! selectButton && ! removeButton) {
681 return;
682 }
683
684 event.preventDefault();
685
686 const imageInput = document.getElementById('maintenance_image');
687 const previewWrapper = document.querySelector('.frbl-maintenance-image-preview');
688 const previewImg = previewWrapper ? previewWrapper.querySelector('img') : null;
689
690 if (removeButton) {
691 if (imageInput) {
692 imageInput.value = '';
693 }
694 if (previewWrapper) {
695 previewWrapper.style.display = 'none';
696 }
697 removeButton.style.display = 'none';
698 return;
699 }
700
701 if (! window.wp || ! window.wp.media) {
702 window.alert('" . esc_js( __( 'The media library failed to load. Please reload the page and try again.', 'frontblocks' ) ) . "');
703 return;
704 }
705
706 if (mediaFrame) {
707 mediaFrame.open();
708 return;
709 }
710
711 mediaFrame = window.wp.media({
712 title: selectButton.textContent,
713 button: { text: selectButton.textContent },
714 multiple: false,
715 library: { type: 'image' },
716 });
717
718 mediaFrame.on('select', function () {
719 const attachment = mediaFrame.state().get('selection').first().toJSON();
720
721 if (imageInput) {
722 imageInput.value = attachment.id;
723 }
724 if (previewImg) {
725 previewImg.src = attachment.sizes && attachment.sizes.medium ? attachment.sizes.medium.url : attachment.url;
726 }
727 if (previewWrapper) {
728 previewWrapper.style.display = 'block';
729 }
730
731 const currentRemoveButton = document.querySelector('.frbl-maintenance-remove-image');
732 if (currentRemoveButton) {
733 currentRemoveButton.style.display = '';
734 }
735 });
736
737 mediaFrame.open();
738 });
739 });
740 "
741 );
742
743 // Enqueue the WordPress media uploader for the maintenance background image field.
744 wp_enqueue_media();
745
746 // Separate, isolated inline script for the cookie notice fields, kept apart so a
747 // failure in another script can never prevent this one from running.
748 wp_add_inline_script(
749 'jquery',
750 "
751 document.addEventListener('DOMContentLoaded', function() {
752 const cookieCheckbox = document.getElementById('enable_cookie_notice');
753 const cookieWrapper = document.getElementById('cookie-notice-fields-wrapper');
754
755 if (cookieCheckbox && cookieWrapper) {
756 cookieCheckbox.addEventListener('change', function () {
757 cookieWrapper.style.display = cookieCheckbox.checked ? 'block' : 'none';
758 });
759 }
760
761 const layoutSelect = document.getElementById('cookie_notice_layout');
762 const positionSelect = document.getElementById('cookie_notice_position');
763 const radiusSelect = document.getElementById('cookie_notice_radius');
764 const positionWrapper = document.getElementById('cookie-notice-position-wrapper');
765 const preview = document.getElementById('frbl-cookie-notice-preview');
766
767 if (layoutSelect && positionWrapper) {
768 layoutSelect.addEventListener('change', function () {
769 positionWrapper.style.display = layoutSelect.value === 'box' ? 'block' : 'none';
770 });
771 }
772
773 function updatePreviewLayout() {
774 if (!preview) {
775 return;
776 }
777
778 var layout = layoutSelect ? layoutSelect.value : 'bar';
779 var position = positionSelect ? positionSelect.value : 'bottom-right';
780
781 preview.className = 'frbl-cookie-notice frbl-cookie-notice-preview frbl-cookie-notice--' + layout;
782
783 if (layout === 'box') {
784 preview.className += ' frbl-cookie-notice--' + (position === 'bottom-left' ? 'left' : 'right');
785 }
786
787 if (radiusSelect) {
788 var radii = { none: '0', small: '12px', large: '24px' };
789 preview.style.setProperty('--frbl-cookie-radius', radii[radiusSelect.value] || radii.small);
790 }
791 }
792
793 if (layoutSelect) {
794 layoutSelect.addEventListener('change', updatePreviewLayout);
795 }
796
797 if (positionSelect) {
798 positionSelect.addEventListener('change', updatePreviewLayout);
799 }
800
801 if (radiusSelect) {
802 radiusSelect.addEventListener('change', updatePreviewLayout);
803 }
804 });
805 "
806 );
807
808 // Enqueue script for custom post types if PRO is active and license is valid.
809 if ( frbl_is_pro_active() && $this->is_license_valid ) {
810 wp_enqueue_script(
811 'frontblocks-cpt-admin',
812 FRBL_PLUGIN_URL . 'assets/admin/custom-post-types.js',
813 array( 'jquery' ),
814 FRBL_VERSION,
815 true
816 );
817
818 wp_localize_script(
819 'frontblocks-cpt-admin',
820 'frontblocksCpt',
821 array(
822 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
823 'nonce' => wp_create_nonce( 'frontblocks_create_cpt' ),
824 'i18n' => array(
825 'creating' => __( 'Creating...', 'frontblocks' ),
826 'error' => __( 'Error creating post type. Please try again.', 'frontblocks' ),
827 'success' => __( 'Post type created successfully!', 'frontblocks' ),
828 ),
829 )
830 );
831 }
832 }
833
834 /**
835 * Register options page as dedicated menu.
836 *
837 * @return void
838 */
839 public function register_menu() {
840 add_theme_page(
841 __( 'FrontBlocks Settings', 'frontblocks' ),
842 __( 'FrontBlocks', 'frontblocks' ),
843 'edit_theme_options',
844 $this->page_slug,
845 array( $this, 'render_page' )
846 );
847 }
848
849 /**
850 * Register settings, sections and fields.
851 *
852 * @return void
853 */
854 public function register_settings() {
855 register_setting(
856 'frontblocks_settings',
857 'frontblocks_settings',
858 array(
859 'type' => 'array',
860 'sanitize_callback' => array( $this, 'sanitize_settings' ),
861 'default' => array(),
862 'show_in_rest' => false,
863 )
864 );
865
866 // Register license setting group for FrontBlocks PRO.
867 global $frblp_license;
868 if ( $frblp_license && class_exists( '\Closemarketing\WPLicenseManager\License' ) ) {
869 // Register each individual license field.
870 register_setting(
871 'frontblocks-pro_license',
872 'frontblocks-pro_license_apikey',
873 array(
874 'type' => 'string',
875 'sanitize_callback' => 'sanitize_text_field',
876 )
877 );
878
879 register_setting(
880 'frontblocks-pro_license',
881 'frontblocks-pro_license_deactivate_checkbox',
882 array(
883 'type' => 'string',
884 'sanitize_callback' => 'sanitize_text_field',
885 )
886 );
887
888 // Hook into admin_init to process license activation/deactivation.
889 add_action(
890 'admin_init',
891 function () use ( $frblp_license ) {
892 // Check if license form was submitted and verify nonce.
893 if ( isset( $_POST['option_page'], $_POST['_wpnonce'] ) && 'frontblocks-pro_license' === $_POST['option_page'] && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), 'frontblocks-pro_license-options' ) ) {
894 if ( isset( $_POST['submit_license'] ) ) {
895 // Build input array for validate_license.
896 $input = array(
897 'frontblocks-pro_license_apikey' => isset( $_POST['frontblocks-pro_license_apikey'] ) ? sanitize_text_field( wp_unslash( $_POST['frontblocks-pro_license_apikey'] ) ) : '',
898 'frontblocks-pro_license_deactivate_checkbox' => isset( $_POST['frontblocks-pro_license_deactivate_checkbox'] ) ? sanitize_text_field( wp_unslash( $_POST['frontblocks-pro_license_deactivate_checkbox'] ) ) : '',
899 );
900
901 // Call the license validation.
902 $frblp_license->validate_license( $input );
903 }
904 }
905 },
906 15
907 );
908 }
909
910 // Always Active Blocks section.
911 add_settings_section(
912 'frontblocks_section_active_blocks',
913 __( 'Active Blocks & Features', 'frontblocks' ),
914 array( $this, 'section_active_blocks_callback' ),
915 $this->page_slug
916 );
917
918 add_settings_section(
919 'frontblocks_section_features',
920 __( 'Optional Features', 'frontblocks' ),
921 array( $this, 'section_features_callback' ),
922 $this->page_slug
923 );
924
925 add_settings_field(
926 $this->option_enable_testimonials,
927 __( 'Enable testimonials', 'frontblocks' ),
928 array( $this, 'field_enable_testimonials' ),
929 $this->page_slug,
930 'frontblocks_section_features'
931 );
932
933 add_settings_field(
934 $this->option_enable_reading_progress,
935 __( 'Enable reading progress bar', 'frontblocks' ),
936 array( $this, 'field_enable_reading_progress' ),
937 $this->page_slug,
938 'frontblocks_section_features'
939 );
940
941 add_settings_field(
942 $this->option_enable_back_button,
943 __( 'Enable Back Button', 'frontblocks' ),
944 array( $this, 'field_enable_back_button' ),
945 $this->page_slug,
946 'frontblocks_section_features'
947 );
948
949 add_settings_field(
950 $this->option_enable_scroll_top,
951 __( 'Enable Scroll to Top', 'frontblocks' ),
952 array( $this, 'field_enable_scroll_top' ),
953 $this->page_slug,
954 'frontblocks_section_features'
955 );
956
957 add_settings_field(
958 $this->option_enable_events,
959 __( 'Enable Events', 'frontblocks' ),
960 array( $this, 'field_enable_events' ),
961 $this->page_slug,
962 'frontblocks_section_features'
963 );
964
965 add_settings_field(
966 $this->option_enable_fluid_typography,
967 __( 'Enable Fluid Typography', 'frontblocks' ),
968 array( $this, 'field_enable_fluid_typography' ),
969 $this->page_slug,
970 'frontblocks_section_features'
971 );
972
973 // Maintenance Mode section (own full-width section, needs room for title + image fields).
974 add_settings_section(
975 'frontblocks_section_maintenance',
976 __( 'Maintenance Mode', 'frontblocks' ),
977 array( $this, 'section_maintenance_callback' ),
978 $this->page_slug
979 );
980
981 add_settings_field(
982 $this->option_enable_maintenance,
983 __( 'Enable Maintenance Mode', 'frontblocks' ),
984 array( $this, 'field_enable_maintenance' ),
985 $this->page_slug,
986 'frontblocks_section_maintenance'
987 );
988
989 // Cookie Notice section (own full-width section: banner copy, layout, colors, GTM/GA4 and stats).
990 add_settings_section(
991 'frontblocks_section_cookie_notice',
992 __( 'Cookie Notice', 'frontblocks' ),
993 array( $this, 'section_cookie_notice_callback' ),
994 $this->page_slug
995 );
996
997 add_settings_field(
998 $this->option_enable_cookie_notice,
999 __( 'Enable Cookie Notice', 'frontblocks' ),
1000 array( $this, 'field_enable_cookie_notice' ),
1001 $this->page_slug,
1002 'frontblocks_section_cookie_notice'
1003 );
1004
1005 // PRO Features section.
1006 add_settings_section(
1007 'frontblocks_section_woocommerce_features',
1008 __( 'WooCommerce Features', 'frontblocks' ),
1009 array( $this, 'section_woo_features_callback' ),
1010 $this->page_slug
1011 );
1012
1013 add_settings_field(
1014 $this->option_enable_gutenberg,
1015 __( 'Enable Gutenberg in Products', 'frontblocks' ),
1016 array( $this, 'field_enable_gutenberg' ),
1017 $this->page_slug,
1018 'frontblocks_section_woocommerce_features'
1019 );
1020
1021 add_settings_field(
1022 $this->option_enable_simple_prices_variable_products,
1023 __( 'Enable Simple Prices Variable Products', 'frontblocks' ),
1024 array( $this, 'field_enable_simple_prices_variable_products' ),
1025 $this->page_slug,
1026 'frontblocks_section_woocommerce_features'
1027 );
1028
1029 add_settings_field(
1030 $this->option_enable_after_add_to_cart,
1031 __( 'Enable After Add to Cart Block', 'frontblocks' ),
1032 array( $this, 'field_enable_after_add_to_cart' ),
1033 $this->page_slug,
1034 'frontblocks_section_woocommerce_features'
1035 );
1036
1037 add_settings_field(
1038 $this->option_deactivate_short_description,
1039 __( 'Deactivate Short Description', 'frontblocks' ),
1040 array( $this, 'field_deactivate_short_description' ),
1041 $this->page_slug,
1042 'frontblocks_section_woocommerce_features'
1043 );
1044
1045 add_settings_field(
1046 $this->option_move_content_to_short_description,
1047 __( 'Move Content to Short Description', 'frontblocks' ),
1048 array( $this, 'field_move_content_to_short_description' ),
1049 $this->page_slug,
1050 'frontblocks_section_woocommerce_features'
1051 );
1052
1053 add_settings_field(
1054 $this->option_disable_zoom_images,
1055 __( 'Disable Zoom in Product Images', 'frontblocks' ),
1056 array( $this, 'field_disable_zoom_images' ),
1057 $this->page_slug,
1058 'frontblocks_section_woocommerce_features'
1059 );
1060
1061 add_settings_field(
1062 $this->option_add_share_buttons,
1063 __( 'Add Share Buttons in Product Page', 'frontblocks' ),
1064 array( $this, 'field_add_share_buttons' ),
1065 $this->page_slug,
1066 'frontblocks_section_woocommerce_features'
1067 );
1068
1069 add_settings_field(
1070 $this->option_deactivate_product_tabs,
1071 __( 'Deactivate Product Tabs', 'frontblocks' ),
1072 array( $this, 'field_deactivate_product_tabs' ),
1073 $this->page_slug,
1074 'frontblocks_section_woocommerce_features'
1075 );
1076
1077 add_settings_field(
1078 $this->option_horizontal_product_form,
1079 __( 'Horizontal Product Form Layout', 'frontblocks' ),
1080 array( $this, 'field_horizontal_product_form' ),
1081 $this->page_slug,
1082 'frontblocks_section_woocommerce_features'
1083 );
1084
1085 add_settings_field(
1086 $this->option_enable_variant_display_mode,
1087 __( 'Variant Display Mode', 'frontblocks' ),
1088 array( $this, 'field_enable_variant_display_mode' ),
1089 $this->page_slug,
1090 'frontblocks_section_woocommerce_features'
1091 );
1092
1093 add_settings_field(
1094 $this->option_disable_cart_coupon,
1095 __( 'Disable Coupon Field in Cart', 'frontblocks' ),
1096 array( $this, 'field_disable_cart_coupon' ),
1097 $this->page_slug,
1098 'frontblocks_section_woocommerce_features'
1099 );
1100
1101 add_settings_field(
1102 $this->option_disable_cart_cross_sells,
1103 __( 'Disable Cross-Sells in Cart', 'frontblocks' ),
1104 array( $this, 'field_disable_cart_cross_sells' ),
1105 $this->page_slug,
1106 'frontblocks_section_woocommerce_features'
1107 );
1108
1109 add_settings_field(
1110 $this->option_disable_checkout_coupon,
1111 __( 'Disable Coupon Field in Checkout', 'frontblocks' ),
1112 array( $this, 'field_disable_checkout_coupon' ),
1113 $this->page_slug,
1114 'frontblocks_section_woocommerce_features'
1115 );
1116
1117 add_settings_field(
1118 $this->option_disable_checkout_order_notes,
1119 __( 'Disable Order Notes in Checkout', 'frontblocks' ),
1120 array( $this, 'field_disable_checkout_order_notes' ),
1121 $this->page_slug,
1122 'frontblocks_section_woocommerce_features'
1123 );
1124
1125 add_settings_field(
1126 $this->option_disable_checkout_login_prompt,
1127 __( 'Disable Login Prompt in Checkout', 'frontblocks' ),
1128 array( $this, 'field_disable_checkout_login_prompt' ),
1129 $this->page_slug,
1130 'frontblocks_section_woocommerce_features'
1131 );
1132
1133 add_settings_field(
1134 $this->option_enable_fullpage_scroll,
1135 __( 'Enable Full Page Scroll', 'frontblocks' ),
1136 array( $this, 'field_enable_fullpage_scroll' ),
1137 $this->page_slug,
1138 'frontblocks_section_woocommerce_features'
1139 );
1140
1141 add_settings_field(
1142 $this->option_enable_language_banner,
1143 __( 'Enable Language Banner', 'frontblocks' ),
1144 array( $this, 'field_enable_language_banner' ),
1145 $this->page_slug,
1146 'frontblocks_section_woocommerce_features'
1147 );
1148
1149 add_settings_field(
1150 $this->option_enable_popups,
1151 __( 'Enable Popups', 'frontblocks' ),
1152 array( $this, 'field_enable_popups' ),
1153 $this->page_slug,
1154 'frontblocks_section_woocommerce_features'
1155 );
1156
1157 add_settings_field(
1158 $this->option_checkout_inline,
1159 __( 'Checkout Inline Fields', 'frontblocks' ),
1160 array( $this, 'field_checkout_inline' ),
1161 $this->page_slug,
1162 'frontblocks_section_woocommerce_features'
1163 );
1164
1165 // Custom Post Types section (PRO).
1166 if ( frbl_is_pro_active() ) {
1167 add_settings_section(
1168 'frontblocks_section_custom_post_types',
1169 __( 'Custom Post Types', 'frontblocks' ),
1170 array( $this, 'section_custom_post_types_callback' ),
1171 $this->page_slug
1172 );
1173
1174 add_settings_field(
1175 $this->option_enable_custom_post_types,
1176 __( 'Enable Custom Post Types Builder', 'frontblocks' ),
1177 array( $this, 'field_enable_custom_post_types' ),
1178 $this->page_slug,
1179 'frontblocks_section_custom_post_types'
1180 );
1181 }
1182
1183 // Note: License section is rendered separately outside the main form.
1184 // See render_license_section() method called from render_page().
1185
1186 do_action( 'frontblocks_register_settings' );
1187 }
1188
1189 /**
1190 * Render settings page.
1191 *
1192 * @return void
1193 */
1194 public function render_page() {
1195 if ( ! current_user_can( 'edit_theme_options' ) ) {
1196 return;
1197 }
1198 ?>
1199 <div class="frbl-settings-wrapper tw:min-h-screen tw:bg-gray-50 tw:py-8">
1200 <div class="tw:max-w-5xl tw:mx-auto tw:px-4 tw:sm:px-6 tw:lg:px-8">
1201 <!-- Header Section -->
1202 <div class="tw:mb-8 frbl-animate-slide-in">
1203 <div class="tw:flex tw:items-center tw:justify-between">
1204 <div>
1205 <h1 class="tw:text-3xl tw:font-bold tw:text-gray-900 tw:mb-2">
1206 <?php echo esc_html__( 'FrontBlocks Settings', 'frontblocks' ); ?>
1207 </h1>
1208 <p class="tw:text-gray-600">
1209 <?php echo esc_html__( 'Add visual enhancements to your website with FrontBlocks.', 'frontblocks' ); ?>
1210 </p>
1211 </div>
1212 <div class="tw:flex tw:items-center tw:space-x-2">
1213 <span class="tw:inline-flex tw:items-center tw:px-3 tw:py-1 tw:rounded-full tw:text-sm tw:font-medium tw:bg-primary-100 tw:text-primary-700">
1214 <?php echo esc_html__( 'Version', 'frontblocks' ) . ' ' . esc_html( FRBL_VERSION ); ?>
1215 </span>
1216 </div>
1217 </div>
1218 </div>
1219
1220 <?php
1221 // Show success message after settings are saved.
1222 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1223 if ( isset( $_GET['settings-updated'] ) && 'true' === sanitize_text_field( wp_unslash( $_GET['settings-updated'] ) ) ) :
1224 ?>
1225 <div style="background-color: #f0fdf4; border-left: 4px solid #4ade80; border-radius: 0.5rem; padding: 1rem; margin-bottom: 1.5rem; box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);">
1226 <div class="tw:flex">
1227 <div class="tw:flex-shrink-0">
1228 <svg class="tw:h-5 tw:w-5" style="color: #4ade80;" viewBox="0 0 20 20" fill="currentColor">
1229 <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
1230 </svg>
1231 </div>
1232 <div class="tw:ml-3">
1233 <p class="tw:text-sm tw:font-medium" style="color: #15803d; margin: 0;">
1234 <?php esc_html_e( 'Changes saved successfully', 'frontblocks' ); ?>
1235 </p>
1236 </div>
1237 </div>
1238 </div>
1239 <?php
1240 endif;
1241
1242 // Show error message if saving failed.
1243 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1244 if ( isset( $_GET['settings-error'] ) && 'true' === sanitize_text_field( wp_unslash( $_GET['settings-error'] ) ) ) :
1245 ?>
1246 <div style="background-color: #fef2f2; border-left: 4px solid #f87171; border-radius: 0.5rem; padding: 1rem; margin-bottom: 1.5rem; box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);">
1247 <div class="tw:flex">
1248 <div class="tw:flex-shrink-0">
1249 <svg class="tw:h-5 tw:w-5" style="color: #f87171;" viewBox="0 0 20 20" fill="currentColor">
1250 <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
1251 </svg>
1252 </div>
1253 <div class="tw:ml-3">
1254 <p class="tw:text-sm tw:font-medium" style="color: #991b1b; margin: 0;">
1255 <?php esc_html_e( 'Failed to save changes. Please try again.', 'frontblocks' ); ?>
1256 </p>
1257 </div>
1258 </div>
1259 </div>
1260 <?php
1261 endif;
1262
1263 // Show license activated message.
1264 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1265 if ( isset( $_GET['license_activated'] ) && '1' === sanitize_text_field( wp_unslash( $_GET['license_activated'] ) ) ) :
1266 ?>
1267 <div style="background-color: #f0fdf4; border-left: 4px solid #4ade80; border-radius: 0.5rem; padding: 1rem; margin-bottom: 1.5rem; box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);">
1268 <div class="tw:flex">
1269 <div class="tw:flex-shrink-0">
1270 <svg class="tw:h-5 tw:w-5" style="color: #4ade80;" viewBox="0 0 20 20" fill="currentColor">
1271 <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
1272 </svg>
1273 </div>
1274 <div class="tw:ml-3">
1275 <p class="tw:text-sm tw:font-medium" style="color: #15803d; margin: 0;">
1276 <?php esc_html_e( 'License activated successfully!', 'frontblocks' ); ?>
1277 </p>
1278 </div>
1279 </div>
1280 </div>
1281 <?php
1282 endif;
1283
1284 // Show license deactivated message.
1285 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1286 if ( isset( $_GET['license_deactivated'] ) && '1' === sanitize_text_field( wp_unslash( $_GET['license_deactivated'] ) ) ) :
1287 ?>
1288 <div style="background-color: #fffbeb; border-left: 4px solid #fbbf24; border-radius: 0.5rem; padding: 1rem; margin-bottom: 1.5rem; box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);">
1289 <div class="tw:flex">
1290 <div class="tw:flex-shrink-0">
1291 <svg class="tw:h-5 tw:w-5" style="color: #fbbf24;" viewBox="0 0 20 20" fill="currentColor">
1292 <path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
1293 </svg>
1294 </div>
1295 <div class="tw:ml-3">
1296 <p class="tw:text-sm tw:font-medium" style="color: #92400e; margin: 0;">
1297 <?php esc_html_e( 'License deactivated successfully.', 'frontblocks' ); ?>
1298 </p>
1299 </div>
1300 </div>
1301 </div>
1302 <?php
1303 endif;
1304
1305 // Show license error message.
1306 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1307 if ( isset( $_GET['license_error'] ) && '1' === sanitize_text_field( wp_unslash( $_GET['license_error'] ) ) ) :
1308 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1309 $error_msg = isset( $_GET['error_msg'] ) ? sanitize_text_field( wp_unslash( $_GET['error_msg'] ) ) : '';
1310 ?>
1311 <div style="background-color: #fef2f2; border-left: 4px solid #f87171; border-radius: 0.5rem; padding: 1rem; margin-bottom: 1.5rem; box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);">
1312 <div class="tw:flex">
1313 <div class="tw:flex-shrink-0">
1314 <svg class="tw:h-5 tw:w-5" style="color: #f87171;" viewBox="0 0 20 20" fill="currentColor">
1315 <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
1316 </svg>
1317 </div>
1318 <div class="tw:ml-3">
1319 <p class="tw:text-sm tw:font-medium" style="color: #991b1b; margin: 0;">
1320 <?php
1321 if ( ! empty( $error_msg ) ) {
1322 echo esc_html__( 'Failed to activate license: ', 'frontblocks' ) . '<br><strong>' . esc_html( $error_msg ) . '</strong>';
1323 } else {
1324 esc_html_e( 'Failed to activate license. Please check your license key and try again.', 'frontblocks' );
1325 }
1326 ?>
1327 </p>
1328 </div>
1329 </div>
1330 </div>
1331 <?php
1332 endif;
1333
1334 $this->render_cookie_notice_cache_notice();
1335 ?>
1336
1337 <!-- Settings Form -->
1338 <form method="post" action="options.php" class="tw:space-y-6">
1339 <?php settings_fields( 'frontblocks_settings' ); ?>
1340
1341 <?php
1342 // Get all sections for this page.
1343 global $wp_settings_sections, $wp_settings_fields;
1344
1345 if ( ! isset( $wp_settings_sections[ $this->page_slug ] ) ) {
1346 return;
1347 }
1348
1349 foreach ( (array) $wp_settings_sections[ $this->page_slug ] as $section ) {
1350 $this->render_settings_section( $section );
1351 }
1352 ?>
1353
1354 <!-- Submit Button -->
1355 <div class="frbl-settings-save-bar tw:flex tw:items-center tw:justify-between">
1356 <div class="tw:text-sm tw:text-gray-500">
1357 <?php echo esc_html__( 'Changes will be applied immediately after saving.', 'frontblocks' ); ?>
1358 </div>
1359 <button type="submit" class="tw:inline-flex tw:items-center tw:px-4 tw:py-3 tw:border tw:border-transparent tw:text-base tw:font-medium tw:rounded-lg tw:shadow-sm tw:text-white tw:bg-primary-500 tw:hover:bg-primary-600 tw:focus:outline-none tw:focus:ring-2 tw:focus:ring-offset-2 tw:focus:ring-primary-500 tw:transition-colors tw:duration-200">
1360 <svg class="tw:w-5 tw:h-5 tw:mr-2 tw:-ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1361 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
1362 </svg>
1363 <?php echo esc_html__( 'Save Settings', 'frontblocks' ); ?>
1364 </button>
1365 </div>
1366 </form>
1367
1368 <?php
1369 // Render license section separately (outside main form) if PRO is active.
1370 if ( frbl_is_pro_active() ) {
1371 $this->render_license_section();
1372 }
1373 ?>
1374
1375 <!-- Footer Info -->
1376 <div class="tw:mt-8 tw:text-center tw:text-sm tw:text-gray-500">
1377 <?php
1378 printf(
1379 /* translators: %s: Close·marketing link */
1380 esc_html__( 'Made with ❤️ by %s', 'frontblocks' ),
1381 '<a href="https://close.technology/?utm_source=frontblocks&utm_medium=plugin&utm_campaign=settings" target="_blank" rel="noopener noreferrer" class="tw:text-primary-500 tw:hover:text-primary-600 tw:font-medium">Close·Technology</a>'
1382 );
1383 ?>
1384 </div>
1385
1386 <?php $this->render_debug_section(); ?>
1387 </div>
1388 </div>
1389 <?php
1390 }
1391
1392 /**
1393 * Render debug section for Fluid Typography.
1394 *
1395 * @return void
1396 */
1397 private function render_debug_section() {
1398 // Only show if Fluid Typography is enabled and user requested debug.
1399 $options = get_option( 'frontblocks_settings', array() );
1400 $enabled = ! empty( $options['enable_fluid_typography'] );
1401
1402 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1403 if ( ! $enabled || ! isset( $_GET['frbl_debug_typography'] ) ) {
1404 return;
1405 }
1406
1407 // Get GeneratePress settings.
1408 $gp_settings = get_option( 'generate_settings', array() );
1409
1410 // Filter only font-related settings.
1411 $font_settings = array();
1412 foreach ( $gp_settings as $key => $value ) {
1413 if ( strpos( $key, 'font' ) !== false || strpos( $key, 'heading' ) !== false ) {
1414 $font_settings[ $key ] = $value;
1415 }
1416 }
1417
1418 ?>
1419 <div class="tw:mt-8 tw:p-6 tw:bg-yellow-50 tw:border tw:border-yellow-200 tw:rounded-lg">
1420 <h3 class="tw:text-lg tw:font-semibold tw:text-gray-900 tw:mb-4">
1421 🐛 Debug: Fluid Typography Settings
1422 </h3>
1423 <p class="tw:text-sm tw:text-gray-600 tw:mb-4">
1424 <?php echo esc_html__( 'This shows the GeneratePress font settings being used by the Fluid Typography module.', 'frontblocks' ); ?>
1425 </p>
1426 <div class="tw:bg-white tw:p-4 tw:rounded tw:border tw:border-gray-300 tw:overflow-auto" style="max-height: 400px;">
1427 <pre style="margin: 0; font-size: 12px;"><?php print_r( $font_settings ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r ?></pre>
1428 </div>
1429 <p class="tw:text-xs tw:text-gray-500 tw:mt-4">
1430 <?php
1431 printf(
1432 /* translators: %s: URL parameter */
1433 esc_html__( 'To hide this debug info, remove %s from the URL.', 'frontblocks' ),
1434 '<code>?frbl_debug_typography=1</code>'
1435 );
1436 ?>
1437 </p>
1438 </div>
1439 <?php
1440 }
1441
1442 /**
1443 * Active Blocks section callback.
1444 *
1445 * @return void
1446 */
1447 private function section_active_blocks_callback() {
1448 $active_blocks = apply_filters(
1449 'frbl_active_blocks',
1450 array(
1451 array(
1452 'icon' => 'animations',
1453 'title' => __( 'Animations', 'frontblocks' ),
1454 'desc' => __( 'Add animations to any block using Animate.css', 'frontblocks' ),
1455 ),
1456 array(
1457 'icon' => 'carousel',
1458 'title' => __( 'Carousel/Slider', 'frontblocks' ),
1459 'desc' => __( 'Transform any Grid block into a carousel or slider', 'frontblocks' ),
1460 ),
1461 array(
1462 'icon' => 'gallery',
1463 'title' => __( 'Native Gallery', 'frontblocks' ),
1464 'desc' => __( 'Enhanced gallery block with carousel and masonry options', 'frontblocks' ),
1465 ),
1466 array(
1467 'icon' => 'sticky',
1468 'title' => __( 'Sticky Columns', 'frontblocks' ),
1469 'desc' => __( 'Make Grid blocks sticky when scrolling', 'frontblocks' ),
1470 ),
1471 array(
1472 'icon' => 'insert_post',
1473 'title' => __( 'Insert Post Block', 'frontblocks' ),
1474 'desc' => __( 'Display content from other posts, pages or custom post types', 'frontblocks' ),
1475 ),
1476 array(
1477 'icon' => 'counter',
1478 'title' => __( 'Counter Block', 'frontblocks' ),
1479 'desc' => __( 'Display animated counters with start and end values', 'frontblocks' ),
1480 ),
1481 array(
1482 'icon' => 'reading_time',
1483 'title' => __( 'Reading Time Block', 'frontblocks' ),
1484 'desc' => __( 'Show estimated reading time for posts', 'frontblocks' ),
1485 ),
1486 array(
1487 'icon' => 'stacked_images',
1488 'title' => __( 'Stacked Images Block', 'frontblocks' ),
1489 'desc' => __( 'Display images with animated stacking effect from different directions', 'frontblocks' ),
1490 ),
1491 array(
1492 'icon' => 'product_categories',
1493 'title' => __( 'Product Categories Block', 'frontblocks' ),
1494 'desc' => __( 'Display WooCommerce product categories', 'frontblocks' ),
1495 ),
1496 array(
1497 'icon' => 'headline_marquee',
1498 'title' => __( 'Headline Marquee', 'frontblocks' ),
1499 'desc' => __( 'Infinite scrolling marquee effect for headline/text blocks with customizable speed', 'frontblocks' ),
1500 ),
1501 array(
1502 'icon' => 'svg_upload',
1503 'title' => __( 'SVG Uploads', 'frontblocks' ),
1504 'desc' => __( 'Upload SVG files to the media library. Files are automatically sanitized to prevent security risks.', 'frontblocks' ),
1505 ),
1506 )
1507 );
1508
1509 $pro_blocks = apply_filters( 'frbl_pro_blocks', $this->get_default_pro_blocks() );
1510
1511 ?>
1512 <p class="tw:text-sm tw:text-gray-600 tw:mt-0 tw:mb-4">
1513 <?php echo esc_html__( 'These blocks and features are always active and available in the block editor.', 'frontblocks' ); ?>
1514 </p>
1515 <div class="frbl-features-grid">
1516 <?php
1517 foreach ( $active_blocks as $block ) {
1518 UI::show_info_card( $block['icon'], $block['title'], $block['desc'] );
1519 }
1520
1521 foreach ( $pro_blocks as $block ) {
1522 UI::show_pro_info_card( $block['icon'], $block['title'], $block['desc'] );
1523 }
1524 ?>
1525 </div>
1526 <?php
1527 }
1528
1529 /**
1530 * Features section callback.
1531 *
1532 * @return void
1533 */
1534 private function section_features_callback() {
1535 $license_valid = function_exists( 'frblp_is_license_valid' ) && frblp_is_license_valid();
1536 ?>
1537 <p class="tw:text-sm tw:text-gray-600 tw:mt-0 tw:mb-4">
1538 <?php echo esc_html__( 'Enable or disable these optional features as needed.', 'frontblocks' ); ?>
1539 </p>
1540 <?php
1541 }
1542
1543 /**
1544 * Maintenance mode section callback.
1545 *
1546 * @return void
1547 */
1548 private function section_maintenance_callback() {
1549 ?>
1550 <p class="tw:text-sm tw:text-gray-600 tw:mt-0 tw:mb-4">
1551 <?php echo esc_html__( 'Show a full-screen curtain page on every URL of the site while you work on it.', 'frontblocks' ); ?>
1552 </p>
1553 <?php
1554 }
1555
1556 /**
1557 * Cookie Notice section callback.
1558 *
1559 * @return void
1560 */
1561 private function section_cookie_notice_callback() {
1562 ?>
1563 <p class="tw:text-sm tw:text-gray-600 tw:mt-0 tw:mb-4">
1564 <?php echo esc_html__( 'Show a cookie consent banner and only load Google Tag Manager / GA4 after a visitor accepts.', 'frontblocks' ); ?>
1565 </p>
1566 <?php
1567 $this->render_advanced_cookies_upsell();
1568 }
1569
1570 /**
1571 * Show what FrontBlocks PRO's Advanced Cookie Management adds on top of
1572 * this banner. The feature's own settings (and the fields to configure
1573 * it) live entirely in the PRO plugin — this is only a plain message,
1574 * shown when PRO isn't active/licensed yet.
1575 *
1576 * @return void
1577 */
1578 private function render_advanced_cookies_upsell() {
1579 if ( frbl_is_pro_active() && $this->is_license_valid ) {
1580 // Already unlocked: the PRO plugin renders its own settings section
1581 // right below this one.
1582 return;
1583 }
1584
1585 if ( ! frbl_is_pro_active() ) {
1586 echo '<div class="tw:bg-blue-50 tw:border-l-4 tw:border-blue-400 tw:p-4 tw:mb-4">';
1587 echo '<p class="tw:text-sm tw:text-blue-700 tw:font-medium tw:mb-1">' . esc_html__( 'Want per-category consent?', 'frontblocks' ) . '</p>';
1588 echo '<p class="tw:text-sm tw:text-blue-700">';
1589 printf(
1590 /* translators: %s: FrontBlocks PRO link */
1591 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' ),
1592 '<a href="https://close.technology/wordpress-plugins/frontblocks-pro/?utm_source=frontblocks&utm_medium=plugin&utm_campaign=settings-cookie-notice" target="_blank" rel="noopener noreferrer" class="tw:font-medium tw:underline">FrontBlocks PRO</a>'
1593 );
1594 echo '</p>';
1595 echo '</div>';
1596 } else {
1597 echo '<div class="tw:bg-yellow-50 tw:border-l-4 tw:border-yellow-400 tw:p-4 tw:mb-4">';
1598 echo '<p class="tw:text-sm tw:text-yellow-700">';
1599 printf(
1600 /* translators: %s: License section link */
1601 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' ),
1602 '<a href="#frontblocks_section_license" class="tw:font-medium tw:underline">' . esc_html__( 'License', 'frontblocks' ) . '</a>'
1603 );
1604 echo '</p>';
1605 echo '</div>';
1606 }
1607 }
1608
1609 /**
1610 * Render a single settings section as a card.
1611 *
1612 * @param array $section Section data.
1613 * @return void
1614 */
1615 private function render_settings_section( $section ) {
1616 global $wp_settings_fields;
1617
1618 $has_fields = isset( $wp_settings_fields[ $this->page_slug ][ $section['id'] ] );
1619
1620 // Si no hay campos Y no hay callback, no renderizar nada.
1621 if ( ! $has_fields && ! $section['callback'] ) {
1622 return;
1623 }
1624
1625 // Check if this is a section with callback only (like active_blocks).
1626 $is_callback_only = ! $has_fields && $section['callback'];
1627
1628 // Check if this is a section that needs full width (rich fields, not a simple toggle grid).
1629 $is_cpt_section = in_array( $section['id'], array( 'frontblocks_section_custom_post_types', 'frontblocks_section_maintenance', 'frontblocks_section_cookie_notice' ), true );
1630
1631 // Show PRO CTA button before the Optional Features section.
1632 if ( 'frontblocks_section_features' === $section['id'] && ! $this->is_license_valid ) {
1633 ?>
1634 <div class="tw:mb-4">
1635 <a
1636 href="https://close.technology/wordpress-plugins/frontblocks-pro/?utm_source=frontblocks&utm_medium=plugin&utm_campaign=settings-optional-cta"
1637 target="_blank"
1638 rel="noopener noreferrer"
1639 class="tw:inline-flex tw:items-center tw:border tw:border-transparent tw:text-sm tw:font-medium tw:rounded-lg tw:shadow-sm tw:transition-colors tw:duration-200"
1640 style="background-color: #ef4444; color: #fff; padding: 10px 20px;"
1641 onmouseover="this.style.backgroundColor='#dc2626'"
1642 onmouseout="this.style.backgroundColor='#ef4444'"
1643 >
1644 <?php echo esc_html__( 'Get FrontBlocks PRO', 'frontblocks' ); ?>
1645 </a>
1646 </div>
1647 <?php
1648 }
1649
1650 if ( $is_callback_only ) {
1651 // Render section with only callback (no fields).
1652 ?>
1653 <div class="frbl-section-wrapper">
1654 <div class="frbl-section-header">
1655 <h2 class="tw:text-2xl tw:font-bold tw:text-gray-900 tw:mb-0">
1656 <?php echo esc_html( $section['title'] ); ?>
1657 </h2>
1658 </div>
1659 <?php call_user_func( $section['callback'], $section ); ?>
1660 </div>
1661 <?php
1662 return;
1663 }
1664
1665 if ( $is_cpt_section ) {
1666 // Render CPT section as a full-width card.
1667 ?>
1668 <div class="frbl-card tw:bg-white tw:rounded-lg tw:shadow-sm tw:border tw:border-gray-200 tw:overflow-hidden frbl-animate-slide-in tw:mb-8">
1669 <div class="tw:px-6 tw:py-5 tw:border-b tw:border-gray-200 tw:bg-gradient-to-r tw:from-gray-50 tw:to-white">
1670 <h2 class="tw:text-xl tw:font-semibold tw:text-gray-900">
1671 <?php echo esc_html( $section['title'] ); ?>
1672 </h2>
1673 <?php
1674 if ( $section['callback'] ) {
1675 echo '<div class="tw:mt-2 tw:text-sm tw:text-gray-600">';
1676 call_user_func( $section['callback'], $section );
1677 echo '</div>';
1678 }
1679 ?>
1680 </div>
1681 <div class="tw:px-6 tw:py-5">
1682 <?php
1683 foreach ( (array) $wp_settings_fields[ $this->page_slug ][ $section['id'] ] as $field ) {
1684 call_user_func( $field['callback'], $field['args'] );
1685 }
1686 ?>
1687 </div>
1688 </div>
1689 <?php
1690 } else {
1691 // Render regular sections with feature grid.
1692 ?>
1693 <div class="frbl-section-wrapper">
1694 <!-- Section Header -->
1695 <div class="frbl-section-header">
1696 <h2 class="tw:text-2xl tw:font-bold tw:text-gray-900 tw:mb-0">
1697 <?php echo esc_html( $section['title'] ); ?>
1698 </h2>
1699 <?php
1700 if ( $section['callback'] ) {
1701 echo '<div class="tw:text-sm tw:text-gray-600">';
1702 call_user_func( $section['callback'], $section );
1703 echo '</div>';
1704 }
1705 ?>
1706 </div>
1707
1708 <!-- Features Grid -->
1709 <div class="frbl-features-grid">
1710 <?php
1711 foreach ( (array) $wp_settings_fields[ $this->page_slug ][ $section['id'] ] as $field ) {
1712 $this->render_settings_field( $field );
1713 }
1714 ?>
1715 </div>
1716 </div>
1717 <?php
1718 }
1719 }
1720
1721 /**
1722 * Render a single settings field as a card.
1723 *
1724 * @param array $field Field data.
1725 * @return void
1726 */
1727 private function render_settings_field( $field ) {
1728 // Determine if this is a PRO feature (always, regardless of license status).
1729 $is_pro_feature = in_array(
1730 $field['id'],
1731 array(
1732 $this->option_enable_gutenberg,
1733 $this->option_enable_simple_prices_variable_products,
1734 $this->option_enable_after_add_to_cart,
1735 $this->option_deactivate_short_description,
1736 $this->option_move_content_to_short_description,
1737 $this->option_disable_zoom_images,
1738 $this->option_add_share_buttons,
1739 $this->option_deactivate_product_tabs,
1740 $this->option_horizontal_product_form,
1741 $this->option_enable_variant_display_mode,
1742 $this->option_enable_fullpage_scroll,
1743 $this->option_enable_language_banner,
1744 $this->option_enable_popups,
1745 $this->option_checkout_inline,
1746 $this->option_disable_cart_coupon,
1747 $this->option_disable_cart_cross_sells,
1748 $this->option_disable_checkout_coupon,
1749 $this->option_disable_checkout_order_notes,
1750 $this->option_disable_checkout_login_prompt,
1751 ),
1752 true
1753 );
1754
1755 // Apply PRO styling only if license is not valid.
1756 $needs_license = $is_pro_feature && ! $this->is_license_valid;
1757
1758 // Get icon for this feature.
1759 $icon = $this->get_feature_icon( $field['id'] );
1760
1761 // Short descriptions for each field.
1762 $descriptions = array(
1763 $this->option_enable_testimonials => __( 'Add a testimonials block to display customer reviews.', 'frontblocks' ),
1764 $this->option_enable_reading_progress => __( 'Show a progress bar at the top of the page while reading posts.', 'frontblocks' ),
1765 $this->option_enable_back_button => __( 'Add a floating back button for easy navigation.', 'frontblocks' ),
1766 $this->option_enable_scroll_top => __( 'Add a floating button to scroll back to the top of the page.', 'frontblocks' ),
1767 $this->option_enable_events => __( 'Register and display events using a CPT or blog posts.', 'frontblocks' ),
1768 $this->option_enable_fluid_typography => __( 'Font sizes scale smoothly between mobile and desktop using CSS clamp().', 'frontblocks' ),
1769 $this->option_enable_popups => __( 'Create popups with the block editor and configure when and where they appear.', 'frontblocks' ),
1770 $this->option_enable_gutenberg => __( 'Use the block editor to write WooCommerce product descriptions.', 'frontblocks' ),
1771 $this->option_enable_simple_prices_variable_products => __( 'Show a simplified price range for variable products.', 'frontblocks' ),
1772 $this->option_enable_after_add_to_cart => __( 'Insert custom block content right after the Add to Cart button.', 'frontblocks' ),
1773 $this->option_deactivate_short_description => __( 'Remove the short description field from product pages.', 'frontblocks' ),
1774 $this->option_move_content_to_short_description => __( 'Move the main product content into the short description area.', 'frontblocks' ),
1775 $this->option_disable_zoom_images => __( 'Remove the zoom effect on WooCommerce product images.', 'frontblocks' ),
1776 $this->option_add_share_buttons => __( 'Add social share buttons to WooCommerce product pages.', 'frontblocks' ),
1777 $this->option_deactivate_product_tabs => __( 'Remove the default description, reviews and attributes tabs.', 'frontblocks' ),
1778 $this->option_horizontal_product_form => __( 'Display quantity and Add to Cart button side by side.', 'frontblocks' ),
1779 $this->option_enable_variant_display_mode => __( 'Choose per attribute whether to display product variations as a dropdown or radio buttons.', 'frontblocks' ),
1780 $this->option_enable_fullpage_scroll => __( 'Enable full-page scroll navigation between sections.', 'frontblocks' ),
1781 $this->option_enable_language_banner => __( 'Show a banner when the visitor language differs from the site language.', 'frontblocks' ),
1782 $this->option_checkout_inline => __( 'Display address, email and phone fields side by side in the WooCommerce checkout.', 'frontblocks' ),
1783 $this->option_disable_zoom_images => __( 'Remove the zoom effect on WooCommerce product images.', 'frontblocks' ),
1784 $this->option_add_share_buttons => __( 'Add social share buttons to WooCommerce product pages.', 'frontblocks' ),
1785 $this->option_deactivate_product_tabs => __( 'Remove the default description, reviews and attributes tabs.', 'frontblocks' ),
1786 $this->option_horizontal_product_form => __( 'Display quantity and Add to Cart button side by side.', 'frontblocks' ),
1787 $this->option_enable_fullpage_scroll => __( 'Enable full-page scroll navigation between sections.', 'frontblocks' ),
1788 $this->option_enable_language_banner => __( 'Show a banner when the visitor language differs from the site language.', 'frontblocks' ),
1789 $this->option_disable_cart_coupon => __( 'Hide the coupon code input field from the Cart page.', 'frontblocks' ),
1790 $this->option_disable_cart_cross_sells => __( 'Remove the cross-sell product suggestions from the Cart page.', 'frontblocks' ),
1791 $this->option_disable_checkout_coupon => __( 'Hide the coupon code form shown above the Checkout form.', 'frontblocks' ),
1792 $this->option_disable_checkout_order_notes => __( 'Remove the order notes / additional information field from Checkout.', 'frontblocks' ),
1793 $this->option_disable_checkout_login_prompt => __( 'Hide the login and registration prompt shown before the Checkout form.', 'frontblocks' ),
1794 );
1795
1796 $desc = $descriptions[ $field['id'] ] ?? '';
1797
1798 ?>
1799 <div class="frbl-feature-card <?php echo $needs_license ? 'frbl-feature-pro' : ''; ?>">
1800 <?php if ( $is_pro_feature ) : ?>
1801 <div class="frbl-pro-badge">PRO</div>
1802 <?php endif; ?>
1803
1804 <div class="frbl-feature-content">
1805 <div class="frbl-feature-icon">
1806 <?php echo $icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
1807 </div>
1808 <div class="frbl-feature-info">
1809 <h3 class="frbl-feature-title">
1810 <?php echo esc_html( $field['title'] ); ?>
1811 </h3>
1812 <?php if ( $desc ) : ?>
1813 <p class="frbl-feature-description">
1814 <?php echo esc_html( $desc ); ?>
1815 </p>
1816 <?php endif; ?>
1817 </div>
1818 <div class="frbl-feature-toggle">
1819 <?php call_user_func( $field['callback'], $field['args'] ); ?>
1820 </div>
1821 </div>
1822 </div>
1823 <?php
1824 }
1825
1826 /**
1827 * Get icon SVG for a feature.
1828 *
1829 * @param string $field_id Field ID.
1830 * @return string SVG icon markup.
1831 */
1832 private function get_feature_icon( $field_id ) {
1833 // Map field IDs to icon file names.
1834 $icon_map = array(
1835 $this->option_enable_testimonials => 'testimonials',
1836 $this->option_enable_reading_progress => 'reading-progress',
1837 $this->option_enable_back_button => 'back-button',
1838 $this->option_enable_scroll_top => 'scroll-top',
1839 $this->option_enable_events => 'events',
1840 $this->option_enable_fluid_typography => 'fluid-typography',
1841 $this->option_enable_popups => 'popups',
1842 $this->option_enable_gutenberg => 'gutenberg',
1843 $this->option_enable_simple_prices_variable_products => 'simple-prices',
1844 $this->option_enable_after_add_to_cart => 'after-add-to-cart',
1845 $this->option_deactivate_short_description => 'deactivate-description',
1846 $this->option_move_content_to_short_description => 'move-content',
1847 $this->option_disable_zoom_images => 'disable-zoom',
1848 $this->option_add_share_buttons => 'share-buttons',
1849 $this->option_deactivate_product_tabs => 'deactivate-tabs',
1850 $this->option_horizontal_product_form => 'horizontal-form',
1851 $this->option_enable_variant_display_mode => 'default',
1852 $this->option_enable_fullpage_scroll => 'fullpage-scroll',
1853 $this->option_enable_language_banner => 'language-banner',
1854 $this->option_checkout_inline => 'horizontal-form',
1855 $this->option_disable_zoom_images => 'disable-zoom',
1856 $this->option_add_share_buttons => 'share-buttons',
1857 $this->option_deactivate_product_tabs => 'deactivate-tabs',
1858 $this->option_horizontal_product_form => 'horizontal-form',
1859 $this->option_enable_fullpage_scroll => 'fullpage-scroll',
1860 $this->option_enable_language_banner => 'language-banner',
1861 $this->option_disable_cart_coupon => 'default',
1862 $this->option_disable_cart_cross_sells => 'default',
1863 $this->option_disable_checkout_coupon => 'default',
1864 $this->option_disable_checkout_order_notes => 'default',
1865 $this->option_disable_checkout_login_prompt => 'default',
1866 );
1867
1868 $icon_name = $icon_map[ $field_id ] ?? 'default';
1869 $icon_path = FRBL_PLUGIN_PATH . 'assets/admin/icons/' . $icon_name . '.svg';
1870
1871 if ( file_exists( $icon_path ) ) {
1872 $svg_content = file_get_contents( $icon_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
1873 return $svg_content ? $svg_content : '';
1874 }
1875
1876 return '';
1877 }
1878
1879 /**
1880 * PRO Features section description.
1881 *
1882 * @return void
1883 */
1884 public function section_woo_features_callback() {
1885 if ( ! frbl_is_pro_active() ) {
1886 echo '<div class="tw:bg-blue-50 tw:border-l-4 tw:border-blue-400 tw:p-4 tw:mb-4">';
1887 echo '<div class="tw:flex">';
1888 echo '<div class="tw:flex-shrink-0">';
1889 echo '<svg class="tw:h-5 tw:w-5 tw:text-blue-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/></svg>';
1890 echo '</div>';
1891 echo '<div class="tw:ml-3">';
1892 echo '<p class="tw:text-sm tw:text-blue-700">';
1893 printf(
1894 /* translators: %s: FrontBlocks PRO link */
1895 esc_html__( 'These features require %s. Upgrade to unlock advanced functionality.', 'frontblocks' ),
1896 '<a href="https://close.technology/wordpress-plugins/frontblocks-pro/?utm_source=frontblocks&utm_medium=plugin&utm_campaign=settings" target="_blank" class="tw:font-medium tw:underline">FrontBlocks PRO</a>'
1897 );
1898 echo '</p>';
1899 echo '</div>';
1900 echo '</div>';
1901 echo '</div>';
1902 } elseif ( ! $this->is_license_valid ) {
1903 echo '<div class="tw:bg-yellow-50 tw:border-l-4 tw:border-yellow-400 tw:p-4 tw:mb-4">';
1904 echo '<div class="tw:flex">';
1905 echo '<div class="tw:flex-shrink-0">';
1906 echo '<svg class="tw:h-5 tw:w-5 tw:text-yellow-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>';
1907 echo '</div>';
1908 echo '<div class="tw:ml-3">';
1909 echo '<p class="tw:text-sm tw:text-yellow-700">';
1910 printf(
1911 /* translators: %s: License section link */
1912 esc_html__( 'License is not activated. Please activate your license in the %s section below to enable these features.', 'frontblocks' ),
1913 '<a href="#frontblocks_section_license" class="tw:font-medium tw:underline">' . esc_html__( 'License', 'frontblocks' ) . '</a>'
1914 );
1915 echo '</p>';
1916 echo '</div>';
1917 echo '</div>';
1918 echo '</div>';
1919 } else {
1920 ?>
1921 <p class="tw:text-sm tw:text-gray-600 tw:mt-0 tw:mb-4">
1922 <?php echo esc_html__( 'Advanced features for WooCommerce and more.', 'frontblocks' ); ?>
1923 </p>
1924 <?php
1925 }
1926 }
1927
1928 /**
1929 * Render toggle field for enable testimonials.
1930 *
1931 * @return void
1932 */
1933 public function field_enable_testimonials() {
1934 $options = get_option( 'frontblocks_settings', array() );
1935 $enabled = (bool) ( $options[ $this->option_enable_testimonials ] ?? false );
1936 ?>
1937 <label class="frbl-toggle">
1938 <input type="checkbox"
1939 id="<?php echo esc_attr( $this->option_enable_testimonials ); ?>"
1940 name="frontblocks_settings[<?php echo esc_attr( $this->option_enable_testimonials ); ?>]"
1941 value="1"
1942 <?php checked( true, $enabled ); ?>
1943 />
1944 <span></span>
1945 </label>
1946 <?php
1947 }
1948
1949 /**
1950 * Render toggle field for enable reading progress bar.
1951 *
1952 * @return void
1953 */
1954 public function field_enable_reading_progress() {
1955 $options = get_option( 'frontblocks_settings', array() );
1956 $enabled = (bool) ( $options[ $this->option_enable_reading_progress ] ?? false );
1957 ?>
1958 <label class="frbl-toggle">
1959 <input type="checkbox"
1960 id="<?php echo esc_attr( $this->option_enable_reading_progress ); ?>"
1961 name="frontblocks_settings[<?php echo esc_attr( $this->option_enable_reading_progress ); ?>]"
1962 value="1"
1963 <?php checked( true, $enabled ); ?>
1964 />
1965 <span></span>
1966 </label>
1967 <?php
1968 }
1969
1970 /**
1971 * Render toggle field for enable back button.
1972 *
1973 * @return void
1974 */
1975 public function field_enable_back_button() {
1976 $options = get_option( 'frontblocks_settings', array() );
1977 $enabled = (bool) ( $options[ $this->option_enable_back_button ] ?? false );
1978 ?>
1979 <label class="frbl-toggle">
1980 <input type="checkbox"
1981 id="<?php echo esc_attr( $this->option_enable_back_button ); ?>"
1982 name="frontblocks_settings[<?php echo esc_attr( $this->option_enable_back_button ); ?>]"
1983 value="1"
1984 <?php checked( true, $enabled ); ?>
1985 />
1986 <span></span>
1987 </label>
1988 <?php
1989 }
1990
1991 /**
1992 * Render toggle field and sub-settings for scroll to top.
1993 *
1994 * @return void
1995 */
1996 public function field_enable_scroll_top() {
1997 $options = get_option( 'frontblocks_settings', array() );
1998 $enabled = (bool) ( $options[ $this->option_enable_scroll_top ] ?? false );
1999 $position = sanitize_text_field( $options[ $this->option_scroll_top_position ] ?? 'bottom-right' );
2000 $icon_url = esc_url( $options[ $this->option_scroll_top_icon_url ] ?? '' );
2001 ?>
2002 <label class="frbl-toggle">
2003 <input type="checkbox"
2004 id="<?php echo esc_attr( $this->option_enable_scroll_top ); ?>"
2005 name="frontblocks_settings[<?php echo esc_attr( $this->option_enable_scroll_top ); ?>]"
2006 value="1"
2007 <?php checked( true, $enabled ); ?>
2008 />
2009 <span></span>
2010 </label>
2011
2012 <!-- Sub-settings - moved below the card by JavaScript -->
2013 <div id="scroll-top-settings-wrapper" style="<?php echo $enabled ? '' : 'display: none;'; ?>">
2014 <div class="tw-mb-4">
2015 <label for="<?php echo esc_attr( $this->option_scroll_top_position ); ?>" class="tw-block tw-text-sm tw-font-medium tw-text-gray-700 tw-mb-2">
2016 <?php echo esc_html__( 'Position', 'frontblocks' ); ?>
2017 </label>
2018 <select
2019 id="<?php echo esc_attr( $this->option_scroll_top_position ); ?>"
2020 name="frontblocks_settings[<?php echo esc_attr( $this->option_scroll_top_position ); ?>]"
2021 class="tw-block tw-pl-3 tw-pr-8 tw-py-2 tw-border tw-border-gray-300 tw-rounded-lg tw-text-sm focus:tw-outline-none focus:tw-ring-2 focus:tw-ring-primary-500 focus:tw-border-transparent"
2022 >
2023 <option value="bottom-right" <?php selected( $position, 'bottom-right' ); ?>>
2024 <?php echo esc_html__( 'Bottom right', 'frontblocks' ); ?>
2025 </option>
2026 <option value="bottom-left" <?php selected( $position, 'bottom-left' ); ?>>
2027 <?php echo esc_html__( 'Bottom left', 'frontblocks' ); ?>
2028 </option>
2029 </select>
2030 </div>
2031
2032 <div>
2033 <label class="tw-block tw-text-sm tw-font-medium tw-text-gray-700 tw-mb-2">
2034 <?php echo esc_html__( 'Custom icon (optional)', 'frontblocks' ); ?>
2035 </label>
2036 <div id="scroll-top-icon-preview" class="tw-mb-2" style="<?php echo $icon_url ? '' : 'display: none;'; ?>">
2037 <img id="scroll-top-icon-img" src="<?php echo esc_url( $icon_url ); ?>" alt="" style="width: 48px; height: 48px; object-fit: contain; border: 1px solid #e5e7eb; border-radius: 0.375rem; padding: 4px;" />
2038 </div>
2039 <input type="hidden" id="scroll_top_icon_url" name="frontblocks_settings[<?php echo esc_attr( $this->option_scroll_top_icon_url ); ?>]" value="<?php echo esc_attr( $icon_url ); ?>" />
2040 <div class="tw-flex tw-gap-2 tw-items-center">
2041 <button type="button" id="scroll-top-icon-upload" class="tw-px-3 tw-py-1.5 tw-text-sm tw-border tw-border-gray-300 tw-rounded-lg tw-bg-white hover:tw-bg-gray-50 tw-text-gray-700 tw-transition-colors">
2042 <?php echo esc_html__( 'Select image', 'frontblocks' ); ?>
2043 </button>
2044 <button type="button" id="scroll-top-icon-remove" class="tw-px-3 tw-py-1.5 tw-text-sm tw-border tw-border-red-200 tw-rounded-lg tw-bg-white hover:tw-bg-red-50 tw-text-red-600 tw-transition-colors" style="<?php echo $icon_url ? '' : 'display: none;'; ?>">
2045 <?php echo esc_html__( 'Remove', 'frontblocks' ); ?>
2046 </button>
2047 </div>
2048 <p class="tw-text-xs tw-text-gray-500 tw-mt-2">
2049 <?php echo esc_html__( 'Upload an SVG, PNG or any image. Leave empty to use the default arrow icon.', 'frontblocks' ); ?>
2050 </p>
2051 </div>
2052 </div>
2053 <?php
2054 }
2055
2056 /**
2057 * Render toggle field for enable events.
2058 *
2059 * @return void
2060 */
2061 public function field_enable_events() {
2062 $options = get_option( 'frontblocks_settings', array() );
2063 $enabled = (bool) ( $options[ $this->option_enable_events ] ?? false );
2064 $events_type = sanitize_text_field( $options[ $this->option_events_type ] ?? 'cpt' );
2065 ?>
2066 <!-- Toggle - stays in horizontal layout with icon and text -->
2067 <label class="frbl-toggle">
2068 <input type="checkbox"
2069 id="<?php echo esc_attr( $this->option_enable_events ); ?>"
2070 name="frontblocks_settings[<?php echo esc_attr( $this->option_enable_events ); ?>]"
2071 value="1"
2072 <?php checked( true, $enabled ); ?>
2073 />
2074 <span></span>
2075 </label>
2076
2077 <!-- Select and description - will be moved below the card by JavaScript -->
2078 <div id="events-type-wrapper" class="tw:mt-4" style="<?php echo $enabled ? 'width: 100%; min-width: 100%; display: block;' : 'display: none;'; ?>">
2079 <label for="<?php echo esc_attr( $this->option_events_type ); ?>" class="tw:block tw:text-sm tw:font-medium tw:text-gray-700 tw:mb-2">
2080 <?php echo esc_html__( 'Event type', 'frontblocks' ); ?>
2081 </label>
2082 <select
2083 id="<?php echo esc_attr( $this->option_events_type ); ?>"
2084 name="frontblocks_settings[<?php echo esc_attr( $this->option_events_type ); ?>]"
2085 class="tw:block tw:w-full tw:px-3 tw:py-2 tw:border tw:border-gray-300 tw:rounded-lg tw:text-base tw:focus:outline-none tw:focus:ring-2 tw:focus:ring-primary-500 tw:focus:border-transparent"
2086 style="width: 100%; min-width: 100%; max-width: 100%; box-sizing: border-box;"
2087 >
2088 <option value="cpt" <?php selected( $events_type, 'cpt' ); ?>>
2089 <?php echo esc_html__( 'Custom Post Type (CPT)', 'frontblocks' ); ?>
2090 </option>
2091 <option value="posts" <?php selected( $events_type, 'posts' ); ?>>
2092 <?php echo esc_html__( 'Blog posts', 'frontblocks' ); ?>
2093 </option>
2094 </select>
2095 <p class="tw:text-xs tw:text-gray-500 tw:mt-2">
2096 <?php echo esc_html__( 'Choose whether events will be created in a dedicated CPT or in regular blog posts.', 'frontblocks' ); ?>
2097 </p>
2098 </div>
2099 <?php
2100 }
2101
2102 /**
2103 * Render toggle field for enable fluid typography.
2104 *
2105 * @return void
2106 */
2107 public function field_enable_fluid_typography() {
2108 $options = get_option( 'frontblocks_settings', array() );
2109 $enabled = (bool) ( $options[ $this->option_enable_fluid_typography ] ?? true );
2110 ?>
2111 <label class="frbl-toggle">
2112 <input type="checkbox"
2113 id="<?php echo esc_attr( $this->option_enable_fluid_typography ); ?>"
2114 name="frontblocks_settings[<?php echo esc_attr( $this->option_enable_fluid_typography ); ?>]"
2115 value="1"
2116 <?php checked( true, $enabled ); ?>
2117 />
2118 <span></span>
2119 </label>
2120 <?php
2121 }
2122
2123 /**
2124 * Render toggle field for enable maintenance mode.
2125 *
2126 * @return void
2127 */
2128 public function field_enable_maintenance() {
2129 $options = get_option( 'frontblocks_settings', array() );
2130 $enabled = (bool) ( $options[ $this->option_enable_maintenance ] ?? false );
2131 $title = (string) ( $options[ $this->option_maintenance_title ] ?? '' );
2132 $image_id = (int) ( $options[ $this->option_maintenance_image ] ?? 0 );
2133 $image_url = $image_id ? wp_get_attachment_image_url( $image_id, 'medium' ) : '';
2134 ?>
2135 <div class="frbl-maintenance-wrapper">
2136 <div class="tw:flex tw:items-center tw:justify-between tw:mb-4">
2137 <label for="<?php echo esc_attr( $this->option_enable_maintenance ); ?>" class="tw:text-base tw:font-medium tw:text-gray-900">
2138 <?php echo esc_html__( 'Enable Maintenance Mode', 'frontblocks' ); ?>
2139 </label>
2140 <label class="frbl-toggle">
2141 <input type="checkbox"
2142 id="<?php echo esc_attr( $this->option_enable_maintenance ); ?>"
2143 name="frontblocks_settings[<?php echo esc_attr( $this->option_enable_maintenance ); ?>]"
2144 value="1"
2145 <?php checked( true, $enabled ); ?>
2146 />
2147 <span></span>
2148 </label>
2149 </div>
2150
2151 <div id="maintenance-fields-wrapper" style="<?php echo $enabled ? '' : 'display: none;'; ?>">
2152 <div class="tw:p-4 tw:bg-gray-50 tw:rounded-lg tw:border tw:border-gray-200">
2153 <label for="<?php echo esc_attr( $this->option_maintenance_title ); ?>" class="tw:block tw:text-sm tw:font-medium tw:text-gray-700 tw:mb-2">
2154 <?php echo esc_html__( 'Maintenance page title', 'frontblocks' ); ?>
2155 </label>
2156 <input
2157 type="text"
2158 id="<?php echo esc_attr( $this->option_maintenance_title ); ?>"
2159 name="frontblocks_settings[<?php echo esc_attr( $this->option_maintenance_title ); ?>]"
2160 value="<?php echo esc_attr( $title ); ?>"
2161 placeholder="<?php echo esc_attr__( 'We are currently performing maintenance', 'frontblocks' ); ?>"
2162 class="tw:block tw:w-full tw:px-3 tw:py-2 tw:border tw:border-gray-300 tw:rounded-lg tw:text-base tw:focus:outline-none tw:focus:ring-2 tw:focus:ring-primary-500 tw:focus:border-transparent"
2163 />
2164
2165 <label class="tw:block tw:text-sm tw:font-medium tw:text-gray-700 tw:mt-4 tw:mb-2">
2166 <?php echo esc_html__( 'Background image', 'frontblocks' ); ?>
2167 </label>
2168 <input type="hidden" id="<?php echo esc_attr( $this->option_maintenance_image ); ?>" name="frontblocks_settings[<?php echo esc_attr( $this->option_maintenance_image ); ?>]" value="<?php echo esc_attr( $image_id ); ?>" />
2169 <div class="frbl-maintenance-image-preview tw:mb-2" style="<?php echo $image_url ? '' : 'display:none;'; ?>">
2170 <img src="<?php echo esc_url( $image_url ? $image_url : '' ); ?>" alt="" style="max-width: 200px; height: auto; border-radius: 8px; display: block;" />
2171 </div>
2172 <div class="tw:flex tw:gap-2">
2173 <button type="button" class="button frbl-maintenance-select-image">
2174 <?php echo esc_html__( 'Select image', 'frontblocks' ); ?>
2175 </button>
2176 <button type="button" class="button frbl-maintenance-remove-image" style="<?php echo $image_url ? '' : 'display:none;'; ?>">
2177 <?php echo esc_html__( 'Remove image', 'frontblocks' ); ?>
2178 </button>
2179 </div>
2180 <p class="tw:text-xs tw:text-gray-500 tw:mt-2 tw:mb-0">
2181 <?php echo esc_html__( 'Shown as the full-screen background while maintenance mode is active. Recommended size: 1920×1080px.', 'frontblocks' ); ?>
2182 </p>
2183 </div>
2184 </div>
2185 </div>
2186 <?php
2187 }
2188
2189 /**
2190 * Render toggle field and sub-settings for the cookie notice banner.
2191 *
2192 * @return void
2193 */
2194 public function field_enable_cookie_notice() {
2195 $options = get_option( 'frontblocks_settings', array() );
2196 $enabled = (bool) ( $options[ $this->option_enable_cookie_notice ] ?? false );
2197 $message = (string) ( $options[ $this->option_cookie_notice_message ] ?? '' );
2198 $accept_label = (string) ( $options[ $this->option_cookie_notice_accept_label ] ?? '' );
2199 $reject_label = (string) ( $options[ $this->option_cookie_notice_reject_label ] ?? '' );
2200 $policy_page_id = (int) ( $options[ $this->option_cookie_notice_policy_page_id ] ?? 0 );
2201 $layout = (string) ( $options[ $this->option_cookie_notice_layout ] ?? 'bar' );
2202 $position = (string) ( $options[ $this->option_cookie_notice_position ] ?? 'bottom-right' );
2203 $color = (string) ( $options[ $this->option_cookie_notice_color ] ?? '#687df9' );
2204 $bg_color = (string) ( $options[ $this->option_cookie_notice_bg_color ] ?? '#ffffff' );
2205 $radius = (string) ( $options[ $this->option_cookie_notice_radius ] ?? 'small' );
2206 $expiration = (int) ( $options[ $this->option_cookie_notice_expiration_days ] ?? 365 );
2207 $gtm_id = (string) ( $options[ $this->option_cookie_notice_gtm_id ] ?? '' );
2208 $ga4_id = (string) ( $options[ $this->option_cookie_notice_ga4_id ] ?? '' );
2209 $tracking_integrations = \FrontBlocks\Frontend\CookieNotice::get_tracking_integrations( $options );
2210 $site_kit_tags = $this->get_google_site_kit_managed_tags();
2211 $accepted_count = (int) get_option( \FrontBlocks\Frontend\CookieNotice::STATS_OPTION_ACCEPTED, 0 );
2212 $rejected_count = (int) get_option( \FrontBlocks\Frontend\CookieNotice::STATS_OPTION_REJECTED, 0 );
2213 $total_count = $accepted_count + $rejected_count;
2214 $acceptance_pct = $total_count > 0 ? round( ( $accepted_count / $total_count ) * 100, 1 ) : 0;
2215 ?>
2216 <div class="frbl-cookie-notice-wrapper">
2217 <div class="tw:flex tw:items-center tw:justify-between tw:mb-4">
2218 <label for="<?php echo esc_attr( $this->option_enable_cookie_notice ); ?>" class="tw:text-base tw:font-medium tw:text-gray-900">
2219 <?php echo esc_html__( 'Enable Cookie Notice', 'frontblocks' ); ?>
2220 </label>
2221 <label class="frbl-toggle">
2222 <input type="checkbox"
2223 id="<?php echo esc_attr( $this->option_enable_cookie_notice ); ?>"
2224 name="frontblocks_settings[<?php echo esc_attr( $this->option_enable_cookie_notice ); ?>]"
2225 value="1"
2226 <?php checked( true, $enabled ); ?>
2227 />
2228 <span></span>
2229 </label>
2230 </div>
2231
2232 <div id="cookie-notice-fields-wrapper" style="<?php echo $enabled ? '' : 'display: none;'; ?>">
2233
2234 <?php if ( $total_count > 0 ) : ?>
2235 <div class="tw:flex tw:gap-4 tw:mb-4 tw:p-4 tw:bg-gray-50 tw:rounded-lg tw:border tw:border-gray-200">
2236 <div class="tw:flex-1">
2237 <p class="tw:text-xs tw:text-gray-500 tw:mb-1"><?php echo esc_html__( 'Acceptance rate', 'frontblocks' ); ?></p>
2238 <p class="tw:text-2xl tw:font-bold tw:text-gray-900 tw:m-0"><?php echo esc_html( $acceptance_pct ); ?>%</p>
2239 </div>
2240 <div class="tw:flex-1">
2241 <p class="tw:text-xs tw:text-gray-500 tw:mb-1"><?php echo esc_html__( 'Accepted', 'frontblocks' ); ?></p>
2242 <p class="tw:text-2xl tw:font-bold tw:text-gray-900 tw:m-0"><?php echo esc_html( $accepted_count ); ?></p>
2243 </div>
2244 <div class="tw:flex-1">
2245 <p class="tw:text-xs tw:text-gray-500 tw:mb-1"><?php echo esc_html__( 'Rejected', 'frontblocks' ); ?></p>
2246 <p class="tw:text-2xl tw:font-bold tw:text-gray-900 tw:m-0"><?php echo esc_html( $rejected_count ); ?></p>
2247 </div>
2248 </div>
2249 <?php endif; ?>
2250
2251 <div class="tw:p-4 tw:bg-gray-50 tw:rounded-lg tw:border tw:border-gray-200 tw:mb-4">
2252 <label for="<?php echo esc_attr( $this->option_cookie_notice_message ); ?>" class="tw:block tw:text-sm tw:font-medium tw:text-gray-700 tw:mb-2">
2253 <?php echo esc_html__( 'Message', 'frontblocks' ); ?>
2254 </label>
2255 <textarea
2256 id="<?php echo esc_attr( $this->option_cookie_notice_message ); ?>"
2257 name="frontblocks_settings[<?php echo esc_attr( $this->option_cookie_notice_message ); ?>]"
2258 rows="3"
2259 placeholder="<?php echo esc_attr__( 'We use cookies to improve your experience on our website. By browsing this website, you agree to our use of cookies.', 'frontblocks' ); ?>"
2260 class="tw:block tw:w-full tw:px-3 tw:py-2 tw:border tw:border-gray-300 tw:rounded-lg tw:text-base tw:focus:outline-none tw:focus:ring-2 tw:focus:ring-primary-500 tw:focus:border-transparent"
2261 ><?php echo esc_textarea( $message ); ?></textarea>
2262
2263 <div class="tw:grid tw:grid-cols-2 tw:gap-4 tw:mt-4">
2264 <div>
2265 <label for="<?php echo esc_attr( $this->option_cookie_notice_accept_label ); ?>" class="tw:block tw:text-sm tw:font-medium tw:text-gray-700 tw:mb-2">
2266 <?php echo esc_html__( 'Accept button label', 'frontblocks' ); ?>
2267 </label>
2268 <input
2269 type="text"
2270 id="<?php echo esc_attr( $this->option_cookie_notice_accept_label ); ?>"
2271 name="frontblocks_settings[<?php echo esc_attr( $this->option_cookie_notice_accept_label ); ?>]"
2272 value="<?php echo esc_attr( $accept_label ); ?>"
2273 placeholder="<?php echo esc_attr__( 'Accept', 'frontblocks' ); ?>"
2274 class="tw:block tw:w-full tw:px-3 tw:py-2 tw:border tw:border-gray-300 tw:rounded-lg tw:text-base tw:focus:outline-none tw:focus:ring-2 tw:focus:ring-primary-500 tw:focus:border-transparent"
2275 />
2276 </div>
2277 <div>
2278 <label for="<?php echo esc_attr( $this->option_cookie_notice_reject_label ); ?>" class="tw:block tw:text-sm tw:font-medium tw:text-gray-700 tw:mb-2">
2279 <?php echo esc_html__( 'Reject button label', 'frontblocks' ); ?>
2280 </label>
2281 <input
2282 type="text"
2283 id="<?php echo esc_attr( $this->option_cookie_notice_reject_label ); ?>"
2284 name="frontblocks_settings[<?php echo esc_attr( $this->option_cookie_notice_reject_label ); ?>]"
2285 value="<?php echo esc_attr( $reject_label ); ?>"
2286 placeholder="<?php echo esc_attr__( 'Reject', 'frontblocks' ); ?>"
2287 class="tw:block tw:w-full tw:px-3 tw:py-2 tw:border tw:border-gray-300 tw:rounded-lg tw:text-base tw:focus:outline-none tw:focus:ring-2 tw:focus:ring-primary-500 tw:focus:border-transparent"
2288 />
2289 </div>
2290 </div>
2291
2292 <label for="<?php echo esc_attr( $this->option_cookie_notice_policy_page_id ); ?>" class="tw:block tw:text-sm tw:font-medium tw:text-gray-700 tw:mt-4 tw:mb-2">
2293 <?php echo esc_html__( 'Cookie policy page (optional)', 'frontblocks' ); ?>
2294 </label>
2295 <select
2296 id="<?php echo esc_attr( $this->option_cookie_notice_policy_page_id ); ?>"
2297 name="frontblocks_settings[<?php echo esc_attr( $this->option_cookie_notice_policy_page_id ); ?>]"
2298 class="tw:block tw:w-full tw:px-3 tw:py-2 tw:border tw:border-gray-300 tw:rounded-lg tw:text-base tw:focus:outline-none tw:focus:ring-2 tw:focus:ring-primary-500 tw:focus:border-transparent"
2299 >
2300 <option value=""><?php echo esc_html__( '— None —', 'frontblocks' ); ?></option>
2301 <?php
2302 $pages_limit = 300;
2303 $pages = get_pages(
2304 array(
2305 'sort_column' => 'post_title',
2306 'number' => $pages_limit,
2307 )
2308 );
2309
2310 // The saved page may fall outside the limited list above (e.g. sorted
2311 // past it alphabetically on a large site) — make sure it still shows up
2312 // and stays selected instead of silently disappearing from the dropdown.
2313 $selected_page_listed = 0 === $policy_page_id;
2314 foreach ( $pages as $page ) {
2315 if ( $policy_page_id === $page->ID ) {
2316 $selected_page_listed = true;
2317 break;
2318 }
2319 }
2320
2321 if ( ! $selected_page_listed ) {
2322 $selected_page = get_post( $policy_page_id );
2323 if ( $selected_page instanceof \WP_Post ) {
2324 array_unshift( $pages, $selected_page );
2325 }
2326 }
2327
2328 foreach ( $pages as $page ) {
2329 printf(
2330 '<option value="%1$d" %2$s>%3$s</option>',
2331 (int) $page->ID,
2332 selected( $policy_page_id, $page->ID, false ),
2333 esc_html( $page->post_title )
2334 );
2335 }
2336 ?>
2337 </select>
2338 <?php if ( count( $pages ) >= $pages_limit ) : ?>
2339 <p class="tw:text-xs tw:text-amber-600 tw:mt-2">
2340 <?php
2341 printf(
2342 /* translators: %d: number of pages shown in the dropdown. */
2343 esc_html__( 'Showing the first %d pages. If the page you need is missing, search for it in the Pages list to find its ID and set it via the frontblocks_settings option.', 'frontblocks' ),
2344 (int) $pages_limit
2345 );
2346 ?>
2347 </p>
2348 <?php endif; ?>
2349 <p class="tw:text-xs tw:text-gray-500 tw:mt-2">
2350 <?php echo esc_html__( 'The banner is hidden on this page so visitors can read it before deciding. A "Learn more" link to it is added to the message.', 'frontblocks' ); ?>
2351 </p>
2352 </div>
2353
2354 <div class="tw:p-4 tw:bg-gray-50 tw:rounded-lg tw:border tw:border-gray-200 tw:mb-4">
2355 <div class="tw:grid tw:grid-cols-3 tw:gap-4">
2356 <div>
2357 <label for="<?php echo esc_attr( $this->option_cookie_notice_layout ); ?>" class="tw:block tw:text-sm tw:font-medium tw:text-gray-700 tw:mb-2">
2358 <?php echo esc_html__( 'Layout', 'frontblocks' ); ?>
2359 </label>
2360 <select
2361 id="<?php echo esc_attr( $this->option_cookie_notice_layout ); ?>"
2362 name="frontblocks_settings[<?php echo esc_attr( $this->option_cookie_notice_layout ); ?>]"
2363 class="tw:block tw:w-full tw:px-3 tw:py-2 tw:border tw:border-gray-300 tw:rounded-lg tw:text-base tw:focus:outline-none tw:focus:ring-2 tw:focus:ring-primary-500 tw:focus:border-transparent"
2364 >
2365 <option value="bar" <?php selected( $layout, 'bar' ); ?>><?php echo esc_html__( 'Full-width bar', 'frontblocks' ); ?></option>
2366 <option value="box" <?php selected( $layout, 'box' ); ?>><?php echo esc_html__( 'Boxed panel', 'frontblocks' ); ?></option>
2367 <option value="popup" <?php selected( $layout, 'popup' ); ?>><?php echo esc_html__( 'Centered popup', 'frontblocks' ); ?></option>
2368 </select>
2369 </div>
2370 <div id="cookie-notice-position-wrapper" style="<?php echo 'box' === $layout ? '' : 'display: none;'; ?>">
2371 <label for="<?php echo esc_attr( $this->option_cookie_notice_position ); ?>" class="tw:block tw:text-sm tw:font-medium tw:text-gray-700 tw:mb-2">
2372 <?php echo esc_html__( 'Position', 'frontblocks' ); ?>
2373 </label>
2374 <select
2375 id="<?php echo esc_attr( $this->option_cookie_notice_position ); ?>"
2376 name="frontblocks_settings[<?php echo esc_attr( $this->option_cookie_notice_position ); ?>]"
2377 class="tw:block tw:w-full tw:px-3 tw:py-2 tw:border tw:border-gray-300 tw:rounded-lg tw:text-base tw:focus:outline-none tw:focus:ring-2 tw:focus:ring-primary-500 tw:focus:border-transparent"
2378 >
2379 <option value="bottom-right" <?php selected( $position, 'bottom-right' ); ?>><?php echo esc_html__( 'Bottom right', 'frontblocks' ); ?></option>
2380 <option value="bottom-left" <?php selected( $position, 'bottom-left' ); ?>><?php echo esc_html__( 'Bottom left', 'frontblocks' ); ?></option>
2381 </select>
2382 </div>
2383 <div>
2384 <label for="<?php echo esc_attr( $this->option_cookie_notice_color ); ?>" class="tw:block tw:text-sm tw:font-medium tw:text-gray-700 tw:mb-2">
2385 <?php echo esc_html__( 'Accent color', 'frontblocks' ); ?>
2386 </label>
2387 <input
2388 type="color"
2389 id="<?php echo esc_attr( $this->option_cookie_notice_color ); ?>"
2390 name="frontblocks_settings[<?php echo esc_attr( $this->option_cookie_notice_color ); ?>]"
2391 value="<?php echo esc_attr( $color ); ?>"
2392 class="tw:h-10 tw:w-full tw:border tw:border-gray-300 tw:rounded-lg"
2393 />
2394 </div>
2395 </div>
2396
2397 <div class="tw:grid tw:grid-cols-2 tw:gap-4 tw:mt-4">
2398 <div>
2399 <label for="<?php echo esc_attr( $this->option_cookie_notice_bg_color ); ?>" class="tw:block tw:text-sm tw:font-medium tw:text-gray-700 tw:mb-2">
2400 <?php echo esc_html__( 'Background color', 'frontblocks' ); ?>
2401 </label>
2402 <input
2403 type="color"
2404 id="<?php echo esc_attr( $this->option_cookie_notice_bg_color ); ?>"
2405 name="frontblocks_settings[<?php echo esc_attr( $this->option_cookie_notice_bg_color ); ?>]"
2406 value="<?php echo esc_attr( $bg_color ); ?>"
2407 class="tw:h-10 tw:w-full tw:border tw:border-gray-300 tw:rounded-lg"
2408 />
2409 </div>
2410 <div>
2411 <label for="<?php echo esc_attr( $this->option_cookie_notice_radius ); ?>" class="tw:block tw:text-sm tw:font-medium tw:text-gray-700 tw:mb-2">
2412 <?php echo esc_html__( 'Corner rounding', 'frontblocks' ); ?>
2413 </label>
2414 <select
2415 id="<?php echo esc_attr( $this->option_cookie_notice_radius ); ?>"
2416 name="frontblocks_settings[<?php echo esc_attr( $this->option_cookie_notice_radius ); ?>]"
2417 class="tw:block tw:w-full tw:px-3 tw:py-2 tw:border tw:border-gray-300 tw:rounded-lg tw:text-base tw:focus:outline-none tw:focus:ring-2 tw:focus:ring-primary-500 tw:focus:border-transparent"
2418 >
2419 <option value="none" <?php selected( $radius, 'none' ); ?>><?php echo esc_html__( 'None', 'frontblocks' ); ?></option>
2420 <option value="small" <?php selected( $radius, 'small' ); ?>><?php echo esc_html__( 'Slightly rounded', 'frontblocks' ); ?></option>
2421 <option value="large" <?php selected( $radius, 'large' ); ?>><?php echo esc_html__( 'Very rounded', 'frontblocks' ); ?></option>
2422 </select>
2423 </div>
2424 </div>
2425
2426 <div class="tw:mt-4">
2427 <p class="tw:block tw:text-sm tw:font-medium tw:text-gray-700 tw:mb-2">
2428 <?php echo esc_html__( 'Preview', 'frontblocks' ); ?>
2429 </p>
2430 <?php
2431 $preview_accent_text = \FrontBlocks\Frontend\CookieNotice::get_readable_text_color( $color );
2432 $preview_accent_link = \FrontBlocks\Frontend\CookieNotice::get_readable_on_white_color( $color );
2433 $preview_panel_text = \FrontBlocks\Frontend\CookieNotice::get_readable_text_color( $bg_color );
2434 $preview_radius = \FrontBlocks\Frontend\CookieNotice::get_radius_value( $radius );
2435 ?>
2436 <div id="frbl-cookie-notice-preview-stage" class="frbl-cookie-notice-preview-stage">
2437 <div
2438 id="frbl-cookie-notice-preview"
2439 class="frbl-cookie-notice frbl-cookie-notice-preview frbl-cookie-notice--<?php echo esc_attr( $layout ); ?><?php echo 'box' === $layout ? ' frbl-cookie-notice--' . ( 'bottom-left' === $position ? 'left' : 'right' ) : ''; ?>"
2440 style="--frbl-cookie-accent: <?php echo esc_attr( $color ); ?>; --frbl-cookie-accent-contrast: <?php echo esc_attr( $preview_accent_text ); ?>; --frbl-cookie-accent-on-light: <?php echo esc_attr( $preview_accent_link ); ?>; --frbl-cookie-bg: <?php echo esc_attr( $bg_color ); ?>; --frbl-cookie-text: <?php echo esc_attr( $preview_panel_text ); ?>; --frbl-cookie-radius: <?php echo esc_attr( $preview_radius ); ?>;"
2441 >
2442 <div class="frbl-cookie-notice__panel">
2443 <span id="frbl-cookie-notice-preview-icon" class="frbl-cookie-notice__icon">
2444 <?php echo \FrontBlocks\Frontend\CookieNotice::get_cookie_icon_svg(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static inline SVG, no dynamic data. ?>
2445 </span>
2446 <p class="frbl-cookie-notice__message">
2447 <?php echo esc_html( '' !== $message ? $message : __( 'We use cookies to improve your experience on our website. By browsing this website, you agree to our use of cookies.', 'frontblocks' ) ); ?>
2448 </p>
2449 <div class="frbl-cookie-notice__actions">
2450 <button type="button" class="frbl-cookie-notice__button frbl-cookie-notice__button--reject" disabled>
2451 <?php echo esc_html( '' !== $reject_label ? $reject_label : __( 'Reject', 'frontblocks' ) ); ?>
2452 </button>
2453 <button type="button" class="frbl-cookie-notice__button frbl-cookie-notice__button--accept" disabled>
2454 <?php echo esc_html( '' !== $accept_label ? $accept_label : __( 'Accept', 'frontblocks' ) ); ?>
2455 </button>
2456 </div>
2457 </div>
2458 </div>
2459 </div>
2460 </div>
2461
2462 <label for="<?php echo esc_attr( $this->option_cookie_notice_expiration_days ); ?>" class="tw:block tw:text-sm tw:font-medium tw:text-gray-700 tw:mt-4 tw:mb-2">
2463 <?php echo esc_html__( 'Cookie expiration (days)', 'frontblocks' ); ?>
2464 </label>
2465 <input
2466 type="number"
2467 min="1"
2468 max="730"
2469 id="<?php echo esc_attr( $this->option_cookie_notice_expiration_days ); ?>"
2470 name="frontblocks_settings[<?php echo esc_attr( $this->option_cookie_notice_expiration_days ); ?>]"
2471 value="<?php echo esc_attr( $expiration ); ?>"
2472 class="tw:block tw:w-32 tw:px-3 tw:py-2 tw:border tw:border-gray-300 tw:rounded-lg tw:text-base tw:focus:outline-none tw:focus:ring-2 tw:focus:ring-primary-500 tw:focus:border-transparent"
2473 />
2474 </div>
2475
2476 <div class="tw:p-4 tw:bg-gray-50 tw:rounded-lg tw:border tw:border-gray-200">
2477 <?php if ( $site_kit_tags['gtm'] || $site_kit_tags['ga4'] ) : ?>
2478 <p class="tw:text-sm tw:text-gray-600 tw:m-0">
2479 <?php echo esc_html__( 'Google Site Kit manages the configured Google tag. FrontBlocks applies Consent Mode to it, so no duplicate ID is needed here.', 'frontblocks' ); ?>
2480 </p>
2481 <?php endif; ?>
2482
2483 <?php if ( ! $site_kit_tags['gtm'] || ! $site_kit_tags['ga4'] ) : ?>
2484 <p class="tw:text-sm tw:text-gray-600 tw:mt-0 tw:mb-4">
2485 <?php echo esc_html__( 'Scripts are only requested after a visitor accepts — never before.', 'frontblocks' ); ?>
2486 </p>
2487 <div class="tw:grid tw:grid-cols-1 tw:gap-4<?php echo ! $site_kit_tags['gtm'] && ! $site_kit_tags['ga4'] ? ' tw:grid-cols-2' : ''; ?>">
2488 <?php if ( ! $site_kit_tags['gtm'] ) : ?>
2489 <div>
2490 <label for="<?php echo esc_attr( $this->option_cookie_notice_gtm_id ); ?>" class="tw:block tw:text-sm tw:font-medium tw:text-gray-700 tw:mb-2">
2491 <?php echo esc_html__( 'Google Tag Manager ID', 'frontblocks' ); ?>
2492 </label>
2493 <input
2494 type="text"
2495 id="<?php echo esc_attr( $this->option_cookie_notice_gtm_id ); ?>"
2496 name="frontblocks_settings[<?php echo esc_attr( $this->option_cookie_notice_gtm_id ); ?>]"
2497 value="<?php echo esc_attr( $gtm_id ); ?>"
2498 placeholder="GTM-XXXXXXX"
2499 class="tw:block tw:w-full tw:px-3 tw:py-2 tw:border tw:border-gray-300 tw:rounded-lg tw:text-base tw:focus:outline-none tw:focus:ring-2 tw:focus:ring-primary-500 tw:focus:border-transparent"
2500 />
2501 </div>
2502 <?php endif; ?>
2503 <?php if ( ! $site_kit_tags['ga4'] ) : ?>
2504 <div>
2505 <label for="<?php echo esc_attr( $this->option_cookie_notice_ga4_id ); ?>" class="tw:block tw:text-sm tw:font-medium tw:text-gray-700 tw:mb-2">
2506 <?php echo esc_html__( 'GA4 Measurement ID', 'frontblocks' ); ?>
2507 </label>
2508 <input
2509 type="text"
2510 id="<?php echo esc_attr( $this->option_cookie_notice_ga4_id ); ?>"
2511 name="frontblocks_settings[<?php echo esc_attr( $this->option_cookie_notice_ga4_id ); ?>]"
2512 value="<?php echo esc_attr( $ga4_id ); ?>"
2513 placeholder="G-XXXXXXXXXX"
2514 class="tw:block tw:w-full tw:px-3 tw:py-2 tw:border tw:border-gray-300 tw:rounded-lg tw:text-base tw:focus:outline-none tw:focus:ring-2 tw:focus:ring-primary-500 tw:focus:border-transparent"
2515 />
2516 </div>
2517 <?php endif; ?>
2518 </div>
2519
2520 <?php if ( $enabled && $this->is_gtm4wp_container_loading( $gtm_id ) ) : ?>
2521 <div class="tw:mt-4 tw:p-4 tw:bg-amber-50 tw:border tw:border-amber-200 tw:rounded-lg" role="alert">
2522 <p class="tw:text-sm tw:font-medium tw:text-amber-900 tw:mt-0 tw:mb-2">
2523 <?php echo esc_html__( 'Google Tag Manager may load twice.', 'frontblocks' ); ?>
2524 </p>
2525 <p class="tw:text-sm tw:text-amber-800 tw:m-0">
2526 <?php
2527 printf(
2528 wp_kses(
2529 /* translators: %s: Google Tag Manager for WordPress settings page URL. */
2530 __( 'The same container is enabled in Google Tag Manager for WordPress. Disable its container-code injection in <a href="%s">its settings</a> so FrontBlocks can load it only after consent. Its data layer can remain enabled.', 'frontblocks' ),
2531 array( 'a' => array( 'href' => array() ) )
2532 ),
2533 esc_url( admin_url( 'options-general.php?page=gtm4wp-settings' ) )
2534 );
2535 ?>
2536 </p>
2537 </div>
2538 <?php endif; ?>
2539 <?php endif; ?>
2540
2541 <div class="tw:mt-4">
2542 <?php
2543 $tracking_labels = array(
2544 'clientify_analytics_plus' => __( 'Clientify Analytics Plus', 'frontblocks' ),
2545 'clientify_analytics_classic' => __( 'Clientify Analytics (classic)', 'frontblocks' ),
2546 'brevo' => __( 'Brevo', 'frontblocks' ),
2547 );
2548 ?>
2549 <p class="tw:block tw:text-sm tw:font-medium tw:text-gray-700 tw:mb-2">
2550 <?php echo esc_html__( 'Added tracking integrations', 'frontblocks' ); ?>
2551 </p>
2552 <?php if ( $tracking_integrations ) : ?>
2553 <ul class="tw:space-y-2 tw:mb-4">
2554 <?php foreach ( $tracking_integrations as $integration ) : ?>
2555 <li class="tw:flex tw:items-center tw:justify-between tw:gap-4 tw:p-3 tw:bg-gray-50 tw:border tw:border-gray-200 tw:rounded-lg">
2556 <span class="tw:text-sm tw:text-gray-700">
2557 <strong><?php echo esc_html( $tracking_labels[ $integration['type'] ] ?? $integration['type'] ); ?></strong>
2558 <span class="tw:font-mono tw:text-xs">(<?php echo esc_html( $integration['id'] ); ?>)</span>
2559 </span>
2560 <label class="tw:text-sm tw:text-red-700 tw:whitespace-nowrap">
2561 <input type="checkbox" name="frontblocks_settings[cookie_notice_tracking_remove][]" value="<?php echo esc_attr( $integration['type'] ); ?>" />
2562 <?php echo esc_html__( 'Remove', 'frontblocks' ); ?>
2563 </label>
2564 </li>
2565 <?php endforeach; ?>
2566 </ul>
2567 <?php else : ?>
2568 <p class="tw:text-sm tw:text-gray-500 tw:mb-4"><?php echo esc_html__( 'No additional tracking integrations have been added.', 'frontblocks' ); ?></p>
2569 <?php endif; ?>
2570 <label for="cookie_notice_tracking_integration_code" class="tw:block tw:text-sm tw:font-medium tw:text-gray-700 tw:mb-2">
2571 <?php echo esc_html__( 'Add a tracking integration', 'frontblocks' ); ?>
2572 </label>
2573 <input
2574 type="text"
2575 id="cookie_notice_tracking_integration_code"
2576 name="frontblocks_settings[cookie_notice_tracking_integration_code]"
2577 value=""
2578 placeholder="<?php echo esc_attr__( 'Paste a Clientify or Brevo tracking code…', 'frontblocks' ); ?>"
2579 class="tw:block tw:w-full tw:px-3 tw:py-2 tw:border tw:border-gray-300 tw:rounded-lg tw:font-mono tw:text-xs tw:focus:outline-none tw:focus:ring-2 tw:focus:ring-primary-500 tw:focus:border-transparent"
2580 />
2581 <p class="tw:text-xs tw:text-gray-500 tw:mt-2 tw:mb-0">
2582 <?php
2583 printf(
2584 wp_kses(
2585 /* translators: %s: contact page URL. */
2586 __( 'For security reasons, only a supported integration ID is extracted and saved; the pasted code is discarded. Need another tool supported? <a href="%s" target="_blank" rel="noopener noreferrer">Contact us</a>.', 'frontblocks' ),
2587 array(
2588 'a' => array(
2589 'href' => array(),
2590 'target' => array(),
2591 'rel' => array(),
2592 ),
2593 )
2594 ),
2595 esc_url( 'https://close.technology/contacto' )
2596 );
2597 ?>
2598 </p>
2599 </div>
2600 </div>
2601 </div>
2602 </div>
2603 <?php
2604 }
2605
2606 /**
2607 * Check whether GTM4WP is set to load the same container as Cookie Notice.
2608 *
2609 * @param string $frontblocks_gtm_id FrontBlocks GTM container ID.
2610 * @return bool
2611 */
2612 private function is_gtm4wp_container_loading( $frontblocks_gtm_id ) {
2613 if ( '' === $frontblocks_gtm_id || ( ! defined( 'GTM4WP_OPTIONS' ) && ! function_exists( 'gtm4wp_the_gtm_tag' ) ) ) {
2614 return false;
2615 }
2616
2617 $gtm4wp_options = get_option( 'gtm4wp-options', array() );
2618 if ( ! is_array( $gtm4wp_options ) ) {
2619 return false;
2620 }
2621
2622 $placement_off = defined( 'GTM4WP_PLACEMENT_OFF' ) ? (int) GTM4WP_PLACEMENT_OFF : 3;
2623 $placement = isset( $gtm4wp_options['gtm-code-placement'] ) ? (int) $gtm4wp_options['gtm-code-placement'] : 0;
2624
2625 if ( $placement_off === $placement ) {
2626 return false;
2627 }
2628
2629 $container_ids = array_filter(
2630 array_map(
2631 'trim',
2632 explode( ',', (string) ( $gtm4wp_options['gtm-code'] ?? '' ) )
2633 )
2634 );
2635
2636 if ( isset( $gtm4wp_options['gtm-containers'] ) && is_array( $gtm4wp_options['gtm-containers'] ) ) {
2637 foreach ( $gtm4wp_options['gtm-containers'] as $container ) {
2638 if ( is_array( $container ) && ! empty( $container['id'] ) ) {
2639 $container_ids[] = (string) $container['id'];
2640 }
2641 }
2642 }
2643
2644 $container_ids = array_map( 'strtoupper', $container_ids );
2645
2646 return in_array( strtoupper( $frontblocks_gtm_id ), $container_ids, true );
2647 }
2648
2649 /**
2650 * Get the Google tags that Site Kit is configured to place.
2651 *
2652 * @return array{gtm: bool, ga4: bool}
2653 */
2654 private function get_google_site_kit_managed_tags() {
2655 $tags = array(
2656 'gtm' => false,
2657 'ga4' => false,
2658 );
2659
2660 if ( ! defined( 'GOOGLESITEKIT_VERSION' ) && ! class_exists( '\\Google\\Site_Kit\\Plugin' ) ) {
2661 return $tags;
2662 }
2663
2664 $tag_manager_settings = get_option( 'googlesitekit_tagmanager_settings', array() );
2665 if ( is_array( $tag_manager_settings ) && ! empty( $tag_manager_settings['containerID'] ) && ( ! isset( $tag_manager_settings['useSnippet'] ) || $tag_manager_settings['useSnippet'] ) ) {
2666 $tags['gtm'] = true;
2667 }
2668
2669 $analytics_settings = get_option( 'googlesitekit_analytics-4_settings', array() );
2670 if ( is_array( $analytics_settings ) && ! empty( $analytics_settings['measurementID'] ) && ( ! isset( $analytics_settings['useSnippet'] ) || $analytics_settings['useSnippet'] ) ) {
2671 $tags['ga4'] = true;
2672 }
2673
2674 return $tags;
2675 }
2676
2677 /**
2678 * Render a one-time cache notice after Cookie Notice settings are saved.
2679 *
2680 * @return void
2681 */
2682 private function render_cookie_notice_cache_notice() {
2683 $user_id = get_current_user_id();
2684 if ( ! $user_id ) {
2685 return;
2686 }
2687
2688 $notice = get_transient( 'frbl_cookie_notice_cache_notice_' . $user_id );
2689 if ( ! $notice ) {
2690 return;
2691 }
2692
2693 delete_transient( 'frbl_cookie_notice_cache_notice_' . $user_id );
2694 ?>
2695 <div style="background-color: #eff6ff; border-left: 4px solid #60a5fa; border-radius: 0.5rem; padding: 1rem; margin-bottom: 1.5rem; box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);">
2696 <p class="tw:text-sm tw:font-medium" style="color: #1d4ed8; margin: 0;">
2697 <?php
2698 if ( 'wp-rocket' === $notice ) {
2699 esc_html_e( 'Cookie Notice settings were updated and the WP Rocket cache was cleared.', 'frontblocks' );
2700 } else {
2701 esc_html_e( 'Cookie Notice settings were updated. If you use a full-page cache, purge it now so visitors receive the new configuration.', 'frontblocks' );
2702 }
2703 ?>
2704 </p>
2705 </div>
2706 <?php
2707 }
2708
2709 /**
2710 * Render toggle field for enable popups (PRO feature).
2711 *
2712 * @return void
2713 */
2714 public function field_enable_popups() {
2715 $this->render_pro_toggle( $this->option_enable_popups );
2716 }
2717
2718 /**
2719 * Render toggle field for enable Gutenberg in products (PRO).
2720 *
2721 * @return void
2722 */
2723 public function field_enable_gutenberg() {
2724 $this->render_pro_toggle( $this->option_enable_gutenberg );
2725 }
2726
2727 /**
2728 * Render toggle field for enable Simple Prices Variable Products (PRO).
2729 *
2730 * @return void
2731 */
2732 public function field_enable_simple_prices_variable_products() {
2733 $this->render_pro_toggle( $this->option_enable_simple_prices_variable_products );
2734 }
2735
2736 /**
2737 * Render After Add to Cart Block field.
2738 *
2739 * @return void
2740 */
2741 public function field_enable_after_add_to_cart() {
2742 $this->render_pro_toggle( $this->option_enable_after_add_to_cart );
2743 }
2744
2745 /**
2746 * Render Deactivate Short Description field.
2747 *
2748 * @return void
2749 */
2750 public function field_deactivate_short_description() {
2751 $this->render_pro_toggle( $this->option_deactivate_short_description );
2752 }
2753
2754 /**
2755 * Render Move Content to Short Description field.
2756 *
2757 * @return void
2758 */
2759 public function field_move_content_to_short_description() {
2760 $this->render_pro_toggle( $this->option_move_content_to_short_description );
2761 }
2762
2763 /**
2764 * Render Disable Zoom in Product Images field.
2765 *
2766 * @return void
2767 */
2768 public function field_disable_zoom_images() {
2769 $this->render_pro_toggle( $this->option_disable_zoom_images );
2770 }
2771
2772 /**
2773 * Render Add Share Buttons in Product Page field.
2774 *
2775 * @return void
2776 */
2777 public function field_add_share_buttons() {
2778 $this->render_pro_toggle( $this->option_add_share_buttons );
2779 }
2780
2781 /**
2782 * Render Deactivate Product Tabs field.
2783 *
2784 * @return void
2785 */
2786 public function field_deactivate_product_tabs() {
2787 $this->render_pro_toggle( $this->option_deactivate_product_tabs );
2788 }
2789
2790 /**
2791 * Render Horizontal Product Form Layout field.
2792 *
2793 * @return void
2794 */
2795 public function field_horizontal_product_form() {
2796 $this->render_pro_toggle( $this->option_horizontal_product_form );
2797 }
2798
2799 /**
2800 * Render Variant Display Mode toggle field.
2801 *
2802 * @return void
2803 */
2804 public function field_enable_variant_display_mode() {
2805 $this->render_pro_toggle( $this->option_enable_variant_display_mode );
2806 }
2807
2808 /**
2809 * Render Disable Coupon Field in Cart field.
2810 *
2811 * @return void
2812 */
2813 public function field_disable_cart_coupon() {
2814 $this->render_pro_toggle( $this->option_disable_cart_coupon );
2815 }
2816
2817 /**
2818 * Render Disable Cross-Sells in Cart field.
2819 *
2820 * @return void
2821 */
2822 public function field_disable_cart_cross_sells() {
2823 $this->render_pro_toggle( $this->option_disable_cart_cross_sells );
2824 }
2825
2826 /**
2827 * Render Disable Coupon Field in Checkout field.
2828 *
2829 * @return void
2830 */
2831 public function field_disable_checkout_coupon() {
2832 $this->render_pro_toggle( $this->option_disable_checkout_coupon );
2833 }
2834
2835 /**
2836 * Render Disable Order Notes in Checkout field.
2837 *
2838 * @return void
2839 */
2840 public function field_disable_checkout_order_notes() {
2841 $this->render_pro_toggle( $this->option_disable_checkout_order_notes );
2842 }
2843
2844 /**
2845 * Render Disable Login Prompt in Checkout field.
2846 *
2847 * @return void
2848 */
2849 public function field_disable_checkout_login_prompt() {
2850 $this->render_pro_toggle( $this->option_disable_checkout_login_prompt );
2851 }
2852
2853 /**
2854 * Render Enable Full Page Scroll field.
2855 *
2856 * @return void
2857 */
2858 public function field_enable_fullpage_scroll() {
2859 $this->render_pro_toggle( $this->option_enable_fullpage_scroll );
2860 }
2861
2862 /**
2863 * Render Enable Language Banner field.
2864 *
2865 * @return void
2866 */
2867 public function field_enable_language_banner() {
2868 $this->render_pro_toggle( $this->option_enable_language_banner );
2869 }
2870
2871 /**
2872 * Render toggle field for checkout inline fields (PRO).
2873 *
2874 * @return void
2875 */
2876 public function field_checkout_inline() {
2877 $this->render_pro_toggle( $this->option_checkout_inline );
2878 }
2879
2880 /**
2881 * Custom Post Types section callback.
2882 *
2883 * @return void
2884 */
2885 public function section_custom_post_types_callback() {
2886 if ( ! frbl_is_pro_active() ) {
2887 echo '<div class="tw:bg-blue-50 tw:border-l-4 tw:border-blue-400 tw:p-4 tw:mb-4">';
2888 echo '<div class="tw:flex">';
2889 echo '<div class="tw:flex-shrink-0">';
2890 echo '<svg class="tw:h-5 tw:w-5 tw:text-blue-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/></svg>';
2891 echo '</div>';
2892 echo '<div class="tw:ml-3">';
2893 echo '<p class="tw:text-sm tw:text-blue-700">';
2894 printf(
2895 /* translators: %s: FrontBlocks PRO link */
2896 esc_html__( 'This feature requires %s. Upgrade to unlock advanced functionality.', 'frontblocks' ),
2897 '<a href="https://close.technology/wordpress-plugins/frontblocks-pro/?utm_source=frontblocks&utm_medium=plugin&utm_campaign=settings" target="_blank" class="tw:font-medium tw:underline">FrontBlocks PRO</a>'
2898 );
2899 echo '</p>';
2900 echo '</div>';
2901 echo '</div>';
2902 echo '</div>';
2903 } elseif ( ! $this->is_license_valid ) {
2904 echo '<div class="tw:bg-yellow-50 tw:border-l-4 tw:border-yellow-400 tw:p-4 tw:mb-4">';
2905 echo '<div class="tw:flex">';
2906 echo '<div class="tw:flex-shrink-0">';
2907 echo '<svg class="tw:h-5 tw:w-5 tw:text-yellow-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>';
2908 echo '</div>';
2909 echo '<div class="tw:ml-3">';
2910 echo '<p class="tw:text-sm tw:text-yellow-700">';
2911 printf(
2912 /* translators: %s: License section link */
2913 esc_html__( 'License is not activated. Please activate your license in the %s section below to enable these features.', 'frontblocks' ),
2914 '<a href="#frontblocks_section_license" class="tw:font-medium tw:underline">' . esc_html__( 'License', 'frontblocks' ) . '</a>'
2915 );
2916 echo '</p>';
2917 echo '</div>';
2918 echo '</div>';
2919 echo '</div>';
2920 } else {
2921 ?>
2922 <p class="tw:text-sm tw:text-gray-600 tw:mt-0 tw:mb-4">
2923 <?php echo esc_html__( 'Create and manage custom post types with advanced configuration options.', 'frontblocks' ); ?>
2924 </p>
2925 <?php
2926 }
2927 }
2928
2929 /**
2930 * Render toggle field for enable custom post types.
2931 *
2932 * @return void
2933 */
2934 public function field_enable_custom_post_types() {
2935 $options = get_option( 'frontblocks_settings', array() );
2936 $enabled = (bool) ( $options[ $this->option_enable_custom_post_types ] ?? false );
2937 $is_enabled = $this->is_license_valid;
2938 $disabled = ! $is_enabled ? 'disabled' : '';
2939 ?>
2940 <div class="frbl-custom-post-types-wrapper">
2941 <div class="tw:flex tw:items-center tw:justify-between tw:mb-4">
2942 <label for="<?php echo esc_attr( $this->option_enable_custom_post_types ); ?>" class="tw:text-base tw:font-medium tw:text-gray-900">
2943 <?php echo esc_html__( 'Enable Custom Post Types Builder', 'frontblocks' ); ?>
2944 </label>
2945 <label class="frbl-toggle">
2946 <input type="checkbox"
2947 id="<?php echo esc_attr( $this->option_enable_custom_post_types ); ?>"
2948 name="frontblocks_settings[<?php echo esc_attr( $this->option_enable_custom_post_types ); ?>]"
2949 value="1"
2950 <?php checked( true, $enabled ); ?>
2951 <?php echo esc_attr( $disabled ); ?>
2952 />
2953 <span></span>
2954 </label>
2955 </div>
2956
2957 <?php if ( $is_enabled ) : ?>
2958 <div id="frbl-cpt-builder" class="frbl-cpt-builder" style="<?php echo $enabled ? '' : 'display: none;'; ?>">
2959 <div class="tw:mt-4 tw:p-4 tw:bg-gray-50 tw:rounded-lg tw:border tw:border-gray-200">
2960 <label for="frbl-cpt-name" class="tw:block tw:text-sm tw:font-medium tw:text-gray-700 tw:mb-2">
2961 <?php echo esc_html__( 'Post Type Name', 'frontblocks' ); ?>
2962 </label>
2963 <div class="tw:flex tw:gap-2">
2964 <input
2965 type="text"
2966 id="frbl-cpt-name"
2967 class="tw:flex-1 tw:px-3 tw:py-2 tw:border tw:border-gray-300 tw:rounded-lg tw:focus:outline-none tw:focus:ring-2 tw:focus:ring-primary-500 tw:focus:border-transparent"
2968 placeholder="<?php echo esc_attr__( 'e.g., Portfolio, Team, Services', 'frontblocks' ); ?>"
2969 />
2970 <button
2971 type="button"
2972 id="frbl-create-cpt-btn"
2973 class="tw:px-4 tw:py-2 tw:bg-primary-500 tw:text-white tw:rounded-lg tw:hover:bg-primary-600 tw:focus:outline-none tw:focus:ring-2 tw:focus:ring-primary-500 tw:transition-colors"
2974 >
2975 <?php echo esc_html__( 'Create', 'frontblocks' ); ?>
2976 </button>
2977 </div>
2978 <p class="tw:text-xs tw:text-gray-500 tw:mt-2">
2979 <?php echo esc_html__( 'Enter a singular name for your custom post type (e.g., "Portfolio" will create "portfolio" post type).', 'frontblocks' ); ?>
2980 </p>
2981 </div>
2982
2983 <?php do_action( 'frontblocks_render_existing_cpts' ); ?>
2984 </div>
2985 <?php endif; ?>
2986 </div>
2987 <?php
2988 }
2989
2990 /**
2991 * Render PRO features section after the main form.
2992 * Shows when frbl_pro_blocks filter returns blocks and license is not active.
2993 *
2994 * @return void
2995 */
2996 /**
2997 * Default PRO blocks list — shown as promotional cards when PRO is not installed.
2998 * The frbl_pro_blocks filter lets the PRO plugin override this list.
2999 *
3000 * @return array<int, array<string, string>>
3001 */
3002 private function get_default_pro_blocks(): array {
3003 return array(
3004 array(
3005 'icon' => 'meta-fields',
3006 'title' => __( 'Dynamic Meta Fields', 'frontblocks' ),
3007 'desc' => __( 'Bind any paragraph or heading to a custom post meta field, editable from the block editor.', 'frontblocks' ),
3008 ),
3009 array(
3010 'icon' => 'user-text',
3011 'title' => __( 'User Data Block', 'frontblocks' ),
3012 'desc' => __( 'Display logged-in user data with placeholders like {nombre}, {email}, {username}.', 'frontblocks' ),
3013 ),
3014 array(
3015 'icon' => 'fullpage-scroll',
3016 'title' => __( 'Full Page Scroll', 'frontblocks' ),
3017 'desc' => __( 'Full-page scroll navigation between sections with smooth transitions.', 'frontblocks' ),
3018 ),
3019 array(
3020 'icon' => 'language-banner',
3021 'title' => __( 'Language Banner', 'frontblocks' ),
3022 'desc' => __( 'Detect visitor language and show a recommendation banner (WPML/Polylang).', 'frontblocks' ),
3023 ),
3024 array(
3025 'icon' => 'gutenberg',
3026 'title' => __( 'Gutenberg in Products', 'frontblocks' ),
3027 'desc' => __( 'Use the full block editor to build WooCommerce product descriptions.', 'frontblocks' ),
3028 ),
3029 array(
3030 'icon' => 'after-add-to-cart',
3031 'title' => __( 'After Add to Cart Block', 'frontblocks' ),
3032 'desc' => __( 'Insert custom block content right after the Add to Cart button.', 'frontblocks' ),
3033 ),
3034 array(
3035 'icon' => 'simple-prices',
3036 'title' => __( 'Simple Prices Variable Products', 'frontblocks' ),
3037 'desc' => __( 'Simplified price display for variable WooCommerce products.', 'frontblocks' ),
3038 ),
3039 array(
3040 'icon' => 'horizontal-form',
3041 'title' => __( 'Horizontal Product Form', 'frontblocks' ),
3042 'desc' => __( 'Switch the WooCommerce product form to a horizontal layout.', 'frontblocks' ),
3043 ),
3044 array(
3045 'icon' => 'deactivate-tabs',
3046 'title' => __( 'Deactivate Product Tabs', 'frontblocks' ),
3047 'desc' => __( 'Remove the default tabs from WooCommerce product pages.', 'frontblocks' ),
3048 ),
3049 array(
3050 'icon' => 'disable-zoom',
3051 'title' => __( 'Disable Product Image Zoom', 'frontblocks' ),
3052 'desc' => __( 'Remove zoom effect on WooCommerce product images.', 'frontblocks' ),
3053 ),
3054 array(
3055 'icon' => 'share-buttons',
3056 'title' => __( 'Share Buttons', 'frontblocks' ),
3057 'desc' => __( 'Add social share buttons to WooCommerce product pages.', 'frontblocks' ),
3058 ),
3059 array(
3060 'icon' => 'deactivate-description',
3061 'title' => __( 'Manage Short Description', 'frontblocks' ),
3062 'desc' => __( 'Deactivate or move the WooCommerce product short description.', 'frontblocks' ),
3063 ),
3064 array(
3065 'icon' => 'default',
3066 'title' => __( 'Custom Post Types Builder', 'frontblocks' ),
3067 'desc' => __( 'Create and manage custom post types directly from the admin panel.', 'frontblocks' ),
3068 ),
3069 array(
3070 'icon' => 'horizontal-form',
3071 'title' => __( 'Checkout Inline Fields', 'frontblocks' ),
3072 'desc' => __( 'Display address, email and phone fields side by side in the WooCommerce checkout.', 'frontblocks' ),
3073 ),
3074 array(
3075 'icon' => 'popups',
3076 'title' => __( 'Popups', 'frontblocks' ),
3077 'desc' => __( 'Create popups with the block editor and configure when and where they appear.', 'frontblocks' ),
3078 ),
3079 );
3080 }
3081
3082 /**
3083 * Render PRO features section after the main form.
3084 *
3085 * @return void
3086 */
3087 private function render_pro_section(): void {
3088 $pro_blocks = apply_filters( 'frbl_pro_blocks', $this->get_default_pro_blocks() );
3089 $license_valid = function_exists( 'frblp_is_license_valid' ) && frblp_is_license_valid();
3090
3091 if ( empty( $pro_blocks ) || $license_valid ) {
3092 return;
3093 }
3094
3095 ?>
3096 <div class="frbl-section-wrapper tw:mt-6">
3097 <div class="frbl-section-header">
3098 <h2 class="tw:text-2xl tw:font-bold tw:text-gray-900 tw:mb-0">
3099 <?php echo esc_html__( 'FrontBlocks PRO Features', 'frontblocks' ); ?>
3100 </h2>
3101 <div class="tw:text-sm tw:text-gray-600">
3102 <p class="tw:mt-0 tw:mb-4">
3103 <?php
3104 $anchor = frbl_is_pro_active()
3105 ? '<a href="#frontblocks_section_license" class="tw:font-medium tw:text-red-600 tw:hover:text-red-700 tw:underline">' . esc_html__( 'License', 'frontblocks' ) . '</a>'
3106 : '<a href="https://close.technology/wordpress-plugins/frontblocks-pro/?utm_source=frontblocks&utm_medium=plugin&utm_campaign=settings-pro-showcase" target="_blank" rel="noopener noreferrer" class="tw:font-medium tw:text-red-600 tw:hover:text-red-700 tw:underline">FrontBlocks PRO</a>';
3107 printf(
3108 frbl_is_pro_active()
3109 /* translators: %s: license section link */
3110 ? esc_html__( 'Activate your license in the %s section below to unlock all features.', 'frontblocks' )
3111 /* translators: %s: FrontBlocks PRO link */
3112 : esc_html__( 'Unlock these features with %s.', 'frontblocks' ),
3113 $anchor // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
3114 );
3115 ?>
3116 </p>
3117 </div>
3118 </div>
3119
3120 <div class="frbl-features-grid">
3121 <?php foreach ( $pro_blocks as $block ) : ?>
3122 <?php UI::show_pro_info_card( $block['icon'], $block['title'], $block['desc'] ); ?>
3123 <?php endforeach; ?>
3124 </div>
3125
3126 <?php if ( ! frbl_is_pro_active() ) : ?>
3127 <div class="tw:mt-6 tw:text-center">
3128 <a
3129 href="https://close.technology/wordpress-plugins/frontblocks-pro/?utm_source=frontblocks&utm_medium=plugin&utm_campaign=settings-pro-cta"
3130 target="_blank"
3131 rel="noopener noreferrer"
3132 class="tw:inline-flex tw:items-center tw:px-6 tw:py-3 tw:border tw:border-transparent tw:text-base tw:font-medium tw:rounded-lg tw:shadow-sm tw:transition-colors tw:duration-200"
3133 style="background-color: #ef4444; color: #fff;"
3134 onmouseover="this.style.backgroundColor='#dc2626'"
3135 onmouseout="this.style.backgroundColor='#ef4444'"
3136 >
3137 <?php echo esc_html__( 'Get FrontBlocks PRO', 'frontblocks' ); ?>
3138 </a>
3139 </div>
3140 <?php endif; ?>
3141 </div>
3142 <?php
3143 }
3144
3145 /**
3146 * Render license section (separate from main form).
3147 *
3148 * @return void
3149 */
3150 private function render_license_section() {
3151 global $frblp_license;
3152
3153 ?>
3154 <div class="tw:mt-6" id="frontblocks_section_license">
3155 <?php
3156 // Check if license instance exists.
3157 if ( ! $frblp_license ) {
3158 ?>
3159 <div class="tw:p-4 tw:rounded-lg tw:bg-red-50 tw:border tw:border-red-200">
3160 <p class="tw:text-sm tw:text-red-700">
3161 <?php echo esc_html__( 'License manager not initialized.', 'frontblocks' ); ?>
3162 </p>
3163 </div>
3164 <?php
3165 return;
3166 }
3167
3168 // Check if License class exists (requires FrontBlocks PRO).
3169 if ( ! class_exists( '\Closemarketing\WPLicenseManager\License' ) ) {
3170 ?>
3171 <div class="tw:p-4 tw:rounded-lg tw:bg-yellow-50 tw:border tw:border-yellow-200">
3172 <p class="tw:text-sm tw:text-yellow-700">
3173 <?php echo esc_html__( 'License management requires FrontBlocks PRO to be installed and active.', 'frontblocks' ); ?>
3174 </p>
3175 </div>
3176 <?php
3177 return;
3178 }
3179
3180 // Render license settings inline.
3181 $this->render_inline_license_settings( $frblp_license );
3182 ?>
3183 </div>
3184 <?php
3185 }
3186
3187 /**
3188 * Render inline license settings.
3189 *
3190 * @param \Closemarketing\WPLicenseManager\License $license License instance.
3191 * @return void
3192 */
3193 private function render_inline_license_settings( $license ) {
3194 // Get license data.
3195 $license_key = $license->get_option_value( 'apikey' );
3196 $is_active = $license->is_license_active();
3197 $license_status = get_option( 'frontblocks-pro_license_activated', 'Deactivated' );
3198
3199 ?>
3200 <div class="formscrm-license-wrapper">
3201 <!-- Main Card -->
3202 <div class="formscrm-card">
3203 <!-- Header -->
3204 <div class="formscrm-card-header">
3205 <h2><?php echo esc_html__( 'FrontBlocks PRO License', 'frontblocks' ); ?></h2>
3206 <p><?php echo esc_html__( 'Manage your license to receive automatic updates and support.', 'frontblocks' ); ?></p>
3207 </div>
3208
3209 <!-- License Status -->
3210 <div class="formscrm-form-group">
3211 <?php if ( $is_active ) : ?>
3212 <div class="formscrm-status-box formscrm-status-active">
3213 <span class="formscrm-status-icon">
3214 <svg class="formscrm-icon" fill="currentColor" viewBox="0 0 20 20">
3215 <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
3216 </svg>
3217 </span>
3218 <span class="formscrm-status-text"><?php echo esc_html__( 'License Active', 'frontblocks' ); ?></span>
3219 </div>
3220 <?php else : ?>
3221 <div class="formscrm-status-box formscrm-status-inactive">
3222 <span class="formscrm-status-icon">
3223 <svg class="formscrm-icon" fill="currentColor" viewBox="0 0 20 20">
3224 <path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
3225 </svg>
3226 </span>
3227 <span class="formscrm-status-text"><?php echo esc_html__( 'License Inactive', 'frontblocks' ); ?></span>
3228 </div>
3229 <?php endif; ?>
3230 </div>
3231
3232 <!-- License Form -->
3233 <form method="post" action="options.php" class="formscrm-license-form">
3234 <?php settings_fields( 'frontblocks-pro_license' ); ?>
3235 <?php wp_nonce_field( 'Update_License_Options', 'license_nonce' ); ?>
3236
3237 <!-- License Key Field -->
3238 <div class="formscrm-form-group">
3239 <label class="formscrm-label" for="frontblocks-pro_license_apikey">
3240 <?php echo esc_html__( 'License Key', 'frontblocks' ); ?>
3241 </label>
3242 <div class="formscrm-input-group">
3243 <input
3244 type="text"
3245 id="frontblocks-pro_license_apikey"
3246 name="frontblocks-pro_license_apikey"
3247 value="<?php echo esc_attr( $license_key ); ?>"
3248 class="formscrm-input"
3249 placeholder="<?php echo esc_attr__( 'CTECH-XXXXX-XXXXX-XXXXX-XXXXX', 'frontblocks' ); ?>"
3250 <?php echo $is_active ? 'readonly' : ''; ?>
3251 />
3252 <?php if ( $is_active ) : ?>
3253 <label class="formscrm-deactivate-label">
3254 <input type="checkbox" name="frontblocks-pro_license_deactivate_checkbox" value="on" />
3255 <span><?php echo esc_html__( 'Deactivate', 'frontblocks' ); ?></span>
3256 </label>
3257 <?php endif; ?>
3258 </div>
3259 <p class="formscrm-help-text">
3260 <?php
3261 printf(
3262 /* translators: %s: Purchase URL */
3263 esc_html__( 'Enter your license key. You can find it in %s.', 'frontblocks' ),
3264 '<a href="https://close.technology/my-account/" target="_blank">' . esc_html__( 'your account', 'frontblocks' ) . '</a>'
3265 );
3266 ?>
3267 </p>
3268 </div>
3269
3270 <!-- License Status -->
3271 <div class="formscrm-form-group">
3272 <label class="formscrm-label"><?php echo esc_html__( 'License Status', 'frontblocks' ); ?></label>
3273 <div class="formscrm-status-box <?php echo $is_active ? 'formscrm-status-active' : 'formscrm-status-inactive'; ?>">
3274 <span class="formscrm-status-icon">
3275 <?php if ( $is_active ) : ?>
3276 <svg class="formscrm-icon" fill="currentColor" viewBox="0 0 20 20">
3277 <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
3278 </svg>
3279 <?php else : ?>
3280 <svg class="formscrm-icon" fill="currentColor" viewBox="0 0 20 20">
3281 <path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
3282 </svg>
3283 <?php endif; ?>
3284 </span>
3285 <span class="formscrm-status-text">
3286 <?php echo $is_active ? esc_html__( 'Active', 'frontblocks' ) : esc_html__( 'Not Activated', 'frontblocks' ); ?>
3287 </span>
3288 </div>
3289 </div>
3290
3291 <!-- Submit Button -->
3292 <div class="formscrm-form-actions">
3293 <button type="submit" name="submit_license" class="formscrm-button formscrm-button-primary">
3294 <?php echo $is_active ? esc_html__( 'Update License', 'frontblocks' ) : esc_html__( 'Activate License', 'frontblocks' ); ?>
3295 </button>
3296 </div>
3297 </form>
3298 </div>
3299
3300 <!-- Sidebar Info -->
3301 <div class="formscrm-info-card">
3302 <h3><?php echo esc_html__( 'License Benefits', 'frontblocks' ); ?></h3>
3303 <p><?php echo esc_html__( 'An active license provides the following benefits:', 'frontblocks' ); ?></p>
3304
3305 <ul class="formscrm-benefits-list">
3306 <li><?php echo esc_html__( 'Automatic plugin updates', 'frontblocks' ); ?></li>
3307 <li><?php echo esc_html__( 'Access to new features', 'frontblocks' ); ?></li>
3308 <li><?php echo esc_html__( 'Priority support', 'frontblocks' ); ?></li>
3309 <li><?php echo esc_html__( 'Security patches', 'frontblocks' ); ?></li>
3310 </ul>
3311
3312 <hr style="margin: 20px 0; border: none; border-top: 1px solid #e2e8f0;">
3313
3314 <div style="font-size: 0.875rem; color: #64748b;">
3315 <p style="margin-bottom: 8px;">
3316 <strong><?php echo esc_html__( 'Need Help?', 'frontblocks' ); ?></strong>
3317 </p>
3318 <p style="margin-bottom: 8px;">
3319 <a href="https://close.technology/wordpress-plugins/frontblocks-pro/" target="_blank" style="color: #8b5cf6; text-decoration: none;">
3320 <?php echo esc_html__( 'Purchase License', 'frontblocks' ); ?>
3321 </a>
3322 </p>
3323 <p style="margin-bottom: 8px;">
3324 <a href="https://close.technology/my-account/" target="_blank" style="color: #8b5cf6; text-decoration: none;">
3325 <?php echo esc_html__( 'My Account', 'frontblocks' ); ?>
3326 </a>
3327 </p>
3328 <p>
3329 <a href="https://close.technology/support/" target="_blank" style="color: #8b5cf6; text-decoration: none;">
3330 <?php echo esc_html__( 'Support', 'frontblocks' ); ?>
3331 </a>
3332 </p>
3333 </div>
3334 </div>
3335 </div>
3336 <?php
3337 }
3338
3339 /**
3340 * Helper method to render PRO toggle fields.
3341 *
3342 * @param string $option_key Option key.
3343 * @return void
3344 */
3345 private function render_pro_toggle( $option_key ) {
3346 $options = get_option( 'frontblocks_settings', array() );
3347 $enabled = (bool) ( $options[ $option_key ] ?? false );
3348 $is_enabled = $this->is_license_valid;
3349 $disabled = ! $is_enabled ? 'disabled' : '';
3350 ?>
3351 <label class="frbl-toggle">
3352 <input type="checkbox"
3353 id="<?php echo esc_attr( $option_key ); ?>"
3354 name="frontblocks_settings[<?php echo esc_attr( $option_key ); ?>]"
3355 value="1"
3356 <?php checked( true, $enabled ); ?>
3357 <?php echo esc_attr( $disabled ); ?>
3358 />
3359 <span></span>
3360 </label>
3361 <?php
3362 }
3363
3364 /**
3365 * Sanitize settings array.
3366 *
3367 * @param array $value Raw value.
3368 * @return array
3369 */
3370 public function sanitize_settings( $value ) {
3371 // Nonce verification.
3372 $nonce = isset( $_POST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
3373 if ( empty( $nonce ) || ! wp_verify_nonce( $nonce, 'frontblocks_settings-options' ) ) {
3374 add_settings_error( 'frontblocks_settings', 'frontblocks_settings_nonce', esc_html__( 'Security check failed. Please try again.', 'frontblocks' ), 'error' );
3375
3376 return get_option( 'frontblocks_settings', array() );
3377 }
3378
3379 if ( ! is_array( $value ) ) {
3380 return array();
3381 }
3382
3383 // Get current options to preserve unchecked checkboxes.
3384 $current_options = get_option( 'frontblocks_settings', array() );
3385
3386 // Initialize sanitized array with current values.
3387 $sanitized = $current_options;
3388
3389 // List of all boolean options (checkboxes).
3390 $boolean_options = array(
3391 $this->option_enable_testimonials,
3392 $this->option_enable_reading_progress,
3393 $this->option_enable_back_button,
3394 $this->option_enable_scroll_top,
3395 $this->option_enable_events,
3396 $this->option_enable_fluid_typography,
3397 $this->option_enable_maintenance,
3398 $this->option_enable_cookie_notice,
3399 $this->option_enable_gutenberg,
3400 $this->option_enable_simple_prices_variable_products,
3401 $this->option_enable_after_add_to_cart,
3402 $this->option_deactivate_short_description,
3403 $this->option_move_content_to_short_description,
3404 $this->option_disable_zoom_images,
3405 $this->option_add_share_buttons,
3406 $this->option_deactivate_product_tabs,
3407 $this->option_horizontal_product_form,
3408 $this->option_enable_variant_display_mode,
3409 $this->option_enable_custom_post_types,
3410 $this->option_enable_fullpage_scroll,
3411 $this->option_enable_language_banner,
3412 $this->option_enable_popups,
3413 $this->option_checkout_inline,
3414 $this->option_disable_cart_coupon,
3415 $this->option_disable_cart_cross_sells,
3416 $this->option_disable_checkout_coupon,
3417 $this->option_disable_checkout_order_notes,
3418 $this->option_disable_checkout_login_prompt,
3419 );
3420
3421 // Initialize all boolean options to false (unchecked checkboxes are not submitted).
3422 foreach ( $boolean_options as $option ) {
3423 $sanitized[ $option ] = false;
3424 }
3425
3426 // Process submitted values.
3427 foreach ( $value as $key => $val ) {
3428 if ( in_array( $key, $boolean_options, true ) ) {
3429 $sanitized[ $key ] = (bool) $val;
3430 } elseif ( $this->option_events_type === $key ) {
3431 // Sanitize events type: only allow 'cpt' or 'posts'.
3432 $sanitized[ $key ] = in_array( $val, array( 'cpt', 'posts' ), true ) ? $val : 'cpt';
3433 } elseif ( $this->option_scroll_top_position === $key ) {
3434 $sanitized[ $key ] = in_array( $val, array( 'bottom-right', 'bottom-left' ), true ) ? $val : 'bottom-right';
3435 } elseif ( $this->option_scroll_top_icon_url === $key ) {
3436 $sanitized[ $key ] = esc_url_raw( $val );
3437 } elseif ( $this->option_maintenance_title === $key ) {
3438 $sanitized[ $key ] = sanitize_text_field( $val );
3439 } elseif ( $this->option_maintenance_image === $key ) {
3440 $sanitized[ $key ] = absint( $val );
3441 } elseif ( $this->option_cookie_notice_message === $key ) {
3442 $sanitized[ $key ] = sanitize_textarea_field( $val );
3443 } elseif ( in_array( $key, array( $this->option_cookie_notice_accept_label, $this->option_cookie_notice_reject_label ), true ) ) {
3444 $sanitized[ $key ] = sanitize_text_field( $val );
3445 } elseif ( $this->option_cookie_notice_policy_page_id === $key ) {
3446 $sanitized[ $key ] = absint( $val );
3447 } elseif ( $this->option_cookie_notice_layout === $key ) {
3448 $sanitized[ $key ] = in_array( $val, array( 'bar', 'box', 'popup' ), true ) ? $val : 'bar';
3449 } elseif ( $this->option_cookie_notice_position === $key ) {
3450 $sanitized[ $key ] = in_array( $val, array( 'bottom-right', 'bottom-left' ), true ) ? $val : 'bottom-right';
3451 } elseif ( $this->option_cookie_notice_color === $key ) {
3452 $hex_color = sanitize_hex_color( $val );
3453 $sanitized[ $key ] = $hex_color ? $hex_color : '#687df9';
3454 } elseif ( $this->option_cookie_notice_bg_color === $key ) {
3455 $hex_color = sanitize_hex_color( $val );
3456 $sanitized[ $key ] = $hex_color ? $hex_color : '#ffffff';
3457 } elseif ( $this->option_cookie_notice_radius === $key ) {
3458 $sanitized[ $key ] = in_array( $val, array( 'none', 'small', 'large' ), true ) ? $val : 'small';
3459 } elseif ( $this->option_cookie_notice_expiration_days === $key ) {
3460 $days = absint( $val );
3461 $sanitized[ $key ] = $days > 0 ? min( $days, 730 ) : 365;
3462 } elseif ( $this->option_cookie_notice_gtm_id === $key ) {
3463 $gtm_id = strtoupper( sanitize_text_field( $val ) );
3464 $sanitized[ $key ] = preg_match( '/^GTM-[A-Z0-9]+$/', $gtm_id ) ? $gtm_id : '';
3465 } elseif ( $this->option_cookie_notice_ga4_id === $key ) {
3466 $ga4_id = strtoupper( sanitize_text_field( $val ) );
3467 $sanitized[ $key ] = preg_match( '/^G-[A-Z0-9]+$/', $ga4_id ) ? $ga4_id : '';
3468 }
3469 }
3470
3471 if ( array_key_exists( 'cookie_notice_tracking_integration_code', $value ) || array_key_exists( 'cookie_notice_tracking_remove', $value ) ) {
3472 $tracking_integrations = \FrontBlocks\Frontend\CookieNotice::get_tracking_integrations( $current_options );
3473 $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();
3474 $tracking_integrations = array_values(
3475 array_filter(
3476 $tracking_integrations,
3477 static function ( $integration ) use ( $remove_types ) {
3478 return ! in_array( $integration['type'], $remove_types, true );
3479 }
3480 )
3481 );
3482
3483 $raw_code = (string) ( $value['cookie_notice_tracking_integration_code'] ?? '' );
3484 $detected = \FrontBlocks\Frontend\CookieNotice::detect_tracking_snippet( $raw_code );
3485
3486 if ( null === $detected && '' !== trim( $raw_code ) ) {
3487 add_settings_error(
3488 'frontblocks_settings',
3489 'frbl_cookie_notice_tracking_unrecognized',
3490 sprintf(
3491 /* translators: %s: contact page URL. */
3492 esc_html__( 'The tracking code was not recognized and was not saved. Need support for this tool? Contact us at %s.', 'frontblocks' ),
3493 'close.technology/contacto'
3494 ),
3495 'error'
3496 );
3497 }
3498
3499 if ( $detected ) {
3500 $tracking_integrations = array_values(
3501 array_filter(
3502 $tracking_integrations,
3503 static function ( $integration ) use ( $detected ) {
3504 return $integration['type'] !== $detected['type'];
3505 }
3506 )
3507 );
3508 $tracking_integrations[] = array(
3509 'type' => $detected['type'],
3510 'id' => sanitize_text_field( $detected['id'] ),
3511 );
3512 }
3513
3514 $sanitized[ $this->option_cookie_notice_tracking_integrations ] = $tracking_integrations;
3515 unset( $sanitized['cookie_notice_tracking_type'], $sanitized['cookie_notice_tracking_id'] );
3516 }
3517
3518 // Ensure mutual exclusion: if both description options are enabled, keep only the last one changed.
3519 if ( ! empty( $sanitized[ $this->option_deactivate_short_description ] ) && ! empty( $sanitized[ $this->option_move_content_to_short_description ] ) ) {
3520 // Get current saved values to determine which one was just changed.
3521 $current_deactivate = ! empty( $current_options[ $this->option_deactivate_short_description ] );
3522 $current_move = ! empty( $current_options[ $this->option_move_content_to_short_description ] );
3523
3524 // If deactivate was already on, turn it off (move is the new one).
3525 if ( $current_deactivate ) {
3526 $sanitized[ $this->option_deactivate_short_description ] = false;
3527 } else {
3528 // Otherwise turn off move (deactivate is the new one).
3529 $sanitized[ $this->option_move_content_to_short_description ] = false;
3530 }
3531 }
3532
3533 do_action( 'frontblocks_sanitize_settings', $sanitized );
3534
3535 return $sanitized;
3536 }
3537 }
3538