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 +809 -26 1.2.52.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;
7 +use Sellkit\Elementor\Modules\Checkout\Classes\{ Local_Hooks, Helper };
8 +use Sellkit\Funnel\Analytics\Data_Updater;
9 +use Sellkit\Funnel\Contacts\Base_Contacts;
8 10 use Sellkit\Funnel\Steps\Checkout;
11 +use Sellkit_Funnel;
9 12
10 13 /**
11 14 * Global hooks.
12 15 *
@@ -12,12 +15,28 @@
12 15 *
13 16 * @SuppressWarnings(PHPMD.UnusedFormalParameter)
14 17 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
15 18 * @SuppressWarnings(PHPMD.TooManyMethods)
19 + * @SuppressWarnings(PHPMD.NPathComplexity)
20 + * @SuppressWarnings(PHPMD.ExcessiveClassLength)
16 21 * @since 1.1.0
17 22 */
18 23 class Global_Hooks {
19 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 + /**
20 39 * Create instance of class without construct.
21 40 *
22 41 * @since 1.1.0
23 42 * @return object
@@ -56,8 +75,12 @@
56 75
57 76 // Save user defined fields.
58 77 add_action( 'woocommerce_checkout_update_order_meta', [ $this, 'save_user_defined_fields' ] );
59 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 +
60 83 // Add custom shipping field to email.
61 84 add_filter( 'woocommerce_order_get_formatted_shipping_address', [ $this, 'attach_user_shipping_fields_to_order_email' ], 10, 3 );
62 85
63 86 // Add custom billing field to email.
@@ -72,8 +95,25 @@
72 95 // Fix shipping method price change on ajax.
73 96 // Manage bump order discounts.
74 97 // Template replacement.
75 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 + }
76 116 }
77 117
78 118 /**
79 119 * Handle ajax calls.
@@ -102,9 +142,9 @@
102 142 * @since 1.1.0
103 143 */
104 144 public function checkout_fragment( $response ) {
105 145 // Get posted data and identify if it's been sent by jupiter widget or not.
106 - $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 );
107 147 $checkout_form_data = explode( '&', $checkout_form_data );
108 148 $posted_data = [];
109 149
110 150 foreach ( $checkout_form_data as $input ) {
@@ -109,9 +149,9 @@
109 149
110 150 foreach ( $checkout_form_data as $input ) {
111 151 $item = explode( '=', urldecode( $input ) );
112 152
113 - $posted_data[ $item[0] ] = $item[1];
153 + $posted_data[ trim( $item[0] ) ] = $item[1];
114 154 }
115 155
116 156 // Identify if it's been sent by jupiter checkout widget or not.
117 157 if ( ! array_key_exists( 'form_id', $posted_data ) && ! array_key_exists( 'post_id', $posted_data ) ) {
@@ -178,17 +218,32 @@
178 218 $id = filter_input( INPUT_POST, 'id', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
179 219 $action = filter_input( INPUT_POST, 'mode', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
180 220 $funnel_id = filter_input( INPUT_POST, 'related_checkout', FILTER_SANITIZE_NUMBER_INT );
181 221
182 - $this->make_changes_after_cart_item_edit( $funnel_id );
222 + self::make_changes_after_cart_item_edit( $funnel_id );
183 223
184 224 if ( 'add' === $action ) {
185 225 WC()->cart->add_to_cart( $id, $qty );
186 - $this->make_changes_after_cart_item_edit( $funnel_id );
226 + self::make_changes_after_cart_item_edit( $funnel_id );
187 227 wp_send_json_success();
188 228 }
189 229
190 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 +
191 246 $product_cart_id = WC()->cart->generate_cart_id( $id );
192 247 $cart_item_key = WC()->cart->find_product_in_cart( $product_cart_id );
193 248
194 249 if ( $cart_item_key ) {
@@ -194,9 +249,9 @@
194 249 if ( $cart_item_key ) {
195 250 WC()->cart->remove_cart_item( $cart_item_key );
196 251 }
197 252
198 - $this->make_changes_after_cart_item_edit( $funnel_id );
253 + self::make_changes_after_cart_item_edit( $funnel_id );
199 254
200 255 wp_send_json_success();
201 256 }
202 257
@@ -201,9 +256,9 @@
201 256 }
202 257
203 258 ( $qty > 0 ) ? WC()->cart->set_quantity( $id, $qty ) : WC()->cart->remove_cart_item( $id );
204 259
205 - $this->make_changes_after_cart_item_edit( $funnel_id );
260 + self::make_changes_after_cart_item_edit( $funnel_id );
206 261
207 262 wp_send_json_success();
208 263 }
209 264
@@ -212,9 +267,9 @@
212 267 *
213 268 * @since 1.2.5
214 269 * @param int $funnel_id funnel step id.
215 270 */
216 - private function make_changes_after_cart_item_edit( $funnel_id ) {
271 + public static function make_changes_after_cart_item_edit( $funnel_id ) {
217 272 if ( empty( $funnel_id ) ) {
218 273 return;
219 274 }
220 275
@@ -224,12 +279,27 @@
224 279 if ( empty( $optimization_data ) ) {
225 280 return;
226 281 }
227 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 +
228 295 if ( Checkout::apply_coupon_validation( $optimization_data ) ) {
229 296 WC()->cart->remove_coupons();
230 297
231 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 + }
232 302 wc()->cart->add_discount( get_the_title( $auto_apply_coupon['value'] ) );
233 303 }
234 304
235 305 wc_clear_notices();
@@ -244,10 +314,20 @@
244 314 */
245 315 public function add_shipping_total_to_review_order() {
246 316 ?>
247 317 <tr class="sellkit-shipping-total">
248 - <th><?php echo __( 'Shipping', 'sellkit' ); ?></th>
249 - <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>
250 330 </tr>
251 331 <?php
252 332 }
253 333
@@ -275,14 +355,19 @@
275 355 $img_html = apply_filters( 'sellkit/includes/elementor/modules/checkout/product-image', $img_html, $product, $cart_item_key );
276 356
277 357 ob_start();
278 358
279 - $extra_class = '';
359 + $extra_class = $product->get_type();
280 360 if ( $cart_items[ $cart_item_key ] === end( $cart_items ) ) {
281 - $extra_class = ' last-cart-item';
361 + $extra_class .= ' last-cart-item';
282 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 + }
283 368 ?>
284 - <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 ); ?>">
285 370
286 371 <td class="product-name sellkit-one-page-checkout-product-name">
287 372 <?php
288 373 echo $img_html;
@@ -297,9 +382,9 @@
297 382 <div class="name-price" style="<?php echo esc_attr( $fix_style ); ?>">
298 383 <span style="display:inline-block">
299 384 <?php echo wp_kses_post( apply_filters( 'woocommerce_cart_item_name', $product->get_name(), $cart_item, $cart_item_key ) ) . '&nbsp;'; ?>
300 385 </span>
301 - <span class="sellkit-checkout-variations">
386 + <span class="sellkit-checkout-variations" id="sellkit-checkout-variations">
302 387 <?php echo wc_get_formatted_cart_item_data( $cart_item ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
303 388 </span>
304 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'] ); ?>
305 390 <?php do_action( 'sellkit-checkout-editor-mode-quantity', $cart_item['quantity'] ); ?>
@@ -313,14 +398,62 @@
313 398 $cart_item = ob_get_clean();
314 399
315 400 echo apply_filters( 'sellkit-checkout-cart-item' , $cart_item );
316 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 + }
317 407 }
318 408
319 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 + /**
320 452 * Modify fields after pressing place order button.
321 453 * we hooked here because some removed fields still get checked and have not removed by previous hooks.
322 454 *
455 + * @SuppressWarnings(PHPMD.NPathComplexity)
323 456 * @param array $data checkout form data.
324 457 * @return array $data data of woocommerce checkout form.
325 458 * @since 1.1.0
326 459 */
@@ -357,8 +490,14 @@
357 490 unset( $data[ $field ] );
358 491 }
359 492 }
360 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 +
361 500 return $data;
362 501 }
363 502
364 503 /**
@@ -697,9 +836,10 @@
697 836 wp_send_json_error( esc_html__( 'Server error.', 'sellkit' ) );
698 837 }
699 838
700 839 if ( '{}' === $response['body'] ) {
701 - 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( '' );
702 842 }
703 843
704 844 $body = json_decode( $response['body'], true );
705 845
@@ -710,24 +850,214 @@
710 850 * Modify cart contents using bundled products.
711 851 *
712 852 * @since 1.1.0
713 853 * @return void
854 + * @SuppressWarnings(PHPMD.CyclomaticComplexity)
714 855 */
715 856 public function sellkit_checkout_modify_cart_by_bundle_products() {
716 - $qty = filter_input( INPUT_POST, 'qty', FILTER_SANITIZE_NUMBER_INT );
717 - $id = filter_input( INPUT_POST, 'id', FILTER_SANITIZE_NUMBER_INT );
718 - $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 );
719 863
720 - 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 + }
721 871
872 + wp_send_json_success();
873 + }
874 +
722 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();
723 908 WC()->cart->add_to_cart( $id, $qty );
724 909 }
725 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 +
726 943 wp_send_json_success();
727 944 }
728 945
729 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 + /**
730 1060 * Fix shipping method price on checkout ajax state change.
731 1061 *
732 1062 * @param string $data checkout form data.
733 1063 * @since 1.1.0
@@ -747,16 +1077,10 @@
747 1077 }
748 1078
749 1079 $this->apply_template_replacement_during_ajax( $clear );
750 1080
751 - if ( array_key_exists( 'shipping_state', $clear ) ) {
752 - $_POST['s_state'] = $clear['shipping_state'];
753 - }
1081 + $_POST = Helper::instance()->assigning_default_ajax_shipping_fields( $_POST, $clear ); // phpcs:ignore
754 1082
755 - if ( array_key_exists( 'billing_state', $clear ) ) {
756 - $_POST['state'] = $clear['billing_state'];
757 - }
758 -
759 1083 Local_Hooks::order_bump( $clear );
760 1084
761 1085 if ( array_key_exists( 'sellkit-bundle-products', $clear ) ) {
762 1086 $option = $clear['sellkit-bundle-products'];
@@ -762,8 +1086,16 @@
762 1086 $option = $clear['sellkit-bundle-products'];
763 1087
764 1088 Local_Hooks::bundle_product_action( $option );
765 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' );
766 1098 }
767 1099
768 1100 /**
769 1101 * Template replacement during ajax call.
@@ -784,8 +1116,9 @@
784 1116 'payment.php',
785 1117 'form-checkout.php',
786 1118 'cart-item-data.php',
787 1119 'cart-shipping.php',
1120 + 'terms.php',
788 1121 ];
789 1122 $template = str_replace( 'checkout/', '', $template_name );
790 1123 $template = str_replace( 'cart/', '', $template );
791 1124
@@ -794,6 +1127,456 @@
794 1127 }
795 1128
796 1129 return $located;
797 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 + }
798 1581 }
799 1582 }