PluginProbe
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell / 3.13.3
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell v3.13.3
3.13.3 3.13.2 3.13.1 3.13.0 3.12.13 3.12.12 3.12.11 3.12.10 3.12.9 3.12.8 3.12.7 3.12.6 3.12.5 3.12.4 3.12.3 3.12.1 3.12.2 3.12.0 3.11.1 3.11.0 3.10.9 3.10.8 3.10.7 3.10.6 2.8.16 All 261 releases
wpfunnels / public / modules / checkout / class-wpfnl-product-selection.php

class-wpfnl-product-selection.php in WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell 3.13.3, at public/modules/checkout/class-wpfnl-product-selection.php

390 lines 14.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Product Selection for Checkout
4 *
5 * This class handles the 'Your Products' section on the checkout page.
6 * It allows customers to select/deselect products based on the Product Options settings.
7 *
8 * Features:
9 * - Display a product selection table before the payment section
10 * - Support for three conditions:
11 * 1. restrict_all: All products are selected ( no UI shown )
12 * 2. select_one: User can select only one product ( radio buttons )
13 * 3. select_multiple: User can select multiple products ( checkboxes )
14 * - Real-time cart updates via AJAX
15 * - Responsive design matching WPFunnels patterns
16 * - Shows product images, names, and prices ( with discounts if applied )
17 * - Prevents cart from being empty ( at least one product must be selected )
18 *
19 * Settings are configured in:
20 * Admin > Funnel > Checkout Step > Products Tab > Product Options
21 *
22 * @package WPFunnels
23 * @since 3.2.0
24 */
25
26 namespace WPFunnels\Modules\Frontend\Checkout;
27
28 use WPFunnels\Wpfnl_functions;
29
30 class Product_Selection {
31
32 /**
33 * Constructor
34 */
35
36 public function __construct() {
37 add_action( 'woocommerce_before_order_notes', [ $this, 'render_product_selection' ], 5 );
38 add_action( 'wp_ajax_wpfnl_update_product_selection', [ $this, 'ajax_update_product_selection' ] );
39 add_action( 'wp_ajax_nopriv_wpfnl_update_product_selection', [ $this, 'ajax_update_product_selection' ] );
40 add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
41 }
42
43 /**
44 * Enqueue scripts and styles
45 */
46
47 public function enqueue_scripts() {
48 if ( ! Wpfnl_functions::check_if_this_is_step_type( 'checkout' ) ) {
49 return;
50 }
51
52 $step_id = get_the_ID();
53 $product_options = get_post_meta( $step_id, '_wpfnl_checkout_product_options', true );
54
55 // Only enqueue if product options are enabled
56 if ( ! $product_options || 'yes' !== $product_options[ 'isEnabled' ] ) {
57 return;
58 }
59
60 $condition = isset( $product_options[ 'condition' ] ) ? $product_options[ 'condition' ] : 'restrict_all';
61
62 // Don't enqueue if restrict all
63 if ( 'restrict_all' === $condition ) {
64 return;
65 }
66
67 // Enqueue CSS
68 wp_enqueue_style(
69 'wpfnl-product-selection',
70 WPFNL_URL . 'public/assets/css/wpfnl-product-selection.css',
71 array(),
72 WPFNL_VERSION
73 );
74
75 // Enqueue JS
76 wp_enqueue_script(
77 'wpfnl-product-selection',
78 WPFNL_URL . 'public/assets/js/wpfnl-product-selection.js',
79 array( 'jquery' ),
80 WPFNL_VERSION,
81 true
82 );
83
84 // Localize script
85 wp_localize_script(
86 'wpfnl-product-selection',
87 'wpfnl_checkout_product_selection',
88 array(
89 'ajax_url' => admin_url( 'admin-ajax.php' ),
90 'nonce' => wp_create_nonce( 'wpfnl-checkout' ),
91 'step_id' => $step_id,
92 )
93 );
94 }
95
96 /**
97 * Render product selection section
98 */
99 public function render_product_selection() {
100 if ( ! Wpfnl_functions::check_if_this_is_step_type( 'checkout' ) ) {
101 return;
102 }
103
104 $step_id = get_the_ID();
105 $product_options = get_post_meta( $step_id, '_wpfnl_checkout_product_options', true );
106
107 // Check if product options are enabled
108 if ( ! $product_options || 'yes' !== $product_options['isEnabled'] ) {
109 return;
110 }
111
112 $condition = isset( $product_options['condition'] ) ? $product_options['condition'] : 'restrict_all';
113
114 // Don't show if restrict all
115 if ( 'restrict_all' === $condition ) {
116 return;
117 }
118
119 $products = get_post_meta( $step_id, '_wpfnl_checkout_products', true );
120 if ( ! is_array( $products ) || empty( $products ) ) {
121 return;
122 }
123
124 // Get cart items to determine selected products
125 $cart_items = WC()->cart->get_cart();
126 $cart_product_ids = array();
127 foreach ( $cart_items as $cart_item ) {
128 $cart_product_ids[] = $cart_item[ 'product_id' ];
129 if ( $cart_item[ 'variation_id' ] ) {
130 $cart_product_ids[] = $cart_item[ 'variation_id' ];
131 }
132 }
133
134 ?>
135 <div class = 'wpfnl-product-selection-wrapper'>
136 <!-- <h3 class = 'wpfnl-product-selection-title'><?php esc_html_e( 'Your Products', 'wpfnl' );
137 ?></h3> -->
138 <div class = 'wpfnl-product-selection-list'>
139 <input type = 'hidden' name = '_wpfunnels_product_option' value = ''>
140 <table class = 'wpfnl-products-table'>
141 <thead>
142 <tr>
143 <th class = 'product-select'><?php esc_html_e( 'SELECT', 'wpfnl' );
144 ?></th>
145 <th class = 'product-name'><?php esc_html_e( 'PRODUCT', 'wpfnl' );
146 ?></th>
147 <th class = 'product-price'><?php esc_html_e( 'PRICE', 'wpfnl' );
148 ?></th>
149 <!-- <th class = 'product-remove'></th> -->
150 </tr>
151 </thead>
152 <tbody>
153 <?php
154 $index = 0;
155 foreach ( $products as $product_data ) {
156 $product_id = $product_data[ 'id' ];
157 $product = wc_get_product( $product_id );
158
159 if ( ! $product || 'trash' === $product->get_status() ) {
160 continue;
161 }
162
163 $is_selected = in_array( $product_id, $cart_product_ids );
164 $input_type = 'select_one' === $condition ? 'radio' : 'checkbox';
165 $input_name = 'select_one' === $condition ? 'wpfnl_selected_product' : 'wpfnl_selected_products[]';
166
167 // Get product details
168 $title = $product->get_type() == 'variation'
169 ? Wpfnl_functions::get_formated_product_name( $product )
170 : $product->get_name();
171 $image = wp_get_attachment_image_src( $product->get_image_id(), 'thumbnail' );
172 $price = $product->get_price();
173
174 // Calculate discount if applied
175 $discount = get_post_meta( $step_id, '_wpfnl_checkout_discount_main_product', true );
176 $discounted_price = $price;
177 if ( $discount && isset( $discount[ 'discountOptions' ] ) && 'original' !== $discount[ 'discountOptions' ] ) {
178 if ( 'discount-percentage' === $discount[ 'discountOptions' ] ) {
179 $discount_amount = ( $price * $discount[ 'mutedDiscountValue' ] ) / 100;
180 $discounted_price = $price - $discount_amount;
181 } elseif ( 'discount-price' === $discount[ 'discountOptions' ] ) {
182 $discounted_price = $price - $discount[ 'mutedDiscountValue' ];
183 }
184 }
185
186 ?>
187 <tr class = "wpfnl-product-row <?php echo $is_selected ? 'selected' : ''; ?>" data-product-id = "<?php echo esc_attr( $product_id ); ?>" data-index = "<?php echo esc_attr( $index ); ?>">
188 <td class = 'product-select'>
189 <input
190 type = "<?php echo esc_attr( $input_type ); ?>"
191 name = "<?php echo esc_attr( $input_name ); ?>"
192 value = "<?php echo esc_attr( $product_id ); ?>"
193
194 class = 'wpfnl-product-checkbox'
195 <?php checked( $is_selected, true );
196 ?>
197 data-product-id = "<?php echo esc_attr( $product_id ); ?>"
198 data-index = "<?php echo esc_attr( $index ); ?>"
199 />
200 </td>
201 <td class = 'product-name'>
202 <div class = 'product-info'>
203 <?php if ( $image ) : ?>
204 <img src = "<?php echo esc_url( $image[0] ); ?>" alt = "<?php echo esc_attr( $title ); ?>" class = 'product-image'>
205 <?php endif;
206 ?>
207 <span class = 'product-title'><?php echo esc_html( $title );
208 ?></span>
209 </div>
210 </td>
211 <td class = 'product-price'>
212 <?php echo wc_price( $discounted_price );
213 ?>
214 <?php if ( $discounted_price < $price ) : ?>
215 <del><?php echo wc_price( $price );
216 ?></del>
217 <?php endif;
218 ?>
219 </td>
220 <!-- <td class = 'product-remove'>
221 <?php if ( $is_selected ) : ?>
222 <button
223 type = 'button'
224
225 class = 'wpfnl-remove-product'
226 data-product-id = "<?php echo esc_attr( $product_id ); ?>"
227 data-index = "<?php echo esc_attr( $index ); ?>"
228 title = "<?php esc_attr_e( 'Remove product', 'wpfnl' ); ?>"
229 >
230 <svg xmlns = 'http://www.w3.org/2000/svg' viewBox = '0 0 24 24' fill = 'currentColor'>
231 <path d = 'M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z'/>
232 </svg>
233 </button>
234 <?php endif;
235 ?>
236 </td> -->
237 </tr>
238 <?php
239 $index++;
240 }
241 ?>
242 </tbody>
243 </table>
244 </div>
245 </div>
246 <?php
247 }
248
249 /**
250 * AJAX handler to update product selection
251 */
252
253 public function ajax_update_product_selection() {
254 check_ajax_referer( 'wpfnl-checkout', 'security' );
255
256 $product_id = isset( $_POST[ 'product_id' ] ) ? absint( $_POST[ 'product_id' ] ) : 0;
257 $action_type = isset( $_POST[ 'action_type' ] ) ? sanitize_text_field( $_POST[ 'action_type' ] ) : 'add';
258 $step_id = isset( $_POST[ 'step_id' ] ) ? absint( $_POST[ 'step_id' ] ) : get_the_ID();
259 if ( ! $product_id || ! $step_id ) {
260 wp_send_json_error( array( 'message' => __( 'Invalid request', 'wpfnl' ) ) );
261 }
262
263 $product_options = get_post_meta( $step_id, '_wpfnl_checkout_product_options', true );
264 $condition = isset( $product_options[ 'condition' ] ) ? $product_options[ 'condition' ] : 'restrict_all';
265
266 // Get all products from this checkout step
267 $products = get_post_meta( $step_id, '_wpfnl_checkout_products', true );
268 if ( ! is_array( $products ) || empty( $products ) ) {
269 wp_send_json_error( array( 'message' => __( 'No products found', 'wpfnl' ) ) );
270 }
271
272 // Build array of all product IDs and find the selected product data
273 $all_product_ids = array();
274 $product_data = null;
275 foreach ( $products as $p ) {
276 if ( isset( $p[ 'id' ] ) ) {
277 $all_product_ids[] = absint( $p[ 'id' ] );
278 if ( absint( $p[ 'id' ] ) === $product_id ) {
279 $product_data = $p;
280 }
281 }
282 }
283
284 if ( ! $product_data ) {
285 wp_send_json_error( array( 'message' => __( 'Product not found', 'wpfnl' ) ) );
286 }
287
288 // Get WooCommerce product object
289 $wc_product = wc_get_product( $product_id );
290 if ( ! $wc_product ) {
291 wp_send_json_error( array( 'message' => __( 'Invalid product', 'wpfnl' ) ) );
292 }
293
294 // Get current cart
295 $cart = WC()->cart->get_cart();
296 // Find which checkout products are currently in cart
297 $cart_product_ids = array();
298 foreach ( $cart as $cart_item_key => $cart_item ) {
299 $cart_product_id = !empty( $cart_item[ 'variation_id' ] ) ? absint( $cart_item[ 'variation_id' ] ) : absint( $cart_item[ 'product_id' ] );
300 if ( in_array( $cart_product_id, $all_product_ids ) ) {
301 $cart_product_ids[ $cart_product_id ] = $cart_item_key;
302 }
303 }
304
305 // Handle based on action type
306 if ( 'add' === $action_type ) {
307
308 // For RADIO buttons ( select_one ): Remove ALL checkout products, then add ONLY selected one
309 if ( 'select_one' === $condition ) {
310 // Remove all checkout products from cart
311 foreach ( $cart_product_ids as $pid => $cart_key ) {
312 WC()->cart->remove_cart_item( $cart_key );
313 }
314
315 // Add only the selected product
316 $quantity = isset( $product_data[ 'quantity' ] ) ? absint( $product_data[ 'quantity' ] ) : 1;
317
318 // Check if it's a variation or simple product
319 if ( $wc_product->is_type( 'variation' ) ) {
320 $parent_id = $wc_product->get_parent_id();
321 $variation_id = $product_id;
322 $cart_item_key = WC()->cart->add_to_cart( $parent_id, $quantity, $variation_id );
323 } else {
324 $cart_item_key = WC()->cart->add_to_cart( $product_id, $quantity );
325 }
326
327 if ( ! $cart_item_key ) {
328 wp_send_json_error( array( 'message' => __( 'Failed to add product', 'wpfnl' ) ) );
329 }
330
331 WC()->cart->calculate_totals(); wp_send_json_success( array(
332 'message' => __( 'Product selected', 'wpfnl' ),
333 'cart_hash' => WC()->cart->get_cart_hash(),
334 'fragments' => apply_filters( 'woocommerce_update_order_review_fragments', array() )
335 ) );
336 }
337
338 // For CHECKBOXES (select_multiple): Add selected product (keep others)
339 elseif ( 'select_multiple' === $condition ) {
340 // Check if product is already in cart
341 if ( isset( $cart_product_ids[ $product_id ] ) ) {
342 // Already in cart, just return success
343 wp_send_json_success( array(
344 'message' => __( 'Product already in cart', 'wpfnl' ),
345 'cart_hash' => WC()->cart->get_cart_hash(),
346 'fragments' => apply_filters( 'woocommerce_update_order_review_fragments', array() )
347 ) );
348 }
349
350 // Add product to cart
351 $quantity = isset( $product_data['quantity'] ) ? absint( $product_data['quantity'] ) : 1;
352
353 // Check if it's a variation or simple product
354 if ( $wc_product->is_type( 'variation' ) ) {
355 $parent_id = $wc_product->get_parent_id();
356 $variation_id = $product_id;
357 $cart_item_key = WC()->cart->add_to_cart( $parent_id, $quantity, $variation_id );
358 } else {
359 $cart_item_key = WC()->cart->add_to_cart( $product_id, $quantity );
360 }
361
362 if ( ! $cart_item_key ) {
363 wp_send_json_error( array( 'message' => __( 'Failed to add product', 'wpfnl' ) ) );
364 }
365 WC()->cart->calculate_totals();
366
367 wp_send_json_success( array(
368 'message' => __( 'Product added to cart', 'wpfnl' ),
369 'cart_hash' => WC()->cart->get_cart_hash(),
370 'fragments' => apply_filters( 'woocommerce_update_order_review_fragments', array() )
371 ) );
372 }
373
374 } elseif ( 'remove' === $action_type ) {
375 // Remove the product from cart
376 WC()->cart->remove_cart_item( $cart_product_ids[ $product_id ] );
377
378 wp_send_json_success( array(
379 'message' => __( 'Product removed from cart', 'wpfnl' ),
380 'cart_hash' => WC()->cart->get_cart_hash(),
381 'fragments' => apply_filters( 'woocommerce_update_order_review_fragments', array() )
382 ) );
383 }
384
385 wp_send_json_error( array( 'message' => __( 'Invalid action', 'wpfnl' ) ) );
386 }
387 }
388
389 new Product_Selection();
390