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

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