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 +251 -6 1.3.12.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 /**
@@ -37,11 +40,52 @@
37 40
38 41 add_action( 'woocommerce_checkout_create_order', [ $this, 'save_order_custom_meta_data' ], 10 );
39 42 add_action( 'woocommerce_after_order_notes', [ $this, 'custom_checkout_hidden_fields' ], 10 );
40 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 + }
41 52 }
42 53
43 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 + /**
44 88 * Adding items to the checkout page.
45 89 *
46 90 * @since 1.1.0
47 91 * @SuppressWarnings(PHPMD.NPathComplexity)
@@ -51,15 +95,29 @@
51 95 // These variables should be used in checkout builder widget.
52 96 $funnel_data = $this->funnel->current_step_data;
53 97 $optimization_data = ! empty( $funnel_data['data']['optimization'] ) ? $funnel_data['data']['optimization'] : '';
54 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';
55 100
56 - 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 ) {
57 114 $product_settings = ! empty( $funnel_data['data']['products']['settings'] ) ? $funnel_data['data']['products']['settings'] : '';
58 115 $bump_data = ! empty( $funnel_data['bump'] ) ? $funnel_data['bump'] : '';
59 116
60 117 if ( ! empty( $product_settings ) ) {
61 118 set_query_var( 'funnel_product_settings', $product_settings );
119 + set_query_var( 'funnel_reset_cart', $reset_cart );
62 120 }
63 121
64 122 if ( ! empty( $bump_data ) ) {
65 123 $bump_data = $this->get_valid_bumps( $bump_data );
@@ -64,9 +122,13 @@
64 122 if ( ! empty( $bump_data ) ) {
65 123 $bump_data = $this->get_valid_bumps( $bump_data );
66 124
67 125 set_query_var( 'bump_data', $bump_data );
126 + } else {
127 + set_query_var( 'bump_data', [] );
68 128 }
129 +
130 + set_query_var( 'funnel_reset_cart', $reset_cart );
69 131 } );
70 132
71 133 if (
72 134 ! empty( $optimization_data['expiration_date'] ) &&
@@ -73,9 +135,9 @@
73 135 ! empty( $optimization_data['expire_checkout_coupons_switch'] ) &&
74 136 time() > $optimization_data['expiration_date'] &&
75 137 'on' === $optimization_data['expire_checkout_coupons_switch']
76 138 ) {
77 - wp_redirect( $redirect_url ); // phpcs:ignore
139 + wp_redirect( $this->add_http_to_url( $redirect_url ) ); // phpcs:ignore
78 140 exit;
79 141 }
80 142
81 143 if ( empty( $funnel_data ) ) {
@@ -97,14 +159,23 @@
97 159
98 160 $action = sellkit_htmlspecialchars( INPUT_GET, 'wc-ajax' );
99 161
100 162 if ( 'update_order_review' !== $action && 'checkout' !== $action ) {
101 - wc()->cart->empty_cart();
163 + if ( 'true' === $reset_cart || true === $reset_cart ) {
164 + wc()->cart->empty_cart();
165 + }
102 166
103 167 foreach ( $funnel_products as $product_id => $product ) {
104 - $quantity = ! empty( $product['quantity'] ) ? $product['quantity'] : 1;
168 + if ( empty( $product ) ) {
169 + continue;
170 + }
105 171
106 - wc()->cart->add_to_cart( $product_id, $quantity );
172 + $quantity = ! empty( $product['quantity'] ) ? $product['quantity'] : 1;
173 + $product_cart_id = WC()->cart->generate_cart_id( $product_id );
174 +
175 + if ( ! WC()->cart->find_product_in_cart( $product_cart_id ) ) {
176 + WC()->cart->add_to_cart( $product_id, $quantity );
177 + }
107 178 }
108 179 }
109 180
110 181 if ( empty( $optimization_data ) ) {
@@ -112,8 +183,12 @@
112 183 }
113 184
114 185 if ( self::apply_coupon_validation( $optimization_data ) ) {
115 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 + }
116 191 wc()->cart->add_discount( get_the_title( $auto_apply_coupon['value'] ) );
117 192 }
118 193
119 194 wc_clear_notices();
@@ -120,8 +195,23 @@
120 195 }
121 196 }
122 197
123 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 + /**
124 214 * Saving the funnel data to the order.
125 215 *
126 216 * @since 1.1.0
127 217 * @param object $order Order object.
@@ -130,8 +220,11 @@
130 220 if ( empty( $this->funnel->next_step_data ) ) {
131 221 return;
132 222 }
133 223
224 + Base_Contacts::step_is_passed();
225 + $this->check_bump_acceptance( $order );
226 +
134 227 $analytics_updater = new Data_Updater();
135 228
136 229 $analytics_updater->set_funnel_id( $this->funnel->funnel_id );
137 230 $analytics_updater->add_new_order_log( $order );
@@ -136,8 +229,9 @@
136 229 $analytics_updater->set_funnel_id( $this->funnel->funnel_id );
137 230 $analytics_updater->add_new_order_log( $order );
138 231
139 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 );
140 234 }
141 235
142 236 /**
143 237 * Adding page id field to the inputs.
@@ -144,12 +238,50 @@
144 238 *
145 239 * @since 1.1.0
146 240 */
147 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 +
148 271 woocommerce_form_field( 'sellkit_current_page_id', [
149 272 'type' => 'hidden',
150 273 'class' => [ 'hidden form-row-wide' ],
151 - ], 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' );
152 284 }
153 285
154 286 /**
155 287 * Check If a coupon should be applied.
@@ -244,13 +376,83 @@
244 376 return $valid_bumps;
245 377 }
246 378
247 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 + /**
248 446 * Updates funnel data.
249 447 *
250 448 * @since 1.1.0
251 449 */
252 450 public function update_funnel_data() {
451 + if ( is_admin() ) {
452 + return;
453 + }
454 +
253 455 $action = sellkit_htmlspecialchars( INPUT_GET, 'wc-ajax' );
254 456
255 457 if ( 'update_order_review' === $action ) {
256 458 $post_data = sellkit_htmlspecialchars( INPUT_POST, 'post_data' );
@@ -256,10 +458,53 @@
256 458 $post_data = sellkit_htmlspecialchars( INPUT_POST, 'post_data' );
257 459
258 460 $this->funnel = new Sellkit_Funnel( $this->get_current_page_id_by_post_data( $post_data ) );
259 461
462 + if ( empty( $this->funnel->funnel_id ) ) {
463 + $this->global_checkout_funnel_integration();
464 + }
465 +
260 466 return;
261 467 }
262 468
263 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 + }
264 509 }
265 510 }