Changing user role in WooCommerce on purchase - php

I'm working on WooCommerce with two product types. I need some of them ($products_to_check) to create an specific role account (subscriber) and the rest of them another one (customer, which is the default one for WooCommerce).
The thing is, if I have an user who's already a costumer, I don't want him to change its role to subscriber (so I check if he's logged in, which mean he's not a new costumer and have a previous defined role). On the other hand, if a subscriber purchase one of the products not included in $products_to_check, he should get a role upgrade (from subscriber to costumer).
This is my function, which is not working but I don't really know why.
add_action( 'woocommerce_order_status_completed', 'change_role_on_purchase' );
function change_role_on_purchase( $order_id ) {
$order = wc_get_order( $order_id );
$items = $order->get_items();
$products_to_check = array( ... );
$user = new WP_User( $order->user_id );
$user_meta = get_userdata( $order->user_id );
$user_roles = $user_meta->roles;
foreach ( $items as $item ) {
if ( $order->user_id > 0 && in_array( $item['product_id'], $products_to_check ) ) {
if (!is_user_logged_in()){
$user->set_role( 'subscriber' );
}
}
elseif ( $order->user_id > 0 && !(in_array( $item['product_id'], $products_to_check)) ) {
if (is_user_logged_in() && in_array('subscriber', $user_roles)){
$user->set_role( 'customer' );
}
}
}
}
I'd really appreciate any help on this issue. What am I doing wrong?

