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/funnel/steps/checkout.php +263 -23 1.2.32.7.0 View file →
@@ -2,9 +2,12 @@
2 2
3 3 namespace Sellkit\Funnel\Steps;
4 4
5 5 use Sellkit\Funnel\Analytics\Data_Updater;
6 +use Sellkit\Funnel\Contacts\Base_Contacts;
7 +use Sellkit\Database;
6 8 use Sellkit_Funnel;
9 +use Sellkit\Global_Checkout\Checkout as Global_Checkout;
7 10
8 11 defined( 'ABSPATH' ) || die();
9 12
10 13 /**
@@ -9,8 +12,9 @@
9 12
10 13 /**
11 14 * Class Sellkit_Checkout.
12 15 *
16 + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
13 17 * @since 1.1.0
14 18 */
15 19 class Checkout {
16 20
@@ -36,11 +40,52 @@
36 40
37 41 add_action( 'woocommerce_checkout_create_order', [ $this, 'save_order_custom_meta_data' ], 10 );
38 42 add_action( 'woocommerce_after_order_notes', [ $this, 'custom_checkout_hidden_fields' ], 10 );
39 43 add_action( 'wc_ajax_update_order_review', [ $this, 'do_actions' ] );
44 +
45 + // Persist the current checkout step id in the WC session so express-checkout
46 + // AJAX requests (which do not submit the sellkit_current_page_id hidden field)
47 + // can still resolve the funnel context and save sellkit_funnel_next_step_data
48 + // on the created order.
49 + if ( ! wp_doing_ajax() && ! is_admin() ) {
50 + add_action( 'wp', [ $this, 'store_checkout_step_in_session' ], 20 );
51 + }
40 52 }
41 53
42 54 /**
55 + * Store the current checkout step id in the WC session when rendering an actual
56 + * SellKit funnel checkout step page. The value is later consumed by
57 + * Sellkit_Funnel::get_page_id() during AJAX requests (e.g. Stripe express
58 + * checkout) that lack the sellkit_current_page_id POST field.
59 + *
60 + * A timestamp is stored alongside the id so the fallback can be ignored if it
61 + * is stale, avoiding cross-contamination between unrelated checkouts.
62 + *
63 + * @since 2.3.3
64 + * @return void
65 + */
66 + public function store_checkout_step_in_session() {
67 + if ( ! function_exists( 'WC' ) || ! is_object( WC()->session ) ) {
68 + return;
69 + }
70 +
71 + $post_id = get_the_ID();
72 +
73 + if ( empty( $post_id ) || 'sellkit_step' !== get_post_type( $post_id ) ) {
74 + return;
75 + }
76 +
77 + $step_data = get_post_meta( $post_id, 'step_data', true );
78 +
79 + if ( empty( $step_data['type']['key'] ) || 'checkout' !== $step_data['type']['key'] ) {
80 + return;
81 + }
82 +
83 + WC()->session->set( 'sellkit_current_page_id', (int) $post_id );
84 + WC()->session->set( 'sellkit_current_page_id_time', time() );
85 + }
86 +
87 + /**
43 88 * Adding items to the checkout page.
44 89 *
45 90 * @since 1.1.0
46 91 * @SuppressWarnings(PHPMD.NPathComplexity)
@@ -50,15 +95,29 @@
50 95 // These variables should be used in checkout builder widget.
51 96 $funnel_data = $this->funnel->current_step_data;
52 97 $optimization_data = ! empty( $funnel_data['data']['optimization'] ) ? $funnel_data['data']['optimization'] : '';
53 98 $redirect_url = ! empty( $optimization_data['closed_checkout_redirect_url'] ) ? $optimization_data['closed_checkout_redirect_url'] : site_url();
99 + $reset_cart = isset( $funnel_data['data']['products']['reset_cart'] ) ? $funnel_data['data']['products']['reset_cart'] : 'true';
54 100
55 - add_action( 'template_redirect', function () use ( $funnel_data ) {
101 + $auto_apply_coupon_cookie_value = null;
102 +
103 + if ( function_exists( 'sellkit_pro' ) && ! sellkit_pro()->is_active_sellkit_pro ) {
104 + $optimization_data = null;
105 + }
106 +
107 + if ( isset( $_COOKIE['sellkit_rejected_coupon'] ) ) {
108 + // Get the value of the cookie.
109 + $auto_apply_coupon_cookie_value = sanitize_text_field( wp_unslash( $_COOKIE['sellkit_rejected_coupon'] ) );
110 + $auto_apply_coupon_cookie_value = explode( ',', $auto_apply_coupon_cookie_value );
111 + }
112 +
113 + add_action( 'template_redirect', function () use ( $funnel_data, $reset_cart ) {
56 114 $product_settings = ! empty( $funnel_data['data']['products']['settings'] ) ? $funnel_data['data']['products']['settings'] : '';
57 115 $bump_data = ! empty( $funnel_data['bump'] ) ? $funnel_data['bump'] : '';
58 116
59 117 if ( ! empty( $product_settings ) ) {
60 118 set_query_var( 'funnel_product_settings', $product_settings );
119 + set_query_var( 'funnel_reset_cart', $reset_cart );
61 120 }
62 121
63 122 if ( ! empty( $bump_data ) ) {
64 123 $bump_data = $this->get_valid_bumps( $bump_data );
@@ -63,9 +122,13 @@
63 122 if ( ! empty( $bump_data ) ) {
64 123 $bump_data = $this->get_valid_bumps( $bump_data );
65 124
66 125 set_query_var( 'bump_data', $bump_data );
126 + } else {
127 + set_query_var( 'bump_data', [] );
67 128 }
129 +
130 + set_query_var( 'funnel_reset_cart', $reset_cart );
68 131 } );
69 132
70 133 if (
71 134 ! empty( $optimization_data['expiration_date'] ) &&
@@ -72,9 +135,9 @@
72 135 ! empty( $optimization_data['expire_checkout_coupons_switch'] ) &&
73 136 time() > $optimization_data['expiration_date'] &&
74 137 'on' === $optimization_data['expire_checkout_coupons_switch']
75 138 ) {
76 - wp_redirect( $redirect_url ); // phpcs:ignore
139 + wp_redirect( $this->add_http_to_url( $redirect_url ) ); // phpcs:ignore
77 140 exit;
78 141 }
79 142
80 143 if ( empty( $funnel_data ) ) {
@@ -89,32 +152,31 @@
89 152 }
90 153
91 154 $funnel_products = $funnel_data['data']['products']['list'];
92 155
156 + if ( ! sellkit()->has_valid_dependencies() ) {
157 + return;
158 + }
159 +
93 160 $action = sellkit_htmlspecialchars( INPUT_GET, 'wc-ajax' );
94 161
95 162 if ( 'update_order_review' !== $action && 'checkout' !== $action ) {
96 - wc()->cart->empty_cart();
163 + if ( 'true' === $reset_cart || true === $reset_cart ) {
164 + wc()->cart->empty_cart();
165 + }
97 166
98 167 foreach ( $funnel_products as $product_id => $product ) {
99 - $quantity = ! empty( $product['quantity'] ) ? $product['quantity'] : 1;
168 + if ( empty( $product ) ) {
169 + continue;
170 + }
100 171
101 - wc()->cart->add_to_cart( $product_id, $quantity );
102 - }
103 - }
172 + $quantity = ! empty( $product['quantity'] ) ? $product['quantity'] : 1;
173 + $product_cart_id = WC()->cart->generate_cart_id( $product_id );
104 174
105 - foreach ( wc()->cart->get_cart() as $cart_item ) {
106 - $price = $cart_item['data']->get_regular_price();
107 - $product_id = $cart_item['product_id'];
108 -
109 - if ( empty( $funnel_products[ $product_id ] ) && empty( $funnel_products[ $cart_item['variation_id'] ] ) ) {
110 - continue;
175 + if ( ! WC()->cart->find_product_in_cart( $product_cart_id ) ) {
176 + WC()->cart->add_to_cart( $product_id, $quantity );
177 + }
111 178 }
112 -
113 - $discount_info = ! empty( $funnel_products[ $cart_item['variation_id'] ] ) ? $funnel_products[ $cart_item['variation_id'] ] : $funnel_products[ $product_id ];
114 - $discount_value = $this->calculate_discount( $price, $discount_info['discount'], $discount_info['discountType'] );
115 -
116 - $cart_item['data']->set_price( floatval( $price ) - ( floatval( $discount_value ) ) );
117 179 }
118 180
119 181 if ( empty( $optimization_data ) ) {
120 182 return;
@@ -119,10 +181,14 @@
119 181 if ( empty( $optimization_data ) ) {
120 182 return;
121 183 }
122 184
123 - if ( $this->apply_coupon_validation( $optimization_data ) ) {
185 + if ( self::apply_coupon_validation( $optimization_data ) ) {
124 186 foreach ( $optimization_data['auto_apply_coupons'] as $auto_apply_coupon ) {
187 +
188 + if ( ! empty( $auto_apply_coupon_cookie_value ) && in_array( $auto_apply_coupon['label'], $auto_apply_coupon_cookie_value, true ) ) {
189 + continue;
190 + }
125 191 wc()->cart->add_discount( get_the_title( $auto_apply_coupon['value'] ) );
126 192 }
127 193
128 194 wc_clear_notices();
@@ -129,8 +195,23 @@
129 195 }
130 196 }
131 197
132 198 /**
199 + * Adds http if it is needed.
200 + *
201 + * @since 1.5.0
202 + * @param string $url Entered url.
203 + * @return string|boolean
204 + */
205 + private function add_http_to_url( $url ) {
206 + if ( ! preg_match( '~^(?:f|ht)tps?://~i', $url ) ) {
207 + return 'http://' . $url;
208 + }
209 +
210 + return $url;
211 + }
212 +
213 + /**
133 214 * Saving the funnel data to the order.
134 215 *
135 216 * @since 1.1.0
136 217 * @param object $order Order object.
@@ -139,8 +220,11 @@
139 220 if ( empty( $this->funnel->next_step_data ) ) {
140 221 return;
141 222 }
142 223
224 + Base_Contacts::step_is_passed();
225 + $this->check_bump_acceptance( $order );
226 +
143 227 $analytics_updater = new Data_Updater();
144 228
145 229 $analytics_updater->set_funnel_id( $this->funnel->funnel_id );
146 230 $analytics_updater->add_new_order_log( $order );
@@ -145,8 +229,9 @@
145 229 $analytics_updater->set_funnel_id( $this->funnel->funnel_id );
146 230 $analytics_updater->add_new_order_log( $order );
147 231
148 232 $order->update_meta_data( 'sellkit_funnel_next_step_data', $this->funnel->next_step_data );
233 + $order->update_meta_data( 'sellkit_funnel_id', $this->funnel->funnel_id );
149 234 }
150 235
151 236 /**
152 237 * Adding page id field to the inputs.
@@ -153,12 +238,50 @@
153 238 *
154 239 * @since 1.1.0
155 240 */
156 241 public function custom_checkout_hidden_fields() {
242 + $id = get_the_ID();
243 + $global_checkout_id = get_option( Global_Checkout::SELLKIT_GLOBAL_CHECKOUT_OPTION, 0 );
244 + $default_checkout_id = wc_get_page_id( 'checkout' );
245 +
246 + if ( ! empty( $default_checkout_id ) ) {
247 + $default_checkout_id = apply_filters( 'wpml_object_id', $default_checkout_id, 'page', true ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML API.
248 + }
249 +
250 + if (
251 + (int) $global_checkout_id > 0 &&
252 + 'publish' === get_post_status( $global_checkout_id ) &&
253 + $id === $default_checkout_id
254 + ) {
255 + $steps = get_post_meta( $global_checkout_id, 'nodes', true );
256 + $checkout_id = 0;
257 +
258 + foreach ( $steps as $step ) {
259 + $step['type'] = (array) $step['type'];
260 +
261 + if ( 'checkout' === $step['type']['key'] ) {
262 + $checkout_id = apply_filters( 'wpml_object_id', $step['page_id'], 'sellkit_step', true ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML API.
263 + }
264 + }
265 +
266 + if ( (int) $checkout_id > 0 ) {
267 + $id = $checkout_id;
268 + }
269 + }
270 +
157 271 woocommerce_form_field( 'sellkit_current_page_id', [
158 272 'type' => 'hidden',
159 273 'class' => [ 'hidden form-row-wide' ],
160 - ], get_the_ID() );
274 + ], $id );
275 +
276 + // This field added for passing upsell discount price to backend.
277 + woocommerce_form_field( 'sellkit_product_prices', [
278 + 'type' => 'hidden',
279 + 'class' => [ 'hidden form-row-wide' ],
280 + 'autocomplete' => 'off',
281 + 'autocorrect' => 'off',
282 + 'autocapitalize' => 'off',
283 + ], '0' );
161 284 }
162 285
163 286 /**
164 287 * Check If a coupon should be applied.
@@ -165,9 +288,9 @@
165 288 *
166 289 * @since 1.1.0
167 290 * @param array $optimization_data Optimization data.
168 291 */
169 - public function apply_coupon_validation( $optimization_data ) {
292 + public static function apply_coupon_validation( $optimization_data ) {
170 293 if (
171 294 empty( $optimization_data['auto_apply_coupons_switch'] ) ||
172 295 'on' !== $optimization_data['auto_apply_coupons_switch']
173 296 ) {
@@ -193,13 +316,17 @@
193 316 * @param string $type Type.
194 317 * @return false|float|int|mixed
195 318 */
196 319 public function calculate_discount( $total, $value, $type ) {
197 - if ( 'fixed' === $type ) {
320 + if ( empty( $value ) || 1 > $value ) {
321 + return 0;
322 + }
323 +
324 + if ( strpos( $type, 'fixed' ) !== false ) {
198 325 return $value;
199 326 }
200 327
201 - if ( 'percentage' === $type ) {
328 + if ( strpos( $type, 'percentage' ) !== false ) {
202 329 return ( floatval( $total ) * floatval( $value ) ) / 100;
203 330 }
204 331
205 332 return 0;
@@ -249,13 +376,83 @@
249 376 return $valid_bumps;
250 377 }
251 378
252 379 /**
380 + * Checks if bump is accepted during checkout or not and updates table accordingly.
381 + *
382 + * @param object $order WooCommerce order object.
383 + * @return void
384 + *
385 + * @since 1.5.0
386 + * @access private
387 + *
388 + * @SuppressWarnings(PHPMD.NPathComplexity)
389 + */
390 + private function check_bump_acceptance( $order ) {
391 + if (
392 + empty( $this->funnel->funnel_id ) ||
393 + empty( $this->funnel->current_step_data['bump'] )
394 + ) {
395 + return;
396 + }
397 +
398 + $bump_product_ids = [];
399 + foreach ( $this->funnel->current_step_data['bump'] as $bump ) {
400 + if ( empty( $bump['data']['products'] ) ) {
401 + continue;
402 + }
403 +
404 + $product_list = ! empty( $bump['data']['products']['list'] ) ? $bump['data']['products']['list'] : [];
405 +
406 + $bump_product_ids = array_merge( $bump_product_ids, array_keys( $product_list ) );
407 + }
408 +
409 + $bump_exists_in_order = false;
410 +
411 + foreach ( $order->get_items() as $item ) {
412 + $product_id = $item->get_changes()['product_id'];
413 +
414 + if ( in_array( $product_id, $bump_product_ids, true ) ) {
415 + $bump_exists_in_order = true;
416 + break;
417 + }
418 + }
419 +
420 + if ( ! $bump_exists_in_order ) {
421 + return;
422 + }
423 +
424 + $database = new Database();
425 + $old_values = [];
426 +
427 + // Getting old data.
428 + $contact_id = isset( $_SESSION['entered_funnel_id'] ) ? absint( $_SESSION['entered_funnel_id'] ) : 0;
429 +
430 + $result = $database->get( 'funnel_contact', [ 'id' => $contact_id ] );
431 +
432 + if ( ! empty( $result[0]['order_bump'] ) ) {
433 + $old_values = unserialize( $result[0]['order_bump'] ); // phpcs:ignore
434 + }
435 +
436 + $new_values = array_merge( $old_values, [ $this->funnel->current_step_data['page_id'] ] );
437 +
438 + $database->update(
439 + 'funnel_contact',
440 + [ 'order_bump' => $new_values ],
441 + [ 'id' => $contact_id ]
442 + );
443 + }
444 +
445 + /**
253 446 * Updates funnel data.
254 447 *
255 448 * @since 1.1.0
256 449 */
257 450 public function update_funnel_data() {
451 + if ( is_admin() ) {
452 + return;
453 + }
454 +
258 455 $action = sellkit_htmlspecialchars( INPUT_GET, 'wc-ajax' );
259 456
260 457 if ( 'update_order_review' === $action ) {
261 458 $post_data = sellkit_htmlspecialchars( INPUT_POST, 'post_data' );
@@ -261,10 +458,53 @@
261 458 $post_data = sellkit_htmlspecialchars( INPUT_POST, 'post_data' );
262 459
263 460 $this->funnel = new Sellkit_Funnel( $this->get_current_page_id_by_post_data( $post_data ) );
264 461
462 + if ( empty( $this->funnel->funnel_id ) ) {
463 + $this->global_checkout_funnel_integration();
464 + }
465 +
265 466 return;
266 467 }
267 468
268 469 $this->funnel = Sellkit_Funnel::get_instance();
470 +
471 + if ( empty( $this->funnel->funnel_id ) ) {
472 + $this->global_checkout_funnel_integration();
473 + }
474 + }
475 +
476 + /**
477 + * Setup global checkout as a native sellkit funnel, so every functions works with no issue.
478 + * detect funnel id in the global checkout.
479 + *
480 + * @since 1.7.4
481 + */
482 + public function global_checkout_funnel_integration() {
483 + $global_checkout_id = get_option( Global_Checkout::SELLKIT_GLOBAL_CHECKOUT_OPTION, 0 );
484 +
485 + if ( $global_checkout_id > 0 && 'publish' === get_post_status( $global_checkout_id ) ) {
486 + $steps = get_post_meta( $global_checkout_id, 'nodes', true );
487 + $checkout_id = 0;
488 +
489 + if ( ! is_array( $steps ) ) {
490 + return;
491 + }
492 +
493 + foreach ( $steps as $step ) {
494 + $step['type'] = (array) $step['type'];
495 +
496 + if ( 'checkout' === $step['type']['key'] ) {
497 + $checkout_id = $step['page_id'];
498 + }
499 + }
500 +
501 + if ( (int) $checkout_id > 0 ) {
502 + $this->funnel = new Sellkit_Funnel( $checkout_id );
503 +
504 + add_filter( 'sellkit_global_checkout_activated', function() {
505 + return true;
506 + } );
507 + }
508 + }
269 509 }
270 510 }