PluginProbe ʕ •ᴥ•ʔ
FrontBlocks for Gutenberg/GeneratePress / 1.3.0
FrontBlocks for Gutenberg/GeneratePress v1.3.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
Settings.php 7 months ago UI.php 7 months ago
Settings.php
1146 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 Gutenberg in products (PRO).
45 *
46 * @var string
47 */
48 private $option_enable_gutenberg = 'enable_gutenberg';
49
50 /**
51 * Option key for Simple Prices Variable Products (PRO).
52 *
53 * @var string
54 */
55 private $option_enable_simple_prices_variable_products = 'enable_simple_prices_variable_products';
56
57 /**
58 * Option key for After Add to Cart Block (PRO).
59 *
60 * @var string
61 */
62 private $option_enable_after_add_to_cart = 'enable_after_add_to_cart';
63
64 /**
65 * Option key for deactivate short description (PRO).
66 *
67 * @var string
68 */
69 private $option_deactivate_short_description = 'deactivate_short_description';
70
71 /**
72 * Option key for move content to short description (PRO).
73 *
74 * @var string
75 */
76 private $option_move_content_to_short_description = 'move_content_to_short_description';
77
78 /**
79 * Option key for disable zoom in WooCommerce images (PRO).
80 *
81 * @var string
82 */
83 private $option_disable_zoom_images = 'disable_zoom_images';
84
85 /**
86 * Option key for add share buttons in product page (PRO).
87 *
88 * @var string
89 */
90 private $option_add_share_buttons = 'add_share_buttons';
91
92 /**
93 * Option key for deactivate product tabs (PRO).
94 *
95 * @var string
96 */
97 private $option_deactivate_product_tabs = 'deactivate_product_tabs';
98
99 /**
100 * Option key for horizontal product form layout (PRO).
101 *
102 * @var string
103 */
104 private $option_horizontal_product_form = 'horizontal_product_form';
105
106 /**
107 * Page slug.
108 *
109 * @var string
110 */
111 private $page_slug = 'frontblocks-settings';
112
113 /**
114 * Is license valid.
115 *
116 * @var bool
117 */
118 private $is_license_valid = false;
119
120 /**
121 * Option key for license key.
122 *
123 * @var string
124 */
125 private $option_license_key;
126
127 /**
128 * Constructor.
129 */
130 public function __construct() {
131 global $frontblocks_pro_license;
132 $this->is_license_valid = ! empty( $frontblocks_pro_license ) && $frontblocks_pro_license->get_api_key_status( true );
133
134 $this->option_license_key = ! empty( $frontblocks_pro_license ) ? $frontblocks_pro_license->get_option_key( 'apikey' ) : '';
135
136 add_action( 'admin_menu', array( $this, 'register_menu' ) );
137 add_action( 'admin_init', array( $this, 'register_settings' ) );
138 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_styles' ) );
139 }
140
141 /**
142 * Enqueue admin styles for settings page.
143 *
144 * @param string $hook Current admin page hook.
145 * @return void
146 */
147 public function enqueue_admin_styles( $hook ) {
148 if ( 'appearance_page_' . $this->page_slug !== $hook ) {
149 return;
150 }
151
152 wp_enqueue_style(
153 'frontblocks-admin-settings',
154 FRBL_PLUGIN_URL . 'assets/admin/settings.css',
155 array(),
156 FRBL_VERSION
157 );
158
159 wp_add_inline_script(
160 'jquery',
161 "
162 document.addEventListener('DOMContentLoaded', function() {
163 const deactivateCheckbox = document.getElementById('deactivate_short_description');
164 const moveContentCheckbox = document.getElementById('move_content_to_short_description');
165
166 if (!deactivateCheckbox || !moveContentCheckbox) return;
167
168 function updateMutualExclusion() {
169 const deactivateWrapper = deactivateCheckbox.closest('.tw-flex');
170 const moveContentWrapper = moveContentCheckbox.closest('.tw-flex');
171
172 // Check if license is valid (not just PRO active).
173 const isLicenseValid = " . ( $this->is_license_valid ? 'true' : 'false' ) . ";
174
175 if (deactivateCheckbox.checked) {
176 moveContentCheckbox.disabled = true;
177 if (moveContentWrapper) {
178 moveContentWrapper.style.opacity = '0.5';
179 moveContentWrapper.style.filter = 'grayscale(100%)';
180 const toggle = moveContentWrapper.querySelector('.frbl-toggle');
181 if (toggle) {
182 toggle.style.borderColor = '#ef4444';
183 toggle.style.opacity = '0.7';
184 }
185 }
186 } else {
187 moveContentCheckbox.disabled = !isLicenseValid;
188 if (moveContentWrapper) {
189 moveContentWrapper.style.opacity = isLicenseValid ? '1' : '0.5';
190 moveContentWrapper.style.filter = '';
191 const toggle = moveContentWrapper.querySelector('.frbl-toggle');
192 if (toggle) {
193 toggle.style.borderColor = '';
194 toggle.style.opacity = '';
195 }
196 }
197 }
198
199 if (moveContentCheckbox.checked) {
200 deactivateCheckbox.disabled = true;
201 if (deactivateWrapper) {
202 deactivateWrapper.style.opacity = '0.5';
203 deactivateWrapper.style.filter = 'grayscale(100%)';
204 const toggle = deactivateWrapper.querySelector('.frbl-toggle');
205 if (toggle) {
206 toggle.style.borderColor = '#ef4444';
207 toggle.style.opacity = '0.7';
208 }
209 }
210 } else {
211 deactivateCheckbox.disabled = !isLicenseValid;
212 if (deactivateWrapper) {
213 deactivateWrapper.style.opacity = isLicenseValid ? '1' : '0.5';
214 deactivateWrapper.style.filter = '';
215 const toggle = deactivateWrapper.querySelector('.frbl-toggle');
216 if (toggle) {
217 toggle.style.borderColor = '';
218 toggle.style.opacity = '';
219 }
220 }
221 }
222 }
223
224 deactivateCheckbox.addEventListener('change', updateMutualExclusion);
225 moveContentCheckbox.addEventListener('change', updateMutualExclusion);
226
227 updateMutualExclusion();
228 });
229 "
230 );
231 }
232
233 /**
234 * Register options page under Appearance.
235 *
236 * @return void
237 */
238 public function register_menu() {
239 add_theme_page(
240 __( 'FrontBlocks Settings', 'frontblocks' ),
241 __( 'FrontBlocks', 'frontblocks' ),
242 'edit_theme_options',
243 $this->page_slug,
244 array( $this, 'render_page' )
245 );
246 }
247
248 /**
249 * Register settings, sections and fields.
250 *
251 * @return void
252 */
253 public function register_settings() {
254 register_setting(
255 'frontblocks_settings',
256 'frontblocks_settings',
257 array(
258 'type' => 'array',
259 'sanitize_callback' => array( $this, 'sanitize_settings' ),
260 'default' => array(),
261 'show_in_rest' => false,
262 )
263 );
264
265 // Always Active Blocks section.
266 add_settings_section(
267 'frontblocks_section_active_blocks',
268 __( 'Active Blocks & Features', 'frontblocks' ),
269 array( $this, 'section_active_blocks_callback' ),
270 $this->page_slug
271 );
272
273 add_settings_section(
274 'frontblocks_section_features',
275 __( 'Optional Features', 'frontblocks' ),
276 array( $this, 'section_features_callback' ),
277 $this->page_slug
278 );
279
280 add_settings_field(
281 $this->option_enable_testimonials,
282 __( 'Enable testimonials', 'frontblocks' ),
283 array( $this, 'field_enable_testimonials' ),
284 $this->page_slug,
285 'frontblocks_section_features'
286 );
287
288 add_settings_field(
289 $this->option_enable_reading_progress,
290 __( 'Enable reading progress bar', 'frontblocks' ),
291 array( $this, 'field_enable_reading_progress' ),
292 $this->page_slug,
293 'frontblocks_section_features'
294 );
295
296 add_settings_field(
297 $this->option_enable_back_button,
298 __( 'Enable Back Button', 'frontblocks' ),
299 array( $this, 'field_enable_back_button' ),
300 $this->page_slug,
301 'frontblocks_section_features'
302 );
303
304 // PRO Features section.
305 add_settings_section(
306 'frontblocks_section_woocommerce_features',
307 __( 'WooCommerce Features', 'frontblocks' ),
308 array( $this, 'section_woo_features_callback' ),
309 $this->page_slug
310 );
311
312 add_settings_field(
313 $this->option_enable_gutenberg,
314 __( 'Enable Gutenberg in Products', 'frontblocks' ),
315 array( $this, 'field_enable_gutenberg' ),
316 $this->page_slug,
317 'frontblocks_section_woocommerce_features'
318 );
319
320 add_settings_field(
321 $this->option_enable_simple_prices_variable_products,
322 __( 'Enable Simple Prices Variable Products', 'frontblocks' ),
323 array( $this, 'field_enable_simple_prices_variable_products' ),
324 $this->page_slug,
325 'frontblocks_section_woocommerce_features'
326 );
327
328 add_settings_field(
329 $this->option_enable_after_add_to_cart,
330 __( 'Enable After Add to Cart Block', 'frontblocks' ),
331 array( $this, 'field_enable_after_add_to_cart' ),
332 $this->page_slug,
333 'frontblocks_section_woocommerce_features'
334 );
335
336 add_settings_field(
337 $this->option_deactivate_short_description,
338 __( 'Deactivate Short Description', 'frontblocks' ),
339 array( $this, 'field_deactivate_short_description' ),
340 $this->page_slug,
341 'frontblocks_section_woocommerce_features'
342 );
343
344 add_settings_field(
345 $this->option_move_content_to_short_description,
346 __( 'Move Content to Short Description', 'frontblocks' ),
347 array( $this, 'field_move_content_to_short_description' ),
348 $this->page_slug,
349 'frontblocks_section_woocommerce_features'
350 );
351
352 add_settings_field(
353 $this->option_disable_zoom_images,
354 __( 'Disable Zoom in Product Images', 'frontblocks' ),
355 array( $this, 'field_disable_zoom_images' ),
356 $this->page_slug,
357 'frontblocks_section_woocommerce_features'
358 );
359
360 add_settings_field(
361 $this->option_add_share_buttons,
362 __( 'Add Share Buttons in Product Page', 'frontblocks' ),
363 array( $this, 'field_add_share_buttons' ),
364 $this->page_slug,
365 'frontblocks_section_woocommerce_features'
366 );
367
368 add_settings_field(
369 $this->option_deactivate_product_tabs,
370 __( 'Deactivate Product Tabs', 'frontblocks' ),
371 array( $this, 'field_deactivate_product_tabs' ),
372 $this->page_slug,
373 'frontblocks_section_woocommerce_features'
374 );
375
376 add_settings_field(
377 $this->option_horizontal_product_form,
378 __( 'Horizontal Product Form Layout', 'frontblocks' ),
379 array( $this, 'field_horizontal_product_form' ),
380 $this->page_slug,
381 'frontblocks_section_woocommerce_features'
382 );
383
384 // License section (only if PRO is active).
385 if ( frbl_is_pro_active() ) {
386 global $frontblocks_pro_license;
387 add_settings_section(
388 'frontblocks_section_license',
389 __( 'License', 'frontblocks' ),
390 array( $this, 'section_license_callback' ),
391 $this->page_slug
392 );
393
394 add_settings_field(
395 $frontblocks_pro_license->get_option_key( 'apikey' ),
396 __( 'License Information', 'frontblocks' ),
397 array( $this, 'field_license_key' ),
398 $this->page_slug,
399 'frontblocks_section_license'
400 );
401 }
402
403 do_action( 'frontblocks_register_settings' );
404 }
405
406 /**
407 * Render settings page.
408 *
409 * @return void
410 */
411 public function render_page() {
412 if ( ! current_user_can( 'edit_theme_options' ) ) {
413 return;
414 }
415 ?>
416 <div class="frbl-settings-wrapper tw-min-h-screen tw-bg-gray-50 tw-py-8">
417 <div class="tw-max-w-5xl tw-mx-auto tw-px-4 sm:tw-px-6 lg:tw-px-8">
418 <!-- Header Section -->
419 <div class="tw-mb-8 frbl-animate-slide-in">
420 <div class="tw-flex tw-items-center tw-justify-between">
421 <div>
422 <h1 class="tw-text-3xl tw-font-bold tw-text-gray-900 tw-mb-2">
423 <?php echo esc_html__( 'FrontBlocks Settings', 'frontblocks' ); ?>
424 </h1>
425 <p class="tw-text-gray-600">
426 <?php echo esc_html__( 'Add visual enhancements to your website with FrontBlocks.', 'frontblocks' ); ?>
427 </p>
428 </div>
429 <div class="tw-flex tw-items-center tw-space-x-2">
430 <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">
431 <?php echo esc_html__( 'Version', 'frontblocks' ) . ' ' . esc_html( FRBL_VERSION ); ?>
432 </span>
433 </div>
434 </div>
435 </div>
436
437 <?php
438 // Show success message after settings are saved.
439 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
440 if ( isset( $_GET['settings-updated'] ) && 'true' === sanitize_text_field( wp_unslash( $_GET['settings-updated'] ) ) ) :
441 ?>
442 <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);">
443 <div class="tw-flex">
444 <div class="tw-flex-shrink-0">
445 <svg class="tw-h-5 tw-w-5" style="color: #4ade80;" viewBox="0 0 20 20" fill="currentColor">
446 <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"/>
447 </svg>
448 </div>
449 <div class="tw-ml-3">
450 <p class="tw-text-sm tw-font-medium" style="color: #15803d; margin: 0;">
451 <?php esc_html_e( 'Changes saved successfully', 'frontblocks' ); ?>
452 </p>
453 </div>
454 </div>
455 </div>
456 <?php
457 endif;
458
459 // Show error message if saving failed.
460 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
461 if ( isset( $_GET['settings-error'] ) && 'true' === sanitize_text_field( wp_unslash( $_GET['settings-error'] ) ) ) :
462 ?>
463 <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);">
464 <div class="tw-flex">
465 <div class="tw-flex-shrink-0">
466 <svg class="tw-h-5 tw-w-5" style="color: #f87171;" viewBox="0 0 20 20" fill="currentColor">
467 <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"/>
468 </svg>
469 </div>
470 <div class="tw-ml-3">
471 <p class="tw-text-sm tw-font-medium" style="color: #991b1b; margin: 0;">
472 <?php esc_html_e( 'Failed to save changes. Please try again.', 'frontblocks' ); ?>
473 </p>
474 </div>
475 </div>
476 </div>
477 <?php
478 endif;
479 ?>
480
481 <!-- Settings Form -->
482 <form method="post" action="options.php" class="tw-space-y-6">
483 <?php settings_fields( 'frontblocks_settings' ); ?>
484
485 <?php
486 // Get all sections for this page.
487 global $wp_settings_sections, $wp_settings_fields;
488
489 if ( ! isset( $wp_settings_sections[ $this->page_slug ] ) ) {
490 return;
491 }
492
493 foreach ( (array) $wp_settings_sections[ $this->page_slug ] as $section ) {
494 $this->render_settings_section( $section );
495 }
496 ?>
497
498 <!-- Submit Button -->
499 <div class="tw-flex tw-items-center tw-justify-between tw-pt-6 tw-border-t tw-border-gray-200">
500 <div class="tw-text-sm tw-text-gray-500">
501 <?php echo esc_html__( 'Changes will be applied immediately after saving.', 'frontblocks' ); ?>
502 </div>
503 <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 hover:tw-bg-primary-600 focus:tw-outline-none focus:tw-ring-2 focus:tw-ring-offset-2 focus:tw-ring-primary-500 tw-transition-colors tw-duration-200">
504 <svg class="tw-w-5 tw-h-5 tw-mr-2 tw--ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
505 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
506 </svg>
507 <?php echo esc_html__( 'Save Settings', 'frontblocks' ); ?>
508 </button>
509 </div>
510 </form>
511
512 <!-- Footer Info -->
513 <div class="tw-mt-8 tw-text-center tw-text-sm tw-text-gray-500">
514 <?php
515 printf(
516 /* translators: %s: Close·marketing link */
517 esc_html__( 'Made with ❤️ by %s', 'frontblocks' ),
518 '<a href="https://close.technology/?utm_source=frontblocks&utm_medium=plugin&utm_campaign=settings" target="_blank" rel="noopener noreferrer" class="tw-text-primary-500 hover:tw-text-primary-600 tw-font-medium">Close·Technology</a>'
519 );
520 ?>
521 </div>
522 </div>
523 </div>
524 <?php
525 }
526
527 /**
528 * Active Blocks section callback.
529 *
530 * @return void
531 */
532 private function section_active_blocks_callback() {
533 ?>
534 <p class="tw-text-sm tw-text-gray-600 tw-mt-0 tw-mb-4">
535 <?php echo esc_html__( 'These blocks and features are always active and available in the block editor.', 'frontblocks' ); ?>
536 </p>
537 <div class="frbl-features-grid">
538 <?php
539 UI::show_info_card( 'animations', __( 'Animations', 'frontblocks' ), __( 'Add animations to any block using Animate.css', 'frontblocks' ) );
540 UI::show_info_card( 'carousel', __( 'Carousel/Slider', 'frontblocks' ), __( 'Transform any Grid block into a carousel or slider', 'frontblocks' ) );
541 UI::show_info_card( 'gallery', __( 'Native Gallery', 'frontblocks' ), __( 'Enhanced gallery block with carousel and masonry options', 'frontblocks' ) );
542 UI::show_info_card( 'sticky', __( 'Sticky Columns', 'frontblocks' ), __( 'Make Grid blocks sticky when scrolling', 'frontblocks' ) );
543 UI::show_info_card( 'insert_post', __( 'Insert Post Block', 'frontblocks' ), __( 'Display content from other posts, pages or custom post types', 'frontblocks' ) );
544 UI::show_info_card( 'counter', __( 'Counter Block', 'frontblocks' ), __( 'Display animated counters with start and end values', 'frontblocks' ) );
545 UI::show_info_card( 'reading_time', __( 'Reading Time Block', 'frontblocks' ), __( 'Show estimated reading time for posts', 'frontblocks' ) );
546 UI::show_info_card( 'product_categories', __( 'Product Categories Block', 'frontblocks' ), __( 'Display WooCommerce product categories', 'frontblocks' ) );
547 ?>
548 </div>
549 <?php
550 }
551
552 /**
553 * Features section callback.
554 *
555 * @return void
556 */
557 private function section_features_callback() {
558 ?>
559 <p class="tw-text-sm tw-text-gray-600 tw-mt-0 tw-mb-4">
560 <?php echo esc_html__( 'Enable or disable these optional features as needed.', 'frontblocks' ); ?>
561 </p>
562 <?php
563 }
564
565 /**
566 * Render a single settings section as a card.
567 *
568 * @param array $section Section data.
569 * @return void
570 */
571 private function render_settings_section( $section ) {
572 global $wp_settings_fields;
573
574 $has_fields = isset( $wp_settings_fields[ $this->page_slug ][ $section['id'] ] );
575
576 // Si no hay campos Y no hay callback, no renderizar nada.
577 if ( ! $has_fields && ! $section['callback'] ) {
578 return;
579 }
580
581 // Check if this is a section with callback only (like active_blocks).
582 $is_callback_only = ! $has_fields && $section['callback'];
583
584 // Check if this is the license section - render it full width.
585 $is_license_section = 'frontblocks_section_license' === $section['id'];
586
587 if ( $is_callback_only ) {
588 // Render section with only callback (no fields).
589 ?>
590 <div class="frbl-section-wrapper">
591 <div class="frbl-section-header">
592 <h2 class="tw-text-2xl tw-font-bold tw-text-gray-900 tw-mb-0">
593 <?php echo esc_html( $section['title'] ); ?>
594 </h2>
595 </div>
596 <?php call_user_func( $section['callback'], $section ); ?>
597 </div>
598 <?php
599 return;
600 }
601
602 if ( $is_license_section ) {
603 // Render license section as a full-width card.
604 ?>
605 <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">
606 <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">
607 <h2 class="tw-text-xl tw-font-semibold tw-text-gray-900">
608 <?php echo esc_html( $section['title'] ); ?>
609 </h2>
610 <?php
611 if ( $section['callback'] ) {
612 echo '<div class="tw-mt-2 tw-text-sm tw-text-gray-600">';
613 call_user_func( $section['callback'], $section );
614 echo '</div>';
615 }
616 ?>
617 </div>
618 <div class="tw-px-6 tw-py-5">
619 <?php
620 foreach ( (array) $wp_settings_fields[ $this->page_slug ][ $section['id'] ] as $field ) {
621 call_user_func( $field['callback'], $field['args'] );
622 }
623 ?>
624 </div>
625 </div>
626 <?php
627 } else {
628 // Render regular sections with feature grid.
629 ?>
630 <div class="frbl-section-wrapper">
631 <!-- Section Header -->
632 <div class="frbl-section-header">
633 <h2 class="tw-text-2xl tw-font-bold tw-text-gray-900 tw-mb-0">
634 <?php echo esc_html( $section['title'] ); ?>
635 </h2>
636 <?php
637 if ( $section['callback'] ) {
638 echo '<div class="tw-text-sm tw-text-gray-600">';
639 call_user_func( $section['callback'], $section );
640 echo '</div>';
641 }
642 ?>
643 </div>
644
645 <!-- Features Grid -->
646 <div class="frbl-features-grid">
647 <?php
648 foreach ( (array) $wp_settings_fields[ $this->page_slug ][ $section['id'] ] as $field ) {
649 $this->render_settings_field( $field );
650 }
651 ?>
652 </div>
653 </div>
654 <?php
655 }
656 }
657
658 /**
659 * Render a single settings field as a card.
660 *
661 * @param array $field Field data.
662 * @return void
663 */
664 private function render_settings_field( $field ) {
665 // Determine if this is a PRO feature (always, regardless of license status).
666 $is_pro_feature = in_array(
667 $field['id'],
668 array(
669 $this->option_enable_gutenberg,
670 $this->option_enable_simple_prices_variable_products,
671 $this->option_enable_after_add_to_cart,
672 $this->option_deactivate_short_description,
673 $this->option_move_content_to_short_description,
674 $this->option_disable_zoom_images,
675 $this->option_add_share_buttons,
676 $this->option_deactivate_product_tabs,
677 $this->option_horizontal_product_form,
678 ),
679 true
680 );
681
682 // Apply PRO styling only if license is not valid.
683 $needs_license = $is_pro_feature && ! $this->is_license_valid;
684
685 // Get icon for this feature.
686 $icon = $this->get_feature_icon( $field['id'] );
687
688 ?>
689 <div class="frbl-feature-card <?php echo $needs_license ? 'frbl-feature-pro' : ''; ?>">
690 <?php if ( $is_pro_feature ) : ?>
691 <div class="frbl-pro-badge">PRO</div>
692 <?php endif; ?>
693
694 <div class="frbl-feature-content">
695 <div class="frbl-feature-icon">
696 <?php echo $icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
697 </div>
698 <div class="frbl-feature-info">
699 <h3 class="frbl-feature-title">
700 <?php echo esc_html( $field['title'] ); ?>
701 </h3>
702 </div>
703 <div class="frbl-feature-toggle">
704 <?php call_user_func( $field['callback'], $field['args'] ); ?>
705 </div>
706 </div>
707 </div>
708 <?php
709 }
710
711 /**
712 * Get icon SVG for a feature.
713 *
714 * @param string $field_id Field ID.
715 * @return string SVG icon markup.
716 */
717 private function get_feature_icon( $field_id ) {
718 // Map field IDs to icon file names.
719 $icon_map = array(
720 $this->option_enable_testimonials => 'testimonials',
721 $this->option_enable_reading_progress => 'reading-progress',
722 $this->option_enable_back_button => 'back-button',
723 $this->option_enable_gutenberg => 'gutenberg',
724 $this->option_enable_simple_prices_variable_products => 'simple-prices',
725 $this->option_enable_after_add_to_cart => 'after-add-to-cart',
726 $this->option_deactivate_short_description => 'deactivate-description',
727 $this->option_move_content_to_short_description => 'move-content',
728 $this->option_disable_zoom_images => 'disable-zoom',
729 $this->option_add_share_buttons => 'share-buttons',
730 $this->option_deactivate_product_tabs => 'deactivate-tabs',
731 $this->option_horizontal_product_form => 'horizontal-form',
732 );
733
734 $icon_name = $icon_map[ $field_id ] ?? 'default';
735 $icon_path = FRBL_PLUGIN_PATH . 'assets/admin/icons/' . $icon_name . '.svg';
736
737 if ( file_exists( $icon_path ) ) {
738 $svg_content = file_get_contents( $icon_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
739 return $svg_content ? $svg_content : '';
740 }
741
742 return '';
743 }
744
745 /**
746 * PRO Features section description.
747 *
748 * @return void
749 */
750 public function section_woo_features_callback() {
751 if ( ! frbl_is_pro_active() ) {
752 echo '<div class="tw-bg-blue-50 tw-border-l-4 tw-border-blue-400 tw-p-4 tw-mb-4">';
753 echo '<div class="tw-flex">';
754 echo '<div class="tw-flex-shrink-0">';
755 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>';
756 echo '</div>';
757 echo '<div class="tw-ml-3">';
758 echo '<p class="tw-text-sm tw-text-blue-700">';
759 printf(
760 /* translators: %s: FrontBlocks PRO link */
761 esc_html__( 'These features require %s. Upgrade to unlock advanced functionality.', 'frontblocks' ),
762 '<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>'
763 );
764 echo '</p>';
765 echo '</div>';
766 echo '</div>';
767 echo '</div>';
768 } elseif ( ! $this->is_license_valid ) {
769 echo '<div class="tw-bg-yellow-50 tw-border-l-4 tw-border-yellow-400 tw-p-4 tw-mb-4">';
770 echo '<div class="tw-flex">';
771 echo '<div class="tw-flex-shrink-0">';
772 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>';
773 echo '</div>';
774 echo '<div class="tw-ml-3">';
775 echo '<p class="tw-text-sm tw-text-yellow-700">';
776 printf(
777 /* translators: %s: License section link */
778 esc_html__( 'License is not activated. Please activate your license in the %s section below to enable these features.', 'frontblocks' ),
779 '<a href="#frontblocks_section_license" class="tw-font-medium tw-underline">' . esc_html__( 'License', 'frontblocks' ) . '</a>'
780 );
781 echo '</p>';
782 echo '</div>';
783 echo '</div>';
784 echo '</div>';
785 } else {
786 ?>
787 <p class="tw-text-sm tw-text-gray-600 tw-mt-0 tw-mb-4">
788 <?php echo esc_html__( 'Advanced features for WooCommerce and more.', 'frontblocks' ); ?>
789 </p>
790 <?php
791 }
792 }
793
794 /**
795 * Render toggle field for enable testimonials.
796 *
797 * @return void
798 */
799 public function field_enable_testimonials() {
800 $options = get_option( 'frontblocks_settings', array() );
801 $enabled = (bool) ( $options[ $this->option_enable_testimonials ] ?? false );
802 ?>
803 <label class="frbl-toggle">
804 <input type="checkbox"
805 id="<?php echo esc_attr( $this->option_enable_testimonials ); ?>"
806 name="frontblocks_settings[<?php echo esc_attr( $this->option_enable_testimonials ); ?>]"
807 value="1"
808 <?php checked( true, $enabled ); ?>
809 />
810 <span></span>
811 </label>
812 <?php
813 }
814
815 /**
816 * Render toggle field for enable reading progress bar.
817 *
818 * @return void
819 */
820 public function field_enable_reading_progress() {
821 $options = get_option( 'frontblocks_settings', array() );
822 $enabled = (bool) ( $options[ $this->option_enable_reading_progress ] ?? false );
823 ?>
824 <label class="frbl-toggle">
825 <input type="checkbox"
826 id="<?php echo esc_attr( $this->option_enable_reading_progress ); ?>"
827 name="frontblocks_settings[<?php echo esc_attr( $this->option_enable_reading_progress ); ?>]"
828 value="1"
829 <?php checked( true, $enabled ); ?>
830 />
831 <span></span>
832 </label>
833 <?php
834 }
835
836 /**
837 * Render toggle field for enable back button.
838 *
839 * @return void
840 */
841 public function field_enable_back_button() {
842 $options = get_option( 'frontblocks_settings', array() );
843 $enabled = (bool) ( $options[ $this->option_enable_back_button ] ?? false );
844 ?>
845 <label class="frbl-toggle">
846 <input type="checkbox"
847 id="<?php echo esc_attr( $this->option_enable_back_button ); ?>"
848 name="frontblocks_settings[<?php echo esc_attr( $this->option_enable_back_button ); ?>]"
849 value="1"
850 <?php checked( true, $enabled ); ?>
851 />
852 <span></span>
853 </label>
854 <?php
855 }
856
857 /**
858 * Render toggle field for enable Gutenberg in products (PRO).
859 *
860 * @return void
861 */
862 public function field_enable_gutenberg() {
863 $this->render_pro_toggle( $this->option_enable_gutenberg );
864 }
865
866 /**
867 * Render toggle field for enable Simple Prices Variable Products (PRO).
868 *
869 * @return void
870 */
871 public function field_enable_simple_prices_variable_products() {
872 $this->render_pro_toggle( $this->option_enable_simple_prices_variable_products );
873 }
874
875 /**
876 * Render After Add to Cart Block field.
877 *
878 * @return void
879 */
880 public function field_enable_after_add_to_cart() {
881 $this->render_pro_toggle( $this->option_enable_after_add_to_cart );
882 }
883
884 /**
885 * Render Deactivate Short Description field.
886 *
887 * @return void
888 */
889 public function field_deactivate_short_description() {
890 $this->render_pro_toggle( $this->option_deactivate_short_description );
891 }
892
893 /**
894 * Render Move Content to Short Description field.
895 *
896 * @return void
897 */
898 public function field_move_content_to_short_description() {
899 $this->render_pro_toggle( $this->option_move_content_to_short_description );
900 }
901
902 /**
903 * Render Disable Zoom in Product Images field.
904 *
905 * @return void
906 */
907 public function field_disable_zoom_images() {
908 $this->render_pro_toggle( $this->option_disable_zoom_images );
909 }
910
911 /**
912 * Render Add Share Buttons in Product Page field.
913 *
914 * @return void
915 */
916 public function field_add_share_buttons() {
917 $this->render_pro_toggle( $this->option_add_share_buttons );
918 }
919
920 /**
921 * Render Deactivate Product Tabs field.
922 *
923 * @return void
924 */
925 public function field_deactivate_product_tabs() {
926 $this->render_pro_toggle( $this->option_deactivate_product_tabs );
927 }
928
929 /**
930 * Render Horizontal Product Form Layout field.
931 *
932 * @return void
933 */
934 public function field_horizontal_product_form() {
935 $this->render_pro_toggle( $this->option_horizontal_product_form );
936 }
937
938 /**
939 * License section description.
940 *
941 * @return void
942 */
943 public function section_license_callback() {
944 echo '<p>' . esc_html__( 'Manage your FrontBlocks PRO license.', 'frontblocks' ) . '</p>';
945 }
946
947 /**
948 * Render license key field.
949 *
950 * @return void
951 */
952 public function field_license_key() {
953 global $frontblocks_pro_license;
954 $license_key = $frontblocks_pro_license->get_option_value( 'apikey' );
955 ?>
956 <div class="tw-space-y-4">
957 <!-- License Key and Product ID Fields in a row -->
958 <div class="tw-flex tw-w-full">
959 <!-- License Key Field - 66.6% (2/3) -->
960 <div style="flex: 4 1 0%;">
961 <input type="text"
962 id="<?php echo esc_attr( $this->option_license_key ); ?>"
963 name="<?php echo esc_attr( $this->option_license_key ); ?>"
964 value="<?php echo esc_attr( $license_key ); ?>"
965 placeholder="<?php echo esc_attr__( 'Enter your license key', 'frontblocks' ); ?>"
966 class="tw-block tw-w-full tw-px-4 tw-py-3 tw-border tw-border-gray-300 tw-rounded-lg tw-text-base focus:tw-outline-none focus:tw-ring-2 focus:tw-ring-primary-500 focus:tw-border-transparent"
967 />
968 </div>
969 </div>
970
971 <!-- Help Text for Product ID -->
972 <p class="tw-text-xs tw-text-gray-500 tw-mt-1">
973 <?php echo esc_html__( 'Enter your license key and product ID. You can find both in your purchase confirmation email.', 'frontblocks' ); ?>
974 </p>
975
976 <!-- License Status Field (Read-only) -->
977 <div>
978 <label class="tw-block tw-text-sm tw-font-medium tw-text-gray-900 tw-mb-2">
979 <?php echo esc_html__( 'License Status', 'frontblocks' ); ?>
980 </label>
981 <?php
982 $status_text = '';
983 $status_class = '';
984 $status_icon = '';
985 $license_data = $frontblocks_pro_license->license_key_status( true );
986 $license_status = empty( $license_data ) || ! isset( $license_data['status_check'] ) ? 'not_activated' : $license_data['status_check'];
987
988 switch ( $license_status ) {
989 case 'active':
990 $status_text = __( 'Active', 'frontblocks' );
991 $status_class = 'tw-bg-green-100 tw-text-green-800 tw-border-green-300';
992 $status_icon = '<svg class="tw-w-5 tw-h-5" fill="currentColor" viewBox="0 0 20 20"><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"/></svg>';
993 break;
994 case 'expired':
995 $status_text = __( 'Expired', 'frontblocks' );
996 $status_class = 'tw-bg-red-100 tw-text-red-800 tw-border-red-300';
997 $status_icon = '<svg class="tw-w-5 tw-h-5" fill="currentColor" viewBox="0 0 20 20"><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"/></svg>';
998 break;
999 default:
1000 $status_text = __( 'Not Activated', 'frontblocks' );
1001 $status_text .= ' ' . ( isset( $license_data['error'] ) ? $license_data['error'] : '' );
1002 $status_class = 'tw-bg-yellow-100 tw-text-yellow-800 tw-border-yellow-300';
1003 $status_icon = '<svg class="tw-w-5 tw-h-5" 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>';
1004 break;
1005 }
1006 ?>
1007 <div class="tw-flex tw-items-center tw-gap-3 tw-px-4 tw-py-3 tw-border tw-rounded-lg <?php echo esc_attr( $status_class ); ?>">
1008 <span class="tw-flex-shrink-0">
1009 <?php echo $status_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
1010 </span>
1011 <span class="tw-font-semibold tw-text-base">
1012 <?php
1013 echo wp_kses(
1014 $status_text,
1015 array(
1016 'br' => array(),
1017 'em' => array(),
1018 'strong' => array(),
1019 'a' => array(
1020 'href' => true,
1021 'target' => true,
1022 'rel' => true,
1023 ),
1024 )
1025 );
1026 ?>
1027 </span>
1028 <?php if ( ! empty( $license_data['expires'] ) && 'valid' === $license_data['status'] ) : ?>
1029 <span class="tw-ml-auto tw-text-sm">
1030 <?php
1031 printf(
1032 /* translators: %s: expiration date */
1033 esc_html__( 'Expires: %s', 'frontblocks' ),
1034 esc_html( $license_data['expires'] )
1035 );
1036 ?>
1037 </span>
1038 <?php endif; ?>
1039 </div>
1040 </div>
1041
1042 <!-- Help Text -->
1043 <?php if ( empty( $license_key ) ) : ?>
1044 <div class="tw-p-4 tw-rounded-lg tw-bg-gray-50 tw-border tw-border-gray-200">
1045 <p class="tw-text-sm tw-text-gray-600">
1046 <?php
1047 printf(
1048 /* translators: %s: purchase link */
1049 esc_html__( 'Don\'t have a license? %s to get started.', 'frontblocks' ),
1050 '<a href="https://close.technology/wordpress-plugins/frontblocks-pro/?utm_source=frontblocks&utm_medium=plugin&utm_campaign=settings-license" target="_blank" rel="noopener noreferrer" class="tw-text-primary-500 hover:tw-text-primary-600 tw-font-medium">' . esc_html__( 'Purchase FrontBlocks PRO', 'frontblocks' ) . '</a>'
1051 );
1052 ?>
1053 </p>
1054 </div>
1055 <?php endif; ?>
1056
1057 <?php if ( 'expired' === $license_status ) : ?>
1058 <div class="tw-p-3 tw-rounded-lg tw-bg-red-50 tw-border tw-border-red-200">
1059 <p class="tw-text-sm tw-text-red-700">
1060 <?php
1061 printf(
1062 /* translators: %s: renewal link */
1063 esc_html__( 'Your license has expired. %s to continue receiving updates and support.', 'frontblocks' ),
1064 '<a href="https://close.technology/my-account/?utm_source=frontblocks&utm_medium=plugin&utm_campaign=renew-license" target="_blank" rel="noopener noreferrer" class="tw-font-medium tw-underline hover:tw-no-underline">' . esc_html__( 'Renew your license', 'frontblocks' ) . '</a>'
1065 );
1066 ?>
1067 </p>
1068 </div>
1069 <?php endif; ?>
1070 </div>
1071 <?php
1072 }
1073
1074 /**
1075 * Helper method to render PRO toggle fields.
1076 *
1077 * @param string $option_key Option key.
1078 * @return void
1079 */
1080 private function render_pro_toggle( $option_key ) {
1081 $options = get_option( 'frontblocks_settings', array() );
1082 $enabled = (bool) ( $options[ $option_key ] ?? false );
1083 $is_enabled = $this->is_license_valid;
1084 $disabled = ! $is_enabled ? 'disabled' : '';
1085 ?>
1086 <label class="frbl-toggle">
1087 <input type="checkbox"
1088 id="<?php echo esc_attr( $option_key ); ?>"
1089 name="frontblocks_settings[<?php echo esc_attr( $option_key ); ?>]"
1090 value="1"
1091 <?php checked( true, $enabled ); ?>
1092 <?php echo esc_attr( $disabled ); ?>
1093 />
1094 <span></span>
1095 </label>
1096 <?php
1097 }
1098
1099 /**
1100 * Sanitize settings array.
1101 *
1102 * @param array $value Raw value.
1103 * @return array
1104 */
1105 public function sanitize_settings( $value ) {
1106 // Nonce verification.
1107 $nonce = isset( $_POST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
1108 if ( empty( $nonce ) || ! wp_verify_nonce( $nonce, 'frontblocks_settings-options' ) ) {
1109 add_settings_error( 'frontblocks_settings', 'frontblocks_settings_nonce', esc_html__( 'Security check failed. Please try again.', 'frontblocks' ), 'error' );
1110
1111 return get_option( 'frontblocks_settings', array() );
1112 }
1113
1114 if ( ! is_array( $value ) ) {
1115 return array();
1116 }
1117
1118 $sanitized = array();
1119 foreach ( $value as $key => $val ) {
1120 if ( $this->option_enable_testimonials === $key || $this->option_enable_reading_progress === $key || $this->option_enable_back_button === $key || $this->option_enable_gutenberg === $key || $this->option_enable_simple_prices_variable_products === $key || $this->option_enable_after_add_to_cart === $key || $this->option_deactivate_short_description === $key || $this->option_move_content_to_short_description === $key || $this->option_disable_zoom_images === $key || $this->option_add_share_buttons === $key || $this->option_deactivate_product_tabs === $key || $this->option_horizontal_product_form === $key ) {
1121 $sanitized[ $key ] = (bool) $val;
1122 }
1123 }
1124
1125 // Ensure mutual exclusion: if both description options are enabled, keep only the last one changed.
1126 if ( ! empty( $sanitized[ $this->option_deactivate_short_description ] ) && ! empty( $sanitized[ $this->option_move_content_to_short_description ] ) ) {
1127 // Get current saved values to determine which one was just changed.
1128 $current_options = get_option( 'frontblocks_settings', array() );
1129 $current_deactivate = ! empty( $current_options[ $this->option_deactivate_short_description ] );
1130 $current_move = ! empty( $current_options[ $this->option_move_content_to_short_description ] );
1131
1132 // If deactivate was already on, turn it off (move is the new one).
1133 if ( $current_deactivate ) {
1134 $sanitized[ $this->option_deactivate_short_description ] = false;
1135 } else {
1136 // Otherwise turn off move (deactivate is the new one).
1137 $sanitized[ $this->option_move_content_to_short_description ] = false;
1138 }
1139 }
1140
1141 do_action( 'frontblocks_sanitize_settings', $sanitized );
1142
1143 return $sanitized;
1144 }
1145 }
1146