There are several things you need to take note here.
Make sure you hook is invoked after or override the WooCommerce's or any other plugins' hooks that set user roles.
Do not use your log in condition to check if users are new. user_register is the hook to use for newly registered users.
Your role logic loop here is also flawed. You are iterating through all product orders and attempts to set role on each iteration. This obviously means that each iteration can change the user role if condition satisfies and can override the previous ones. (Though it seems like you are hoping your logged in condition will do the trick, I think that's very error prone even if it could maybe work.)
The WooCommerce subscription plugin actually sets the subscriber role to users that have active subscriptions and customer role for expired subscription users. This is configureable but I think that's a more clear assignment.
So first make sure your hook triggers after or overrides the WooCommerce user role hooks. And I think you can do away completely with the new or old user logic, as the only important thing is whether they purchased a product that's going to upgrade them; if not, they should have the default role.

Related

Show extra sections on a product page ONLY to users who already purchased that product (Elementor, Crocoblock, JetEngine)

I have a website with few WooCommerce products, and the product page is created with Elementor Pro. I want users to be able to click Add to Cart and buy the product normally.
But then when the user goes back to the product page (after buying the product), they would not see the the Add to Cart button (if will be hidden for those that purchased the product) and they will see another extra section.
How can I do this?
I also have Crocoblock and JetEngine that have a Dynamic Visibility functionality built in Elementor.
The easiest way to achieve this would be by creating a child theme. The child theme should contain 2 added functionalities.
For this to fully work you will need all users to be registered and NOT have checkout as a guest enabled. Also, this will only work for new orders. If you want to add compatibility for old orders you'd have to create a script that sets the meta value for all users that have purchased this product before the code was implemented.
First of we create a function that adds metadata to the purchasing user account if the product was in the cart. Add the following code to your child theme functions.php
add_action( 'woocommerce_thankyou', 'custom_add_user_meta');
function custom_add_user_meta( $order_id ){
$user_id = get_current_user_id();
$order = wc_get_order( $order_id );
$items = $order->get_items();
foreach ( $items as $item_id => $item ) {
$product_id = $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id();
if ( $product_id == 12345 ) {
add_user_meta( $user_id, '_product_xyz_purchased', "true");
}
}
}
Now we add the following code to the same functions.php file to render additional content on the single product page of WooCommerce. NOTE: This step is for a more complete overview that can be implemented outside of Crocoblocks/Jetegine. The following step can be skipped because Jetengine allows setting conditions based on user_meta
add_action("woocommerce_after_single_product", 'render_additional_content', 50);
function render_additional_content() {
$user_id = get_current_user_id();
$product_purchased = get_user_meta( $user_id, '_product_xyz_purchased', true);
if($product_purchased == "true"){
echo "here you can put the HTML you want to display";
}
}

Disable AJAX to cart on Private products only Woocommerce

My initial problem is that AJAX add-to-cart button on Woocommerce doesn't seem to work on Private products (which we're only displaying to a selection of customers): the wheel appears on the add-to-cart button, the page reloads, and the product is added to cart but there's no notice that it has, so it's quite confusing.
After lots of research, all I could find is this thread https://wordpress.org/support/topic/add-to-cart-redirecting-only-on-private-products-2/ which led me to believe it's most likely a bug?
I tried to come up with a work-around: disable AJAX add-to-cart and revert to default behaviour on Private products only so we can still display some kind of notification that the product has been added to cart. I've thought of this code as a starting point:
add_action( 'wp_enqueue_scripts', 'bbloomer_disable_woocommerce_cart_fragments', 11 );
function bbloomer_disable_woocommerce_cart_fragments() {
if ( is_front_page() ) wp_dequeue_script( 'wc-cart-fragments' );
}
Would this work? And how would I modify it so the condition is "if is private product"?
You could solve the problem by thinking the other way around. Instead of showing private products only for specific users you could hide them for all other user roles (leaving the products with the "publish" status).
This way the Ajax problem does not arise.
If the user has one of the roles defined in the $user_roles array of the following function, it hides all products in the $product_ids_to_hide array:
// hide products based on user role
add_filter( 'woocommerce_product_is_visible', 'hide_products_by_user_role', 10, 2 );
function hide_products_by_user_role( $visible, $product_id ) {
// only if the user is logged in
if ( ! is_user_logged_in() ) {
return $visible;
}
// set the product ids to hide
$product_ids_to_hide = array( 12, 14 );
// sets the user roles for which products are to be hidden
$user_roles = array( 'custom_role_1', 'custom_role_2' );
$user = wp_get_current_user();
if ( array_intersect( $user_roles, $user->roles ) && in_array( $product_id, $product_ids_to_hide ) ) {
return false;
}
return $visible;
}
To reply to your comment:
If you want to allow the user to modify the product ids to be hidden based on the user role you can create a .csv file to be uploaded to FTP with the list of product ids, one per line.
If your client in the future wants to remove or add a product id, he can do so simply by overwriting the .csv file with an FTP import or directly in the file manager (if the hosting allows it).
The file I created as an example will be called product-ids-to-hide.csv and will need to be uploaded to the same directory as the functions.php file. No header is required. Here is an example:
349
235
456
745
and this will be the new updated function:
// hide products based on user role
add_filter( 'woocommerce_product_is_visible', 'hide_products_by_user_role', 10, 2 );
function hide_products_by_user_role( $visible, $product_id ) {
// only if the user is logged in
if ( ! is_user_logged_in() ) {
return $visible;
}
// sets the user roles for which products are to be hidden
$user_roles = array( 'custom_role_1', 'custom_role_2' );
$user = wp_get_current_user();
// only if the user has at least one role present in the array
if ( empty( array_intersect( $user_roles, $user->roles ) ) ) {
return $visible;
}
// get an array with product ids
$product_ids_to_hide = file( realpath( __DIR__ . '/product-ids-to-hide.csv' ), FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES );
if ( in_array( $product_id, $product_ids_to_hide ) ) {
return false;
}
return $visible;
}
The code has been tested and works. Add it to your active theme's functions.php.

Prevent User Role from changing woocommerce order status

