PluginProbe
SellKit – Funnel builder and checkout optimizer for WooCommerce to sell more, faster / trunk
SellKit – Funnel builder and checkout optimizer for WooCommerce to sell more, faster vtrunk
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 1.8.1 All 42 releases
sellkit / includes / funnel / steps / checkout.php

checkout.php in SellKit – Funnel builder and checkout optimizer for WooCommerce to sell more, faster trunk, at includes/funnel/steps/checkout.php

511 lines 13.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Sellkit\Funnel\Steps;
4
5 use Sellkit\Funnel\Analytics\Data_Updater;
6 use Sellkit\Funnel\Contacts\Base_Contacts;
7 use Sellkit\Database;
8 use Sellkit_Funnel;
9 use Sellkit\Global_Checkout\Checkout as Global_Checkout;
10
11 defined( 'ABSPATH' ) || die();
12
13 /**
14 * Class Sellkit_Checkout.
15 *
16 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
17 * @since 1.1.0
18 */
19 class Checkout {
20
21 /**
22 * Funnel instance.
23 *
24 * @var Sellkit_Funnel Funnel object.
25 * @since 1.1.0
26 */
27 public $funnel;
28
29 /**
30 * Checkout constructor.
31 *
32 * @since 1.1.0
33 */
34 public function __construct() {
35 $this->update_funnel_data();
36
37 if ( empty( $this->funnel->funnel_id ) ) {
38 return;
39 }
40
41 add_action( 'woocommerce_checkout_create_order', [ $this, 'save_order_custom_meta_data' ], 10 );
42 add_action( 'woocommerce_after_order_notes', [ $this, 'custom_checkout_hidden_fields' ], 10 );
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 }
52 }
53
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 /**
88 * Adding items to the checkout page.
89 *
90 * @since 1.1.0
91 * @SuppressWarnings(PHPMD.NPathComplexity)
92 * @SuppressWarnings(PHPMD.CyclomaticComplexity)
93 */
94 public function do_actions() {
95 // These variables should be used in checkout builder widget.
96 $funnel_data = $this->funnel->current_step_data;
97 $optimization_data = ! empty( $funnel_data['data']['optimization'] ) ? $funnel_data['data']['optimization'] : '';
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';
100
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 ) {
114 $product_settings = ! empty( $funnel_data['data']['products']['settings'] ) ? $funnel_data['data']['products']['settings'] : '';
115 $bump_data = ! empty( $funnel_data['bump'] ) ? $funnel_data['bump'] : '';
116
117 if ( ! empty( $product_settings ) ) {
118 set_query_var( 'funnel_product_settings', $product_settings );
119 set_query_var( 'funnel_reset_cart', $reset_cart );
120 }
121
122 if ( ! empty( $bump_data ) ) {
123 $bump_data = $this->get_valid_bumps( $bump_data );
124
125 set_query_var( 'bump_data', $bump_data );
126 } else {
127 set_query_var( 'bump_data', [] );
128 }
129
130 set_query_var( 'funnel_reset_cart', $reset_cart );
131 } );
132
133 if (
134 ! empty( $optimization_data['expiration_date'] ) &&
135 ! empty( $optimization_data['expire_checkout_coupons_switch'] ) &&
136 time() > $optimization_data['expiration_date'] &&
137 'on' === $optimization_data['expire_checkout_coupons_switch']
138 ) {
139 wp_redirect( $this->add_http_to_url( $redirect_url ) ); // phpcs:ignore
140 exit;
141 }
142
143 if ( empty( $funnel_data ) ) {
144 return;
145 }
146
147 if (
148 empty( $funnel_data['data']['products']['list'] ) ||
149 ! is_array( $funnel_data['data']['products']['list'] )
150 ) {
151 return;
152 }
153
154 $funnel_products = $funnel_data['data']['products']['list'];
155
156 if ( ! sellkit()->has_valid_dependencies() ) {
157 return;
158 }
159
160 $action = sellkit_htmlspecialchars( INPUT_GET, 'wc-ajax' );
161
162 if ( 'update_order_review' !== $action && 'checkout' !== $action ) {
163 if ( 'true' === $reset_cart || true === $reset_cart ) {
164 wc()->cart->empty_cart();
165 }
166
167 foreach ( $funnel_products as $product_id => $product ) {
168 if ( empty( $product ) ) {
169 continue;
170 }
171
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 }
178 }
179 }
180
181 if ( empty( $optimization_data ) ) {
182 return;
183 }
184
185 if ( self::apply_coupon_validation( $optimization_data ) ) {
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 }
191 wc()->cart->add_discount( get_the_title( $auto_apply_coupon['value'] ) );
192 }
193
194 wc_clear_notices();
195 }
196 }
197
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 /**
214 * Saving the funnel data to the order.
215 *
216 * @since 1.1.0
217 * @param object $order Order object.
218 */
219 public function save_order_custom_meta_data( $order ) {
220 if ( empty( $this->funnel->next_step_data ) ) {
221 return;
222 }
223
224 Base_Contacts::step_is_passed();
225 $this->check_bump_acceptance( $order );
226
227 $analytics_updater = new Data_Updater();
228
229 $analytics_updater->set_funnel_id( $this->funnel->funnel_id );
230 $analytics_updater->add_new_order_log( $order );
231
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 );
234 }
235
236 /**
237 * Adding page id field to the inputs.
238 *
239 * @since 1.1.0
240 */
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
271 woocommerce_form_field( 'sellkit_current_page_id', [
272 'type' => 'hidden',
273 'class' => [ 'hidden form-row-wide' ],
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' );
284 }
285
286 /**
287 * Check If a coupon should be applied.
288 *
289 * @since 1.1.0
290 * @param array $optimization_data Optimization data.
291 */
292 public static function apply_coupon_validation( $optimization_data ) {
293 if (
294 empty( $optimization_data['auto_apply_coupons_switch'] ) ||
295 'on' !== $optimization_data['auto_apply_coupons_switch']
296 ) {
297 return false;
298 }
299
300 if (
301 empty( $optimization_data['auto_apply_coupons'] ) ||
302 ! is_array( $optimization_data['auto_apply_coupons'] )
303 ) {
304 return false;
305 }
306
307 return true;
308 }
309
310 /**
311 * Calculate total discount.
312 *
313 * @since 1.1.0
314 * @param string $total Total.
315 * @param string $value Value.
316 * @param string $type Type.
317 * @return false|float|int|mixed
318 */
319 public function calculate_discount( $total, $value, $type ) {
320 if ( empty( $value ) || 1 > $value ) {
321 return 0;
322 }
323
324 if ( strpos( $type, 'fixed' ) !== false ) {
325 return $value;
326 }
327
328 if ( strpos( $type, 'percentage' ) !== false ) {
329 return ( floatval( $total ) * floatval( $value ) ) / 100;
330 }
331
332 return 0;
333 }
334
335 /**
336 * Gets extracted post data.
337 *
338 * @since 1.1.0
339 * @param string $post_data Post data.
340 */
341 private function get_current_page_id_by_post_data( $post_data ) {
342 $vars = explode( '&', $post_data );
343 $extracted_data = [];
344
345 foreach ( $vars as $value ) {
346 $result = explode( '=', urldecode( $value ) );
347 $extracted_data[ $result[0] ] = $result[1];
348 }
349
350 if ( empty( $extracted_data['sellkit_current_page_id'] ) ) {
351 return null;
352 }
353
354 return $extracted_data['sellkit_current_page_id'];
355 }
356
357 /**
358 * Checks all bumps and return data.
359 *
360 * @since 1.1.0
361 * @param array $bump_data Bump data.
362 */
363 public function get_valid_bumps( $bump_data ) {
364 $valid_bumps = [];
365
366 foreach ( $bump_data as $bump ) {
367 $conditions = ! empty( $bump['data']['conditions'] ) ? $bump['data']['conditions'] : '';
368
369 if ( ! empty( $conditions ) && empty( sellkit_conditions_validation( $conditions ) ) ) {
370 continue;
371 }
372
373 $valid_bumps[] = $bump;
374 }
375
376 return $valid_bumps;
377 }
378
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 /**
446 * Updates funnel data.
447 *
448 * @since 1.1.0
449 */
450 public function update_funnel_data() {
451 if ( is_admin() ) {
452 return;
453 }
454
455 $action = sellkit_htmlspecialchars( INPUT_GET, 'wc-ajax' );
456
457 if ( 'update_order_review' === $action ) {
458 $post_data = sellkit_htmlspecialchars( INPUT_POST, 'post_data' );
459
460 $this->funnel = new Sellkit_Funnel( $this->get_current_page_id_by_post_data( $post_data ) );
461
462 if ( empty( $this->funnel->funnel_id ) ) {
463 $this->global_checkout_funnel_integration();
464 }
465
466 return;
467 }
468
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 }
509 }
510 }
511