Combined WooCommerce Checkout/Cart page and adding cross-sell - php

I've combined the shortcodes for checkout and cart onto the Checkout page in two columns to reduce the number of clicks to finish payment.
However, I'm finding that hooks seem to act strangely with this arrangement.
For example, I'm trying to place the cross-sell section below the cart section. On the default cart page, it appears by default. Combining cart and checkout together makes it disappear.
The logical approach is to do this:
function add_cart_collaterals() {
if (is_checkout()) {
add_action( 'woocommerce_after_cart_contents', 'woocommerce_cross_sell_display' );
}
}
add_action('wp', 'add_cart_collaterals');
That did nothing.
Out of desperation, I then copied and adapted the cross-sell.php template code and put it directly into my child theme's functions.php file like this:
/* Display Cross-Sells below cart */
function show_cross_sell() {
if ( $cross_sells ) :
echo '<div class="cross-sells"><h2>';
_e( 'You may be interested in…', 'woocommerce' );
echo '</h2>';
woocommerce_product_loop_start();
foreach ( $cross_sells as $cross_sell ) :
$post_object = get_post( $cross_sell->get_id() );
setup_postdata( $GLOBALS['post'] =& $post_object );
wc_get_template_part( 'content', 'product' );
endforeach;
woocommerce_product_loop_end();
echo '</div>';
else : {
echo 'No cross sells to display';
}
endif;
}
add_action( 'woocommerce_after_cart_table', 'show_cross_sell', 10 );
But, only the else condition runs and displays "No cross sells to display". So maybe it lost scope on the $cross_sells object.
Is there any hope of my being able to achieve this?

WooCommerce support came through with an answer that helped me. I wasn't assigning $cross_sells to anything. I needed to assign it like this $cross_sells = array_filter( array_map( 'wc_get_product', WC()->cart->get_cross_sells() ), 'wc_products_array_filter_visible' );
But I'm still curious to know why the hook action wasn't working.