We would like to prevent shop manager from changing order status, we found a help here in the link below Restrict user role to change only some order statuses in Woocommerce
But the issue here that it limits certain role ( shop Manager ) to some order statuses, we need to deny the shop manager from changing the order status completely not limit it to some order statuses.
Also the snippet we mentioned remove the the order statuses from the bulk action drop down & the order details here: https://prnt.sc/mpfl3b, we need to remove the statuses too from the quick action column here https://snipboard.io/B6SYHb.jpg
Simply we try to have the shop manager to when he try to change order status from bulk, order details page, or actions column to find there is no order statuses to select to change it or disable it completely.
Best Regards
As you can see in the example code the conditions of the statuses are determined in the if statement, because you want to apply this without a limit, it's just a matter of removing that if statement and returning empty arrays
p.s; if you mark my answer as the solution, then also vote for #LoicTheAztec original answer if you have not already done this, since his code just about contained the solution.
// Admin orders list: bulk order status change dropdown
function filter_dropdown_bulk_actions_shop_order( $actions ) {
// Targeting shop_manager
if( current_user_can( 'shop_manager' ) ) {
$actions = (array) null;
}
return $actions;
}
add_filter( 'bulk_actions-edit-shop_order', 'filter_dropdown_bulk_actions_shop_order', 20, 1 );
// Admin orders list: quick action
function filter_order_actions( $actions, $order ) {
// Targeting shop_manager
if( current_user_can( 'shop_manager' ) ) {
$actions = (array) null;
}
return $actions;
}
add_filter( 'woocommerce_admin_order_actions', 'filter_order_actions', 10, 2 );
// Admin order pages: order status dropdown
function filter_order_statuses( $order_statuses ) {
global $post, $pagenow;
if( $pagenow === 'post.php' || $pagenow === 'post-new.php' ) {
// Get ID
$order_id = $post->ID;
// Get an instance of the WC_Order object
$order = wc_get_order( $order_id );
// TRUE
if ( $order ) {
// Get current order status
$order_status = 'wc-' . $order->get_status();
// New order status
$new_order_statuses = array();
foreach ($order_statuses as $key => $option ) {
// Targeting "shop_manager"
if( current_user_can('shop_manager') && $key == $order_status ) {
$new_order_statuses[$key] = $option;
}
}
if( sizeof($new_order_statuses) > 0 ) {
return $new_order_statuses;
}
}
}
return $order_statuses;
}
add_filter('wc_order_statuses', 'filter_order_statuses', 10, 1 );
//Since the suggested answer apparently causes some new issues and doesn't solve the original issue in a couple of other cases, there are options to hide elements according to user type, something like the below - which is a bit of kludge, but might serve:
First, to load an admin style sheet applying only to Shop Managers:
/**
* SHOP MANAGER STYLES
* Front (Optional) and Back End stylesheet
* Style interface for users logged in with'shop_manager' role
* Add to theme functions.php
*/
add_action('admin_enqueue_scripts', 'shop_manager_styles');
//if front end stylesheet needs to be added to cover admin bar:
//add_action('wp_enqueue_scripts', 'shop_manager_styles' ) ;
function shop_manager_styles() {
$user = wp_get_current_user() ;
//uncomment following and remove next if not confined to admin
//if ( $user && in_array( 'shop_manager', $user->roles ) ) {
if ( in_array( 'shop_manager', $user->roles ) ) {
//time() as stylesheeet version to help bust caching - may not be necessary but doesn't hurt:
wp_enqueue_style(
'shop_manager_styles', get_stylesheet_directory_uri()
. '/css/shop_manager_styles.css', array(), time()
);
}
}
...and the css to hide the order-status label and menu completely, as well as related columns in shop_order sub-pages:
/** HIDE ORDER STATUS LABEL, SELECTION MENU IN ORDER EDIT
* AND RELATED COLUMNS IN shop_order SUB-PAGE
*/
.wc-order-status,
.column-order_status,
.column-wc_actions {
display: none;
}
You'd save that in your theme css folder in a new shop_manager_styles.css.
Now, you may have some need to show the order status to shop managers without their being able to edit it. That would also be doable with CSS, if also (even more) a kludge. It may be that you have other peculiarities in your installation that will prevent the above code or a minimally customized variation of it from working, but, even if it is a little less clean than removing the option via function, this kind of thing usually will work in a pinch.
(Edited to provide option to add stylesheet on front end - in case relevant options appearing in admin bar, otherwise no need to enqueue extra non-admin script.)

Showing a product to registered users that haven't purchase it yet in WooCommerce

