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 / compatibility / class-merchant-ohio-theme.php

class-merchant-ohio-theme.php in Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant 2.3.0, at inc/compatibility/class-merchant-ohio-theme.php

660 lines 24.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Ohio Theme compatibility layer
4 *
5 * @package Merchant
6 */
7
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 /**
13 * Ohio Theme compatibility layer
14 */
15 if ( ! class_exists( 'Merchant_Ohio_Theme' ) ) {
16 class Merchant_Ohio_Theme {
17
18 /**
19 * Constructor.
20 */
21 public function __construct() {
22 if ( ( is_admin() && ! wp_doing_ajax() ) || ! merchant_is_ohio_active() ) {
23 return;
24 }
25
26 // Custom CSS.
27 add_filter( 'merchant_custom_css', array( $this, 'frontend_custom_css' ) );
28
29 $ajax_cart = ! class_exists( 'OhioOptions' ) || OhioOptions::get( 'woocommerce_product_ajax_cart', true );
30
31 // Custom scripts
32 add_action( 'wp_footer', function() {
33 ?>
34 <script>
35 jQuery( document ).ready( function ( $ ) {
36
37 /**
38 * Ohio theme using their own AJAX scripts and not using 'added_to_cart' event which is required.
39 * So adding `added_to_cart` event here
40 */
41 const originalAjax = $.ajax;
42 $.ajax = function( options ) {
43 if (
44 options.url === wc_cart_fragments_params.ajax_url
45 && ( options.data.action === 'ohio_ajax_add_to_cart_woo' || options.data.action === 'ohio_ajax_add_to_cart_woo_single' ) ) {
46 const originalSuccess = options.success;
47 options.success = function(response) {
48 // Call the original Ohio success handler
49 originalSuccess.apply( this, arguments );
50 if ( response && ! response.error ) {
51 // Ensure fragments and cart_hash are present
52 const fragments = response.fragments || {};
53 const cart_hash = response.cart_hash || '';
54 const $button = $( '.data_button_ajax.loading' ); // Find the button that triggered it
55
56 // Trigger added_to_cart with the response data
57 $( document.body ).trigger( 'added_to_cart', [ fragments, cart_hash, $button ] );
58 }
59 };
60 }
61 return originalAjax.apply( this, arguments );
62 };
63
64 // Add loading class to show loading indicator
65 $( '.merchant-sticky-add-to-cart-wrapper button.button' ).addClass( 'data_button_ajax' );
66 $( 'body' ).on( 'adding_to_cart', function( event, $button, data ) {
67 if ( $button.closest( '.merchant-quick-view-content' ).length || $button.closest( '.merchant-sticky-add-to-cart-wrapper' ).length ) {
68 $button.addClass( 'btn-loading' );
69 }
70 } );
71
72 // Quick View, Wishlist, Product Labels, Product Video/Audio Position
73 $( '.merchant-product-labels, .merchant-quick-view-button, .merchant-wishlist-button, .merchant-product-video, .merchant-product-audio' ).each( function() {
74 const $product = $( this ).closest( 'li.product' );
75 const $thumbnail = $product.find( '.product-item-thumbnail' );
76
77 if ( $thumbnail.length ) {
78 $( this )
79 .appendTo( $thumbnail )
80 .css( { 'visibility': 'visible' } );
81 } else {
82 $( this )
83 .appendTo( $product )
84 .css( { 'visibility': 'visible' } );
85 }
86 } );
87 } );
88 </script>
89 <?php
90 } );
91
92 // Product labels
93 if ( Merchant_Modules::is_module_active( Merchant_Product_Labels::MODULE_ID ) ) {
94 $Product_Labels = Merchant_Modules::get_module( Merchant_Product_Labels::MODULE_ID );
95
96 // Archives/Loop
97 remove_action( 'woocommerce_before_shop_loop_item', array( $Product_Labels, 'loop_product_output' ) );
98 add_action( 'woocommerce_after_shop_loop_item_title', array( $Product_Labels, 'loop_product_output' ) );
99
100 // Single Product
101 remove_action( 'woocommerce_product_thumbnails', array( $Product_Labels, 'single_product_output' ) );
102 add_action( 'woocommerce_before_single_product_summary', array( $Product_Labels, 'single_product_output' ) );
103 }
104
105 // Quick View
106 if ( Merchant_Modules::is_module_active( Merchant_Quick_View::MODULE_ID ) && $ajax_cart ) {
107 $Quick_View = Merchant_Modules::get_module( Merchant_Quick_View::MODULE_ID );
108 remove_action( 'woocommerce_after_shop_loop_item', array( $Quick_View, 'quick_view_button' ) );
109 add_action( 'woocommerce_after_shop_loop_item_title', array( $Quick_View, 'quick_view_button' ) );
110 }
111
112 // Buy Now
113 if ( Merchant_Modules::is_module_active( Merchant_Buy_Now::MODULE_ID ) && $ajax_cart ) {
114 $Buy_Now = Merchant_Modules::get_module( Merchant_Buy_Now::MODULE_ID );
115 $button_renderer = $Buy_Now->get_button_renderer();
116
117 if ( $button_renderer ) {
118 remove_action( 'woocommerce_after_shop_loop_item', array( $button_renderer, 'shop_archive_button' ) );
119 add_action( 'woocommerce_after_shop_loop_item_title', array( $button_renderer, 'shop_archive_button' ) );
120 }
121 }
122
123 if ( merchant_is_pro_active() ) {
124 // FBT
125 if ( Merchant_Modules::is_module_active( Merchant_Frequently_Bought_Together::MODULE_ID ) ) {
126 add_filter( 'merchant_frequently_bought_together_woo_hook', function( $hook ) {
127 return 'woocommerce_product_after_tabs';
128 } );
129 }
130
131 // Recently Viewed Products
132 if ( Merchant_Modules::is_module_active( Merchant_Recently_Viewed_Products::MODULE_ID ) ) {
133 add_filter( 'merchant_recently_viewed_products_woo_hook', function( $hook ) {
134 return 'woocommerce_product_after_tabs';
135 } );
136
137 add_filter( 'merchant_recently_viewed_products_wrapper_classes', function( $classes ) {
138 $classes[] = 'woo-products';
139
140 return $classes;
141 } );
142 }
143
144 // Wishlist
145 if ( Merchant_Modules::is_module_active( Merchant_Wishlist::MODULE_ID ) ) {
146 add_filter( 'merchant_wishlist_archive_woo_hook', function( $hook ) {
147 return 'woocommerce_after_shop_loop_item_title';
148 } );
149 }
150
151 // Advanced Reviews
152 if ( Merchant_Modules::is_module_active( Merchant_Advanced_Reviews::MODULE_ID ) ) {
153 add_filter( 'merchant_advanced_review_woo_hook', function( $hook ) {
154 return 'woocommerce_product_after_tabs';
155 } );
156 }
157
158 // Side Cart
159 if ( Merchant_Modules::is_module_active( Merchant_Side_Cart::MODULE_ID ) ) {
160 // remove_filter( 'woocommerce_cart_item_name', 'ohio_add_cart_product_category', 99 );
161 }
162
163 // Product Video/Audio
164 if ( Merchant_Modules::is_module_active( Merchant_Product_Video::MODULE_ID ) || Merchant_Modules::is_module_active( Merchant_Product_Audio::MODULE_ID ) ) {
165 // Archive
166 add_filter( 'merchant_product_video_before_woo_hook', function() {
167 return 'woocommerce_after_shop_loop_item_title';
168 } );
169
170 add_filter( 'merchant_product_audio_before_woo_hook', function() {
171 return 'woocommerce_after_shop_loop_item_title';
172 } );
173
174 /*
175 * Single Product
176 *
177 * `YITH_Featured_Audio_Video_Init` function is needed in Ohio theme for firing `woocommerce_single_product_image_thumbnail_html` action
178 * That's why just defining it here to make sure it exists.
179 * No need any implementation
180 *
181 */
182 if ( ! function_exists( 'YITH_Featured_Audio_Video_Init' ) ) {
183 function YITH_Featured_Audio_Video_Init() {}
184 }
185 }
186
187 if ( Merchant_Modules::is_module_active( Merchant_Complementary_Products::MODULE_ID ) ) {
188 // Complementary products
189 add_action( 'woocommerce_ajax_added_to_cart', array( $this, 'complementary_products_support' ) );
190 }
191 }
192 }
193
194 /**
195 * Frontend custom CSS.
196 *
197 * @param string $css The custom CSS.
198 * @return string $css The custom CSS.
199 */
200 public function frontend_custom_css( $css ) {
201 $css .= Merchant_Side_Cart::get_module_custom_css();
202
203 $css .= '
204 body .cart_item .product-name .variation,
205 body .cart_item .product-total .variation {
206 display: block;
207 }
208 body .cart_item .product-name .variation dd,
209 body .cart_item .product-total .variation dd {
210 margin-inline: 0;
211 }
212 body .cart_item .product-name .variation p,
213 body .cart_item .product-total .variation p {
214 margin: 0;
215 }
216 body.wp-theme-ohio .merchant-cart-offers .merchant-cart-offers-container{
217 position: static;
218 }
219 ';
220
221 // Product labels
222 if ( Merchant_Modules::is_module_active( Merchant_Product_Labels::MODULE_ID ) ) {
223 $css .= '
224 .product-item {
225 position: relative;
226 }
227 li.product .merchant-product-labels {
228 visibility: hidden;
229 }
230 ';
231 }
232
233 // Quick View
234 if ( Merchant_Modules::is_module_active( Merchant_Quick_View::MODULE_ID ) ) {
235 $css .= '
236 li.product .merchant-quick-view-button {
237 visibility: hidden;
238 }
239 ';
240 }
241
242 // Buy Now
243 if ( Merchant_Modules::is_module_active( Merchant_Buy_Now::MODULE_ID ) ) {
244 $css .= '
245 .data_button_ajax.btn-loading.loading:before {
246 position: absolute;
247 }
248 ';
249 }
250
251 if ( merchant_is_ohio_active() ) {
252 // Wishlist
253 if ( Merchant_Modules::is_module_active( Merchant_Wishlist::MODULE_ID ) ) {
254 $css .= '
255 li.product .merchant-wishlist-button {
256 visibility: hidden;
257 }
258 ';
259 }
260
261 // Side Cart
262 if ( Merchant_Modules::is_module_active( Merchant_Side_Cart::MODULE_ID ) ) {
263 $css .= '
264 .merchant-side-cart-item .woo-product-name,
265 .merchant-side-cart-item .woo-category {
266 display: none !important;
267 }
268 .merchant-side-cart-widget .product_list_widget .merchant-quantity-wrap span.merchant-cart-item-name a {
269 display: block !important;
270 }
271 ';
272 }
273
274 // Advanced Review
275 if ( Merchant_Modules::is_module_active( Merchant_Advanced_Reviews::MODULE_ID ) ) {
276 $css .= '
277 .merchant-adv-reviews-modal-photo-slider .star-rating:before,
278 .merchant-adv-reviews .star-rating:before {
279 content: "�
280
281
282
283
284 " !important;
285 }
286 ';
287 }
288
289 // Product Video/Audio
290 if ( Merchant_Modules::is_module_active( Merchant_Product_Video::MODULE_ID ) || Merchant_Modules::is_module_active( Merchant_Product_Audio::MODULE_ID ) ) {
291 $css .= '
292 li.product:has(.merchant-product-video) .product-item-thumbnail .image-holder,
293 li.product:has(.merchant-product-audio) .product-item-thumbnail .image-holder {
294 display: none;
295 }
296 li.product .merchant-product-video,
297 li.product .merchant-product-audio {
298 visibility: hidden;
299 }
300 ';
301 }
302 }
303
304 return $css;
305 }
306
307 /**
308 * Get the variation ID from attributes for a variable product.
309 *
310 * @param int $product_id The variable product ID.
311 * @param array $variation_data The selected variation attributes.
312 *
313 * @return int The variation ID or 0 if not found.
314 */
315 private function get_variation_id_from_attributes( $product_id, $variation_data ) {
316 // Get the product object.
317 $product = wc_get_product( $product_id );
318 if ( ! $product || $product->get_type() !== 'variable' ) {
319 return 0;
320 }
321
322 // Ensure variation data keys are prefixed correctly.
323 $formatted_variation_data = array();
324 foreach ( $variation_data as $key => $value ) {
325 // If the key doesn't start with 'attribute_', assume it's the attribute name and prefix it.
326 $formatted_key = strpos( $key, 'attribute_' ) === 0 ? $key : 'attribute_' . sanitize_title( $key );
327 $formatted_variation_data[ $formatted_key ] = sanitize_text_field( $value );
328 }
329
330 // Get available variations for comparison (optional debugging).
331 $available_variations = $product->get_available_variations();
332 $data_store = WC_Data_Store::load( 'product' );
333 $variation_id = $data_store->find_matching_product_variation( $product, $formatted_variation_data );
334
335 /**
336 * Filter the variation ID found for a complementary product.
337 *
338 * @param int $variation_id The found variation ID (0 if not found).
339 * @param int $product_id The variable product ID.
340 * @param array $formatted_variation_data The formatted variation attributes.
341 * @param array $available_variations The available variations for the product.
342 *
343 * @since 2.2.1
344 */
345 return apply_filters( 'merchant_ohio_complementary_products_variation_id',
346 (int) $variation_id,
347 $product_id,
348 $formatted_variation_data,
349 $available_variations
350 );
351 }
352
353 /**
354 * Add support for complementary products module with Ohio theme custom add to cart action.
355 *
356 * @param int $cart_product_id The ID of the product added to cart.
357 *
358 * @return void
359 * @throws Exception When the cart fails to add the product.
360 */
361 public function complementary_products_support( $cart_product_id ) {
362 if ( ! $this->should_handle_complementary_products() ) {
363 return;
364 }
365
366 $request = $this->get_complementary_request_data();
367 if ( ! $request ) {
368 return;
369 }
370
371 $cart = WC()->cart;
372 $cart_item = $this->find_parent_cart_item( $cart, $cart_product_id, $request['variation_id'] );
373
374 if ( ! $cart_item ) {
375 return;
376 }
377
378 $this->mark_parent_cart_item(
379 $cart,
380 $cart_item['key'],
381 $cart_item['data'],
382 $request['campaign_id']
383 );
384
385 $this->add_complementary_products_to_cart(
386 $cart,
387 $cart_product_id,
388 $cart_item['key'],
389 $request['campaign_id'],
390 $request['products'],
391 $request['quantity']
392 );
393 }
394
395 /**
396 * Check if complementary products logic should run.
397 *
398 * @return bool
399 */
400 private function should_handle_complementary_products() {
401 if ( ! Merchant_Modules::is_module_active( Merchant_Complementary_Products::MODULE_ID ) ) {
402 return false;
403 }
404
405 if ( ! merchant_is_ohio_active() ) {
406 return false;
407 }
408
409 if ( empty( $_POST['complementary_offer_products'] ) || '[]' === $_POST['complementary_offer_products'] ) {
410 return false;
411 }
412
413 if (
414 ! isset( $_POST['complementary_offer_products_nonce'] )
415 || ! wp_verify_nonce(
416 sanitize_text_field( wp_unslash( $_POST['complementary_offer_products_nonce'] ) ),
417 'complementary_offer_products'
418 )
419 ) {
420 return false;
421 }
422
423 return true;
424 }
425
426 /**
427 * Read and sanitize complementary products request data.
428 *
429 * We are ignoring nonce verification here because it is already verified in should_handle_complementary_products().
430 *
431 * @return array|null
432 */
433 private function get_complementary_request_data() {
434 if ( ! isset( $_POST['complementary_offer_products'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Missing
435 return null;
436 }
437 //phpcs:ignore WordPress.Security.NonceVerification.Missing
438 $complementary_products_json = sanitize_text_field( wp_unslash( $_POST['complementary_offer_products'] ) );
439 $complementary_products = json_decode( $complementary_products_json, true );
440
441 if ( ! is_array( $complementary_products ) || empty( $complementary_products ) ) {
442 return null;
443 }
444
445 //phpcs:ignore WordPress.Security.NonceVerification.Missing
446 if ( ! isset( $_POST['complementary_offer_id'] ) ) {
447 return null;
448 }
449
450 //phpcs:ignore WordPress.Security.NonceVerification.Missing
451 $quantity = isset( $_POST['quantity'] ) ? absint( wp_unslash( $_POST['quantity'] ) ) : 1;
452
453 //phpcs:ignore WordPress.Security.NonceVerification.Missing
454 $variation_id = isset( $_POST['variation_id'] ) ? absint( wp_unslash( $_POST['variation_id'] ) ) : 0;
455
456 return array(
457 'products' => $complementary_products,
458 //phpcs:ignore WordPress.Security.NonceVerification.Missing
459 'campaign_id' => sanitize_text_field( wp_unslash( $_POST['complementary_offer_id'] ) ),
460 'quantity' => $quantity,
461 'variation_id' => $variation_id,
462 );
463 }
464
465 /**
466 * Find the parent cart item (main product) in the cart.
467 *
468 * @param WC_Cart $cart Cart instance.
469 * @param int $product_id Product ID added to cart.
470 * @param int $variation_id Variation ID (if any).
471 *
472 * @return array|null {
473 * @type string $key
474 * @type array $data
475 * }
476 */
477 private function find_parent_cart_item( WC_Cart $cart, $product_id, $variation_id ) {
478 $cart_items = $cart->get_cart();
479
480 foreach ( $cart_items as $key => $cart_item ) {
481 if ( $variation_id && isset( $cart_item['variation_id'] ) && (int) $cart_item['variation_id'] === (int) $variation_id ) {
482 return array(
483 'key' => $key,
484 'data' => $cart_item,
485 );
486 }
487
488 if ( ! $variation_id && isset( $cart_item['product_id'] ) && (int) $cart_item['product_id'] === (int) $product_id ) {
489 return array(
490 'key' => $key,
491 'data' => $cart_item,
492 );
493 }
494 }
495
496 return null;
497 }
498
499 /**
500 * Mark the parent cart item with complementary metadata.
501 *
502 * @param WC_Cart $cart Cart instance.
503 * @param string $cart_item_key Cart item key.
504 * @param array $cart_item_data Existing cart item data.
505 * @param string $campaign_id Campaign/offer ID.
506 *
507 * @return void
508 */
509 private function mark_parent_cart_item( WC_Cart $cart, $cart_item_key, array $cart_item_data, $campaign_id ) {
510 $cart_item_data['is_complementary_parent'] = true;
511 $cart_item_data['merchant_added_via'] = 'complementary_products';
512 $cart_item_data['merchant_module_id'] = Merchant_Complementary_Products::MODULE_ID;
513 $cart_item_data['complementary_offer_id'] = $campaign_id;
514
515 $cart->cart_contents[ $cart_item_key ] = array_merge( $cart->cart_contents[ $cart_item_key ], $cart_item_data );
516 }
517
518 /**
519 * Add all complementary products to the cart.
520 *
521 * @param WC_Cart $cart
522 * @param int $main_product_id
523 * @param string $parent_cart_item_key
524 * @param string $campaign_id
525 * @param array $complementary_products
526 * @param int $quantity
527 *
528 * @return void
529 */
530 private function add_complementary_products_to_cart(
531 WC_Cart $cart,
532 $main_product_id,
533 $parent_cart_item_key,
534 $campaign_id,
535 $complementary_products,
536 $quantity
537 ) {
538 foreach ( $complementary_products as $comp_product ) {
539 $this->add_single_complementary_product(
540 $cart,
541 $main_product_id,
542 $parent_cart_item_key,
543 $campaign_id,
544 $comp_product,
545 $quantity
546 );
547 }
548 }
549
550 /**
551 * Add a single complementary product to the cart.
552 *
553 * @param WC_Cart $cart
554 * @param int $main_product_id
555 * @param string $parent_cart_item_key
556 * @param string $campaign_id
557 * @param array $comp_product
558 * @param int $quantity
559 *
560 * @return void
561 */
562 private function add_single_complementary_product(
563 WC_Cart $cart,
564 $main_product_id,
565 $parent_cart_item_key,
566 $campaign_id,
567 array $comp_product,
568 $quantity
569 ) {
570 $comp_product_id = absint( $comp_product['product_id'] );
571 $comp_product_object = wc_get_product( $comp_product_id );
572
573 if ( ! $comp_product_object ) {
574 wc_add_notice(
575 sprintf(
576 /* translators: %s: Product ID */
577 __( 'Complementary product with ID "%s" does not exist.', 'merchant' ),
578 $comp_product_id
579 ),
580 'error'
581 );
582
583 return;
584 }
585
586 $product_type = sanitize_text_field( $comp_product['product_type'] );
587 $variation_data = $this->sanitize_variation_data(
588 isset( $comp_product['variation_data'] ) ? $comp_product['variation_data'] : array()
589 );
590
591 $comp_variation_id = 0;
592 if ( 'variable' === $product_type && ! empty( $variation_data ) ) {
593 $comp_variation_id = $this->get_variation_id_from_attributes( $comp_product_id, $variation_data );
594 if ( ! $comp_variation_id ) {
595 wc_add_notice(
596 sprintf(
597 /* translators: %s: Product name */
598 __( 'Invalid variation selected for complementary product "%s".', 'merchant' ),
599 $comp_product_object->get_name()
600 ),
601 'error'
602 );
603
604 return;
605 }
606 }
607
608 $comp_cart_item_data = array(
609 'complementary_main_product' => $main_product_id,
610 'complementary_parent' => $parent_cart_item_key,
611 'complementary_offer_id' => $campaign_id,
612 'merchant_added_via' => 'complementary_products',
613 'merchant_module_id' => Merchant_Complementary_Products::MODULE_ID,
614 'is_complementary' => true,
615 );
616
617 $added_to_cart = $cart->add_to_cart(
618 $comp_product_id,
619 $quantity,
620 $comp_variation_id,
621 $variation_data,
622 $comp_cart_item_data
623 );
624
625 if ( ! $added_to_cart ) {
626 wc_add_notice(
627 sprintf(
628 /* translators: %s: Product name */
629 __( 'Failed to add complementary product "%s" to the cart.', 'merchant' ),
630 $comp_product_object->get_name()
631 ),
632 'error'
633 );
634 }
635 }
636
637 /**
638 * Sanitize variation data array.
639 *
640 * @param array $variation_data
641 *
642 * @return array
643 */
644 private function sanitize_variation_data( array $variation_data ) {
645 $sanitized = array();
646
647 foreach ( $variation_data as $key => $value ) {
648 $sanitized_key = sanitize_key( $key );
649 $sanitized_value = map_deep( $value, 'sanitize_text_field' );
650 $sanitized[ $sanitized_key ] = $sanitized_value;
651 }
652
653 return $sanitized;
654 }
655 }
656
657 add_action( 'init', function() {
658 new Merchant_Ohio_Theme();
659 } );
660 }