PluginProbe
SellKit – Funnel builder and checkout optimizer for WooCommerce to sell more, faster / 2.7.0
SellKit – Funnel builder and checkout optimizer for WooCommerce to sell more, faster v2.7.0
2.7.0 2.6.0 trunk 1.1.0 1.1.4 1.2.1 1.2.2 1.2.3 1.2.5 1.2.9 1.3.1 1.3.2 1.5.0 1.5.1 1.5.4 1.5.7 1.5.8 1.5.9 1.6.2 1.6.5 1.6.8 1.7.2 1.7.4 1.7.5 1.7.9 All 43 releases
← All changes | includes/elementor/modules/checkout/classes/global-hooks.php +851 -28 1.2.32.7.0 View file →
@@ -3,10 +3,13 @@
3 3 namespace Sellkit\Elementor\Modules\Checkout\Classes;
4 4
5 5 defined( 'ABSPATH' ) || exit;
6 6
7 -use Sellkit\Elementor\Modules\Checkout\Classes\Local_Hooks;
8 -use Elementor\Plugin as Elementor;
7 +use Sellkit\Elementor\Modules\Checkout\Classes\{ Local_Hooks, Helper };
8 +use Sellkit\Funnel\Analytics\Data_Updater;
9 +use Sellkit\Funnel\Contacts\Base_Contacts;
10 +use Sellkit\Funnel\Steps\Checkout;
11 +use Sellkit_Funnel;
9 12
10 13 /**
11 14 * Global hooks.
12 15 *
@@ -11,12 +14,29 @@
11 14 * Global hooks.
12 15 *
13 16 * @SuppressWarnings(PHPMD.UnusedFormalParameter)
14 17 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
18 + * @SuppressWarnings(PHPMD.TooManyMethods)
19 + * @SuppressWarnings(PHPMD.NPathComplexity)
20 + * @SuppressWarnings(PHPMD.ExcessiveClassLength)
15 21 * @since 1.1.0
16 22 */
17 23 class Global_Hooks {
18 24 /**
25 + * Helper id during checkout process.
26 + *
27 + * @var int
28 + */
29 + private $helper_id = 0;
30 +
31 + /**
32 + * List of upsell product with price
33 + *
34 + * @var array
35 + */
36 + private static $sellkit_upsell_products;
37 +
38 + /**
19 39 * Create instance of class without construct.
20 40 *
21 41 * @since 1.1.0
22 42 * @return object
@@ -55,8 +75,12 @@
55 75
56 76 // Save user defined fields.
57 77 add_action( 'woocommerce_checkout_update_order_meta', [ $this, 'save_user_defined_fields' ] );
58 78
79 + // Display user defined fields in order edit page.
80 + add_action( 'woocommerce_admin_order_data_after_billing_address', [ $this, 'add_item_to_order_details' ], 10, 1 );
81 + add_action( 'woocommerce_admin_order_data_after_shipping_address', [ $this, 'add_item_to_order_details' ], 10, 1 );
82 +
59 83 // Add custom shipping field to email.
60 84 add_filter( 'woocommerce_order_get_formatted_shipping_address', [ $this, 'attach_user_shipping_fields_to_order_email' ], 10, 3 );
61 85
62 86 // Add custom billing field to email.
@@ -71,8 +95,25 @@
71 95 // Fix shipping method price change on ajax.
72 96 // Manage bump order discounts.
73 97 // Template replacement.
74 98 add_action( 'woocommerce_checkout_update_order_review', [ $this, 'sellkit_checkout_during_ajax' ], 10, 1 );
99 +
100 + add_filter( 'sellkit-shipping-methods-choosen-method', [ $this, 'sellkit_default_shipping_method' ], 10, 1 );
101 +
102 + // Modify discounted products.
103 + add_action( 'woocommerce_before_calculate_totals', [ $this, 'before_cart_calculate' ], 999 );
104 +
105 + // Allow viewing of Checkout page in the Editor with an empty cart.
106 + $action = isset( $_REQUEST['action'] ) ? filter_var( $_REQUEST['action'], FILTER_SANITIZE_FULL_SPECIAL_CHARS ) : ''; // phpcs:ignore
107 + $elementor_preview = isset( $_REQUEST['elementor-preview'] ) ? filter_var( $_REQUEST['elementor-preview'], FILTER_SANITIZE_FULL_SPECIAL_CHARS ) : ''; // phpcs:ignore
108 +
109 + if (
110 + ( 'elementor' === $action && is_admin() ) // Elementor Editor.
111 + || 'elementor_ajax' === $action // Elementor Editor Preview - Ajax Render Widget.
112 + || ! empty( $elementor_preview ) // Elementor Editor Preview.
113 + ) {
114 + add_filter( 'woocommerce_checkout_redirect_empty_cart', '__return_false', 5 );
115 + }
75 116 }
76 117
77 118 /**
78 119 * Handle ajax calls.
@@ -101,9 +142,9 @@
101 142 * @since 1.1.0
102 143 */
103 144 public function checkout_fragment( $response ) {
104 145 // Get posted data and identify if it's been sent by jupiter widget or not.
105 - $checkout_form_data = filter_input( INPUT_POST, 'post_data', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
146 + $checkout_form_data = filter_input( INPUT_POST, 'post_data', FILTER_DEFAULT );
106 147 $checkout_form_data = explode( '&', $checkout_form_data );
107 148 $posted_data = [];
108 149
109 150 foreach ( $checkout_form_data as $input ) {
@@ -108,9 +149,9 @@
108 149
109 150 foreach ( $checkout_form_data as $input ) {
110 151 $item = explode( '=', urldecode( $input ) );
111 152
112 - $posted_data[ $item[0] ] = $item[1];
153 + $posted_data[ trim( $item[0] ) ] = $item[1];
113 154 }
114 155
115 156 // Identify if it's been sent by jupiter checkout widget or not.
116 157 if ( ! array_key_exists( 'form_id', $posted_data ) && ! array_key_exists( 'post_id', $posted_data ) ) {
@@ -172,18 +213,37 @@
172 213 * @return void
173 214 * @since 1.1.0
174 215 */
175 216 public function change_cart_item_qty() {
176 - $qty = filter_input( INPUT_POST, 'qty', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
177 - $id = filter_input( INPUT_POST, 'id', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
178 - $action = filter_input( INPUT_POST, 'mode', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
217 + $qty = filter_input( INPUT_POST, 'qty', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
218 + $id = filter_input( INPUT_POST, 'id', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
219 + $action = filter_input( INPUT_POST, 'mode', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
220 + $funnel_id = filter_input( INPUT_POST, 'related_checkout', FILTER_SANITIZE_NUMBER_INT );
179 221
222 + self::make_changes_after_cart_item_edit( $funnel_id );
223 +
180 224 if ( 'add' === $action ) {
181 225 WC()->cart->add_to_cart( $id, $qty );
182 - return;
226 + self::make_changes_after_cart_item_edit( $funnel_id );
227 + wp_send_json_success();
183 228 }
184 229
185 230 if ( 'remove' === $action ) {
231 + $post_type = get_post_type( $id );
232 +
233 + if ( 'product_variation' === $post_type ) {
234 + foreach ( WC()->cart->get_cart() as $item_key => $item ) {
235 + if ( $item['variation_id'] === (int) $id ) {
236 + WC()->cart->remove_cart_item( $item_key ); // we remove it.
237 + break; // stop the loop.
238 + }
239 + }
240 +
241 + self::make_changes_after_cart_item_edit( $funnel_id );
242 +
243 + wp_send_json_success();
244 + }
245 +
186 246 $product_cart_id = WC()->cart->generate_cart_id( $id );
187 247 $cart_item_key = WC()->cart->find_product_in_cart( $product_cart_id );
188 248
189 249 if ( $cart_item_key ) {
@@ -189,17 +249,65 @@
189 249 if ( $cart_item_key ) {
190 250 WC()->cart->remove_cart_item( $cart_item_key );
191 251 }
192 252
193 - return;
253 + self::make_changes_after_cart_item_edit( $funnel_id );
254 +
255 + wp_send_json_success();
194 256 }
195 257
196 258 ( $qty > 0 ) ? WC()->cart->set_quantity( $id, $qty ) : WC()->cart->remove_cart_item( $id );
197 259
260 + self::make_changes_after_cart_item_edit( $funnel_id );
261 +
198 262 wp_send_json_success();
199 263 }
200 264
201 265 /**
266 + * Apply changes after editing cart items.
267 + *
268 + * @since 1.2.5
269 + * @param int $funnel_id funnel step id.
270 + */
271 + public static function make_changes_after_cart_item_edit( $funnel_id ) {
272 + if ( empty( $funnel_id ) ) {
273 + return;
274 + }
275 +
276 + $funnel_data = get_post_meta( $funnel_id, 'step_data', true );
277 + $optimization_data = ! empty( $funnel_data['data']['optimization'] ) ? $funnel_data['data']['optimization'] : '';
278 +
279 + if ( empty( $optimization_data ) ) {
280 + return;
281 + }
282 +
283 + $auto_apply_coupon_cookie_value = null;
284 +
285 + if ( isset( $_COOKIE['sellkit_rejected_coupon'] ) ) {
286 + // Get the value of the cookie.
287 + $auto_apply_coupon_cookie_value = sanitize_text_field( wp_unslash( $_COOKIE['sellkit_rejected_coupon'] ) );
288 + $auto_apply_coupon_cookie_value = explode( ',', $auto_apply_coupon_cookie_value );
289 + }
290 +
291 + // Help to apply funnel discount prices and coupons when changing product quantity.
292 + self::apply_discounted_prices( wc()->cart, $funnel_id );
293 + self::apply_discounted_prices( wc()->cart, $funnel_id, 'bumps' );
294 +
295 + if ( Checkout::apply_coupon_validation( $optimization_data ) ) {
296 + WC()->cart->remove_coupons();
297 +
298 + foreach ( $optimization_data['auto_apply_coupons'] as $auto_apply_coupon ) {
299 + if ( ! empty( $auto_apply_coupon_cookie_value ) && in_array( $auto_apply_coupon['label'], $auto_apply_coupon_cookie_value, true ) ) {
300 + continue;
301 + }
302 + wc()->cart->add_discount( get_the_title( $auto_apply_coupon['value'] ) );
303 + }
304 +
305 + wc_clear_notices();
306 + }
307 + }
308 +
309 + /**
202 310 * Place shipping price based on design to checkout order-review.
203 311 *
204 312 * @return void
205 313 * @since 1.1.0
@@ -206,10 +314,20 @@
206 314 */
207 315 public function add_shipping_total_to_review_order() {
208 316 ?>
209 317 <tr class="sellkit-shipping-total">
210 - <th><?php echo __( 'Shipping', 'sellkit' ); ?></th>
211 - <td><?php echo wc_price( WC()->cart->get_shipping_total() ); ?></td>
318 + <th><?php echo esc_html__( 'Shipping', 'sellkit' ); ?></th>
319 + <td>
320 + <?php
321 + $price = WC()->cart->get_shipping_total();
322 +
323 + if ( WC()->cart->display_prices_including_tax() ) {
324 + $price = WC()->cart->get_shipping_total() + WC()->cart->shipping_tax_total;
325 + }
326 +
327 + echo wp_kses_post( wc_price( $price ) );
328 + ?>
329 + </td>
212 330 </tr>
213 331 <?php
214 332 }
215 333
@@ -237,14 +355,19 @@
237 355 $img_html = apply_filters( 'sellkit/includes/elementor/modules/checkout/product-image', $img_html, $product, $cart_item_key );
238 356
239 357 ob_start();
240 358
241 - $extra_class = '';
359 + $extra_class = $product->get_type();
242 360 if ( $cart_items[ $cart_item_key ] === end( $cart_items ) ) {
243 - $extra_class = ' last-cart-item';
361 + $extra_class .= ' last-cart-item';
244 362 }
363 +
364 + if ( 'variation' === $product->get_type() ) {
365 + add_filter( 'woocommerce_cart_item_name', [ $this, 'modify_variation_title' ], 50, 2 );
366 + add_filter( 'woocommerce_get_item_data', [ $this, 'modify_variation_items' ], 10, 2 );
367 + }
245 368 ?>
246 - <tr class="<?php echo esc_attr( apply_filters( 'woocommerce_cart_item_class', 'cart_item', $cart_item, $cart_item_key ) ) . $extra_class; ?>">
369 + <tr class="<?php echo esc_attr( apply_filters( 'woocommerce_cart_item_class', 'cart_item', $cart_item, $cart_item_key ) ) . ' ' . esc_attr( $extra_class ); ?>">
247 370
248 371 <td class="product-name sellkit-one-page-checkout-product-name">
249 372 <?php
250 373 echo $img_html;
@@ -259,9 +382,9 @@
259 382 <div class="name-price" style="<?php echo esc_attr( $fix_style ); ?>">
260 383 <span style="display:inline-block">
261 384 <?php echo wp_kses_post( apply_filters( 'woocommerce_cart_item_name', $product->get_name(), $cart_item, $cart_item_key ) ) . '&nbsp;'; ?>
262 385 </span>
263 - <span class="sellkit-checkout-variations">
386 + <span class="sellkit-checkout-variations" id="sellkit-checkout-variations">
264 387 <?php echo wc_get_formatted_cart_item_data( $cart_item ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
265 388 </span>
266 389 <?php echo apply_filters( 'sellkit-checkout-widget-disable-quantity', '<input type="number" data-id="' . esc_attr( $cart_item_key ) . '" value="' . esc_attr( $cart_item['quantity'] ) . '" class="sellkit-one-page-checkout-product-qty" >', $cart_item['quantity'] ); ?>
267 390 <?php do_action( 'sellkit-checkout-editor-mode-quantity', $cart_item['quantity'] ); ?>
@@ -275,14 +398,62 @@
275 398 $cart_item = ob_get_clean();
276 399
277 400 echo apply_filters( 'sellkit-checkout-cart-item' , $cart_item );
278 401 //phpcs:enable
402 +
403 + if ( 'variation' === $product->get_type() ) {
404 + remove_filter( 'woocommerce_cart_item_name', [ $this, 'modify_variation_title' ], 50 );
405 + remove_filter( 'woocommerce_get_item_data', [ $this, 'modify_variation_items' ], 10 );
406 + }
279 407 }
280 408
281 409 /**
410 + * Remove variation attributes from variation product title.
411 + *
412 + * @param string $default default value.
413 + * @param array $cart_item cart item data.
414 + * @since 1.6.8
415 + */
416 + public function modify_variation_title( $default, $cart_item ) {
417 + $product = $cart_item['product_id'];
418 +
419 + return get_the_title( $product );
420 + }
421 +
422 + /**
423 + * Display variation attributes separated from title even if those are less than 3.
424 + *
425 + * @param array $item_data attributes array.
426 + * @param array $cart_item cart item data.
427 + * @since 1.6.8
428 + */
429 + public function modify_variation_items( $item_data, $cart_item ) {
430 + $product = new \WC_Product_Variation( $cart_item['variation_id'] );
431 + $attributes = $product->get_attributes();
432 +
433 + if ( count( $attributes ) > 2 ) {
434 + return $item_data;
435 + }
436 +
437 + foreach ( $attributes as $key => $value ) {
438 + $key = str_replace( 'pa_', '', $key );
439 + $key = str_replace( '-', '', $key );
440 + $key = str_replace( '_', '', $key );
441 +
442 + $item_data[] = [
443 + 'key' => $key,
444 + 'value' => $value,
445 + ];
446 + }
447 +
448 + return $item_data;
449 + }
450 +
451 + /**
282 452 * Modify fields after pressing place order button.
283 453 * we hooked here because some removed fields still get checked and have not removed by previous hooks.
284 454 *
455 + * @SuppressWarnings(PHPMD.NPathComplexity)
285 456 * @param array $data checkout form data.
286 457 * @return array $data data of woocommerce checkout form.
287 458 * @since 1.1.0
288 459 */
@@ -319,8 +490,14 @@
319 490 unset( $data[ $field ] );
320 491 }
321 492 }
322 493
494 + $mail = filter_input( INPUT_POST, 'billing_email', FILTER_SANITIZE_EMAIL );
495 +
496 + if ( ! array_key_exists( 'billing_email', $data ) && ! empty( $mail ) ) {
497 + $data['billing_email'] = $mail;
498 + }
499 +
323 500 return $data;
324 501 }
325 502
326 503 /**
@@ -659,12 +836,15 @@
659 836 wp_send_json_error( esc_html__( 'Server error.', 'sellkit' ) );
660 837 }
661 838
662 839 if ( '{}' === $response['body'] ) {
663 - wp_send_json_error( esc_html__( 'Selected country or postcode is not supported.', 'sellkit' ) );
840 + // Country or postcode is not supported. it's kina an error but we show nothing in frontend.
841 + wp_send_json_error( '' );
664 842 }
665 843
666 - wp_send_json_success( $response );
844 + $body = json_decode( $response['body'], true );
845 +
846 + wp_send_json_success( $body );
667 847 }
668 848
669 849 /**
670 850 * Modify cart contents using bundled products.
@@ -670,24 +850,214 @@
670 850 * Modify cart contents using bundled products.
671 851 *
672 852 * @since 1.1.0
673 853 * @return void
854 + * @SuppressWarnings(PHPMD.CyclomaticComplexity)
674 855 */
675 856 public function sellkit_checkout_modify_cart_by_bundle_products() {
676 - $qty = filter_input( INPUT_POST, 'qty', FILTER_SANITIZE_NUMBER_INT );
677 - $id = filter_input( INPUT_POST, 'id', FILTER_SANITIZE_NUMBER_INT );
678 - $type = filter_input( INPUT_POST, 'type', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
857 + $qty = filter_input( INPUT_POST, 'qty', FILTER_SANITIZE_NUMBER_INT );
858 + $id = filter_input( INPUT_POST, 'id', FILTER_SANITIZE_NUMBER_INT );
859 + $type = filter_input( INPUT_POST, 'type', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
860 + $checkout_id = filter_input( INPUT_POST, 'checkout_id', FILTER_SANITIZE_NUMBER_INT );
861 + $modify = filter_input( INPUT_POST, 'modify', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
862 + $key = filter_input( INPUT_POST, 'key', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
679 863
680 - WC()->cart->empty_cart();
864 + if ( ! empty( $key ) ) {
865 + foreach ( WC()->cart->get_cart() as $item_key => $item ) {
866 + if ( $item_key === $key ) {
867 + WC()->cart->set_quantity( $item_key, $qty, true );
868 + break; // stop the loop.
869 + }
870 + }
681 871
872 + wp_send_json_success();
873 + }
874 +
682 875 if ( 'radio' === $type ) {
876 + $step_data = get_post_meta( $checkout_id, 'step_data', true );
877 + $products = $step_data['data']['products']['list'];
878 + $reset_cart = isset( $step_data['data']['products']['reset_cart'] ) ? $step_data['data']['products']['reset_cart'] : 'true';
879 +
880 + if ( 'true' === $reset_cart ) {
881 + foreach ( WC()->cart->get_cart() as $item ) {
882 + if ( empty( $item['product_id'] ) ) {
883 + continue;
884 + }
885 +
886 + if ( array_key_exists( $item['product_id'], $products ) ) {
887 + continue;
888 + }
889 +
890 + $products[ $item['product_id'] ] = [
891 + 'quantity' => $item['quantity'],
892 + 'discount' => '',
893 + 'discountType' => 'fixed',
894 + ];
895 + }
896 + }
897 +
898 + $selected = ! empty( $products[ $id ] ) ? $products[ $id ] : [];
899 + $real_quantity = ( empty( $selected['quantity'] ) ) ? 1 : $selected['quantity'];
900 +
901 + if ( (int) $real_quantity !== (int) $qty ) {
902 + wp_send_json_error( esc_html__( 'Oh do not be tricky.', 'sellkit' ) );
903 + }
904 + }
905 +
906 + if ( 'radio' === $type ) {
907 + WC()->cart->empty_cart();
683 908 WC()->cart->add_to_cart( $id, $qty );
684 909 }
685 910
911 + if ( 'checkbox' === $type && 'add' === $modify ) {
912 + WC()->cart->add_to_cart( $id, $qty );
913 + }
914 +
915 + if ( 'checkbox' === $type && 'remove' === $modify ) {
916 + $post_type = get_post_type( $id );
917 +
918 + // Maybe product is a variation.
919 + if ( 'product_variation' === $post_type ) {
920 + foreach ( WC()->cart->get_cart() as $item_key => $item ) {
921 + if ( $item['variation_id'] === (int) $id ) {
922 + WC()->cart->remove_cart_item( $item_key ); // we remove it.
923 + break; // stop the loop.
924 + }
925 + }
926 +
927 + self::make_changes_after_cart_item_edit( $checkout_id );
928 +
929 + wp_send_json_success();
930 + }
931 +
932 + // Default product.
933 + $product_cart_id = WC()->cart->generate_cart_id( $id );
934 + $cart_item_key = WC()->cart->find_product_in_cart( $product_cart_id );
935 +
936 + if ( $cart_item_key ) {
937 + WC()->cart->remove_cart_item( $cart_item_key );
938 + }
939 + }
940 +
941 + self::make_changes_after_cart_item_edit( $checkout_id );
942 +
686 943 wp_send_json_success();
687 944 }
688 945
689 946 /**
947 + * Apply funnel discount.
948 + * Will be called twice, first when page is loading , second during checkout ajax.
949 + *
950 + * @param object $cart cart object.
951 + * @param int $checkout_id_ajax checkout id.
952 + * @param string $source source of products, bump or default products to be checked.
953 + * @param array $upsell_data upsell product data coming from popup.
954 + * @since 1.2.8
955 + * @SuppressWarnings(PHPMD.CyclomaticComplexity)
956 + */
957 + public static function apply_discounted_prices( $cart = [], $checkout_id_ajax = null, $source = 'default', $upsell_data = [] ) {
958 + $checkout_id = get_queried_object_id();
959 +
960 + if ( wp_doing_ajax() ) {
961 + $checkout_id = $checkout_id_ajax;
962 + }
963 +
964 + if ( empty( $checkout_id ) ) {
965 + return;
966 + }
967 +
968 + $step_data = get_post_meta( $checkout_id, 'step_data', true );
969 +
970 + if ( empty( $step_data ) ) {
971 + return;
972 + }
973 +
974 + $products = [];
975 +
976 + if (
977 + 'default' === $source &&
978 + array_key_exists( 'data', $step_data ) &&
979 + ( ! empty( $step_data['data']['products'] ) && array_key_exists( 'list', $step_data['data']['products'] ) )
980 + ) {
981 + $products = $step_data['data']['products']['list'];
982 + }
983 +
984 + if ( 'bumps' === $source && ! empty( $step_data['bump'] ) ) {
985 + $bumps = $step_data['bump'];
986 +
987 + foreach ( $bumps as $bump ) {
988 + if ( ! array_key_exists( 'products', $bump['data'] ) ) {
989 + continue;
990 + }
991 +
992 + $key = ! empty( $bump['data']['products']['list'] ) ? array_key_first( $bump['data']['products']['list'] ) : null;
993 +
994 + if ( empty( $key ) ) {
995 + continue;
996 + }
997 +
998 + $products[ $key ] = $bump['data']['products']['list'][ $key ];
999 + }
1000 + }
1001 +
1002 + if ( 'upsell' === $source ) {
1003 + $products = $upsell_data['data']['products']['list'];
1004 + }
1005 +
1006 + if ( empty( $products ) ) {
1007 + return;
1008 + }
1009 +
1010 + $final_price = 0;
1011 +
1012 + foreach ( $cart->get_cart() as $key => $details ) {
1013 + $item_id = $details['product_id'];
1014 + $price = false;
1015 +
1016 + if ( ! empty( $details['variation_id'] ) ) {
1017 + $item_id = $details['variation_id'];
1018 + }
1019 +
1020 + foreach ( $products as $product_id => $product_details ) {
1021 + if ( $item_id === $product_id ) {
1022 + $discount_type = isset( $product_details['discountType'] ) ? $product_details['discountType'] : '';
1023 + $discount_value = isset( $product_details['discount'] ) ? $product_details['discount'] : '';
1024 +
1025 + $price = Helper::calculate_discount( $item_id, $discount_type, $discount_value );
1026 +
1027 + if ( 'upsell' === $source ) {
1028 + self::$sellkit_upsell_products[ $product_id ] = $price;
1029 + }
1030 + }
1031 + }
1032 +
1033 + if ( false !== $price ) {
1034 + $details['data']->set_price( $price );
1035 + }
1036 +
1037 + $final_price += $details['data']->get_price() * $details['quantity'];
1038 + }
1039 +
1040 + self::modify_products_price_before_woo_apply_discounts( $final_price );
1041 + }
1042 +
1043 + /**
1044 + * Modify cart subtotal before validating discounts.
1045 + *
1046 + * @since 1.2.8
1047 + * @param int $final_price cart new subtotal.
1048 + */
1049 + public static function modify_products_price_before_woo_apply_discounts( $final_price ) {
1050 + add_filter( 'woocommerce_coupon_validate_minimum_amount', function( $value, $coupon ) use ( $final_price ) {
1051 + return $coupon->get_minimum_amount() > $final_price;
1052 + }, 99, 2 );
1053 +
1054 + add_filter( 'woocommerce_coupon_validate_maximum_amount', function( $value, $coupon ) use ( $final_price ) {
1055 + return $coupon->get_maximum_amount() < $final_price;
1056 + }, 99, 2 );
1057 + }
1058 +
1059 + /**
690 1060 * Fix shipping method price on checkout ajax state change.
691 1061 *
692 1062 * @param string $data checkout form data.
693 1063 * @since 1.1.0
@@ -707,16 +1077,10 @@
707 1077 }
708 1078
709 1079 $this->apply_template_replacement_during_ajax( $clear );
710 1080
711 - if ( array_key_exists( 'shipping_state', $clear ) ) {
712 - $_POST['s_state'] = $clear['shipping_state'];
713 - }
1081 + $_POST = Helper::instance()->assigning_default_ajax_shipping_fields( $_POST, $clear ); // phpcs:ignore
714 1082
715 - if ( array_key_exists( 'billing_state', $clear ) ) {
716 - $_POST['state'] = $clear['billing_state'];
717 - }
718 -
719 1083 Local_Hooks::order_bump( $clear );
720 1084
721 1085 if ( array_key_exists( 'sellkit-bundle-products', $clear ) ) {
722 1086 $option = $clear['sellkit-bundle-products'];
@@ -722,8 +1086,16 @@
722 1086 $option = $clear['sellkit-bundle-products'];
723 1087
724 1088 Local_Hooks::bundle_product_action( $option );
725 1089 }
1090 +
1091 + // Apply discounts.
1092 + if ( ! array_key_exists( 'sellkit_current_page_id', $clear ) ) {
1093 + return;
1094 + }
1095 +
1096 + self::apply_discounted_prices( wc()->cart, $clear['sellkit_current_page_id'] );
1097 + self::apply_discounted_prices( wc()->cart, $clear['sellkit_current_page_id'], 'bumps' );
726 1098 }
727 1099
728 1100 /**
729 1101 * Template replacement during ajax call.
@@ -744,8 +1116,9 @@
744 1116 'payment.php',
745 1117 'form-checkout.php',
746 1118 'cart-item-data.php',
747 1119 'cart-shipping.php',
1120 + 'terms.php',
748 1121 ];
749 1122 $template = str_replace( 'checkout/', '', $template_name );
750 1123 $template = str_replace( 'cart/', '', $template );
751 1124
@@ -754,6 +1127,456 @@
754 1127 }
755 1128
756 1129 return $located;
757 1130 }, 10, 2 );
1131 + }
1132 +
1133 + /**
1134 + * Modify default selected shipping method.
1135 + *
1136 + * @since 1.2.8
1137 + * @param string $default default selected method.
1138 + * @return string
1139 + */
1140 + public function sellkit_default_shipping_method( $default ) {
1141 + $applied_coupons = WC()->cart->get_applied_coupons();
1142 +
1143 + if ( count( $applied_coupons ) < 1 ) {
1144 + return $default;
1145 + }
1146 +
1147 + foreach ( $applied_coupons as $coupon_code ) {
1148 +
1149 + $coupon = new \WC_Coupon( $coupon_code );
1150 +
1151 + if ( $coupon->get_free_shipping() ) {
1152 +
1153 + if ( count( WC()->session->get( 'shipping_for_package_0' )['rates'] ) > 0 ) {
1154 + // Loop through.
1155 + foreach ( WC()->session->get( 'shipping_for_package_0' )['rates'] as $rate_id => $rate ) {
1156 + // For free shipping.
1157 + if ( 'free_shipping' === $rate->method_id ) {
1158 + return $rate_id;
1159 + }
1160 + }
1161 + }
1162 + }
1163 + }
1164 +
1165 + return $default;
1166 + }
1167 +
1168 + /**
1169 + * Recalculate checkout and cart prices.
1170 + *
1171 + * @since 1.2.8
1172 + * @SuppressWarnings(PHPMD.CyclomaticComplexity)
1173 + */
1174 + public function before_cart_calculate() {
1175 + // More than twice isn't necessary. this is called multiple times by WooCommerce.
1176 + if ( did_action( 'woocommerce_before_calculate_totals' ) >= 2 && wp_doing_ajax() ) {
1177 + return;
1178 + }
1179 +
1180 + // Gather checkout id.
1181 + $checkout_id = get_queried_object_id();
1182 +
1183 + if ( wp_doing_ajax() ) {
1184 + // First try to catch id using our ajax call.
1185 + $checkout_id = filter_input( INPUT_POST, 'related_checkout', FILTER_SANITIZE_NUMBER_INT );
1186 +
1187 + // Second try to catch id using woocommerce ajax.
1188 + if ( empty( $checkout_id ) ) {
1189 + $checkout_id = filter_input( INPUT_POST, 'sellkit_current_page_id', FILTER_SANITIZE_NUMBER_INT );
1190 + }
1191 +
1192 + // Catch id after pressing place order button. none of above methods worked.
1193 + if ( empty( $checkout_id ) ) {
1194 + $data = filter_input( INPUT_POST, 'post_data', FILTER_DEFAULT );
1195 +
1196 + if ( ! empty( $data ) ) {
1197 + parse_str( $data, $data );
1198 + }
1199 +
1200 + if ( is_array( $data ) && array_key_exists( 'sellkit_current_page_id', $data ) ) {
1201 + $checkout_id = $data['sellkit_current_page_id'];
1202 + }
1203 + }
1204 + }
1205 +
1206 + // Empty id ? no action.
1207 + if ( empty( $checkout_id ) ) {
1208 + return;
1209 + }
1210 +
1211 + $checkout_id = (int) $checkout_id;
1212 +
1213 + // Apply funnel prices.
1214 + self::apply_discounted_prices( wc()->cart, $checkout_id );
1215 + self::apply_discounted_prices( wc()->cart, $checkout_id, 'bumps' );
1216 +
1217 + // Apply upsell discount.
1218 + $sellkit_upsell_prices = null;
1219 + if ( isset( $_POST['woocommerce-process-checkout-nonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['woocommerce-process-checkout-nonce'] ) ), 'woocommerce-process_checkout' ) ) {
1220 + $sellkit_upsell_prices = isset( $_POST['sellkit_product_prices'] ) ? sanitize_textarea_field( wp_unslash( $_POST['sellkit_product_prices'] ) ) : null;
1221 + }
1222 + $upsell_product_ids;
1223 +
1224 + if ( isset( $sellkit_upsell_prices ) && ! empty( $sellkit_upsell_prices ) ) {
1225 + $sellkit_upsell_prices = json_decode( $sellkit_upsell_prices, true );
1226 + $upsell_product_ids = array_keys( $sellkit_upsell_prices );
1227 + }
1228 +
1229 + if ( isset( $upsell_product_ids ) ) {
1230 + foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
1231 + $product = $cart_item['data'];
1232 + $product_name = $product->get_name();
1233 + $product_id = $product->get_id();
1234 +
1235 + if ( in_array( $product_id, $upsell_product_ids, true ) ) {
1236 + $upsell_price = $sellkit_upsell_prices[ $product_id ];
1237 + $product->set_price( $upsell_price );
1238 + }
1239 +
1240 + $price = $product->get_price();
1241 + }
1242 + }
1243 +
1244 + $auto_apply_coupon_cookie_value = null;
1245 +
1246 + if ( isset( $_COOKIE['sellkit_rejected_coupon'] ) ) {
1247 + // Get the value of the cookie.
1248 + $auto_apply_coupon_cookie_value = sanitize_text_field( wp_unslash( $_COOKIE['sellkit_rejected_coupon'] ) );
1249 + $auto_apply_coupon_cookie_value = explode( ',', $auto_apply_coupon_cookie_value );
1250 + }
1251 +
1252 + // We should re apply coupons after each price changes. to make sure everything is correct.
1253 + $optimization_data = ! empty( $funnel_data['data']['optimization'] ) ? $funnel_data['data']['optimization'] : '';
1254 +
1255 + if ( Checkout::apply_coupon_validation( $optimization_data ) ) {
1256 + WC()->cart->remove_coupons();
1257 +
1258 + foreach ( $optimization_data['auto_apply_coupons'] as $auto_apply_coupon ) {
1259 + if ( ! empty( $auto_apply_coupon_cookie_value ) && in_array( $auto_apply_coupon['label'], $auto_apply_coupon_cookie_value, true ) ) {
1260 + continue;
1261 + }
1262 + wc()->cart->add_discount( get_the_title( $auto_apply_coupon['value'] ) );
1263 + }
1264 +
1265 + wc_clear_notices();
1266 + }
1267 + }
1268 +
1269 + /**
1270 + * Check sellkit step through ajax.
1271 + * And in order decide to show upsell or downsell steps through a popup.
1272 + *
1273 + * @since 1.6.2
1274 + */
1275 + public function call_funnel_popups() {
1276 + $step = filter_input( INPUT_POST, 'step', FILTER_SANITIZE_NUMBER_INT );
1277 + $funnel = new Sellkit_Funnel( $step );
1278 + $next_step = $funnel->next_step_data;
1279 + $popups = [ 'upsell', 'downsell' ];
1280 +
1281 + $funnel->next_step_data['type'] = (array) $funnel->next_step_data['type'];
1282 +
1283 + if ( 'decision' === $funnel->next_step_data['type']['key'] ) {
1284 + $page_id = isset( $funnel->next_step_data['page_id'] ) ? apply_filters( 'wpml_object_id', $funnel->next_step_data['page_id'], 'sellkit_step', true ) : 0; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML API.
1285 +
1286 + $this->helper_id = $step;
1287 + $this->take_care_of_decision_step( $page_id, $funnel->funnel_id );
1288 + return;
1289 + }
1290 +
1291 + if ( in_array( $funnel->next_step_data['type']['key'], $popups, true ) ) {
1292 +
1293 + $next_step = $this->is_set_upsell_product( $next_step );
1294 +
1295 + wp_send_json_success( [
1296 + 'next_id' => apply_filters( 'wpml_object_id', $next_step['page_id'], 'sellkit_step', true ), // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML API.
1297 + 'next_type' => $next_step['type']['key'],
1298 + ] );
1299 + }
1300 + }
1301 +
1302 + /**
1303 + * Check the upsell or downsell have product or not, ignore upsell/downsell step if product doesn't set.
1304 + *
1305 + * @param array $next_step Next step data.
1306 + * @since 2.6.0
1307 + */
1308 + private function is_set_upsell_product( $next_step ) {
1309 + if ( isset( $next_step['type']['key'] ) && ! in_array( $next_step['type']['key'], [ 'upsell', 'downsell' ], true ) ) {
1310 + return $next_step;
1311 + }
1312 +
1313 + $products = isset( $next_step['data'] ) ? $next_step['data']['products'] : [];
1314 + $list = isset( $products['list'] ) ? $products['list'] : [];
1315 +
1316 + if ( empty( $list ) ) {
1317 + $funnel = isset( $next_step['page_id'] ) ? new Sellkit_Funnel( $next_step['page_id'] ) : null;
1318 + $next_step = ! empty( $funnel ) ? $funnel->next_no_step_data : [];
1319 +
1320 + if ( empty( $next_step ) && ! empty( $funnel ) ) {
1321 + return $funnel->end_node_step_data;
1322 + }
1323 +
1324 + if ( isset( $next_step['type']['key'] ) && in_array( $next_step['type']['key'], [ 'upsell', 'downsell' ], true ) ) {
1325 +
1326 + $next_step = $this->is_set_upsell_product( $next_step );
1327 + }
1328 + }
1329 +
1330 + return $next_step;
1331 + }
1332 +
1333 + /**
1334 + * Gets step id before the decision step and return result.
1335 + *
1336 + * @param int $step_id id of the step before decision step.
1337 + * @param int $funnel_id id of the funnel.
1338 + * @since 1.6.2
1339 + */
1340 + private function take_care_of_decision_step( $step_id, $funnel_id = null ) {
1341 + // Failed decision step due to no page id should be checked a little different.
1342 + if ( empty( $step_id ) && ! empty( $funnel_id ) ) {
1343 + $this->check_decision_step_with_no_page_id();
1344 + }
1345 +
1346 + $funnel = new Sellkit_Funnel( $step_id );
1347 + $conditions = ! empty( $funnel->current_step_data['data']['conditions'] ) ? $funnel->current_step_data['data']['conditions'] : [];
1348 + $is_valid = sellkit_conditions_validation( $conditions );
1349 + $next_step = $funnel->next_no_step_data;
1350 +
1351 + if ( $is_valid ) {
1352 + $next_step = $funnel->next_step_data;
1353 + }
1354 +
1355 + $next_step = $this->is_set_upsell_product( $next_step );
1356 + $next_step['type'] = (array) $next_step['type'];
1357 +
1358 + if ( 'decision' === $next_step['type']['key'] ) {
1359 + $this->take_care_of_decision_step( apply_filters( 'wpml_object_id', $next_step['page_id'], 'sellkit_step', true ) ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML API.
1360 +
1361 + return;
1362 + }
1363 +
1364 + wp_send_json_success( [
1365 + 'next_id' => apply_filters( 'wpml_object_id', $next_step['page_id'], 'sellkit_step', true ), // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML API.
1366 + 'next_type' => $next_step['type']['key'],
1367 + ] );
1368 + }
1369 +
1370 + /**
1371 + * Decision next step directly using funnel data.
1372 + *
1373 + * @since 1.8.6
1374 + */
1375 + private function check_decision_step_with_no_page_id() {
1376 + $funnel = new Sellkit_Funnel( $this->helper_id );
1377 + $decicion_data = $funnel->next_step_data;
1378 + $conditions = $decicion_data['data']['conditions'];
1379 + $funnel_data = get_post_meta( $funnel->funnel_id, 'nodes', true );
1380 + $next_no = 'none' !== $decicion_data['targets'][1]['nodeId'] ? $funnel_data[ $decicion_data['targets'][1]['nodeId'] ] : $funnel->end_node_step_data;
1381 + $next_yes = 'none' !== $decicion_data['targets'][0]['nodeId'] ? $funnel_data[ $decicion_data['targets'][0]['nodeId'] ] : $funnel->end_node_step_data;
1382 + $is_valid = sellkit_conditions_validation( $conditions );
1383 + $next_step = $next_no;
1384 +
1385 + if ( $is_valid ) {
1386 + $next_step = $next_yes;
1387 + }
1388 +
1389 + $next_step = $this->is_set_upsell_product( $next_step );
1390 +
1391 + wp_send_json_success( [
1392 + 'next_id' => apply_filters( 'wpml_object_id', $next_step['page_id'], 'sellkit_step', true ), // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML API.
1393 + 'next_type' => $next_step['type']['key'],
1394 + ] );
1395 + }
1396 +
1397 + /**
1398 + * Perform upsell popup accept button.
1399 + *
1400 + * @since 1.6.2
1401 + */
1402 + public function perform_upsell_accept_button() {
1403 + // Gather and validate information.
1404 + $upsell_id = filter_input( INPUT_POST, 'upsell_id', FILTER_SANITIZE_NUMBER_INT );
1405 + $checkout_id = filter_input( INPUT_POST, 'checkout_id', FILTER_SANITIZE_NUMBER_INT );
1406 + $response = [];
1407 +
1408 + if ( empty( $upsell_id ) ) {
1409 + wp_send_json_error( esc_html__( 'Empty Upsell ID.', 'sellkit' ) );
1410 + }
1411 +
1412 + $upsell_data = get_post_meta( $upsell_id, 'step_data', true );
1413 +
1414 + if ( empty( $upsell_data ) ) {
1415 + wp_send_json_error( esc_html__( 'Empty Step Data.', 'sellkit' ) );
1416 + }
1417 +
1418 + // Add product to cart.
1419 + $product_id = '';
1420 + $qty = '';
1421 +
1422 + if (
1423 + is_array( $upsell_data['data'] ) &&
1424 + array_key_exists( 'products', $upsell_data['data'] ) &&
1425 + ! empty( $upsell_data['data']['products']['list'] )
1426 + ) {
1427 + $product_id = array_key_first( $upsell_data['data']['products']['list'] );
1428 + $qty = ! empty( $upsell_data['data']['products']['list'][ $product_id ] ) ? $upsell_data['data']['products']['list'][ $product_id ]['quantity'] : 0;
1429 + }
1430 +
1431 + if ( empty( $qty ) ) {
1432 + $qty = 1;
1433 + }
1434 +
1435 + $added_product_hash = '';
1436 +
1437 + if ( ! in_array( $product_id, array_column( WC()->cart->get_cart(), 'product_id' ), true ) ) {
1438 + $added_product_hash = WC()->cart->add_to_cart( $product_id, $qty );
1439 +
1440 + // Take care of cart and applied discounts.
1441 + self::apply_discounted_prices( WC()->cart, $checkout_id, 'upsell', $upsell_data );
1442 + self::make_changes_after_cart_item_edit( $checkout_id );
1443 + }
1444 +
1445 + // Response.
1446 + $funnel = new Sellkit_Funnel( $upsell_id );
1447 + $next_step = $funnel->next_step_data;
1448 + $next_step = $this->is_set_upsell_product( $next_step );
1449 + $next_step['type'] = ! empty( $next_step['type'] ) ? (array) $next_step['type'] : null;
1450 +
1451 + if ( 'upsell' === $funnel->current_step_data['type']['key'] && isset( WC()->cart->cart_contents[ $added_product_hash ] ) ) {
1452 + $analytics_updater = new Data_Updater();
1453 + $total = empty( WC()->cart->cart_contents[ $added_product_hash ]['line_total'] ) ? 0 : WC()->cart->cart_contents[ $added_product_hash ]['line_total'];
1454 +
1455 + $analytics_updater->set_funnel_id( $funnel->funnel_id );
1456 + $analytics_updater->add_new_upsell_revenue_log( $total );
1457 + }
1458 +
1459 + $contact_data = [
1460 + 'key' => $funnel->current_step_data['type']['key'],
1461 + 'page_id' => $funnel->current_step_data['page_id'],
1462 + ];
1463 +
1464 + Base_Contacts::step_is_passed( $contact_data );
1465 +
1466 + // Step with empty data, will be redirected to thankyou page.
1467 + if ( empty( $next_step ) || empty( $next_step['type'] ) ) {
1468 + wp_send_json_success(
1469 + [
1470 + 'next_id' => $funnel->end_node_step_data['page_id'],
1471 + 'next_type' => $funnel->end_node_step_data['type']['key'],
1472 + 'upsell_prices' => wp_json_encode( self::$sellkit_upsell_products ),
1473 + ]
1474 + );
1475 + }
1476 +
1477 + if ( 'decision' === $next_step['type']['key'] ) {
1478 + $this->take_care_of_decision_step( $next_step['page_id'] );
1479 + return;
1480 + }
1481 +
1482 + $response = [
1483 + 'next_id' => apply_filters( 'wpml_object_id', $next_step['page_id'], 'sellkit_step', true ), // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML API.
1484 + 'next_type' => $next_step['type']['key'],
1485 + 'upsell_prices' => wp_json_encode( self::$sellkit_upsell_products ),
1486 + ];
1487 +
1488 + wp_send_json_success( $response );
1489 + }
1490 +
1491 + /**
1492 + * Perform upsell popup reject button.
1493 + *
1494 + * @since 1.6.2
1495 + */
1496 + public function perform_upsell_reject_button() {
1497 + $upsell_id = filter_input( INPUT_POST, 'upsell_id', FILTER_SANITIZE_NUMBER_INT );
1498 +
1499 + if ( empty( $upsell_id ) ) {
1500 + wp_send_json_error( esc_html__( 'Empty Upsell ID.', 'sellkit' ) );
1501 + }
1502 +
1503 + $funnel = new Sellkit_Funnel( $upsell_id );
1504 + $next_step = $funnel->next_no_step_data;
1505 + $next_step = $this->is_set_upsell_product( $next_step );
1506 +
1507 + if ( $next_step ) {
1508 + $next_step['type'] = (array) $next_step['type'];
1509 + }
1510 +
1511 + // Step with empty data, will be redirected to thankyou page.
1512 + if ( empty( $next_step ) || empty( $next_step['type'] ) ) {
1513 + wp_send_json_success(
1514 + [
1515 + 'next_id' => $funnel->end_node_step_data['page_id'],
1516 + 'next_type' => $funnel->end_node_step_data['type']['key'],
1517 + ]
1518 + );
1519 + }
1520 +
1521 + if ( 'decision' === $next_step['type']['key'] ) {
1522 + $this->take_care_of_decision_step( $next_step['page_id'] );
1523 + return;
1524 + }
1525 +
1526 + $response = [
1527 + 'next_id' => apply_filters( 'wpml_object_id', $next_step['page_id'], 'sellkit_step', true ), // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML API.
1528 + 'next_type' => $next_step['type']['key'],
1529 + ];
1530 +
1531 + wp_send_json_success( $response );
1532 + }
1533 +
1534 + /**
1535 + * Add new item to the WooCommerce order details on the edit page.
1536 + *
1537 + * @param object $order woocommerce order object.
1538 + * @since 1.8.9
1539 + */
1540 + public function add_item_to_order_details( $order ) {
1541 + $order_id = $order->get_id();
1542 + $custom_fields = get_post_meta( $order_id, 'sellkit_checkout_widget_custom_field_of_order', true );
1543 + $current_action = current_action();
1544 +
1545 + if ( empty( $custom_fields ) || ! is_array( $custom_fields ) ) {
1546 + return;
1547 + }
1548 +
1549 + foreach ( $custom_fields as $field ) {
1550 + $key_name = $field['key_name'];
1551 + $label = $field['label'];
1552 +
1553 + // For old users data.
1554 + if ( empty( $label ) ) {
1555 + $label = $key_name;
1556 + $label = str_replace( '_', ' ', $label );
1557 + $label = str_replace( '-', ' ', $label );
1558 + $label = ucwords( $label );
1559 + }
1560 +
1561 + if (
1562 + 'woocommerce_admin_order_data_after_shipping_address' === $current_action &&
1563 + 'shipping' === substr( $key_name, 0, 8 )
1564 + ) {
1565 + ?>
1566 + <h3><?php echo esc_html( $label ); ?></h3>
1567 + <div><?php echo esc_html( get_post_meta( $order_id, $key_name, true ) ); ?></div>
1568 + <?php
1569 + }
1570 +
1571 + if (
1572 + 'woocommerce_admin_order_data_after_billing_address' === $current_action &&
1573 + 'billing' === substr( $key_name, 0, 7 )
1574 + ) {
1575 + ?>
1576 + <h3><?php echo esc_html( $label ); ?></h3>
1577 + <div><?php echo esc_html( get_post_meta( $order_id, $key_name, true ) ); ?></div>
1578 + <?php
1579 + }
1580 + }
758 1581 }
759 1582 }