PluginProbe
Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant / 2.2.7
Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant v2.2.7
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.2.7, at inc/compatibility/class-merchant-ohio-theme.php

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