As to why you couldn't achieve it with the hook, it's probably because of the first line of code in the function that you hooked into that action:
function woocommerce_cross_sell_display( $limit = 2, $columns = 2, $orderby = 'rand', $order = 'desc' ) {
if ( is_checkout() ) {
return;
}

Related

Remove added restrictions tab in WooCommerce product data

I'm using WooCommerce and WooCommerce Catalog Visibility Options plugins.
I want to remove some tabs in product data box, such as General, Inventory, Shipping and Restrictions tabs. I have removed the first three tabs, except for the Restrictions, by using the method below:
add_filter( 'woocommerce_product_data_tabs', 'woo_remove_product_tabs', 10, 1 );
function woo_remove_product_tabs( $tabs ) {
unset( $tabs['general'] );
unset( $tabs['inventory'] );
unset( $tabs['shipping'] );
unset( $tabs['restrictions'] );
return $tabs;
}
From researching, I know that the Restrictions tab is added by WooCommerce Catalog Visibility Options plugin. I dig round into the source code and I found the way how the Restrictions was added
public function __construct() {
add_action( 'woocommerce_product_write_panel_tabs', array( $this, 'add_tab' ) );
}
public function add_tab() {
?>
<li class="wc_catalog_restrictions_tab wc_catalog_restrictions">
<span><?php _e( 'Restrictions', 'wc_catalog_restrictions' ); ?></span>
</li><?php
}
I also tried this:
add_filter( 'woocommerce_product_write_panel_tabs', 'remove_restrictions_tab' );
function remove_restrictions_tab( $tabs ) {
unset( $tabs['restrictions'] );
return $tabs;
}
But it didn't work as well. I'm kind new to php, can anyone help me to remove the restrictions tab properly?

AJAX add custom product order meta data from single product page

I've built an add to cart process that successfully creates a "personalisation" field when the item is added to the cart. Using the standard 'add to cart' button, the page reloads and all is well. I wanted to improve the UX by making the basket update via AJAX, which I've implemented and again working ok.
However, this stops the cart item data meta from registering because the request is not defined - the hook isn't able to grab input values from the page any more. The hook I'm using to define and write the meta fields to is 'woocommerce_add_cart_item_data'
I tried creating my own separate AJAX function to define the content of the field separately, using $_SESSION instead of $_REQUEST (inspired by this answer -> how to pass ajax data to woocommerce filter?)
Unfortunately this seemingly fires after 'woocommerce_add_cart_item_data' has run, so the meta data does not come through (in fact it ends up showing on the next product added, since the variable is defined late and not accessed until the 2nd product is added).
I'd really appreciate some help with either:
(1) Amending the code so 'woocommerce_add_cart_item_data' is able to take data from the AJAX add to cart action I made, or
(2) Amend my custom AJAX call so session data is able to be applied in a timely order (unsure if this is achievable based on what I've seen so far).
// Create AJAX add to cart button
function add_cart_btn($prod) {
echo apply_filters( 'woocommerce_loop_add_to_cart_link',
sprintf( 'Add to bag',
esc_url( $prod->add_to_cart_url() ),
esc_attr( $prod->id ),
esc_attr( $prod->get_sku() ),
$prod->is_purchasable() ? 'add_to_cart_button' : '',
esc_attr( $prod->product_type ),
esc_html( $prod->add_to_cart_text() )
),
$prod );
}
add_cart_btn($product);
// functions.php
/* Saves field data */
function save_add_custom_info_field( $cart_item_data, $product_id ) {
if( isset( $_REQUEST['custom_info_message'] ) ) {
// *** $_REQUEST['custom_info_message'] not defined ***
$cart_item_data[ 'custom_info_message' ] = $_REQUEST['custom_info_message'];
$cart_item_data['unique_key'] = md5( microtime().rand() );
}
return $cart_item_data;
}
add_action( 'woocommerce_add_cart_item_data', 'save_add_custom_info_field', 10, 2 );
/* Renders field entry on cart and checkout */
function render_mssg_meta_on_cart_and_checkout( $cart_data, $cart_item = null ) {
$custom_items = array();
if( !empty( $cart_data ) ) {
$custom_items = $cart_data;
}
if( isset( $cart_item['custom_info_message'] ) ) {
$custom_items[] = array( "name" => 'Personalisation / Customisation', "value" => $cart_item['custom_info_message'] );
}
return $custom_items;
}
add_filter( 'woocommerce_get_item_data',
'render_mssg_meta_on_cart_and_checkout', 10, 2 );
// *** Attempted solution with AJAX - Use $_SESSION['message'] in 'save_add_custom_info_field' function above instead of $_REQUEST... delivered after 'woocommerce_add_cart_item_data' so doesn't work
function set_customised_message() {
$message = $_POST['message'];
define( 'DOING_AJAX', true );
if ( ! defined( 'WP_ADMIN' ) ) {
define( 'WP_ADMIN', true );
}
session_start();
$_SESSION['message'] = $message;
echo $message;
die();
}
I expect custom meta to show on the cart/checkout pages, but it isn't due to the variable being either $cart_item['custom_info_message'] being undefined, or session variable from AJAX call not being available.
I've managed to solve this by updating the product meta data after it is added to the cart (via AJAX) - amending set_customised_message function above. Note the AJAX call required a timeout delay of 1s in order to work.
Leaving it here in case anyone else is having a similar issue.
function set_customised_message() {
$cart = WC()->cart->cart_contents;
foreach( $cart as $cart_item_id=>$cart_item ) {
echo $cart_item['unique_key'];
$cart_item['custom_info_message'] = $message;
echo $cart_item['custom_info_message'];
WC()->cart->cart_contents[$cart_item_id] = $cart_item;
}
WC()->cart->set_session();
}
Thanks to https://pluginrepublic.com/how-to-update-existing-woocommerce-cart-meta-data/ for this solution

Is there a shortcode for product description in WooCommerce

Is there any shortcode to call the product description(text field under the title)?
For now, I'm using another custom field to do this job but it will be better if I use the WooCommerce field.
You can build your own shortcode this way:
add_shortcode( 'product_description', 'display_product_description' );
function display_product_description( $atts ){
$atts = shortcode_atts( array(
'id' => get_the_id(),
), $atts, 'product_description' );
global $product;
if ( ! is_a( $product, 'WC_Product') )
$product = wc_get_product($atts['id']);
return $product->get_description();
}
Code goes in function.php file of your active child theme (active theme). Tested and works.
Examples of USAGE [product_description]
1) In the current product page ph:
echo do_shortcode( "[product_description]" );
2) In any php code providing the related product ID
echo do_shortcode( "[product_description id='37']" );
I wrote a very similar solution to #LoicTheAztec's answer above, but slightly more defensively (as his solution was breaking Elementor edits for me due to there not being a product context when it executes the shortcode on save).
It also solves your paragraph/newline issue as the content is formatted (basically replacing newlines with <p> tags) before returning.
function custom_product_description($atts){
global $product;
try {
if( is_a($product, 'WC_Product') ) {
return wc_format_content( $product->get_description("shortcode") );
}
return "Product description shortcode run outside of product context";
} catch (Exception $e) {
return "Product description shortcode encountered an exception";
}
}
add_shortcode( 'custom_product_description', 'custom_product_description' );

