PluginProbe
Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant / 2.3.0
Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant v2.3.0
2.3.2 2.3.1 2.3.0 2.2.8 2.2.7 trunk 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.10.5 1.11.0 1.11.1 1.11.2 1.6 1.7 1.8 1.8.1 1.8.2 1.8.3 1.9.0 1.9.1 1.9.10 1.9.11 All 60 releases
merchant / inc / modules / payment-logos / class-payment-logos.php

class-payment-logos.php in Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant 2.3.0, at inc/modules/payment-logos/class-payment-logos.php

1,049 lines 29.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Payment Logos
5 *
6 * @package Merchant
7 */
8
9 if ( ! defined( 'ABSPATH' ) ) {
10 exit; // Exit if accessed directly
11 }
12
13 /**
14 * Payment Logos Class.
15 *
16 */
17 class Merchant_Payment_Logos extends Merchant_Add_Module {
18
19 /**
20 * Module ID.
21 *
22 */
23 const MODULE_ID = 'payment-logos';
24
25 /**
26 * Is module preview.
27 *
28 * @var bool
29 */
30 public static $is_module_preview = false;
31
32 /**
33 * Whether the module has a shortcode or not.
34 *
35 * @var bool
36 */
37 public $has_shortcode = true;
38
39 /**
40 * Initialize the module's option group definitions.
41 *
42 * @return array<int, array<string, mixed>> Array of option group arrays.
43 */
44 protected function init_option_groups() {
45 return require MERCHANT_DIR . 'inc/modules/' . self::MODULE_ID . '/admin/options.php';
46 }
47
48 /**
49 * Constructor.
50 *
51 */
52 public function __construct() {
53
54 $this->module_id = self::MODULE_ID;
55 $this->wc_only = true;
56
57 parent::__construct();
58
59 $this->module_section = 'build-trust';
60
61 $this->module_default_settings = array(
62 'icon_source' => 'gallery',
63 'logos' => '',
64 'library_icons' => array( 'visa', 'mastercard', 'paypal' ),
65 'title' => __( '🔒 Safe & Secure Checkout', 'merchant' ),
66 'show_on_single_product' => 1,
67 'show_on_cart_page' => 0,
68 'cart_position' => 'after_table',
69 'show_on_checkout_page' => 0,
70 'checkout_position' => 'before_checkout_form',
71 'show_on_footer' => 0,
72 'show_on_archive_pages' => 0,
73 'archive_position' => 'after_loop',
74 'container_bg_color' => 'transparent',
75 'container_padding' => 0,
76 'container_border_radius' => 0,
77 'container_border_color' => 'transparent',
78 'container_border_width' => 0,
79 'exclude_products_toggle' => 0,
80 'excluded_products' => array(),
81 'exclude_categories_toggle' => 0,
82 'excluded_categories' => array(),
83 );
84
85 $this->module_data = Merchant_Admin_Modules::$modules_data[ self::MODULE_ID ];
86
87 // AI prompt examples.
88 $this->ai_examples = array(
89 'blurb' => esc_html__( 'See what AI can do for your payment logos, from picking which cards and wallets display to choosing where they appear across your store.', 'merchant' ),
90 'prompts' => array(
91 esc_html__( 'Explore the abilities WPVibe gives you access to, then show the Visa, Mastercard, and PayPal logos from the built-in icon library on my product pages', 'merchant' ),
92 esc_html__( 'Check what abilities you have available through WPVibe, then also show the payment logos on the cart page, right before the "Proceed to checkout" button', 'merchant' ),
93 esc_html__( "Look at your available WPVibe abilities, then change the text above the payment logos to 'Guaranteed Safe Checkout' and center them", 'merchant' ),
94 esc_html__( 'See what abilities WPVibe exposes, then hide the payment logos on all products in the Gift Cards category', 'merchant' ),
95 ),
96 );
97
98 $this->module_options_path = MERCHANT_DIR . 'inc/modules/' . self::MODULE_ID . '/admin/options.php';
99
100 if ( is_admin() && parent::is_module_settings_page() ) {
101 $this->register_admin_preview_hooks();
102 }
103
104 if ( Merchant_Modules::is_module_active( self::MODULE_ID ) && is_admin() ) {
105 $this->init_translations();
106 }
107
108 if ( ! Merchant_Modules::is_module_active( self::MODULE_ID ) ) {
109 return;
110 }
111
112 if ( is_admin() && ! wp_doing_ajax() && ! parent::is_module_settings_page() ) {
113 return;
114 }
115
116 $this->register_frontend_hooks();
117 }
118
119 /**
120 * Register hooks required by the admin settings/preview page.
121 *
122 * Called only when the current admin page is this module's settings page.
123 *
124 * @since 2.2.8
125 *
126 * @return void
127 */
128 private function register_admin_preview_hooks() {
129 self::$is_module_preview = true;
130
131 add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_css' ) );
132 add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_js' ) );
133 add_filter( 'merchant_admin_localize_script', array( $this, 'localize_script' ) );
134 add_filter( 'merchant_module_preview', array( $this, 'render_admin_preview' ), 10, 2 );
135
136 // Custom CSS must also be added here so the preview box renders correctly.
137 add_filter( 'merchant_custom_css', array( $this, 'admin_custom_css' ) );
138 }
139
140 /**
141 * Register all frontend hooks for an active module.
142 *
143 * Called only when the module is active and we are either on the frontend
144 * or performing an AJAX request.
145 *
146 * @since 2.2.8
147 *
148 * @return void
149 */
150 private function register_frontend_hooks() {
151 add_action( 'admin_init', array( $this, 'add_image_sizes' ) );
152 add_action( 'wp_ajax_merchant_regenerate_images', array( $this, 'regenerate_images' ) );
153 add_action( 'merchant_enqueue_before_main_css_js', array( $this, 'enqueue_css' ) );
154 add_filter( 'merchant_custom_css', array( $this, 'frontend_custom_css' ) );
155
156 $this->register_display_hooks();
157
158 require_once MERCHANT_DIR . 'inc/modules/' . self::MODULE_ID . '/class-payment-logos-blocks-integration.php';
159 Merchant_Payment_Logos_Blocks_Integration::get_instance()->load_hooks();
160 }
161
162 /**
163 * Register hooks for all enabled display locations.
164 *
165 * @since 2.2.8
166 *
167 * @return void
168 */
169 private function register_display_hooks() {
170 $settings = $this->get_module_settings();
171
172 $this->maybe_register_product_hook( $settings );
173 $this->maybe_register_cart_hook( $settings );
174 $this->maybe_register_checkout_hook( $settings );
175 $this->maybe_register_footer_hook( $settings );
176 $this->maybe_register_archive_hook( $settings );
177 }
178
179 /**
180 * Register the single-product display hook if enabled.
181 *
182 * @param array<string, mixed> $settings Module settings.
183 *
184 * @return void
185 */
186 private function maybe_register_product_hook( $settings ) {
187 if ( ! empty( $settings['show_on_single_product'] ) ) {
188 add_action( 'woocommerce_single_product_summary', array( $this, 'payment_logos_output' ), 30 );
189
190 /**
191 * Add payment logos inside the Quick View modal.
192 *
193 * @since 2.3.0
194 */
195 add_action( 'merchant_quick_view_product_summary', array( $this, 'payment_logos_output' ), 30 );
196 }
197 }
198
199 /**
200 * Register the cart page display hook if enabled and not using block layout.
201 *
202 * @param array<string, mixed> $settings Module settings.
203 *
204 * @return void
205 */
206 private function maybe_register_cart_hook( $settings ) {
207 if ( empty( $settings['show_on_cart_page'] ) || merchant_is_cart_block_layout() ) {
208 return;
209 }
210
211 $position = $settings['cart_position'] ?? 'after_table';
212 $hook_map = array(
213 'before_table' => 'woocommerce_before_cart_table',
214 'after_table' => 'woocommerce_after_cart_table',
215 'before_proceed_to_checkout' => 'woocommerce_proceed_to_checkout',
216 'after_cart_totals' => 'woocommerce_after_cart_totals',
217 );
218
219 add_action( $hook_map[ $position ] ?? 'woocommerce_after_cart_table', array( $this, 'cart_page_output' ) );
220 }
221
222 /**
223 * Register the checkout page display hook if enabled and not using block layout.
224 *
225 * @param array<string, mixed> $settings Module settings.
226 *
227 * @return void
228 */
229 private function maybe_register_checkout_hook( $settings ) {
230 if ( empty( $settings['show_on_checkout_page'] ) || merchant_is_checkout_block_layout() ) {
231 return;
232 }
233
234 $position = $settings['checkout_position'] ?? 'before_checkout_form';
235 $hook_map = array(
236 'before_checkout_form' => 'woocommerce_before_checkout_form',
237 'before_customer_details' => 'woocommerce_before_checkout_billing_form',
238 'after_customer_details' => 'woocommerce_after_checkout_billing_form',
239 'before_place_order' => 'woocommerce_review_order_before_submit',
240 'after_checkout_form' => 'woocommerce_after_checkout_form',
241 );
242
243 add_action( $hook_map[ $position ] ?? 'woocommerce_before_checkout_form', array( $this, 'checkout_page_output' ), 5 );
244 }
245
246 /**
247 * Register the footer display hook if enabled.
248 *
249 * @param array<string, mixed> $settings Module settings.
250 *
251 * @return void
252 */
253 private function maybe_register_footer_hook( $settings ) {
254 if ( ! empty( $settings['show_on_footer'] ) ) {
255 add_action( 'wp_footer', array( $this, 'footer_output' ) );
256 }
257 }
258
259 /**
260 * Register the archive/shop page display hook if enabled.
261 *
262 * @param array<string, mixed> $settings Module settings.
263 *
264 * @return void
265 */
266 private function maybe_register_archive_hook( $settings ) {
267 if ( empty( $settings['show_on_archive_pages'] ) ) {
268 return;
269 }
270
271 $position = $settings['archive_position'] ?? 'after_loop';
272 $hook = 'before_loop' === $position
273 ? 'woocommerce_before_shop_loop'
274 : 'woocommerce_after_shop_loop';
275
276 add_action( $hook, array( $this, 'archive_page_output' ) );
277 }
278
279 /**
280 * Add custom image sizes for the merchant module.
281 *
282 * It runs on AJAX requests as it hooks into the `admin_init` hook, so
283 * there is no need to call it again in the `$this->regenerate_images` method.
284 *
285 * @return void
286 */
287 public function add_image_sizes() {
288 $settings = $this->get_module_settings();
289 $width = (int) ( $settings['image-max-width'] ?? 80 );
290 $height = (int) ( $settings['image-max-height'] ?? 100 );
291
292 add_image_size( 'merchant_payment_logos', $width, $height );
293 }
294
295 /**
296 * AJAX handler: regenerate image sizes for specified attachments.
297 *
298 * @return void
299 */
300 public function regenerate_images() {
301 check_ajax_referer( 'merchant', 'nonce' );
302
303 if ( ! current_user_can( 'manage_options' ) ) {
304 wp_send_json_error( esc_html__( 'You are not allowed to do this.', 'merchant' ) );
305 }
306
307 $attachments = $this->parse_attachment_ids_from_request();
308 $is_dimension_changed = filter_var(
309 sanitize_text_field( wp_unslash( $_POST['is_dimension_changed'] ?? '' ) ),
310 FILTER_VALIDATE_BOOLEAN
311 );
312
313 if ( ! function_exists( 'wp_get_attachment_metadata' ) ) {
314 require_once ABSPATH . 'wp-admin/includes/image.php';
315 }
316
317 $generated_images = array();
318
319 foreach ( $attachments as $attachment_id ) {
320 if ( $this->regenerate_single_attachment( $attachment_id, $is_dimension_changed ) ) {
321 $generated_images[] = $attachment_id;
322 }
323 }
324
325 wp_send_json_success( $generated_images );
326 }
327
328 /**
329 * Parse and sanitize attachment IDs from the current POST request.
330 *
331 * @return int[] Array of valid attachment IDs.
332 */
333 private function parse_attachment_ids_from_request() {
334 // phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified in regenerate_images().
335 $raw = isset( $_POST['attachments'] ) && is_array( $_POST['attachments'] )
336 ? array_map( 'sanitize_text_field', wp_unslash( $_POST['attachments'] ) )
337 : array();
338 // phpcs:enable WordPress.Security.NonceVerification.Missing
339
340 $ids = array();
341 foreach ( $raw as $value ) {
342 $id = (int) $value;
343 if ( $id > 0 ) {
344 $ids[] = $id;
345 }
346 }
347
348 return $ids;
349 }
350
351 /**
352 * Regenerate metadata for a single attachment if needed.
353 *
354 * @param int $attachment_id The attachment ID.
355 * @param bool $is_dimension_changed Whether dimensions have changed (forces regeneration).
356 *
357 * @return bool True if the attachment was regenerated, false otherwise.
358 */
359 private function regenerate_single_attachment( $attachment_id, $is_dimension_changed ) {
360 $metadata = wp_get_attachment_metadata( $attachment_id );
361
362 if ( ! $is_dimension_changed && ! empty( $metadata['sizes']['merchant_payment_logos'] ) ) {
363 return false;
364 }
365
366 $file = get_attached_file( $attachment_id );
367 if ( false === $file ) {
368 return false;
369 }
370
371 $updated_metadata = wp_generate_attachment_metadata( $attachment_id, $file );
372 if ( empty( $updated_metadata ) ) {
373 return false;
374 }
375
376 wp_update_attachment_metadata( $attachment_id, $updated_metadata );
377
378 return true;
379 }
380
381 /**
382 * Print shortcode content.
383 *
384 * Validates context then delegates rendering to render_logos() via
385 * render_logos_to_string() to avoid a duplicate render path.
386 *
387 * @return string
388 */
389 public function shortcode_handler() {
390 if ( ! Merchant_Modules::is_module_active( $this->module_id ) ) {
391 return '';
392 }
393
394 if ( ! $this->is_shortcode_enabled() ) {
395 return '';
396 }
397
398 if ( ! is_singular( 'product' ) ) {
399 if ( current_user_can( 'manage_options' ) ) {
400 return $this->shortcode_placement_error();
401 }
402
403 return '';
404 }
405
406 $settings = $this->get_module_settings();
407 $product_id = get_the_ID();
408 if ( false !== $product_id && ! $this->should_display_on_product( $product_id, $settings ) ) {
409 return '';
410 }
411
412 $shortcode_content = $this->render_logos_to_string( '', $settings );
413
414 /**
415 * Filter the shortcode html content.
416 *
417 * @param string $shortcode_content shortcode html content.
418 * @param string $module_id module id.
419 * @param int|false $post_id product id.
420 *
421 * @since 1.8
422 */
423 return apply_filters( 'merchant_module_shortcode_content_html', $shortcode_content, $this->module_id, get_the_ID() );
424 }
425
426 /**
427 * Init translations.
428 *
429 * @return void
430 */
431 public function init_translations() {
432 $settings = $this->get_module_settings();
433 if ( ! empty( $settings['title'] ) ) {
434 Merchant_Translator::register_string( $settings['title'], esc_html__( 'Payment logos text above logos', 'merchant' ) );
435 }
436 }
437
438 /**
439 * Localize script data.
440 *
441 * Exposes all library icon URLs and the current icon_source to the admin preview JS.
442 *
443 * @since 2.2.8
444 *
445 * @param array<string, mixed> $data Existing localized data.
446 *
447 * @return array<string, mixed>
448 */
449 public function localize_script( $data ) {
450 $settings = $this->get_module_settings();
451
452 $data['payment_logos'] = array(
453 'icon_source' => $settings['icon_source'] ?? 'gallery',
454 'library_icons' => $settings['library_icons'] ?? array( 'visa', 'mastercard', 'paypal' ),
455 'icons_url_map' => Merchant_Payment_Icons_Library::get_all_icons(),
456 );
457
458 return $data;
459 }
460
461 /**
462 * Admin enqueue CSS.
463 *
464 * @return void
465 */
466 public function admin_enqueue_css() {
467 $page = ( ! empty( $_GET['page'] ) ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
468 $module = ( ! empty( $_GET['module'] ) ) ? sanitize_text_field( wp_unslash( $_GET['module'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
469
470 if ( 'merchant' === $page && self::MODULE_ID === $module ) {
471 wp_enqueue_style( 'merchant-' . self::MODULE_ID, MERCHANT_URI . 'assets/css/modules/' . self::MODULE_ID . '/payment-logos.min.css', array(), MERCHANT_VERSION );
472 wp_enqueue_style( 'merchant-admin-' . self::MODULE_ID, MERCHANT_URI . 'assets/css/modules/' . self::MODULE_ID . '/admin/preview.min.css', array(), MERCHANT_VERSION );
473 }
474 }
475
476 /**
477 * Admin enqueue scripts.
478 *
479 * @return void
480 */
481 public function admin_enqueue_js() {
482 if ( $this->is_module_settings_page() ) {
483 wp_enqueue_script( "merchant-{$this->module_id}", MERCHANT_URI . "assets/js/modules/{$this->module_id}/admin/preview.min.js", array( 'jquery' ), MERCHANT_VERSION, true );
484 }
485 }
486
487 /**
488 * Determine whether the module's assets/CSS should be output on the current page.
489 *
490 * Centralises the repeated page-detection logic shared between enqueue_css()
491 * and frontend_custom_css().
492 *
493 * @since 2.2.8
494 *
495 * @param array<string, mixed> $settings Module settings.
496 *
497 * @return bool
498 */
499 private function should_show_on_current_page( $settings ) {
500 if ( is_singular( 'product' ) && ! empty( $settings['show_on_single_product'] ) ) {
501 return true;
502 }
503
504 if ( function_exists( 'is_cart' ) && is_cart() && ! empty( $settings['show_on_cart_page'] ) ) {
505 return true;
506 }
507
508 if ( function_exists( 'is_checkout' ) && is_checkout() && ! empty( $settings['show_on_checkout_page'] ) ) {
509 return true;
510 }
511
512 if ( ! empty( $settings['show_on_footer'] ) ) {
513 return true;
514 }
515
516 if ( ( is_shop() || is_product_taxonomy() ) && ! empty( $settings['show_on_archive_pages'] ) ) {
517 return true;
518 }
519
520 if ( Merchant_Modules::is_module_active( 'quick-view' ) ) {
521 return true;
522 }
523
524 return false;
525 }
526
527 /**
528 * Enqueue CSS.
529 *
530 * @return void
531 */
532 public function enqueue_css() {
533 $settings = $this->get_module_settings();
534
535 if ( ! $this->should_show_on_current_page( $settings ) ) {
536 return;
537 }
538
539 wp_enqueue_style( 'merchant-' . self::MODULE_ID, MERCHANT_URI . 'assets/css/modules/' . self::MODULE_ID . '/payment-logos.min.css', array(), MERCHANT_VERSION );
540 }
541
542 /**
543 * Render admin preview
544 *
545 * @param mixed $preview The admin preview object.
546 * @param string $module The module ID.
547 *
548 * @return mixed
549 */
550 public function render_admin_preview( $preview, $module ) {
551 if ( self::MODULE_ID === $module ) {
552 ob_start();
553 self::admin_preview_content();
554 $content = ob_get_clean();
555
556 $preview->set_html( $content );
557 $preview->set_text( 'title', '.merchant-payment-logos-title > strong' );
558
559 }
560
561 return $preview;
562 }
563
564 /**
565 * Admin preview content.
566 *
567 * @return void
568 */
569 public function admin_preview_content() {
570 ?>
571
572 <div class="mrc-preview-single-product-elements">
573 <div class="mrc-preview-left-column">
574 <div class="mrc-preview-product-image-wrapper">
575 <div class="mrc-preview-product-image"></div>
576 <div class="mrc-preview-product-image-thumbs">
577 <div class="mrc-preview-product-image-thumb"></div>
578 <div class="mrc-preview-product-image-thumb"></div>
579 <div class="mrc-preview-product-image-thumb"></div>
580 </div>
581 </div>
582 </div>
583 <div class="mrc-preview-right-column">
584 <div class="mrc-preview-text-placeholder"></div>
585 <div class="mrc-preview-text-placeholder mrc-mw-70"></div>
586 <div class="mrc-preview-text-placeholder mrc-mw-30"></div>
587 <div class="mrc-preview-text-placeholder mrc-mw-40 mrc-hide-on-smaller-screens"></div>
588 <div class="mrc-preview-addtocart-placeholder mrc-hide-on-smaller-screens"></div>
589 <?php $this->payment_logos_output(); ?>
590 </div>
591 </div>
592
593 <?php
594 }
595
596 /**
597 * Get logos based on the current icon source setting.
598 *
599 * Returns different value types depending on the icon_source setting:
600 * - 'library' mode → string[] of absolute icon URLs.
601 * - 'gallery' mode with logos saved → string[] of attachment IDs (numeric strings).
602 * - 'gallery' mode with no logos (placeholder) → string[] of placeholder image URLs.
603 *
604 * @param array<string, mixed>|string $settings_or_logos Module settings array or legacy logos string (deprecated).
605 *
606 * @return string[] Array of URLs or attachment IDs (see above).
607 */
608 public function get_logos( $settings_or_logos = array() ) {
609 if ( is_string( $settings_or_logos ) ) {
610 $settings = array(
611 'icon_source' => 'gallery',
612 'logos' => $settings_or_logos,
613 );
614 } else {
615 $settings = $settings_or_logos;
616 }
617
618 $icon_source = $settings['icon_source'] ?? 'gallery';
619
620
621 if ( 'library' === $icon_source ) {
622 $selected_keys = $settings['library_icons'] ?? array( 'visa', 'mastercard', 'paypal' );
623 if ( ! is_array( $selected_keys ) ) {
624 $selected_keys = array( 'visa', 'mastercard', 'paypal' );
625 }
626
627 $urls = array();
628 foreach ( $selected_keys as $key ) {
629 $url = Merchant_Payment_Icons_Library::get_icon_url( $key );
630 if ( ! empty( $url ) ) {
631 $urls[] = $url;
632 }
633 }
634
635 return $urls;
636 }
637
638
639 $logos = $settings['logos'] ?? '';
640
641 if ( is_array( $logos ) ) {
642 return $logos;
643 }
644
645 return ! empty( $logos )
646 ? array_filter( explode( ',', $logos ) )
647 : array(
648 MERCHANT_URI . 'inc/modules/' . self::MODULE_ID . '/admin/images/visa.svg',
649 MERCHANT_URI . 'inc/modules/' . self::MODULE_ID . '/admin/images/mastercard.svg',
650 MERCHANT_URI . 'inc/modules/' . self::MODULE_ID . '/admin/images/paypal.svg',
651 );
652 }
653
654 /**
655 * Get placeholder logos alternative descriptions.
656 *
657 * @since 2.2.8
658 *
659 * @return array<string, string> Array of logos alternative descriptions.
660 */
661 public function get_placeholder_logos_alt_map() {
662 return array(
663 'visa.svg' => __( 'Visa', 'merchant' ),
664 'mastercard.svg' => __( 'Mastercard', 'merchant' ),
665 'paypal.svg' => __( 'PayPal', 'merchant' ),
666 );
667 }
668
669 /**
670 * Check if payment logos should display on a specific product.
671 *
672 * @since 2.2.8
673 *
674 * @param int $product_id The product ID to check.
675 * @param array<string, mixed> $settings Module settings (fetched if not provided).
676 *
677 * @return bool True if logos should display, false if excluded.
678 */
679 public function should_display_on_product( $product_id, $settings = array() ) {
680 $product_id = (int) $product_id;
681
682 if ( empty( $settings ) ) {
683 $settings = $this->get_module_settings();
684 }
685
686
687 if ( ! empty( $settings['exclude_products_toggle'] ) && ! empty( $settings['excluded_products'] ) ) {
688 $excluded_products = is_array( $settings['excluded_products'] )
689 ? $settings['excluded_products']
690 : array( $settings['excluded_products'] );
691
692 if ( in_array( $product_id, array_map( 'intval', $excluded_products ), true ) ) {
693 return false;
694 }
695 }
696
697
698 if ( ! empty( $settings['exclude_categories_toggle'] ) && ! empty( $settings['excluded_categories'] ) ) {
699 $excluded_categories = is_array( $settings['excluded_categories'] )
700 ? $settings['excluded_categories']
701 : array( $settings['excluded_categories'] );
702
703 $product_categories = wp_get_post_terms( $product_id, 'product_cat', array( 'fields' => 'slugs' ) );
704
705 if ( ! is_wp_error( $product_categories ) && ! empty( array_intersect( $product_categories, $excluded_categories ) ) ) {
706 return false;
707 }
708 }
709
710 return true;
711 }
712
713 /**
714 * Get the inline style attribute for the container.
715 *
716 * @since 2.2.8
717 *
718 * @param array<string, mixed> $settings Module settings.
719 *
720 * @return string The style attribute string.
721 */
722 private function get_container_style_attr( $settings ) {
723 $styles = array();
724
725 $bg_color = $settings['container_bg_color'] ?? 'transparent';
726 if ( 'transparent' !== $bg_color && ! empty( $bg_color ) ) {
727 $styles[] = 'background-color:' . esc_attr( $bg_color );
728 }
729
730 $padding = (int) ( $settings['container_padding'] ?? 0 );
731 if ( $padding > 0 ) {
732 $styles[] = 'padding:' . $padding . 'px';
733 }
734
735 $border_radius = (int) ( $settings['container_border_radius'] ?? 0 );
736 if ( $border_radius > 0 ) {
737 $styles[] = 'border-radius:' . $border_radius . 'px';
738 }
739
740 $border_color = $settings['container_border_color'] ?? 'transparent';
741 $border_width = (int) ( $settings['container_border_width'] ?? 0 );
742 if ( $border_width > 0 && 'transparent' !== $border_color && ! empty( $border_color ) ) {
743 $styles[] = 'border:' . $border_width . 'px solid ' . esc_attr( $border_color );
744 }
745
746 if ( empty( $styles ) ) {
747 return '';
748 }
749
750 return 'style="' . esc_attr( implode( ';', $styles ) ) . '"';
751 }
752
753 /**
754 * Render the payment logos markup.
755 *
756 * Shared render method used by all output hooks.
757 *
758 * @since 2.2.8
759 *
760 * @param string $location_class Optional CSS class for the location context.
761 * @param array<string, mixed> $settings Module settings (fetched if not provided).
762 *
763 * @return void
764 */
765 private function render_logos( $location_class = '', $settings = array() ) {
766 if ( empty( $settings ) ) {
767 $settings = $this->get_module_settings();
768 }
769
770 $icon_source = $settings['icon_source'] ?? 'gallery';
771 $is_placeholder = 'gallery' === $icon_source && empty( $settings['logos'] );
772 $logos = $this->get_logos( $settings );
773
774 $container_classes = 'merchant-payment-logos';
775 if ( ! empty( $location_class ) ) {
776 $container_classes .= ' ' . $location_class;
777 }
778 ?>
779
780 <div class="<?php echo esc_attr( $container_classes ); ?>" <?php echo $this->get_container_style_attr( $settings ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
781
782 <?php if ( ! empty( $settings['title'] ) ) : ?>
783 <div class="merchant-payment-logos-title">
784 <strong><?php echo esc_html( Merchant_Translator::translate( $settings['title'] ) ); ?></strong>
785 </div>
786 <?php endif; ?>
787
788 <?php
789 if ( 'library' === $icon_source ) :
790 $this->render_library_images( $logos );
791 elseif ( ! $is_placeholder ) :
792 $this->render_gallery_images( $logos );
793 else :
794 $this->render_placeholder_images( $logos );
795 endif;
796 ?>
797
798 </div>
799
800 <?php
801 }
802
803 /**
804 * Render the library icon images block.
805 *
806 * @since 2.2.8
807 *
808 * @param string[] $logos Array of icon URLs from the built-in library.
809 *
810 * @return void
811 */
812 private function render_library_images( $logos ) {
813 ?>
814 <div class="merchant-payment-logos-images is-library">
815 <?php
816 foreach ( $logos as $icon_url ) :
817 $icon_key = $this->get_icon_key_from_url( $icon_url );
818 $alt_text = ! empty( $icon_key ) ? Merchant_Payment_Icons_Library::get_icon_label( $icon_key ) : '';
819 ?>
820 <img src="<?php echo esc_url( $icon_url ); ?>" alt="<?php echo esc_attr( $alt_text ); ?>" />
821 <?php endforeach; ?>
822 </div>
823 <?php
824 }
825
826 /**
827 * Render the gallery (media-library) images block.
828 *
829 * @since 2.2.8
830 *
831 * @param string[] $logos Array of attachment IDs (numeric strings).
832 *
833 * @return void
834 */
835 private function render_gallery_images( $logos ) {
836 ?>
837 <div class="merchant-payment-logos-images">
838 <?php
839 foreach ( $logos as $image_id ) {
840 echo wp_kses_post( wp_get_attachment_image( (int) $image_id, 'merchant_payment_logos' ) );
841 }
842 ?>
843 </div>
844 <?php
845 }
846
847 /**
848 * Render the placeholder images block (shown when no gallery logos are saved).
849 *
850 * @since 2.2.8
851 *
852 * @param string[] $logos Array of placeholder image URLs.
853 *
854 * @return void
855 */
856 private function render_placeholder_images( $logos ) {
857 $alt_map = $this->get_placeholder_logos_alt_map();
858 ?>
859 <div class="merchant-payment-logos-images is-placeholder">
860 <?php
861 foreach ( $logos as $logo_src ) :
862 $basename = basename( $logo_src );
863 $logo_alt = isset( $alt_map[ $basename ] ) ? $alt_map[ $basename ] : '';
864 ?>
865 <img src="<?php echo esc_url( $logo_src ); ?>" alt="<?php echo esc_attr( $logo_alt ); ?>" />
866 <?php endforeach; ?>
867 </div>
868 <?php
869 }
870
871 /**
872 * Extract the icon key from a library icon URL.
873 *
874 * @since 2.2.8
875 *
876 * @param string $url The icon URL.
877 *
878 * @return string The icon key, or empty string if not found.
879 */
880 private function get_icon_key_from_url( $url ) {
881 $filename = basename( $url, '.svg' );
882
883 if ( Merchant_Payment_Icons_Library::has_icon( $filename ) ) {
884 return $filename;
885 }
886
887 return '';
888 }
889
890 /**
891 * Render payment logos to a string (for WC Blocks injection).
892 *
893 * Used by Merchant_Payment_Logos_Blocks_Integration to capture
894 * the rendered HTML without echoing it.
895 *
896 * @since 2.2.8
897 *
898 * @param string $location_class CSS location modifier class.
899 * @param array<string, mixed> $settings Module settings (fetched if not provided).
900 *
901 * @return string
902 */
903 public function render_logos_to_string( $location_class = '', $settings = array() ) {
904 ob_start();
905 $this->render_logos( $location_class, $settings );
906
907 return (string) ob_get_clean();
908 }
909
910 /**
911 * Render payment logos on the single product page.
912 * TODO: Render through template files.
913 *
914 * @return void
915 */
916 public function payment_logos_output() {
917 if ( $this->is_shortcode_enabled() ) {
918 return;
919 }
920
921 if ( is_archive() || is_page() ) {
922 return;
923 }
924
925 $settings = $this->get_module_settings();
926 $product_id = get_the_ID();
927 if ( false !== $product_id && ! $this->should_display_on_product( $product_id, $settings ) ) {
928 return;
929 }
930
931 $this->render_logos( '', $settings );
932 }
933
934 /**
935 * Render payment logos on the cart page.
936 *
937 * @since 2.2.8
938 *
939 * @return void
940 */
941 public function cart_page_output() {
942 $this->render_logos( 'merchant-payment-logos--cart' );
943 }
944
945 /**
946 * Render payment logos on the checkout page.
947 *
948 * @since 2.2.8
949 *
950 * @return void
951 */
952 public function checkout_page_output() {
953 $this->render_logos( 'merchant-payment-logos--checkout' );
954 }
955
956 /**
957 * Render payment logos in the footer.
958 *
959 * @since 2.2.8
960 *
961 * @return void
962 */
963 public function footer_output() {
964 $this->render_logos( 'merchant-payment-logos--footer' );
965 }
966
967 /**
968 * Render payment logos on archive / shop pages.
969 *
970 * @since 2.2.8
971 *
972 * @return void
973 */
974 public function archive_page_output() {
975 $this->render_logos( 'merchant-payment-logos--archive' );
976 }
977
978 /**
979 * Custom CSS.
980 *
981 * @return string
982 */
983 public function get_module_custom_css() {
984 $css = '';
985
986 // Font Size.
987 $css .= Merchant_Custom_CSS::get_variable_css( 'payment-logos', 'font-size', '18', '.merchant-payment-logos', '--mrc-plogos-font-size', 'px' );
988
989 // Text Color.
990 $css .= Merchant_Custom_CSS::get_variable_css( 'payment-logos', 'text-color', '#212121', '.merchant-payment-logos', '--mrc-plogos-text-color' );
991
992 // Margin Top.
993 $css .= Merchant_Custom_CSS::get_variable_css( 'payment-logos', 'margin-top', '20', '.merchant-payment-logos', '--mrc-plogos-margin-top', 'px' );
994
995 // Margin Bottom.
996 $css .= Merchant_Custom_CSS::get_variable_css( 'payment-logos', 'margin-bottom', '20', '.merchant-payment-logos', '--mrc-plogos-margin-bottom', 'px' );
997
998 // Align.
999 $css .= Merchant_Custom_CSS::get_variable_css( 'payment-logos', 'align', 'flex-start', '.merchant-payment-logos', '--mrc-plogos-align' );
1000
1001 // Image Max Width.
1002 $css .= Merchant_Custom_CSS::get_variable_css( 'payment-logos', 'image-max-width', '80', '.merchant-payment-logos', '--mrc-plogos-image-max-width', 'px' );
1003
1004 // Image Max Height.
1005 $css .= Merchant_Custom_CSS::get_variable_css( 'payment-logos', 'image-max-height', '100', '.merchant-payment-logos', '--mrc-plogos-image-max-height', 'px' );
1006
1007 return $css;
1008 }
1009
1010 /**
1011 * Admin custom CSS.
1012 *
1013 * @param string $css The custom CSS.
1014 * @return string $css The custom CSS.
1015 */
1016 public function admin_custom_css( $css ) {
1017 $css .= $this->get_module_custom_css();
1018
1019 return $css;
1020 }
1021
1022 /**
1023 * Frontend custom CSS.
1024 *
1025 * @param string $css The custom CSS.
1026 *
1027 * @return string
1028 */
1029 public function frontend_custom_css( $css ) {
1030 $settings = $this->get_module_settings();
1031
1032 if ( ! $this->should_show_on_current_page( $settings ) ) {
1033 return $css;
1034 }
1035
1036 $css .= $this->get_module_custom_css();
1037
1038 return $css;
1039 }
1040 }
1041
1042 // Load the icon library.
1043 require_once MERCHANT_DIR . 'inc/modules/payment-logos/class-payment-icons-library.php';
1044
1045 // Initialize the module.
1046 add_action( 'init', function() {
1047 Merchant_Modules::create_module( new Merchant_Payment_Logos() );
1048 } );
1049