I wanted to show a product only when the user has registered and has not bought this product (new user product).
After they buy this product once, it is no longer available to that user to purchase (because it's a 1-shot deal).
So this means after I bought it, and do things like try to navigate directly to that product's URL, or do a search for all products, etc, this item would not show up.
If I was a new user and didn't buy it, of course, it should show up everywhere.
I have a custom property (metadata) on a user that indicates whether they buy it or not. After they purchase it, I will set this user flag to true indicating it can no longer show.
I had two questions:
Is there a way to hook into the actual successful purchasing of an item, and set this user flag to be true?
How can I instruct Woo to not show this product for a certain user when their flag is true?
Thank you!
Yes this is possible trough 3 functions:
1) A conditional function that will check that customer has bought your specific product:
function has_bought_items( $user_id = 0, $product_id = 0 ) {
// The customer ID
$customer_id = $user_id == 0 || $user_id == '' ? get_current_user_id() : $user_id;
// Retrieve your customer flag '_has_bought_flag' (or replace it by your slug)
if ( get_user_meta( $customer_id, '_has_bought_flag', true ) )
return true;
else
return false;
}
Code goes in function.php file of your active child theme (or theme) or also in any plugin file.
2) A custom function hooked in pre_get_posts that will change the WP_Query on shop and archives pages, checking if user is logged in and if he has already bought this specific product:
// Changing the WP_Query loop conditionally
add_action( 'pre_get_posts', 'conditional_product_query', 10, 1 );
function conditional_product_query( $q ) {
// HERE set your product ID
$product_id = 37;
if( ! is_user_logged_in() || has_bought_items( '', $product_id ) )
$q->set( 'post__not_in', array($product_id) );
}
It will remove completely this product everywhere, when the condition match.
Code goes in function.php file of your active child theme (or theme) or also in any plugin file.
3) A custom function hooked in woocommerce_order_status_completed that will set the customer flag when order status get "completed" when the product is in that order and when the customer flag doesn't exist yet:
// When Order get the "completed" status (paid) we check and we set the user flag (if necessary)
add_action( 'woocommerce_order_status_completed', 'set_customer_specific_product_flag', 10, 2 );
function set_customer_specific_product_flag( $order_id, $order ) {
// HERE set your product ID
$product_id = 37;
// If customer has already bought the product we exit
if( has_bought_items( $order->get_user_id(), $product_id ) ) return;
// Checking order items (if it match we update user meta data
foreach( $order->get_items() as $product_item ){
if ( $product_item->get_product_id() == $product_id ){
update_user_meta( $order->get_user_id(), '_has_bought_flag', '1' );
break;
}
}
}
Code goes in function.php file of your active child theme (or theme) or also in any plugin file.
This code is tested on Woocommerce 3+ and works (It should work on previous versions too).
Related answer: Checking if customer has already bought something in WooCommerce

Woocommerce default role and role after purchase

I found this thread but it doesn't exactly do what I'm looking for.
My question is, how can I change the default Woocommerce role "customer" to, e.g. "Subscriber" for new registered users.
And then, if user checks out (purchases product), change the role from "Subscriber" to "Customer".
I'm asking this as I want to show different content per user roles: "registered customer" and "subscribed (paid) customer".
The first part of your question is changing the default role of user that is created by WooCommerce. Honestly, I'd probably leave the default role as customer. And then create a new role/capability for people who purchase your specific product.
add_filter( 'woocommerce_new_customer_data', 'so_29647785_default_role' );
function so_29647785_default_role( $data ){
$data['role'] = 'subscriber'; // the new default role
return $data;
}
As you saw in my other answer (well my re-posted answer), you can do things once the user has finished paying, but hooking into the woocommerce_order_status_completed hook.
To adapt that code to do something specific to a particular product, you need to loop through the order items and check them against a product ID. Replace 999 with the ID of the product in question.
add_action( 'woocommerce_order_status_completed', 'so_29647785_convert_customer_role' );
function so_29647785_convert_customer_role( $order_id ) {
$order = new WC_Order( $order_id );
if ( $order->user_id > 0 ) {
foreach ( $order->get_items() as $order_item ) {
if( 999 == $order_item[ 'product_id' ] ) {
$user = new WP_User( $order->user_id );
// Remove existing role
$user->remove_role( 'customer' );
// Add new role
$user->add_role( 'subscriber' );
}
}
}
}
Note: Totally untested, but seems right in theory.

Categories