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 +255 -6 1.2.92.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 ) ) {
@@ -90,17 +152,30 @@
90 152 }
91 153
92 154 $funnel_products = $funnel_data['data']['products']['list'];
93 155
156 + if ( ! sellkit()->has_valid_dependencies() ) {
157 + return;
158 + }
159 +
94 160 $action = sellkit_htmlspecialchars( INPUT_GET, 'wc-ajax' );
95 161
96 162 if ( 'update_order_review' !== $action && 'checkout' !== $action ) {
97 - wc()->cart->empty_cart();
163 + if ( 'true' === $reset_cart || true === $reset_cart ) {
164 + wc()->cart->empty_cart();
165 + }
98 166
99 167 foreach ( $funnel_products as $product_id => $product ) {
100 - $quantity = ! empty( $product['quantity'] ) ? $product['quantity'] : 1;
168 + if ( empty( $product ) ) {
169 + continue;
170 + }
101 171
102 - 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 + }
103 178 }
104 179 }
105 180
106 181 if ( empty( $optimization_data ) ) {
@@ -108,8 +183,12 @@
108 183 }
109 184
110 185 if ( self::apply_coupon_validation( $optimization_data ) ) {
111 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 + }
112 191 wc()->cart->add_discount( get_the_title( $auto_apply_coupon['value'] ) );
113 192 }
114 193
115 194 wc_clear_notices();
@@ -116,8 +195,23 @@
116 195 }
117 196 }
118 197
119 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 + /**
120 214 * Saving the funnel data to the order.
121 215 *
122 216 * @since 1.1.0
123 217 * @param object $order Order object.
@@ -126,8 +220,11 @@
126 220 if ( empty( $this->funnel->next_step_data ) ) {
127 221 return;
128 222 }
129 223
224 + Base_Contacts::step_is_passed();
225 + $this->check_bump_acceptance( $order );
226 +
130 227 $analytics_updater = new Data_Updater();
131 228
132 229 $analytics_updater->set_funnel_id( $this->funnel->funnel_id );
133 230 $analytics_updater->add_new_order_log( $order );
@@ -132,8 +229,9 @@
132 229 $analytics_updater->set_funnel_id( $this->funnel->funnel_id );
133 230 $analytics_updater->add_new_order_log( $order );
134 231
135 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 );
136 234 }
137 235
138 236 /**
139 237 * Adding page id field to the inputs.
@@ -140,12 +238,50 @@
140 238 *
141 239 * @since 1.1.0
142 240 */
143 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 +
144 271 woocommerce_form_field( 'sellkit_current_page_id', [
145 272 'type' => 'hidden',
146 273 'class' => [ 'hidden form-row-wide' ],
147 - ], 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' );
148 284 }
149 285
150 286 /**
151 287 * Check If a coupon should be applied.
@@ -240,13 +376,83 @@
240 376 return $valid_bumps;
241 377 }
242 378
243 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 + /**
244 446 * Updates funnel data.
245 447 *
246 448 * @since 1.1.0
247 449 */
248 450 public function update_funnel_data() {
451 + if ( is_admin() ) {
452 + return;
453 + }
454 +
249 455 $action = sellkit_htmlspecialchars( INPUT_GET, 'wc-ajax' );
250 456
251 457 if ( 'update_order_review' === $action ) {
252 458 $post_data = sellkit_htmlspecialchars( INPUT_POST, 'post_data' );
@@ -252,10 +458,53 @@
252 458 $post_data = sellkit_htmlspecialchars( INPUT_POST, 'post_data' );
253 459
254 460 $this->funnel = new Sellkit_Funnel( $this->get_current_page_id_by_post_data( $post_data ) );
255 461
462 + if ( empty( $this->funnel->funnel_id ) ) {
463 + $this->global_checkout_funnel_integration();
464 + }
465 +
256 466 return;
257 467 }
258 468
259 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 + }
260 509 }
261 510 }