Filter out unwanted order item meta data from Woocommerce email notifications

In the order email templates (for example email-order-items.php), WooCommerce uses the function wc_display_item_meta to display product details in the order table. The function code is present in the wc-template-functions.php file (line number 3011). I am copying the function code below for reference
function wc_display_item_meta( $item, $args = array() ) {
$strings = array();
$html = '';
$args = wp_parse_args( $args, array(
'before' => '<ul class="wc-item-meta"><li>',
'after' => '</li></ul>',
'separator' => '</li><li>',
'echo' => true,
'autop' => false,
) );
foreach ( $item->get_formatted_meta_data() as $meta_id => $meta ) {
$value = $args['autop'] ? wp_kses_post( $meta->display_value ) : wp_kses_post( make_clickable( trim( $meta->display_value ) ) );
$strings[] = '<strong class="wc-item-meta-label">' . wp_kses_post( $meta->display_key ) . ':</strong> ' . $value;
}
if ( $strings ) {
$html = $args['before'] . implode( $args['separator'], $strings ) . $args['after'];
}
$html = apply_filters( 'woocommerce_display_item_meta', $html, $item, $args );
if ( $args['echo'] ) {
echo $html; // WPCS: XSS ok.
} else {
return $html;
}
}
The problem is: it doesn't take any arguments that can help me filter out item data that I don't want to show in the order email. I don't want to change this function in the wc-template-functions.php as it's a core file. So, I want to know if there's a piece of code that I can add to functions.php that'll somehow modify this wc_display_item_meta function to filter out specific item meta.
Note: I know someone might suggest why not just remove that particular item data from the product details, but that data is essential to internal order processing. I just don't want it to show to the customers.
Update #1: What meta data I don't want to show in the order email? Below is a screenshot of an order email. I have highlighted three item data.."Qty Selector", "Qty" and "Total". I want all these three to not show in the order email.
Try the following without any guarantee (as I don't really have the real necessary keys):
add_filter( 'woocommerce_order_item_get_formatted_meta_data', 'unset_specific_order_item_meta_data', 10, 2);
function unset_specific_order_item_meta_data($formatted_meta, $item){
// Only on emails notifications
if( is_admin() || is_wc_endpoint_url() )
return $formatted_meta;
foreach( $formatted_meta as $key => $meta ){
if( in_array( $meta->key, array('Qty Selector', 'Qty', 'Total') ) )
unset($formatted_meta[$key]);
}
return $formatted_meta;
}
Code goes in function.php file of your active child theme (active theme). Tested with other meta data than yours and works. I hope it will work for you too.
Now, the hook used with this code is the right filter hook. It's located in the WC_Order_Item method get_formatted_meta_data() and allows to filter the order item meta data.
There is a bug with the accepted answer, and all of the other snippets that I've found around the internet, so I'm posting my own answer here in the hopes that stores around the world don't accidentally leak information.
The problem is that when you use the Order actions meta box to resend the email, the filter check fails because is_admin() === true.
The order actions is a meta box down the side of the Orders page:
So the first time, when the order is created, it filters the email like you want, but then if an admin resends the email to a customer then it will be broken and show all of the meta fields to the user in the resent email.
The code that fixes this scenario is this:
$is_resend = isset($_POST['wc_order_action']) ? wc_clean( wp_unslash( $_POST['wc_order_action'] ) ) === 'send_order_details' : false;
if ( !$is_resend && (is_admin() || is_wc_endpoint_url() ) ) {
return $formatted_meta;
}
So if you look at the linked snippet then you will see the meta box adds that field to the $_POST. It has to be cleaned up like that as well or it won't match.
The full example integrated into the accepted solution's answer is:
add_filter( 'woocommerce_order_item_get_formatted_meta_data', 'unset_specific_order_item_meta_data', 10, 2);
function unset_specific_order_item_meta_data($formatted_meta, $item){
// Only on emails notifications
$is_resend = isset($_POST['wc_order_action']) ? wc_clean( wp_unslash( $_POST['wc_order_action'] ) ) === 'send_order_details' : false;
if ( !$is_resend && (is_admin() || is_wc_endpoint_url() ) ) {
return $formatted_meta;
}
foreach( $formatted_meta as $key => $meta ){
if( in_array( $meta->key, array('Qty Selector', 'Qty', 'Total') ) )
unset($formatted_meta[$key]);
}
return $formatted_meta;
}
I hear, that you want to show the Order Item Meta Data in the admin backend only. That's actually a tricky one. I have played around for some hours but no solution I have found, guarentee that the Order Itema Meta Data doesn't show up in e-mails to the customer.
The thing is that there are several ways these e-mails are fired (eg. through the resend meta box (which #rtpHarry mentions) or by changing order status either at the order overview, the single order view or an automatic/programmatically order status change). That gives many cases where it's neccessary to unset the Order Item Meta Data - you need to find all cases except the admin backend.
Therefore, my suggestion is to first completely remove the Order Item Meta Data using the above mentioned woocommerce_order_item_get_formatted_meta_data filter and then add them again using an action like woocommerce_before_order_itemmeta which ONLY fires in the admin backend. Because the Order Item Meta Data is unset you cannot use the get_formatted_meta_data method to get the data. Instead you can use the function wc_get_order_item_meta.
Complete code (tested and works):
//Hide 'Qty Selector', 'Qty' and 'Total' completely
add_filter( 'woocommerce_order_item_get_formatted_meta_data', 'unset_specific_order_item_meta_data');
function unset_specific_order_item_meta_data($formatted_meta){
foreach( $formatted_meta as $key => $meta ){
if( in_array( $meta->key, array('Qty Selector', 'Qty', 'Total') ) )
unset($formatted_meta[$key]);
}
return $formatted_meta;
}
//Add 'Qty Selector', 'Qty' and 'Total' in the admin backend only
add_action('woocommerce_before_order_itemmeta', 'add_specific_order_item_meta_data_in_backend', 10, 2);
function add_specific_order_item_meta_data_in_backend( $item_id, $item ) {
//Only applies for line items
if( $item->get_type() !== 'line_item' ) return;
$qty_sel_lines = wc_get_order_item_meta($item_id, 'Qty Selector', false);
$qty_lines = wc_get_order_item_meta($item_id, 'Qty', false);
$total_lines = wc_get_order_item_meta($item_id, 'Total', false);
foreach ($qty_sel_lines as $qty_sel_line){
echo $qty_sel_line . '<br>';
}
foreach ($qty_lines as $qty_line){
echo $qty_line . '<br>';
}
foreach ($total_lines as $total_line){
echo $total_line. '<br>';
}
}
Note:
If you need to add the Order Item Meta Data to the admin e-mails, you need to do that seperately. I have not examined the options on that.
I kind of agreed with #pstidsen argument. So I was thinking about how to solve this without to re-add all the metadata, since it kind of disturbed me not to handle it in the same way as it was added before. I have additional filters to add css classes and so on to the metadata. So there would've been a need to take care of.
So here is my approach which gives you the opportunity to use it for emails, custom emails, pdf invoices or similar scenarios. I also uses a fallback to filter for our frontend or any situation we didn't consider.
Please keep the order of if else in mind. I checked the admin filter the last, to make sure any other filter gets fired before. The situation for an email at example is: It's sent from the admin interface, so the admin filter is true but also is the email filter.
Functionality for a different filter for admin emails is given as well.
/**
* This function filters all unwanted item metadata, if the specific filter are hooked in
* we also use a fallback filter, if none of the hooks are fired
*
* #params array() $metadata
*
*/
add_filter( 'woocommerce_order_item_get_formatted_meta_data', 'custom_filter_item_meta_data', 50, 1);
function custom_filter_item_meta_data( $metadata ){
if ( empty( $metadata ) ) return $metadata;
$filter_array = array();
if ( apply_filters( 'custom_filter_item_meta_email', false ) ){
// email filter goes here
$filter_array = array( 'whatever','you', 'wanna', 'filter, 'for', 'email' );
}elseif ( apply_filters( 'custom_filter_item_meta_admin_email', false ) ){
// admin email filter goes here
// pass
elseif ( apply_filters( 'custom_filter_item_meta_invoice', false ) ){
// invoice filter goes here
$filter_array = array( 'whatever','you', 'wanna', 'filter, 'for', 'invoices' );
}elseif ( apply_filters( 'custom_filter_item_meta_admin', false ) ){
// general admin filter goes here
$filter_array = array( 'whatever','you', 'wanna', 'filter, 'for', 'admin_backend' );
}else{
// fallback filter
$filter_array = array( 'whatever','you', 'wanna', 'filter, 'for', 'fallback' );
}
foreach ( $metadata as $key => $meta ){
if ( in_array( $meta->key, $filter_array ) ){
unset ( $metadata[ $key ] );
}
}
return $metadata;
}
/**
* Is used to enable our item meta filter for our admin backend
* Hooked:
* #admin_init
*/
add_action( 'admin_init', 'custom_init_item_meta_filter_admin', 50, 1 );
function custom_init_item_meta_filter_admin(){
add_filter( 'custom_filter_item_meta_admin', function(){ return true; });
}
/**
* Is used to enable our item meta filter for emails
* Hooked:
* #woocommerce_email_order_details
*/
add_action( 'woocommerce_email_order_details', 'custom_init_item_meta_filter_email' ), 10, 2);
function custom_init_item_meta_filter_email( $order, $sent_to_admin ){
if ( $sent_to_admin ){
add_filter('custom_filter_item_meta_admin_email', function(){ return true; } );
}else{
add_filter('custom_filter_item_meta_email', function(){ return true; } );
}
}
/**
* Is used to enable our item meta filter for invoices
* Hooked:
* #wpo_wcpdf_before_order_details
*/
add_filter( 'wpo_wcpdf_before_order_details', 'custom_init_item_meta_filter_invoice', 10, 1);
function custom_init_item_meta_filter_invoice(){
add_filter( 'custom_filter_item_meta_invoice', function(){ return true; });
}
I didn't test it in that "flattened" format. I used it within different classes of my oop coded plugin and edited it to post it here.
Structure of the html code of the metadata tags
If I want to delete meta_data 2 and meta_data 3:
add_filter( 'woocommerce_display_item_meta', 'filter_woocommerce_display_item_meta', 10, 3 );
function filter_woocommerce_display_item_meta( $html, $item, $args ) {
$arrayPortionsTags = explode("<li", $html);
unset($arrayPortionsTags[2],$arrayPortionsTags[3]);
$firstLi = array( '<li' );
$lastUl = array( '</ul>' );
array_splice( $arrayPortionsTags, 1, 0, $firstLi );
array_splice( $arrayPortionsTags, 3, 0, $lastUl );
$html= implode('',$arrayPortionsTags);
return $html;
};

Why does the_title() filter is also applied in menu title?

I have created below function to hide page title. But when I execute this code, it also hides the menu name.
function wsits_post_page_title( $title ) {
if( is_admin())
return $title;
$selected_type = get_option('wsits_page_show_hide');
if(!is_array($selected_type)) return $title;
if ( ( in_array(get_post_type(), $selected_type ) ) && get_option('wsits_page_show_hide') )
{
$title = '';
}
return $title;
}
add_filter( 'the_title', array($this, 'wsits_post_page_title') );
Nikola is correct:
Because menu items also have titles and they need to be filtered :).
To make this only call in the posts, and not in menus, you can add a check for in_the_loop() - if it is true, you're in a post.
So change the first line in the function to:
if( is_admin() || !in_the_loop() )
and all should be well.
It's a bit of a hack but you can solve this by adding your action to loop_start.
function make_custom_title( $title, $id ) {
// Your Code Here
}
function set_custom_title() {
add_filter( 'the_title', 'make_custom_title', 10, 2 );
}
add_action( 'loop_start', 'set_custom_title' );
By embedding the_title filter inside of a loop_start action, we avoid overwriting the menu title attributes.
You can do something like that :
In your function.php :
add_filter( 'the_title', 'ze_title');
function ze_title($a) {
global $dontTouch;
if(!$dontTouch && !is_admin())
$a = someChange($a);
return $a;
}
In your template :
$dontTouch = 1;
wp_nav_menu( array('menu' => 'MyMenu') );
$dontTouch = 0;
Posting this answer because it was the search result I ended up clicking on while searching about targeting the filter hook the_title while ignoring the filter effect for navigation items.
I was working on a section in a theme which I wanted to add buttons to the page title within the heading one tag.
It looked similar to this:
<?php echo '<h1>' . apply_filters( 'the_title', $post->post_title ) . '</h1>'.PHP_EOL; ?>
I was then "hooking in" like this:
add_filter( 'the_title', 'my_callback_function' );
However, the above targets literally everything which calls the_title filter hook, and this includes navigation items.
I changed the filter hook definition like this:
<?php echo '<h1>' . apply_filters( 'the_title', $post->post_title, $post->ID, true ) . '</h1>'.PHP_EOL; ?>
Pretty much every call to the_title filter passes parameter 1 as the $post->post_title and parameter 2 as the $post->ID. Search the WordPress core code for apply_filters( 'the_title'* and you'll see for yourself.
So I decided to add a third parameter for situations where I want to target specific items which call the_title filter. This way, I can still receive the benefit of all callbacks which apply to the_title filter hook by default, while also having the ability to semi-uniquely target items that use the_title filter hook with the third parameter.
It's a simple boolean parameter:
/**
* #param String $title
* #param Int $object_id
* #param bool $theme
*
* #return mixed
*/
function filter_the_title( String $title = null, Int $object_id = null, Bool $theme = false ) {
if( ! $object_id ){
return $title;
}
if( ! $theme ){
return $title;
}
// your code here...
return $title;
}
add_filter( 'the_title', 'filter_the_title', 10, 3 );
Label the variables however you want. This is what worked for me, and it does exactly what I need it to do. This answer may not be 100% relevant to the question asked, but this is where I arrived while searching to solve this problem. Hope this helps someone in a similar situation.
The global $dontTouch; solution didn't work for me for some reason. So I simply removed the filter around the menu thus in header.php:
remove_filter( 'the_title', 'change_title' );
get_template_part( 'template-parts/navigation/navigation', 'top' );
add_filter( 'the_title', 'change_title' );
And all is well.
I think you're looking for this:
function change_title($title) {
if( in_the_loop() && !is_archive() ) { // This will skip the menu items and the archive titles
return $new_title;
}
return $title;
}
add_filter('the_title', array($this, 'change_title'), 10, 2);

Categories