PluginProbe
Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant / 2.3.2
Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant v2.3.2
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.2, at inc/modules/payment-logos/class-payment-logos.php

1,051 lines 29.8 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 $excluded_categories = merchant_maybe_expand_categories( $excluded_categories, $settings );
704
705 $product_categories = wp_get_post_terms( $product_id, 'product_cat', array( 'fields' => 'slugs' ) );
706
707 if ( ! is_wp_error( $product_categories ) && ! empty( array_intersect( $product_categories, $excluded_categories ) ) ) {
708 return false;
709 }
710 }
711
712 return true;
713 }
714
715 /**
716 * Get the inline style attribute for the container.
717 *
718 * @since 2.2.8
719 *
720 * @param array<string, mixed> $settings Module settings.
721 *
722 * @return string The style attribute string.
723 */
724 private function get_container_style_attr( $settings ) {
725 $styles = array();
726
727 $bg_color = $settings['container_bg_color'] ?? 'transparent';
728 if ( 'transparent' !== $bg_color && ! empty( $bg_color ) ) {
729 $styles[] = 'background-color:' . esc_attr( $bg_color );
730 }
731
732 $padding = (int) ( $settings['container_padding'] ?? 0 );
733 if ( $padding > 0 ) {
734 $styles[] = 'padding:' . $padding . 'px';
735 }
736
737 $border_radius = (int) ( $settings['container_border_radius'] ?? 0 );
738 if ( $border_radius > 0 ) {
739 $styles[] = 'border-radius:' . $border_radius . 'px';
740 }
741
742 $border_color = $settings['container_border_color'] ?? 'transparent';
743 $border_width = (int) ( $settings['container_border_width'] ?? 0 );
744 if ( $border_width > 0 && 'transparent' !== $border_color && ! empty( $border_color ) ) {
745 $styles[] = 'border:' . $border_width . 'px solid ' . esc_attr( $border_color );
746 }
747
748 if ( empty( $styles ) ) {
749 return '';
750 }
751
752 return 'style="' . esc_attr( implode( ';', $styles ) ) . '"';
753 }
754
755 /**
756 * Render the payment logos markup.
757 *
758 * Shared render method used by all output hooks.
759 *
760 * @since 2.2.8
761 *
762 * @param string $location_class Optional CSS class for the location context.
763 * @param array<string, mixed> $settings Module settings (fetched if not provided).
764 *
765 * @return void
766 */
767 private function render_logos( $location_class = '', $settings = array() ) {
768 if ( empty( $settings ) ) {
769 $settings = $this->get_module_settings();
770 }
771
772 $icon_source = $settings['icon_source'] ?? 'gallery';
773 $is_placeholder = 'gallery' === $icon_source && empty( $settings['logos'] );
774 $logos = $this->get_logos( $settings );
775
776 $container_classes = 'merchant-payment-logos';
777 if ( ! empty( $location_class ) ) {
778 $container_classes .= ' ' . $location_class;
779 }
780 ?>
781
782 <div class="<?php echo esc_attr( $container_classes ); ?>" <?php echo $this->get_container_style_attr( $settings ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
783
784 <?php if ( ! empty( $settings['title'] ) ) : ?>
785 <div class="merchant-payment-logos-title">
786 <strong><?php echo esc_html( Merchant_Translator::translate( $settings['title'] ) ); ?></strong>
787 </div>
788 <?php endif; ?>
789
790 <?php
791 if ( 'library' === $icon_source ) :
792 $this->render_library_images( $logos );
793 elseif ( ! $is_placeholder ) :
794 $this->render_gallery_images( $logos );
795 else :
796 $this->render_placeholder_images( $logos );
797 endif;
798 ?>
799
800 </div>
801
802 <?php
803 }
804
805 /**
806 * Render the library icon images block.
807 *
808 * @since 2.2.8
809 *
810 * @param string[] $logos Array of icon URLs from the built-in library.
811 *
812 * @return void
813 */
814 private function render_library_images( $logos ) {
815 ?>
816 <div class="merchant-payment-logos-images is-library">
817 <?php
818 foreach ( $logos as $icon_url ) :
819 $icon_key = $this->get_icon_key_from_url( $icon_url );
820 $alt_text = ! empty( $icon_key ) ? Merchant_Payment_Icons_Library::get_icon_label( $icon_key ) : '';
821 ?>
822 <img src="<?php echo esc_url( $icon_url ); ?>" alt="<?php echo esc_attr( $alt_text ); ?>" />
823 <?php endforeach; ?>
824 </div>
825 <?php
826 }
827
828 /**
829 * Render the gallery (media-library) images block.
830 *
831 * @since 2.2.8
832 *
833 * @param string[] $logos Array of attachment IDs (numeric strings).
834 *
835 * @return void
836 */
837 private function render_gallery_images( $logos ) {
838 ?>
839 <div class="merchant-payment-logos-images">
840 <?php
841 foreach ( $logos as $image_id ) {
842 echo wp_kses_post( wp_get_attachment_image( (int) $image_id, 'merchant_payment_logos' ) );
843 }
844 ?>
845 </div>
846 <?php
847 }
848
849 /**
850 * Render the placeholder images block (shown when no gallery logos are saved).
851 *
852 * @since 2.2.8
853 *
854 * @param string[] $logos Array of placeholder image URLs.
855 *
856 * @return void
857 */
858 private function render_placeholder_images( $logos ) {
859 $alt_map = $this->get_placeholder_logos_alt_map();
860 ?>
861 <div class="merchant-payment-logos-images is-placeholder">
862 <?php
863 foreach ( $logos as $logo_src ) :
864 $basename = basename( $logo_src );
865 $logo_alt = isset( $alt_map[ $basename ] ) ? $alt_map[ $basename ] : '';
866 ?>
867 <img src="<?php echo esc_url( $logo_src ); ?>" alt="<?php echo esc_attr( $logo_alt ); ?>" />
868 <?php endforeach; ?>
869 </div>
870 <?php
871 }
872
873 /**
874 * Extract the icon key from a library icon URL.
875 *
876 * @since 2.2.8
877 *
878 * @param string $url The icon URL.
879 *
880 * @return string The icon key, or empty string if not found.
881 */
882 private function get_icon_key_from_url( $url ) {
883 $filename = basename( $url, '.svg' );
884
885 if ( Merchant_Payment_Icons_Library::has_icon( $filename ) ) {
886 return $filename;
887 }
888
889 return '';
890 }
891
892 /**
893 * Render payment logos to a string (for WC Blocks injection).
894 *
895 * Used by Merchant_Payment_Logos_Blocks_Integration to capture
896 * the rendered HTML without echoing it.
897 *
898 * @since 2.2.8
899 *
900 * @param string $location_class CSS location modifier class.
901 * @param array<string, mixed> $settings Module settings (fetched if not provided).
902 *
903 * @return string
904 */
905 public function render_logos_to_string( $location_class = '', $settings = array() ) {
906 ob_start();
907 $this->render_logos( $location_class, $settings );
908
909 return (string) ob_get_clean();
910 }
911
912 /**
913 * Render payment logos on the single product page.
914 * TODO: Render through template files.
915 *
916 * @return void
917 */
918 public function payment_logos_output() {
919 if ( $this->is_shortcode_enabled() ) {
920 return;
921 }
922
923 if ( is_archive() || is_page() ) {
924 return;
925 }
926
927 $settings = $this->get_module_settings();
928 $product_id = get_the_ID();
929 if ( false !== $product_id && ! $this->should_display_on_product( $product_id, $settings ) ) {
930 return;
931 }
932
933 $this->render_logos( '', $settings );
934 }
935
936 /**
937 * Render payment logos on the cart page.
938 *
939 * @since 2.2.8
940 *
941 * @return void
942 */
943 public function cart_page_output() {
944 $this->render_logos( 'merchant-payment-logos--cart' );
945 }
946
947 /**
948 * Render payment logos on the checkout page.
949 *
950 * @since 2.2.8
951 *
952 * @return void
953 */
954 public function checkout_page_output() {
955 $this->render_logos( 'merchant-payment-logos--checkout' );
956 }
957
958 /**
959 * Render payment logos in the footer.
960 *
961 * @since 2.2.8
962 *
963 * @return void
964 */
965 public function footer_output() {
966 $this->render_logos( 'merchant-payment-logos--footer' );
967 }
968
969 /**
970 * Render payment logos on archive / shop pages.
971 *
972 * @since 2.2.8
973 *
974 * @return void
975 */
976 public function archive_page_output() {
977 $this->render_logos( 'merchant-payment-logos--archive' );
978 }
979
980 /**
981 * Custom CSS.
982 *
983 * @return string
984 */
985 public function get_module_custom_css() {
986 $css = '';
987
988 // Font Size.
989 $css .= Merchant_Custom_CSS::get_variable_css( 'payment-logos', 'font-size', '18', '.merchant-payment-logos', '--mrc-plogos-font-size', 'px' );
990
991 // Text Color.
992 $css .= Merchant_Custom_CSS::get_variable_css( 'payment-logos', 'text-color', '#212121', '.merchant-payment-logos', '--mrc-plogos-text-color' );
993
994 // Margin Top.
995 $css .= Merchant_Custom_CSS::get_variable_css( 'payment-logos', 'margin-top', '20', '.merchant-payment-logos', '--mrc-plogos-margin-top', 'px' );
996
997 // Margin Bottom.
998 $css .= Merchant_Custom_CSS::get_variable_css( 'payment-logos', 'margin-bottom', '20', '.merchant-payment-logos', '--mrc-plogos-margin-bottom', 'px' );
999
1000 // Align.
1001 $css .= Merchant_Custom_CSS::get_variable_css( 'payment-logos', 'align', 'flex-start', '.merchant-payment-logos', '--mrc-plogos-align' );
1002
1003 // Image Max Width.
1004 $css .= Merchant_Custom_CSS::get_variable_css( 'payment-logos', 'image-max-width', '80', '.merchant-payment-logos', '--mrc-plogos-image-max-width', 'px' );
1005
1006 // Image Max Height.
1007 $css .= Merchant_Custom_CSS::get_variable_css( 'payment-logos', 'image-max-height', '100', '.merchant-payment-logos', '--mrc-plogos-image-max-height', 'px' );
1008
1009 return $css;
1010 }
1011
1012 /**
1013 * Admin custom CSS.
1014 *
1015 * @param string $css The custom CSS.
1016 * @return string $css The custom CSS.
1017 */
1018 public function admin_custom_css( $css ) {
1019 $css .= $this->get_module_custom_css();
1020
1021 return $css;
1022 }
1023
1024 /**
1025 * Frontend custom CSS.
1026 *
1027 * @param string $css The custom CSS.
1028 *
1029 * @return string
1030 */
1031 public function frontend_custom_css( $css ) {
1032 $settings = $this->get_module_settings();
1033
1034 if ( ! $this->should_show_on_current_page( $settings ) ) {
1035 return $css;
1036 }
1037
1038 $css .= $this->get_module_custom_css();
1039
1040 return $css;
1041 }
1042 }
1043
1044 // Load the icon library.
1045 require_once MERCHANT_DIR . 'inc/modules/payment-logos/class-payment-icons-library.php';
1046
1047 // Initialize the module.
1048 add_action( 'init', function() {
1049 Merchant_Modules::create_module( new Merchant_Payment_Logos() );
1050 } );
1051