PluginProbe
SellKit – Funnel builder and checkout optimizer for WooCommerce to sell more, faster / 2.8.0
SellKit – Funnel builder and checkout optimizer for WooCommerce to sell more, faster v2.8.0
2.8.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 All 44 releases
sellkit / includes / block-editor / blocks / checkout / helper.php

helper.php in SellKit – Funnel builder and checkout optimizer for WooCommerce to sell more, faster 2.8.0, at includes/block-editor/blocks/checkout/helper.php

2,489 lines 75.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Sellkit\Blocks\Helpers\Checkout;
3
4 use Sellkit\Funnel\Analytics\Data_Updater;
5 use Sellkit\Funnel\Contacts\Base_Contacts;
6 use Sellkit\Funnel\Steps\Checkout;
7 use Sellkit_Funnel;
8 use Sellkit\Global_Checkout\Checkout as Global_Checkout;
9
10 defined( 'ABSPATH' ) || die();
11
12 /**
13 * Checkout block helper class.
14 *
15 * @since 2.3.0
16 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
17 * @SuppressWarnings(ExcessiveClassLength)
18 * @SuppressWarnings(ExcessivePublicCount)
19 * @SuppressWarnings(TooManyMethods)
20 */
21 class Helper {
22 /**
23 * Current post ID.
24 *
25 * @since 2.3.0
26 * @var null|integer
27 */
28 public $post_id = null;
29
30 /**
31 * Checks if bumps applied.
32 *
33 * @since 2.3.0
34 * @var bool
35 */
36 public $is_bumps_applied = false;
37
38 /**
39 * Array of added products to cart.
40 *
41 * @since 2.3.0
42 * @var array
43 */
44 public $in_cart = [];
45
46 /**
47 * Helper id during checkout process.
48 *
49 * @var int
50 */
51 private $helper_id = 0;
52
53 /**
54 * Accept and reject button registration flag.
55 *
56 * @var bool
57 */
58 private $is_accept_reject_button_registered = false;
59
60 /**
61 * Class constructor.
62 *
63 * @since 2.3.0
64 */
65 public function __construct() {
66 // Create empty shortcode to simulate our block page as checkout page.
67 add_shortcode( 'sellkit_block_checkout_simulated', function() {
68 return '';
69 } );
70
71 add_action( 'wp_ajax_sellkit_block_checkout_ajax_handler', [ $this, 'ajax_handler' ] );
72 add_action( 'wp_ajax_nopriv_sellkit_block_checkout_ajax_handler', [ $this, 'ajax_handler' ] );
73
74 // Add shipping total to review order by custom hook.
75 add_action( 'sellkit-block-checkout-display-shipping-price', [ $this, 'add_shipping_total_to_review_order' ] );
76
77 // Custom coupon form.
78 add_action( 'sellkit-block-checkout-custom-coupon-form', [ $this, 'coupon_form' ] );
79
80 // Modify checkout order-review item.
81 add_action( 'sellkit-block-one-page-checkout-custom-order-item', [ $this, 'modify_checkout_order_item' ], 1, 4 );
82
83 // Validate user defined fields.
84 add_action( 'woocommerce_checkout_process', [ $this, 'validate_user_defined_fields' ] );
85
86 // Save user defined fields.
87 add_action( 'woocommerce_checkout_update_order_meta', [ $this, 'save_user_defined_fields' ] );
88
89 // Filter checkout ajax fragment.
90 add_filter( 'woocommerce_update_order_review_fragments', [ $this, 'checkout_fragment' ] );
91
92 // Simulate checkout page for our block.
93 add_action( 'wp', [ $this, 'simulate_our_block_page_as_checkout' ] );
94
95 // Fix shipping method price change on ajax.
96 // Manage bump order discounts.
97 // Template replacement.
98 add_action( 'woocommerce_checkout_update_order_review', [ $this, 'sellkit_checkout_during_ajax' ], 10, 1 );
99
100 add_filter( 'sellkit-block-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 // Add order notes to checkout form to desired location.
106 add_action( 'sellkit_block_checkout_after_term_and_condition', [ $this, 'order_notes' ] );
107
108 // Order Bump.
109 add_action( 'woocommerce_checkout_init', [ $this, 'order_bump' ] );
110
111 // Trigger for upsell steps popup.
112 add_action( 'sellkit_block_checkout_required_hidden_fields', [ $this, 'add_trigger_field_for_upsell_steps' ] );
113
114 // Add upsell templates at the end of page.
115 add_action( 'wp_footer', [ $this, 'sellkit_funnel_display_upsell_as_popup' ] );
116
117 // Bundle products.
118 add_action( 'sellkit_block_before_checkout_form', [ $this, 'bundled_products' ] );
119
120 // Remove coupon from it's default location. will add custom coupon form based on design at desired location.
121 remove_action( 'woocommerce_before_checkout_form', 'woocommerce_checkout_coupon_form', 10 );
122
123 // Rename Shipping text to Shipping Method.
124 add_filter( 'woocommerce_shipping_package_name', [ $this, 'rename_shipping_text' ] );
125 }
126
127 /**
128 * Handle ajax calls.
129 *
130 * @since 2.3.0
131 * @return void
132 */
133 public function ajax_handler() {
134 check_ajax_referer( 'sellkit_block_checkout', 'nonce' );
135
136 $sub_action = filter_input( INPUT_POST, 'sub_action', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
137
138 if ( method_exists( $this, $sub_action ) ) {
139 call_user_func( [ $this, $sub_action ] );
140 return;
141 }
142
143 wp_send_json_error();
144 }
145
146 /**
147 * Modify checkout ajax response HTML.
148 *
149 * @param array $response woocommerce checkout page ajax response.
150 * @return array
151 * @since 2.3.0
152 */
153 public function checkout_fragment( $response ) {
154 // Get posted data and identify if it's been sent by jupiter block or not.
155 $checkout_form_data = filter_input( INPUT_POST, 'post_data', FILTER_DEFAULT );
156 $checkout_form_data = explode( '&', $checkout_form_data );
157 $posted_data = [];
158
159 foreach ( $checkout_form_data as $input ) {
160 $item = explode( '=', urldecode( $input ) );
161
162 $posted_data[ trim( $item[0] ) ] = $item[1];
163 }
164
165 if ( ! array_key_exists( 'sellkit_current_page_id', $posted_data ) ) {
166 return $response;
167 }
168
169 // Order review fragment.
170 ob_start();
171 woocommerce_order_review();
172 $order_review = ob_get_clean();
173
174 // Payment fragment.
175 ob_start();
176 woocommerce_checkout_payment();
177 $payment = ob_get_clean();
178
179 $response['.woocommerce-checkout-review-order-table'] = $order_review;
180 $response['.woocommerce-checkout-payment'] = $payment;
181 $response['.sellkit-shipping-methods-one-page'] = $this->shipping_methods();
182
183 return $response;
184 }
185
186 /**
187 * Apply coupon code using ajax.
188 *
189 * @return void
190 * @since 2.3.0
191 */
192 public function apply_coupon() {
193 $code = filter_input( INPUT_POST, 'code', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
194
195 if ( empty( $code ) ) {
196 wp_send_json_error();
197 }
198
199 $result = WC()->cart->add_discount( $code );
200 wp_send_json_success( $result );
201 }
202
203 /**
204 * Update cart item quantity in checkout page by ajax.
205 *
206 * @return void
207 * @since 2.3.0
208 */
209 public function change_cart_item_qty() {
210 $qty = filter_input( INPUT_POST, 'qty', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
211 $id = filter_input( INPUT_POST, 'id', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
212 $action = filter_input( INPUT_POST, 'mode', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
213 $funnel_id = filter_input( INPUT_POST, 'related_checkout', FILTER_SANITIZE_NUMBER_INT );
214
215 $this->make_changes_after_cart_item_edit( $funnel_id );
216
217 if ( 'add' === $action ) {
218 WC()->cart->add_to_cart( $id, $qty );
219 $this->make_changes_after_cart_item_edit( $funnel_id );
220 wp_send_json_success();
221 }
222
223 if ( 'remove' === $action ) {
224 $post_type = get_post_type( $id );
225
226 if ( 'product_variation' === $post_type ) {
227 foreach ( WC()->cart->get_cart() as $item_key => $item ) {
228 if ( $item['variation_id'] === (int) $id ) {
229 WC()->cart->remove_cart_item( $item_key ); // we remove it.
230 break; // stop the loop.
231 }
232 }
233
234 $this->make_changes_after_cart_item_edit( $funnel_id );
235
236 wp_send_json_success();
237 }
238
239 $product_cart_id = WC()->cart->generate_cart_id( $id );
240 $cart_item_key = WC()->cart->find_product_in_cart( $product_cart_id );
241
242 if ( $cart_item_key ) {
243 WC()->cart->remove_cart_item( $cart_item_key );
244 }
245
246 $this->make_changes_after_cart_item_edit( $funnel_id );
247
248 wp_send_json_success();
249 }
250
251 ( $qty > 0 ) ? WC()->cart->set_quantity( $id, $qty ) : WC()->cart->remove_cart_item( $id );
252
253 $this->make_changes_after_cart_item_edit( $funnel_id );
254
255 wp_send_json_success();
256 }
257
258 /**
259 * Apply changes after editing cart items.
260 *
261 * @since 2.3.0
262 * @param int $funnel_id funnel step id.
263 */
264 public function make_changes_after_cart_item_edit( $funnel_id ) {
265 if ( empty( $funnel_id ) ) {
266 return;
267 }
268
269 $funnel_data = get_post_meta( $funnel_id, 'step_data', true );
270 $optimization_data = ! empty( $funnel_data['data']['optimization'] ) ? $funnel_data['data']['optimization'] : '';
271
272 if ( empty( $optimization_data ) ) {
273 return;
274 }
275
276 if ( function_exists( 'sellkit_pro' ) && ! sellkit_pro()->is_active_sellkit_pro ) {
277 $optimization_data = null;
278 }
279
280 // Help to apply funnel discount prices and coupons when changing product quantity.
281 $this->apply_discounted_prices( wc()->cart, $funnel_id );
282 $this->apply_discounted_prices( wc()->cart, $funnel_id, 'bumps' );
283
284 if ( Checkout::apply_coupon_validation( $optimization_data ) ) {
285 WC()->cart->remove_coupons();
286
287 foreach ( $optimization_data['auto_apply_coupons'] as $auto_apply_coupon ) {
288 wc()->cart->add_discount( get_the_title( $auto_apply_coupon['value'] ) );
289 }
290
291 wc_clear_notices();
292 }
293 }
294
295 /**
296 * Place shipping price based on design to checkout order-review.
297 *
298 * @return void
299 * @since 2.3.0
300 */
301 public function add_shipping_total_to_review_order() {
302 ?>
303 <tr class="sellkit-shipping-total">
304 <th><?php echo esc_html__( 'Shipping', 'sellkit' ); ?></th>
305 <td>
306 <?php
307 $price = WC()->cart->get_shipping_total();
308
309 if ( WC()->cart->display_prices_including_tax() ) {
310 $price = WC()->cart->get_shipping_total() + WC()->cart->shipping_tax_total;
311 }
312
313 echo wc_price( $price ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
314 ?>
315 </td>
316 </tr>
317 <?php
318 }
319
320 /**
321 * Modify checkout review-order based on design.
322 *
323 * @return void
324 * @param string $row html string of cart row.
325 * @param object $product product object.
326 * @param array $cart_item cart item information.
327 * @param string $cart_item_key cart item unique key.
328 * @since 2.3.0
329 *
330 * @SuppressWarnings(PHPMD.UnusedFormalParameter)
331 */
332 public function modify_checkout_order_item( $row, $product, $cart_item, $cart_item_key ) {
333 $post_id = get_the_ID();
334 $review_order_attributes = $this->get_inner_block_attributes( 'checkout', 'checkout-review-order', $post_id );
335
336 if ( isset( $review_order_attributes['disableCartEditing'] ) && $review_order_attributes['disableCartEditing'] ) {
337 add_filter( 'sellkit-checkout-block-disable-quantity', [ $this, 'checkout_order_hidden_quantity' ], 10, 2 );
338 }
339
340 $cart_items = WC()->cart->get_cart();
341
342 //phpcs:disable
343 ob_start();
344 ?>
345 <div style="display: inline-block" class="sellkit-checkout-widget-item-image">
346 <?php echo $product->get_image(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
347 </div>
348 <?php
349 $img_html = ob_get_clean();
350
351 ob_start();
352
353 $extra_class = $product->get_type();
354
355 if ( $cart_items[ $cart_item_key ] === end( $cart_items ) ) {
356 $extra_class .= ' last-cart-item';
357 }
358
359 if ( 'variation' === $product->get_type() ) {
360 add_filter( 'woocommerce_cart_item_name', [ $this, 'modify_variation_title' ], 50, 2 );
361 add_filter( 'woocommerce_get_item_data', [ $this, 'modify_variation_items' ], 10, 2 );
362 }
363 ?>
364 <tr class="<?php echo esc_attr( apply_filters( 'woocommerce_cart_item_class', 'cart_item', $cart_item, $cart_item_key ) ) . ' ' . esc_attr( $extra_class ); ?>">
365
366 <td class="product-name sellkit-one-page-checkout-product-name">
367 <?php
368 echo $img_html;
369
370 $fix_style = '';
371
372 if ( '' === $img_html ) {
373 $fix_style = 'margin-left: 0px;';
374 }
375 ?>
376
377 <div class="name-price" style="<?php echo esc_attr( $fix_style ); ?>">
378 <span style="display:inline-block">
379 <?php echo wp_kses_post( apply_filters( 'woocommerce_cart_item_name', $product->get_name(), $cart_item, $cart_item_key ) ) . '&nbsp;'; ?>
380 </span>
381 <span class="sellkit-checkout-variations" id="sellkit-checkout-variations">
382 <?php echo wc_get_formatted_cart_item_data( $cart_item ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
383 </span>
384 <?php echo apply_filters( 'sellkit-checkout-block-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'] ); ?>
385 </div>
386 </td>
387 <td class="product-total sellkit-one-page-checkout-product-price">
388 <?php echo apply_filters( 'woocommerce_cart_item_subtotal', WC()->cart->get_product_subtotal( $product, $cart_item['quantity'] ), $cart_item, $cart_item_key ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
389 </td>
390 </tr>
391 <?php
392 $cart_item = ob_get_clean();
393 echo $cart_item;
394
395 //phpcs:enable
396
397 if ( 'variation' === $product->get_type() ) {
398 remove_filter( 'woocommerce_cart_item_name', [ $this, 'modify_variation_title' ], 50 );
399 remove_filter( 'woocommerce_get_item_data', [ $this, 'modify_variation_items' ], 10 );
400 }
401 }
402
403 /**
404 * Remove variation attributes from variation product title.
405 *
406 * @param string $default default value.
407 * @param array $cart_item cart item data.
408 * @since 2.3.0
409 *
410 * @SuppressWarnings(PHPMD.UnusedFormalParameter)
411 */
412 public function modify_variation_title( $default, $cart_item ) {
413 $product = $cart_item['product_id'];
414
415 return get_the_title( $product );
416 }
417
418 /**
419 * Display variation attributes separated from title even if those are less than 3.
420 *
421 * @param array $item_data attributes array.
422 * @param array $cart_item cart item data.
423 * @since 2.3.0
424 */
425 public function modify_variation_items( $item_data, $cart_item ) {
426 $product = new \WC_Product_Variation( $cart_item['variation_id'] );
427 $attributes = $product->get_attributes();
428
429 if ( count( $attributes ) > 2 ) {
430 return $item_data;
431 }
432
433 foreach ( $attributes as $key => $value ) {
434 $key = str_replace( 'pa_', '', $key );
435 $key = str_replace( '-', '', $key );
436 $key = str_replace( '_', '', $key );
437
438 $item_data[] = [
439 'key' => $key,
440 'value' => $value,
441 ];
442 }
443
444 return $item_data;
445 }
446
447 /**
448 * Login user by ajax.
449 * sign user in by using form included in block.
450 *
451 * @return void
452 * @since 2.3.0
453 */
454 public function auth_user() {
455 $email = filter_input( INPUT_POST, 'email', FILTER_SANITIZE_EMAIL );
456 $pass = filter_input( INPUT_POST, 'pass', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
457 $verify = filter_var( $email, FILTER_VALIDATE_EMAIL );
458
459 if ( ! $verify || empty( $email ) || empty( $pass ) ) {
460 wp_send_json_error( esc_html__( 'Email or password field is not valid.', 'sellkit' ) );
461 }
462
463 $result = wp_authenticate( $email, $pass );
464
465 if ( is_wp_error( $result ) ) {
466 wp_send_json_error( $result->get_error_message() );
467 }
468
469 wp_clear_auth_cookie();
470 wp_set_current_user( $result->ID );
471 wp_set_auth_cookie( $result->ID );
472
473 wp_send_json_success( $result );
474 }
475
476 /**
477 * Validate user defined custom value.
478 *
479 * @return void
480 * @since 2.3.0
481 */
482 public function validate_user_defined_fields() {
483 $post_id = filter_input( INPUT_POST, 'sellkit_current_page_id', FILTER_DEFAULT, FILTER_SANITIZE_NUMBER_INT );
484
485 $shipping_attributes = $this->get_inner_block_attributes( 'checkout', 'checkout-form-shipping', $post_id );
486 $billing_attributes = $this->get_inner_block_attributes( 'checkout', 'checkout-billing-details', $post_id );
487 $billing_roles = array_map( function( $field ) {
488 return $field['role'];
489 }, $billing_attributes['locations'] ?? [] );
490 $shipping_roles = array_map( function( $field ) {
491 return $field['role'];
492 }, $shipping_attributes['locations'] ?? [] );
493
494 if ( ! isset( $shipping_attributes['locations'] ) ) {
495 $this->checkout_fields_validation( null, $billing_attributes['locations'] );
496 } else {
497 $this->checkout_fields_validation( $shipping_attributes['locations'], $billing_attributes['locations'] );
498 }
499
500 add_filter( 'woocommerce_checkout_fields', function( $default_fields ) use ( $billing_roles, $shipping_roles ) {
501 unset( $default_fields['billing']['billing_email'] );
502
503 // Let Sellkit validate configured block fields while leaving third-party billing fields untouched.
504 foreach ( $billing_roles as $field_key ) {
505 if ( isset( $default_fields['billing'][ $field_key ] ) ) {
506 $default_fields['billing'][ $field_key ]['required'] = false;
507 }
508 }
509
510 foreach ( $shipping_roles as $field_key ) {
511 if ( isset( $default_fields['shipping'][ $field_key ] ) ) {
512 $default_fields['shipping'][ $field_key ]['required'] = false;
513 }
514 }
515
516 return $default_fields;
517 } );
518 }
519
520 /**
521 * Save user defined custom fields value to database.
522 *
523 * @param int $order_id id of woocommerce order.
524 * @return void
525 * @since 2.3.0
526 *
527 * @SuppressWarnings(PHPMD.CyclomaticComplexity)
528 * @SuppressWarnings(PHPMD.NPathComplexity)
529 */
530 public function save_user_defined_fields( $order_id ) {
531 $order = wc_get_order( $order_id );
532 $user_id = $order->get_user_id();
533
534 $fields = [
535 'shipping_first_name',
536 'shipping_last_name',
537 'shipping_company',
538 'shipping_address_1',
539 'shipping_address_2',
540 'shipping_city',
541 'shipping_postcode',
542 'shipping_country',
543 'shipping_state',
544 'shipping_phone',
545 ];
546
547 foreach ( $fields as $field ) {
548 $this->update_shipping_field( $order, $user_id, $field );
549 }
550
551 $order->save();
552 }
553
554 /**
555 * Update shipping field for both order and user meta.
556 *
557 * @param WC_Order $order The WooCommerce order object.
558 * @param int $user_id The user ID, if available.
559 * @param string $field The field name to update.
560 * @since 2.3.0
561 */
562 private function update_shipping_field( $order, $user_id, $field ) {
563 if ( ! empty( $_POST[ $field ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
564 $sanitized_value = sanitize_text_field( wp_unslash( $_POST[ $field ] ) ); // phpcs:ignore WordPress.Security.NonceVerification
565
566 $setter_method = 'set_' . $field;
567 if ( method_exists( $order, $setter_method ) ) {
568 $order->$setter_method( $sanitized_value );
569 }
570
571 if ( $user_id ) {
572 update_user_meta( $user_id, $field, $sanitized_value );
573 }
574 }
575 }
576
577 /**
578 * Place custom coupon form in checkout order-review based design.
579 *
580 * @return void
581 * @since 2.3.0
582 */
583 public function coupon_form() {
584 $post_id = get_the_ID();
585
586 if ( ! empty( $_POST ) ) { //phpcs:ignore WordPress.Security.NonceVerification
587 $checkout_form_data = filter_input( INPUT_POST, 'post_data', FILTER_DEFAULT );
588 $checkout_form_data = explode( '&', $checkout_form_data );
589 $posted_data = [];
590
591 foreach ( $checkout_form_data as $input ) {
592 $item = explode( '=', urldecode( $input ) );
593
594 $posted_data[ trim( $item[0] ) ] = $item[1];
595 }
596
597 $post_id = $posted_data['sellkit_current_page_id'];
598 }
599
600 $review_order_attributes = $this->get_inner_block_attributes( 'checkout', 'checkout-review-order', $post_id );
601
602 if ( isset( $review_order_attributes['disableCoupon'] ) && $review_order_attributes['disableCoupon'] ) {
603 return;
604 }
605
606 echo '<tr class="coupon-form border-none"><td colspan="2">';
607 $this->form( $review_order_attributes );
608 echo '</td></tr>';
609 }
610
611 /**
612 * New custom coupon form HTML.
613 *
614 * @param array $review_order_attributes Review order innerblock attributes.
615 * @return void
616 * @since 2.3.0
617 */
618 public function form( $review_order_attributes ) {
619 ?>
620 <div class="sellkit-custom-coupon-form sellkit-normal-coupon-form">
621 <p class="sellkit-form-row-first">
622 <input class="sellkit-coupon" type="text" placeholder="<?php echo esc_attr( $review_order_attributes['couponPlaceholderText'] ); ?>" />
623 </p>
624
625 <p class="sellkit-form-row-last">
626 <span class="sellkit-checkout-widget-secondary-button sellkit-apply-coupon sellkit-apply-coupon" >
627 <?php echo esc_html( $review_order_attributes['couponButtonText'] ); ?>
628 </span>
629 </p>
630 </div>
631 <?php
632 }
633
634 /**
635 * Look for an email if exists or not.
636 * Is used for login process.
637 *
638 * @return void
639 * @since 2.3.0
640 */
641 public function search_for_email() {
642 $email = filter_input( INPUT_POST, 'email', FILTER_SANITIZE_EMAIL );
643 $valid = filter_var( $email, FILTER_VALIDATE_EMAIL );
644
645 if ( ! $valid || empty( $email ) ) {
646 wp_send_json_error();
647 }
648
649 $check = email_exists( $email );
650
651 if ( false === $check ) {
652 wp_send_json_error();
653 }
654
655 wp_send_json_success();
656 }
657
658 /**
659 * Look for an username if exists or not.
660 * Is used for register process.
661 *
662 * @return void
663 * @since 2.3.0
664 */
665 public function search_for_username() {
666 $username = filter_input( INPUT_POST, 'user', FILTER_SANITIZE_EMAIL );
667
668 if ( empty( $username ) ) {
669 wp_send_json_error();
670 }
671
672 $username = sanitize_user( $username );
673 $check = username_exists( $username );
674
675 if ( false !== $check ) {
676 wp_send_json_error();
677 }
678
679 wp_send_json_success();
680 }
681
682 /**
683 * Simulate checkout page where our block is present.
684 *
685 * @since 2.3.0
686 * @return void
687 */
688 public function simulate_our_block_page_as_checkout() {
689 $page_id = get_the_ID();
690 $content = get_post_field( 'post_content', $page_id );
691
692 if ( false === strpos( $content, 'sellkit-blocks/checkout' ) ) {
693 return;
694 }
695
696 add_filter( 'woocommerce_is_checkout', function() {
697 return true;
698 } );
699
700 add_filter( 'theme_mod_jupiterx_jupiterx_checkout_cart_elements', function() {
701 return [];
702 }, 10 );
703 }
704
705 /**
706 * Integrate user country with our block.
707 *
708 * @since 2.3.0
709 * @return void
710 */
711 public function set_customer_details_ajax() {
712 $country = filter_input( INPUT_POST, 'country', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
713 $state = filter_input( INPUT_POST, 'state', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
714 $shipping_country = filter_input( INPUT_POST, 'shipping_country', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
715 $shipping_state = filter_input( INPUT_POST, 'shipping_state', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
716
717 if ( ! empty( $country ) ) {
718 setcookie( 'sellkit-checkout-billing-cc', $country, time() + ( 86400 * 3 ), '/' );
719 setcookie( 'sellkit-checkout-billing-state', $state, time() + ( 86400 * 3 ), '/' );
720 }
721
722 if ( ! empty( $shipping_country ) ) {
723 setcookie( 'sellkit-checkout-shipping-cc', $shipping_country, time() + ( 86400 * 3 ), '/' );
724 setcookie( 'sellkit-checkout-shipping-state', $shipping_state, time() + ( 86400 * 3 ), '/' );
725 }
726
727 wp_send_json_success();
728 }
729
730 /**
731 * Modify cart contents using bundled products.
732 *
733 * @since 2.3.0
734 * @return void
735 * @SuppressWarnings(PHPMD.CyclomaticComplexity)
736 * @SuppressWarnings(PHPMD.NPathComplexity)
737 */
738 public function sellkit_checkout_modify_cart_by_bundle_products() {
739 $qty = filter_input( INPUT_POST, 'qty', FILTER_SANITIZE_NUMBER_INT );
740 $id = filter_input( INPUT_POST, 'id', FILTER_SANITIZE_NUMBER_INT );
741 $type = filter_input( INPUT_POST, 'type', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
742 $checkout_id = filter_input( INPUT_POST, 'checkout_id', FILTER_SANITIZE_NUMBER_INT );
743 $modify = filter_input( INPUT_POST, 'modify', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
744 $key = filter_input( INPUT_POST, 'key', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
745
746 if ( ! empty( $key ) ) {
747 foreach ( WC()->cart->get_cart() as $item_key => $item ) {
748 if ( $item_key === $key ) {
749 WC()->cart->set_quantity( $item_key, $qty, true );
750 break; // stop the loop.
751 }
752 }
753
754 wp_send_json_success();
755 }
756
757 if ( 'radio' === $type ) {
758 $step_data = get_post_meta( $checkout_id, 'step_data', true );
759 $products = $step_data['data']['products']['list'];
760 $reset_cart = isset( $step_data['data']['products']['reset_cart'] ) ? $step_data['data']['products']['reset_cart'] : 'true';
761
762 if ( 'true' === $reset_cart ) {
763 foreach ( WC()->cart->get_cart() as $item ) {
764 if ( empty( $item['product_id'] ) ) {
765 continue;
766 }
767
768 if ( array_key_exists( $item['product_id'], $products ) ) {
769 continue;
770 }
771
772 $products[ $item['product_id'] ] = [
773 'quantity' => $item['quantity'],
774 'discount' => '',
775 'discountType' => 'fixed',
776 ];
777 }
778 }
779
780 $selected = ! empty( $products[ $id ] ) ? $products[ $id ] : [];
781 $real_quantity = ( empty( $selected['quantity'] ) ) ? 1 : $selected['quantity'];
782
783 if ( (int) $real_quantity !== (int) $qty ) {
784 wp_send_json_error( esc_html__( 'Oh do not be tricky.', 'sellkit' ) );
785 }
786 }
787
788 if ( 'radio' === $type ) {
789 WC()->cart->empty_cart();
790 WC()->cart->add_to_cart( $id, $qty );
791 }
792
793 if ( 'checkbox' === $type && 'add' === $modify ) {
794 WC()->cart->add_to_cart( $id, $qty );
795 }
796
797 if ( 'checkbox' === $type && 'remove' === $modify ) {
798 $post_type = get_post_type( $id );
799
800 // Maybe product is a variation.
801 if ( 'product_variation' === $post_type ) {
802 foreach ( WC()->cart->get_cart() as $item_key => $item ) {
803 if ( $item['variation_id'] === (int) $id ) {
804 WC()->cart->remove_cart_item( $item_key ); // we remove it.
805 break; // stop the loop.
806 }
807 }
808
809 $this->make_changes_after_cart_item_edit( $checkout_id );
810
811 wp_send_json_success();
812 }
813
814 // Default product.
815 $product_cart_id = WC()->cart->generate_cart_id( $id );
816 $cart_item_key = WC()->cart->find_product_in_cart( $product_cart_id );
817
818 if ( $cart_item_key ) {
819 WC()->cart->remove_cart_item( $cart_item_key );
820 }
821 }
822
823 $this->make_changes_after_cart_item_edit( $checkout_id );
824
825 wp_send_json_success();
826 }
827
828 /**
829 * Apply funnel discount.
830 * Will be called twice, first when page is loading , second during checkout ajax.
831 *
832 * @param object $cart cart object.
833 * @param int $checkout_id_ajax checkout id.
834 * @param string $source source of products, bump or default products to be checked.
835 * @param array $upsell_data upsell product data coming from popup.
836 * @since 2.3.0
837 * @SuppressWarnings(PHPMD.CyclomaticComplexity)
838 * @SuppressWarnings(PHPMD.NPathComplexity)
839 */
840 public function apply_discounted_prices( $cart = [], $checkout_id_ajax = null, $source = 'default', $upsell_data = [] ) {
841 $checkout_id = get_queried_object_id();
842
843 if ( wp_doing_ajax() ) {
844 $checkout_id = $checkout_id_ajax;
845 }
846
847 if ( empty( $checkout_id ) ) {
848 return;
849 }
850
851 $step_data = get_post_meta( $checkout_id, 'step_data', true );
852
853 if ( empty( $step_data ) ) {
854 return;
855 }
856
857 $products = [];
858
859 if (
860 'default' === $source &&
861 isset( $step_data['data']['products'] ) &&
862 array_key_exists( 'data', $step_data ) &&
863 array_key_exists( 'list', $step_data['data']['products'] )
864 ) {
865 $products = $step_data['data']['products']['list'];
866 }
867
868 if ( 'bumps' === $source && ! empty( $step_data['bump'] ) ) {
869 $bumps = $step_data['bump'];
870
871 foreach ( $bumps as $bump ) {
872 if ( ! array_key_exists( 'products', $bump['data'] ) ) {
873 continue;
874 }
875
876 $key = ! empty( $bump['data']['products']['list'] ) ? array_key_first( $bump['data']['products']['list'] ) : null;
877
878 if ( empty( $key ) ) {
879 continue;
880 }
881
882 $products[ $key ] = $bump['data']['products']['list'][ $key ];
883 }
884 }
885
886 if ( 'upsell' === $source ) {
887 $products = $upsell_data['data']['products']['list'];
888 }
889
890 if ( empty( $products ) ) {
891 return;
892 }
893
894 $final_price = 0;
895
896 foreach ( $cart->get_cart() as $key => $details ) {
897 $item_id = $details['product_id'];
898 $price = false;
899
900 if ( ! empty( $details['variation_id'] ) ) {
901 $item_id = $details['variation_id'];
902 }
903
904 foreach ( $products as $product_id => $product_details ) {
905 if ( $item_id === $product_id ) {
906 $discount_type = isset( $product_details['discountType'] ) ? $product_details['discountType'] : '';
907 $discount_value = isset( $product_details['discount'] ) ? $product_details['discount'] : '';
908
909 $price = self::calculate_discount( $item_id, $discount_type, $discount_value );
910 }
911 }
912
913 if ( false !== $price ) {
914 $details['data']->set_price( $price );
915 }
916
917 $final_price += $details['data']->get_price() * $details['quantity'];
918 }
919
920 $this->modify_products_price_before_woo_apply_discounts( $final_price );
921 }
922
923 /**
924 * Modify cart subtotal before validating discounts.
925 *
926 * @since 2.3.0
927 * @param int $final_price cart new subtotal.
928 *
929 * @SuppressWarnings(PHPMD.UnusedFormalParameter)
930 */
931 public function modify_products_price_before_woo_apply_discounts( $final_price ) {
932 add_filter( 'woocommerce_coupon_validate_minimum_amount', function( $value, $coupon ) use ( $final_price ) {
933 return $coupon->get_minimum_amount() > $final_price;
934 }, 99, 2 );
935
936 add_filter( 'woocommerce_coupon_validate_maximum_amount', function( $value, $coupon ) use ( $final_price ) {
937 return $coupon->get_maximum_amount() < $final_price;
938 }, 99, 2 );
939 }
940
941 /**
942 * Fix shipping method price on checkout ajax state change.
943 *
944 * @param string $data checkout form data.
945 * @since 2.3.0
946 * @return void
947 */
948 public function sellkit_checkout_during_ajax( $data ) {
949 $data = explode( '&', $data );
950 $clear = [];
951
952 foreach ( $data as $key => $input ) {
953 $input = explode( '=', $input );
954 $clear[ $input[0] ] = $input[1];
955 }
956
957 if ( ! array_key_exists( 'sellkit_current_page_id', $clear ) ) {
958 return;
959 }
960
961 add_action( 'sellkit-block-checkout-custom-coupon-form', [ $this, 'coupon_form' ] );
962
963 $this->apply_template_replacement_during_ajax( $clear );
964
965 $_POST = $this->assigning_default_ajax_shipping_fields( $_POST, $clear ); // phpcs:ignore
966
967 $this->order_bump( $clear );
968
969 if ( array_key_exists( 'sellkit-bundle-products', $clear ) ) {
970 $option = $clear['sellkit-bundle-products'];
971
972 $this->bundle_product_action( $option );
973 }
974
975 // Apply discounts.
976 if ( ! array_key_exists( 'sellkit_current_page_id', $clear ) ) {
977 return;
978 }
979
980 $this->apply_discounted_prices( wc()->cart, $clear['sellkit_current_page_id'] );
981 $this->apply_discounted_prices( wc()->cart, $clear['sellkit_current_page_id'], 'bumps' );
982 }
983
984 /**
985 * Assign checkout ajax default shipping fields after ajax is sent.
986 *
987 * @param array $post post request.
988 * @param array $clear checkout form data.
989 * @return array
990 * @since 2.3.0
991 *
992 * @SuppressWarnings(PHPMD.NPathComplexity)
993 */
994 public function assigning_default_ajax_shipping_fields( $post, $clear ) {
995 $field_mappings = [
996 'billing_state' => 'state',
997 'shipping_country' => 's_country',
998 'shipping_state' => 's_state',
999 'shipping_postcode' => 's_postcode',
1000 'shipping_city' => 's_city',
1001 'shipping_phone' => 's_phone',
1002 'shipping_company' => 's_company',
1003 'shipping_address_1' => 's_address',
1004 'shipping_address_2' => 's_address_2',
1005 ];
1006
1007 foreach ( $field_mappings as $clear_key => $post_key ) {
1008 $post = $this->update_post_field( $post, $clear, $clear_key, $post_key );
1009 }
1010
1011 return $post;
1012 }
1013
1014 /**
1015 * Helper function to update post array with the corresponding value from clear array.
1016 *
1017 * @param array $post The post request array.
1018 * @param array $clear The checkout form data array.
1019 * @param string $clear_key The key in the $clear array.
1020 * @param string $post_key The key in the $post array.
1021 * @return array
1022 */
1023 private function update_post_field( $post, $clear, $clear_key, $post_key ) {
1024 if ( array_key_exists( $clear_key, $clear ) ) {
1025 $post[ $post_key ] = $clear[ $clear_key ];
1026 }
1027
1028 return $post;
1029 }
1030
1031 /**
1032 * Template replacement during ajax call.
1033 *
1034 * @param array $data checkout form data.
1035 * @return void
1036 * @since 2.3.0
1037 *
1038 * @SuppressWarnings(PHPMD.UnusedFormalParameter)
1039 */
1040 private function apply_template_replacement_during_ajax( $data ) {
1041 add_filter( 'wc_get_template', function( $located, $template_name, $args, $template_path, $default_path ) {
1042 $our_path = sellkit()->plugin_dir() . 'includes/block-editor/blocks/checkout/templates/';
1043 $files = [
1044 'review-order.php',
1045 'payment-method.php',
1046 'payment.php',
1047 'form-checkout.php',
1048 'cart-item-data.php',
1049 'cart-shipping.php',
1050 'terms.php',
1051 ];
1052 $template = str_replace( 'checkout/', '', $template_name );
1053 $template = str_replace( 'cart/', '', $template );
1054
1055 if ( in_array( $template, $files, true ) ) {
1056 $located = $our_path . $template;
1057 }
1058
1059 return $located;
1060 }, 10, 5 );
1061
1062 $this->review_order_template_attributes();
1063 }
1064
1065 /**
1066 * Pass attribute args to review order template.
1067 *
1068 * @return void
1069 * @since 2.3.0
1070 */
1071 public function review_order_template_attributes() {
1072 $post_data = filter_input( INPUT_POST, 'post_data', FILTER_DEFAULT );
1073 $post_id = 0;
1074
1075 if ( ! empty( $post_data ) ) {
1076 parse_str( $post_data, $parsed_data );
1077
1078 if ( isset( $parsed_data['sellkit_current_page_id'] ) ) {
1079 $post_id = intval( $parsed_data['sellkit_current_page_id'] );
1080 }
1081 }
1082
1083 $review_order_attributes = $this->get_inner_block_attributes( 'checkout', 'checkout-review-order', $post_id );
1084
1085 $args = [
1086 'review_order_attributes' => $review_order_attributes,
1087 ];
1088
1089 wc_get_template_html( 'checkout/review-order.php', $args );
1090 }
1091
1092 /**
1093 * Modify default selected shipping method.
1094 *
1095 * @since 2.3.0
1096 * @param string $default default selected method.
1097 * @return string
1098 */
1099 public function sellkit_default_shipping_method( $default ) {
1100 $applied_coupons = isset( WC()->cart ) ? WC()->cart->get_applied_coupons() : [];
1101
1102 if ( count( $applied_coupons ) < 1 ) {
1103 return $default;
1104 }
1105
1106 foreach ( $applied_coupons as $coupon_code ) {
1107
1108 $coupon = new \WC_Coupon( $coupon_code );
1109
1110 if ( $coupon->get_free_shipping() ) {
1111 if ( count( WC()->session->get( 'shipping_for_package_0' )['rates'] ) > 0 ) {
1112 // Loop through.
1113 foreach ( WC()->session->get( 'shipping_for_package_0' )['rates'] as $rate_id => $rate ) {
1114 // For free shipping.
1115 if ( 'free_shipping' === $rate->method_id ) {
1116 return $rate_id;
1117 }
1118 }
1119 }
1120 }
1121 }
1122
1123 return $default;
1124 }
1125
1126 /**
1127 * Recalculate checkout and cart prices.
1128 *
1129 * @since 2.3.0
1130 *
1131 * @SuppressWarnings(PHPMD.NPathComplexity)
1132 * @SuppressWarnings(PHPMD.ExcessiveClassLength)
1133 * @SuppressWarnings(PHPMD.CyclomaticComplexity)
1134 */
1135 public function before_cart_calculate() {
1136 // More than twice isn't necessary. this is called multiple times by WooCommerce.
1137 if ( did_action( 'woocommerce_before_calculate_totals' ) >= 2 && wp_doing_ajax() ) {
1138 return;
1139 }
1140
1141 $checkout_id = self::get_valid_checkout_id();
1142
1143 if ( 0 === $checkout_id ) {
1144 return;
1145 }
1146
1147 // Apply funnel prices.
1148 $this->apply_discounted_prices( wc()->cart, $checkout_id );
1149 $this->apply_discounted_prices( wc()->cart, $checkout_id, 'bumps' );
1150
1151 // Apply upsell discount.
1152 $sellkit_upsell_ids = null;
1153 if ( isset( $_POST['woocommerce-process-checkout-nonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['woocommerce-process-checkout-nonce'] ) ), 'woocommerce-process_checkout' ) ) {
1154 $sellkit_upsell_ids = isset( $_POST['sellkit_upsell_ids'] ) ? sanitize_textarea_field( wp_unslash( $_POST['sellkit_upsell_ids'] ) ) : null;
1155 }
1156
1157 if ( ! empty( $sellkit_upsell_ids ) ) {
1158 $upsell_ids = explode( ',', $sellkit_upsell_ids );
1159 $list = self::upsell_downsell_ids_of_funnel( $checkout_id );
1160
1161 if ( ! empty( $list ) ) {
1162 foreach ( $upsell_ids as $id ) {
1163
1164 if ( in_array( $id, $list, true ) ) {
1165 self::applay_upsell_downsell_discount( $id );
1166 }
1167 }
1168 }
1169 }
1170
1171 // We should re apply coupons after each price changes. to make sure everything is correct.
1172 $optimization_data = ! empty( $funnel_data['data']['optimization'] ) ? $funnel_data['data']['optimization'] : '';
1173
1174 if ( Checkout::apply_coupon_validation( $optimization_data ) ) {
1175 WC()->cart->remove_coupons();
1176
1177 foreach ( $optimization_data['auto_apply_coupons'] as $auto_apply_coupon ) {
1178 wc()->cart->add_discount( get_the_title( $auto_apply_coupon['value'] ) );
1179 }
1180
1181 wc_clear_notices();
1182 }
1183 }
1184
1185 /**
1186 * Extract upsell and downsell ids from funnel.
1187 *
1188 * @since 2.8.0
1189 *
1190 * @param int $checkout_id id of checkout.
1191 *
1192 * @return array
1193 */
1194 private static function upsell_downsell_ids_of_funnel( $checkout_id ) {
1195 $step_data = get_post_meta( $checkout_id, 'step_data', true );
1196 $funnel_id = $step_data['funnel_id'];
1197 $nodes = get_post_meta( $funnel_id, 'nodes', true );
1198 $ids = [];
1199
1200 foreach ( $nodes as $node ) {
1201 if ( isset( $node['type'] ) && in_array( $node['type']['key'], [ 'upsell', 'downsell' ], true ) ) {
1202 $ids[] = $node['page_id'];
1203 }
1204 }
1205
1206 return $ids;
1207 }
1208
1209 /**
1210 * Apply discount to upsell or downsell.
1211 *
1212 * @since 2.8.0
1213 *
1214 * @param int $id id of upsell or downsell.
1215 */
1216 private static function applay_upsell_downsell_discount( $id ) {
1217 $upsell_data = get_post_meta( $id, 'step_data', true );
1218 $product = $upsell_data['data']['products']['list'];
1219
1220 foreach ( $product as $key => $value ) {
1221 foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
1222 $product = $cart_item['data'];
1223 $product_id = $product->get_id();
1224
1225 if ( $key === $product_id ) {
1226 $discount_type = isset( $value['discountType'] ) ? $value['discountType'] : '';
1227 $discount_value = isset( $value['discount'] ) ? $value['discount'] : '';
1228
1229 $price = self::calculate_discount( $product_id, $discount_type, $discount_value );
1230 $product->set_price( $price );
1231 }
1232 }
1233 }
1234 }
1235
1236 /**
1237 * Get the checkout step associated with the current visitor.
1238 *
1239 * @since 2.8.0
1240 *
1241 * @return int
1242 *
1243 * @SuppressWarnings(PHPMD.NPathComplexity)
1244 */
1245 private static function get_valid_checkout_id() {
1246 $checkout_id = get_queried_object_id();
1247
1248 if ( wp_doing_ajax() ) {
1249 $checkout_id = filter_input( INPUT_POST, 'related_checkout', FILTER_SANITIZE_NUMBER_INT );
1250
1251 if ( empty( $checkout_id ) ) {
1252 $checkout_id = filter_input( INPUT_POST, 'sellkit_current_page_id', FILTER_SANITIZE_NUMBER_INT );
1253 }
1254
1255 if ( empty( $checkout_id ) ) {
1256 $data = filter_input( INPUT_POST, 'post_data', FILTER_DEFAULT );
1257
1258 if ( ! empty( $data ) ) {
1259 parse_str( $data, $data );
1260 }
1261
1262 if ( is_array( $data ) && array_key_exists( 'sellkit_current_page_id', $data ) ) {
1263 $checkout_id = $data['sellkit_current_page_id'];
1264 }
1265 }
1266 }
1267
1268 $checkout_id = absint( $checkout_id );
1269 $step_data = get_post_meta( $checkout_id, 'step_data', true );
1270
1271 if (
1272 0 === $checkout_id ||
1273 'publish' !== get_post_status( $checkout_id ) ||
1274 ! is_array( $step_data ) ||
1275 ! isset( $step_data['type']['key'] ) ||
1276 'checkout' !== $step_data['type']['key']
1277 ) {
1278 return 0;
1279 }
1280
1281 if ( wp_doing_ajax() && ! function_exists( 'WC' ) ) {
1282 return 0;
1283 }
1284
1285 return $checkout_id;
1286 }
1287
1288 /**
1289 * Check sellkit step through ajax.
1290 * And in order decide to show upsell or downsell steps through a popup.
1291 *
1292 * @since 2.3.0
1293 */
1294 public function call_funnel_popups() {
1295 $step = filter_input( INPUT_POST, 'step', FILTER_SANITIZE_NUMBER_INT );
1296 $funnel = new Sellkit_Funnel( $step );
1297 $next_step = $funnel->next_step_data;
1298 $popups = [ 'upsell', 'downsell' ];
1299
1300 $funnel->next_step_data['type'] = (array) $funnel->next_step_data['type'];
1301
1302 if ( 'decision' === $funnel->next_step_data['type']['key'] ) {
1303 $page_id = isset( $funnel->next_step_data['page_id'] ) ? $funnel->next_step_data['page_id'] : 0;
1304
1305 $this->helper_id = $step;
1306 $this->take_care_of_decision_step( $page_id, $funnel->funnel_id );
1307 return;
1308 }
1309
1310 if ( in_array( $funnel->next_step_data['type']['key'], $popups, true ) ) {
1311
1312 $next_step = $this->is_set_upsell_product( $next_step );
1313
1314 wp_send_json_success( [
1315 'next_id' => $next_step['page_id'],
1316 'next_type' => $next_step['type']['key'],
1317 ] );
1318 }
1319 }
1320
1321 /**
1322 * Check the upsell or downsell have product or not, ignore upsell/downsell step if product doesn't set.
1323 *
1324 * @param array $next_step Next step data.
1325 * @since 2.6.0
1326 * @SuppressWarnings(PHPMD.NPathComplexity)
1327 */
1328 private function is_set_upsell_product( $next_step ) {
1329 if ( isset( $next_step['type']['key'] ) && ! in_array( $next_step['type']['key'], [ 'upsell', 'downsell' ], true ) ) {
1330 return $next_step;
1331 }
1332
1333 $products = isset( $next_step['data'] ) ? $next_step['data']['products'] : [];
1334 $list = isset( $products['list'] ) ? $products['list'] : [];
1335
1336 if ( empty( $list ) ) {
1337 $funnel = isset( $next_step['page_id'] ) ? new Sellkit_Funnel( $next_step['page_id'] ) : null;
1338 $next_step = ! empty( $funnel ) ? $funnel->next_no_step_data : [];
1339
1340 if ( empty( $next_step ) && ! empty( $funnel ) ) {
1341 return $funnel->end_node_step_data;
1342 }
1343
1344 if ( isset( $next_step['type']['key'] ) && in_array( $next_step['type']['key'], [ 'upsell', 'downsell' ], true ) ) {
1345
1346 $next_step = $this->is_set_upsell_product( $next_step );
1347 }
1348 }
1349
1350 return $next_step;
1351 }
1352
1353 /**
1354 * Gets step id before the decision step and return result.
1355 *
1356 * @param int $step_id id of the step before decision step.
1357 * @param int $funnel_id id of the funnel.
1358 * @since 2.3.0
1359 */
1360 private function take_care_of_decision_step( $step_id, $funnel_id = null ) {
1361 // Failed decision step due to no page id should be checked a little different.
1362 if ( empty( $step_id ) && ! empty( $funnel_id ) ) {
1363 $this->check_decision_step_with_no_page_id();
1364 }
1365
1366 $funnel = new Sellkit_Funnel( $step_id );
1367 $conditions = ! empty( $funnel->current_step_data['data']['conditions'] ) ? $funnel->current_step_data['data']['conditions'] : [];
1368 $is_valid = sellkit_conditions_validation( $conditions );
1369 $next_step = $funnel->next_no_step_data;
1370
1371 if ( $is_valid ) {
1372 $next_step = $funnel->next_step_data;
1373 }
1374
1375 $next_step = $this->is_set_upsell_product( $next_step );
1376 $next_step['type'] = (array) $next_step['type'];
1377
1378 if ( 'decision' === $next_step['type']['key'] ) {
1379 $this->take_care_of_decision_step( $next_step['page_id'] );
1380
1381 return;
1382 }
1383
1384 wp_send_json_success( [
1385 'next_id' => $next_step['page_id'],
1386 'next_type' => $next_step['type']['key'],
1387 ] );
1388 }
1389
1390 /**
1391 * Decision next step directly using funnel data.
1392 *
1393 * @since 2.3.0
1394 */
1395 private function check_decision_step_with_no_page_id() {
1396 $funnel = new Sellkit_Funnel( $this->helper_id );
1397 $decicion_data = $funnel->next_step_data;
1398 $conditions = $decicion_data['data']['conditions'];
1399 $funnel_data = get_post_meta( $funnel->funnel_id, 'nodes', true );
1400 $next_no = 'none' !== $decicion_data['targets'][1]['nodeId'] ? $funnel_data[ $decicion_data['targets'][1]['nodeId'] ] : $funnel->end_node_step_data;
1401 $next_yes = 'none' !== $decicion_data['targets'][0]['nodeId'] ? $funnel_data[ $decicion_data['targets'][0]['nodeId'] ] : $funnel->end_node_step_data;
1402 $is_valid = sellkit_conditions_validation( $conditions );
1403 $next_step = $next_no;
1404
1405 if ( $is_valid ) {
1406 $next_step = $next_yes;
1407 }
1408
1409 $next_step = $this->is_set_upsell_product( $next_step );
1410
1411 wp_send_json_success( [
1412 'next_id' => $next_step['page_id'],
1413 'next_type' => $next_step['type']['key'],
1414 ] );
1415 }
1416
1417 /**
1418 * Perform upsell popup accept button.
1419 *
1420 * @since 2.3.0
1421 *
1422 * @SuppressWarnings(PHPMD.NPathComplexity)
1423 */
1424 public function perform_upsell_accept_button() {
1425 // Gather and validate information.
1426 $upsell_id = filter_input( INPUT_POST, 'upsell_id', FILTER_SANITIZE_NUMBER_INT );
1427 $checkout_id = filter_input( INPUT_POST, 'checkout_id', FILTER_SANITIZE_NUMBER_INT );
1428 $response = [];
1429
1430 if ( empty( $upsell_id ) ) {
1431 wp_send_json_error( esc_html__( 'Empty Upsell ID.', 'sellkit' ) );
1432 }
1433
1434 $upsell_data = get_post_meta( $upsell_id, 'step_data', true );
1435
1436 if ( empty( $upsell_data ) ) {
1437 wp_send_json_error( esc_html__( 'Empty Step Data.', 'sellkit' ) );
1438 }
1439
1440 // Add product to cart.
1441 $product_id = '';
1442 $qty = '';
1443
1444 if (
1445 is_array( $upsell_data['data'] ) &&
1446 array_key_exists( 'products', $upsell_data['data'] ) &&
1447 ! empty( $upsell_data['data']['products']['list'] )
1448 ) {
1449 $product_id = array_key_first( $upsell_data['data']['products']['list'] );
1450 $qty = ! empty( $upsell_data['data']['products']['list'][ $product_id ] ) ? $upsell_data['data']['products']['list'][ $product_id ]['quantity'] : 0;
1451 }
1452
1453 if ( empty( $qty ) ) {
1454 $qty = 1;
1455 }
1456
1457 $added_product_hash = '';
1458
1459 if ( ! in_array( $product_id, array_column( WC()->cart->get_cart(), 'product_id' ), true ) ) {
1460 $added_product_hash = WC()->cart->add_to_cart( $product_id, $qty );
1461
1462 // Take care of cart and applied discounts.
1463 $this->apply_discounted_prices( WC()->cart, $checkout_id, 'upsell', $upsell_data );
1464 $this->make_changes_after_cart_item_edit( $checkout_id );
1465 }
1466
1467 // Response.
1468 $funnel = new Sellkit_Funnel( $upsell_id );
1469 $next_step = $funnel->next_step_data;
1470 $next_step = $this->is_set_upsell_product( $next_step );
1471 $next_step['type'] = ! empty( $next_step['type'] ) ? (array) $next_step['type'] : null;
1472
1473 if ( 'upsell' === $funnel->current_step_data['type']['key'] && isset( WC()->cart->cart_contents[ $added_product_hash ] ) ) {
1474 $analytics_updater = new Data_Updater();
1475 $total = empty( WC()->cart->cart_contents[ $added_product_hash ]['line_total'] ) ? 0 : WC()->cart->cart_contents[ $added_product_hash ]['line_total'];
1476
1477 $analytics_updater->set_funnel_id( $funnel->funnel_id );
1478 $analytics_updater->add_new_upsell_revenue_log( $total );
1479 }
1480
1481 $contact_data = [
1482 'key' => $funnel->current_step_data['type']['key'],
1483 'page_id' => $funnel->current_step_data['page_id'],
1484 ];
1485
1486 Base_Contacts::step_is_passed( $contact_data );
1487
1488 // Step with empty data, will be redirected to thankyou page.
1489 if ( empty( $next_step ) || empty( $next_step['type'] ) ) {
1490 wp_send_json_success(
1491 [
1492 'next_id' => $funnel->end_node_step_data['page_id'],
1493 'next_type' => $funnel->end_node_step_data['type']['key'],
1494 'upsell_ids' => $upsell_id,
1495 ]
1496 );
1497 }
1498
1499 if ( 'decision' === $next_step['type']['key'] ) {
1500 $this->take_care_of_decision_step( $next_step['page_id'] );
1501 return;
1502 }
1503
1504 $response = [
1505 'next_id' => $next_step['page_id'],
1506 'next_type' => $next_step['type']['key'],
1507 'upsell_ids' => $upsell_id,
1508 ];
1509
1510 wp_send_json_success( $response );
1511 }
1512
1513 /**
1514 * Perform upsell popup reject button.
1515 *
1516 * @since 2.3.0
1517 */
1518 public function perform_upsell_reject_button() {
1519 $upsell_id = filter_input( INPUT_POST, 'upsell_id', FILTER_SANITIZE_NUMBER_INT );
1520
1521 if ( empty( $upsell_id ) ) {
1522 wp_send_json_error( esc_html__( 'Empty Upsell ID.', 'sellkit' ) );
1523 }
1524
1525 $funnel = new Sellkit_Funnel( $upsell_id );
1526 $next_step = $funnel->next_no_step_data;
1527 $next_step = $this->is_set_upsell_product( $next_step );
1528
1529 if ( isset( $next_step['type'] ) && false !== $next_step['type'] ) {
1530 $next_step['type'] = (array) $next_step['type'];
1531 }
1532
1533 // Step with empty data, will be redirected to thankyou page.
1534 if ( empty( $next_step ) || empty( $next_step['type'] ) ) {
1535 wp_send_json_success(
1536 [
1537 'next_id' => $funnel->end_node_step_data['page_id'],
1538 'next_type' => $funnel->end_node_step_data['type']['key'],
1539 ]
1540 );
1541 }
1542
1543 if ( 'decision' === $next_step['type']['key'] ) {
1544 $this->take_care_of_decision_step( $next_step['page_id'] );
1545 return;
1546 }
1547
1548 $response = [
1549 'next_id' => $next_step['page_id'],
1550 'next_type' => $next_step['type']['key'],
1551 ];
1552
1553 wp_send_json_success( $response );
1554 }
1555
1556 /**
1557 * Checkout fields validation.
1558 *
1559 * @param int $shipping_fields shipping fields.
1560 * @param int $billing_fields billing fields.
1561 * @since 2.3.0
1562 *
1563 * @SuppressWarnings(PHPMD.NPathComplexity)
1564 */
1565 public function checkout_fields_validation( $shipping_fields, $billing_fields ) {
1566 $shipping_prefix = esc_html__( 'Shipping', 'sellkit' );
1567
1568 if ( $shipping_fields ) {
1569 foreach ( $shipping_fields as $data ) {
1570 if ( ! WC()->cart->needs_shipping() ) {
1571 continue;
1572 }
1573
1574 $final_name = isset( $data['role'] ) ? $data['role'] : '';
1575 $value = filter_input( INPUT_POST, $final_name, FILTER_DEFAULT );
1576
1577 if ( $data['required'] && ( empty( $value ) ) ) {
1578 /* translators: %s field_name. */
1579 wc_add_notice( sprintf( esc_html__( '%s is a required field.', 'sellkit' ), '<strong>' . esc_html( $shipping_prefix ) . ' ' . esc_html( $data['label'] ) . '</strong>' ), 'error' );
1580 }
1581 }
1582 }
1583
1584 $billing_method = filter_input( INPUT_POST, 'billing-method', FILTER_DEFAULT );
1585 $billing_prefix = esc_html__( 'Billing', 'sellkit' );
1586
1587 foreach ( $billing_fields as $data ) {
1588 if ( 'same' === $billing_method ) {
1589 continue;
1590 }
1591
1592 $final_name = isset( $data['role'] ) ? $data['role'] : '';
1593 $value = filter_input( INPUT_POST, $final_name, FILTER_DEFAULT );
1594
1595 if ( $data['required'] && ( ! array_key_exists( $final_name, $_POST ) || empty( $value ) ) ) { //phpcs:ignore WordPress.Security.NonceVerification
1596 wc_add_notice( sprintf(
1597 /* translators: %s field_name. */
1598 esc_html__( '%s is a required field.', 'sellkit' ),
1599 '<strong>' . esc_html( $billing_prefix ) . ' ' . esc_html( $data['label'] ) . '</strong>'
1600 ), 'error' );
1601 }
1602 }
1603
1604 // Validate main checkout email.
1605 $email = filter_input( INPUT_POST, 'billing_email', FILTER_SANITIZE_EMAIL );
1606 $valid = filter_var( $email, FILTER_VALIDATE_EMAIL );
1607
1608 if ( ! $email ) {
1609 /* translators: %s field_name. */
1610 wc_add_notice( sprintf( esc_html__( '%s is a required field.', 'sellkit' ), '<strong>' . esc_html__( 'Email address', 'sellkit' ) . '</strong>' ), 'error' );
1611 return;
1612 }
1613
1614 if ( ! $valid ) {
1615 /* translators: %s field_name. */
1616 wc_add_notice( sprintf( esc_html__( '%s is not a valid email.', 'sellkit' ), '<strong>' . esc_html__( 'Email address', 'sellkit' ) . '</strong>' ), 'error' );
1617 }
1618 }
1619
1620 /**
1621 * Display checkout order notes field.
1622 *
1623 * @since 2.3.0
1624 */
1625 public function order_notes() {
1626 // Get woocommerce order notes field.
1627 $notes = WC()->checkout()->get_checkout_fields( 'order' );
1628
1629 // print notes field.
1630 foreach ( $notes as $key => $field ) {
1631 ?>
1632 <div class="sellkit-checkout-order-fields sellkit-widget-checkout-fields">
1633 <?php
1634 if ( 'order_comments' === $key ) {
1635 ?>
1636 <div class="sellkit-checkout-order-notes">
1637 <div class="sellkit-order-note-field-wrapper">
1638 <input id="sellkit-add-notes-to-order-box" type="checkbox">
1639 <label for="sellkit-add-notes-to-order-box">
1640 <?php echo esc_html__( 'Add a note to your order', 'sellkit' ); ?>
1641 </label>
1642 </div>
1643 <?php
1644 woocommerce_form_field( $key, $field, WC()->checkout()->get_value( $key ) );
1645 ?>
1646 </div>
1647 <?php
1648 }
1649 ?>
1650 </div>
1651 <?php
1652 }
1653 }
1654
1655 /**
1656 * Trigger to activate order bumb or not for this checkout.
1657 *
1658 * @param array $data checkout form data.
1659 * @since 2.3.0
1660 * @return void
1661 *
1662 * @SuppressWarnings(PHPMD.UnusedFormalParameter)
1663 */
1664 public function order_bump( $data = [] ) {
1665 if ( 'woocommerce_checkout_init' === current_action() ) {
1666 $this->order_bump_frontend_manager();
1667 }
1668 }
1669
1670 /**
1671 * Order bump to manage frontend.
1672 *
1673 * @since 2.3.0
1674 * @return void
1675 */
1676 private function order_bump_frontend_manager() {
1677 if ( $this->is_bumps_applied ) {
1678 return;
1679 }
1680
1681 global $wp_query;
1682 $query = $wp_query->query_vars;
1683
1684 if ( ! array_key_exists( 'bump_data', $query ) ) {
1685 return;
1686 }
1687
1688 $bumps = $query['bump_data'];
1689
1690 foreach ( $bumps as $bump ) {
1691 if ( ! array_key_exists( 'products', $bump['data'] ) ) {
1692 continue;
1693 }
1694
1695 $design = $bump['data']['design'];
1696 $products = $bump['data']['products'];
1697
1698 $design['sellkit_funnels_bump_position'] = strpos( $design['sellkit_funnels_bump_position'], 'sellkit' ) === 0
1699 ? 'sellkit-block' . substr( $design['sellkit_funnels_bump_position'], 7 )
1700 : $design['sellkit_funnels_bump_position'];
1701
1702 add_action( $design['sellkit_funnels_bump_position'], function() use ( $design, $products ) {
1703 $this->bump_html( $design, $products );
1704 }, 5 );
1705 }
1706
1707 $this->is_bumps_applied = true;
1708 }
1709
1710 /**
1711 * Order bump html.
1712 *
1713 * @param array $design selected option in checkout step.
1714 * @param array $products products in checkout step.
1715 * @since 2.3.0
1716 * @return void
1717 *
1718 * @SuppressWarnings(PHPMD.NPathComplexity)
1719 */
1720 public function bump_html( $design, $products ) {
1721 $list = empty( $products['list'] ) ? [] : $products['list'];
1722 $ids = [];
1723
1724 foreach ( $list as $id => $details ) {
1725 if ( ! is_array( $details ) ) {
1726 continue;
1727 }
1728
1729 $ids[] = $id;
1730 $qty = $details['quantity'];
1731 $discount = $details['discount'];
1732 $type = $details['discountType'];
1733 }
1734
1735 $ids_string = implode( '|', $ids );
1736 $product = wc_get_product( $ids_string );
1737 $qty = ( empty( $qty ) ) ? 1 : $qty;
1738
1739 if ( empty( $product ) || ! $product->get_id() || ! $product->is_in_stock() ) {
1740 return;
1741 }
1742
1743 $unique_id = 'bump-title-' . $ids_string;
1744
1745 $class = '';
1746 if (
1747 'sellkit-block-checkout-before-order-summary' === current_action() ||
1748 'sellkit-block-checkout-after-order-summary' === current_action()
1749 ) {
1750 $class = 'sellkit-bump-review-order';
1751 }
1752
1753 $checked = empty( WC()->cart->find_product_in_cart( WC()->cart->generate_cart_id( $product->get_id() ) ) ) ? false : true;
1754 ?>
1755 <div class="sellkit-checkout-step-bump-wrapper <?php echo esc_attr( $class ); ?>" >
1756 <div class="sellkit-checkout-bump-order-header">
1757 <div class="sellkit-bump-order-left-header">
1758 <img src="<?php echo esc_attr( sellkit()->plugin_assets_url() . 'img/right-arrow.svg' ); ?>" >
1759 <input
1760 type="checkbox"
1761 value="<?php echo esc_attr( $ids_string ); ?>"
1762 class="sellkit-checkout-bump-order-products"
1763 data-qty="<?php echo esc_attr( $qty ); ?>"
1764 name="sellkit_bump_data_<?php echo esc_attr( $ids_string ); ?>"
1765 id="<?php echo esc_html( $unique_id ); ?>"
1766 <?php echo ( true === $checked ) ? 'checked="true"' : ''; ?>
1767 >
1768 <label
1769 for="<?php echo esc_attr( $unique_id ); ?>"
1770 class="sellkit-checkout-order-bump-title"
1771 >
1772 <?php echo esc_html( $design['sellkit_funnels_bump_checkbox_label'] ); ?>
1773 </label>
1774 </div>
1775 <div class="sellkit-bump-order-right-header">
1776 <span class="sellkit-checkout-order-bump-price">
1777 <?php
1778 $discounted_price = self::calculate_discount( (int) $ids_string, $type, $discount );
1779 $sale_price = $product->get_sale_price();
1780 $regular_price = $product->get_regular_price();
1781 $main_price = ( strpos( $type, 'sale' ) !== false ) ? $sale_price : $regular_price;
1782
1783 if ( floatval( $main_price ) > floatval( $discounted_price ) ) {
1784 ?>
1785 <del aria-hidden="true">
1786 <span class="woocommerce-Price-amount amount">
1787 <bdi>
1788 <span class="woocommerce-Price-currencySymbol">
1789 <?php echo wc_price( $main_price ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
1790 </span>
1791 </bdi>
1792 </span>
1793 </del>
1794 <bdi class="bump-price-bolded"><?php echo wc_price( $discounted_price ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></bdi>
1795 <?php
1796 } else {
1797 ?>
1798 <bdi><?php echo wc_price( $main_price ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></bdi>
1799 <?php
1800 }
1801 ?>
1802 </span>
1803 </div>
1804 </div>
1805 <div class="sellkit-checkout-bump-order-body" >
1806 <div class="sellkit-bump-order-left-body">
1807 <?php $image = wp_get_attachment_image_src( $product->get_image_id() ); ?>
1808 <?php if ( 'true' === $design['sellkit_funnels_bump_product_image'] && ! empty( $image[0] ) ) : ?>
1809 <img src="<?php echo esc_attr( $image[0] ); ?>" >
1810 <?php endif; ?>
1811 </div>
1812 <div class="sellkit-bump-order-right-body">
1813 <div class="sellkit-bump-order-description">
1814 <?php echo wp_kses_post( $design['sellkit_content'] ); ?>
1815 </div>
1816 </div>
1817 </div>
1818 </div>
1819 <?php
1820 }
1821
1822 /**
1823 * Hide checkout order product quantity input and show raw quantity.
1824 *
1825 * @param string $input_html default quantity input.
1826 * @param int $quantity product quantity in cart.
1827 * @since 2.3.0
1828 * @return void
1829 *
1830 * @SuppressWarnings(PHPMD.UnusedFormalParameter)
1831 */
1832 public function checkout_order_hidden_quantity( $input_html, $quantity ) {
1833 echo sprintf(
1834 /** Translators: %s: product quantity */
1835 '<strong class="product-quantity">%s</strong>',
1836 esc_html( $quantity )
1837 );
1838 }
1839
1840 /**
1841 * Calculate item discount.
1842 *
1843 * @param int $id product id.
1844 * @param string $type discount type.
1845 * @param int $value discount value.
1846 * @return int|boolean
1847 * @since 2.3.0
1848 */
1849 public static function calculate_discount( $id, $type, $value ) {
1850 if ( false === $type && false === $value ) {
1851 return false;
1852 }
1853
1854 $product = wc_get_product( $id );
1855 $regular = $product->get_regular_price();
1856 $sale = $product->get_sale_price();
1857 $price = false;
1858
1859 if ( strpos( $type, 'sale' ) !== false ) {
1860 $discount = ( 'fixed-sale' === $type ) ? floatval( $value ) : ( floatval( $sale ) * floatval( $value ) ) / 100;
1861 $discount = floatval( $sale ) - $discount;
1862
1863 return ( $discount > 0 ) ? $discount : 0;
1864 }
1865
1866 if ( strpos( $type, 'sale' ) === false ) {
1867 $discount = ( 'fixed' === $type ) ? floatval( $value ) : ( floatval( $regular ) * floatval( $value ) ) / 100;
1868 $discount = floatval( $regular ) - $discount;
1869
1870 return ( $discount > 0 ) ? $discount : 0;
1871 }
1872
1873 return $price;
1874 }
1875
1876 /**
1877 * Add an extra hidden field to checkout form if funnel includes upsell step and also add sellkit current page id.
1878 *
1879 * @since 2.3.0
1880 */
1881 public function add_trigger_field_for_upsell_steps() {
1882 echo sprintf( '<input type="hidden" id="sellkit_current_page_id" name="sellkit_current_page_id" value="%1$s">',
1883 esc_attr( get_the_ID() )
1884 );
1885
1886 $include_upsell = $this->is_funnel_includes_upsell();
1887
1888 if ( false === $include_upsell ) {
1889 return;
1890 }
1891
1892 echo '<input type="hidden" id="sellkit_funnel_has_upsell" value="upsell" >';
1893 echo '<input type="hidden" id="sellkit_funnel_popup_step_id" value="0" >';
1894 echo '<input type="hidden" id="sellkit_upsell_ids" autocomplete="off" name="sellkit_upsell_ids" value="0" >';
1895 }
1896
1897 /**
1898 * Checks if funnel includes upsell step.
1899 *
1900 * @param string $goal customize return value.
1901 * @param int $ajax_id upsell id through ajax.
1902 * @since 2.3.0
1903 *
1904 * @SuppressWarnings(PHPMD.NPathComplexity)
1905 */
1906 public function is_funnel_includes_upsell( $goal = 'is_upsell', $ajax_id = null ) {
1907 $id = get_queried_object_id();
1908
1909 if ( wp_doing_ajax() ) {
1910 $id = $ajax_id;
1911 }
1912
1913 $step_data = get_post_meta( $id, 'step_data', true );
1914 $include_upsell = false;
1915 $upsell_steps = [];
1916 $upsell_ids = [];
1917 $global_checkout_id = get_option( Global_Checkout::SELLKIT_GLOBAL_CHECKOUT_OPTION, 0 );
1918
1919 // We are in default WooCommerce checkout page.
1920 if ( empty( $step_data ) && $global_checkout_id > 0 && 'publish' === get_post_status( $global_checkout_id ) ) {
1921 $steps = get_post_meta( $global_checkout_id, 'nodes', true );
1922 $checkout_id = 0;
1923
1924 foreach ( $steps as $step ) {
1925 $step['type'] = (array) $step['type'];
1926
1927 if ( 'checkout' === $step['type']['key'] ) {
1928 $checkout_id = $step['page_id'];
1929 }
1930 }
1931
1932 $step_data = get_post_meta( $checkout_id, 'step_data', true );
1933 }
1934
1935 if ( empty( $step_data ) ) {
1936 return $include_upsell;
1937 }
1938
1939 $funnel_id = intval( $step_data['funnel_id'] );
1940 $funnel_data = get_post_meta( $funnel_id, 'nodes', true );
1941
1942 if ( empty( $funnel_data ) ) {
1943 return $include_upsell;
1944 }
1945
1946 $popups = [ 'downsell', 'upsell' ];
1947
1948 foreach ( $funnel_data as $step ) {
1949 if ( isset( $step['data'] ) && isset( $step['data']['products'] ) && isset( $step['data']['products']['list'] ) ) {
1950 $product_id = array_keys( $step['data']['products']['list'] )[0] ?? 0;
1951
1952 if ( $product_id ) {
1953 $product = wc_get_product( $product_id );
1954
1955 if ( $product && ! $product->is_in_stock() ) {
1956 return $include_upsell;
1957 }
1958 }
1959 }
1960
1961 $step['type'] = (array) $step['type'];
1962
1963 if ( in_array( $step['type']['key'], $popups, true ) ) {
1964 $include_upsell = true;
1965 $upsell_steps[] = $step;
1966 $upsell_ids[] = $step['page_id'];
1967 }
1968 }
1969
1970 if ( 'get_ids' === $goal ) {
1971 return $upsell_ids;
1972 }
1973
1974 // Send data to the filter that keeps steps data to prepare popup.
1975 add_filter( 'sellkit_upsell_steps_popup_information', function( $default ) use ( $upsell_steps ) {
1976 if ( ! empty( $upsell_steps ) ) {
1977 return $upsell_steps;
1978 }
1979
1980 return $default;
1981 } );
1982
1983 return $include_upsell;
1984 }
1985
1986 /**
1987 * Display sellkit upsell steps as popup.
1988 *
1989 * @since 2.3.0
1990 */
1991 public function sellkit_funnel_display_upsell_as_popup() {
1992 $upsell_data = apply_filters( 'sellkit_upsell_steps_popup_information', [] );
1993 $i = 1;
1994
1995 if ( empty( $upsell_data ) ) {
1996 return;
1997 }
1998
1999 foreach ( $upsell_data as $data ) {
2000 $id = 'sellkit_funnel_upsell_popup_' . $i;
2001 $class = 'sellkit_funnel_upsell_popup sellkit_funnel_upsell_popup_' . $data['page_id'];
2002
2003 // Check if the page was built with Elementor and get the content.
2004 if ( 'elementor' === sellkit()->page_builder() && class_exists( 'Elementor\Plugin' ) && \Elementor\Plugin::$instance->db->is_built_with_elementor( $data['page_id'] ) ) {
2005 $content = \Elementor\Plugin::$instance->frontend->get_builder_content_for_display( $data['page_id'] );
2006 }
2007
2008 // Render block editor content.
2009 if ( 'gutenberg' === sellkit()->page_builder() ) {
2010 $content = '';
2011
2012 if ( ! $this->is_accept_reject_button_registered ) {
2013 $this->load_accept_reject_button_frontend();
2014 }
2015
2016 $page_content = get_the_content( null, true, $data['page_id'] );
2017 $blocks = parse_blocks( $page_content );
2018
2019 foreach ( $blocks as $block ) {
2020 $content .= $this->render_block_html_or_styles( render_block( $block ), $block );
2021 }
2022 }
2023
2024 if ( ! empty( $content ) ) {
2025 ?>
2026 <div class="<?php echo esc_attr( $class ); ?>" id="<?php echo esc_attr( $id ); ?>">
2027 <?php echo $this->render_block_html_or_styles( render_block( $block ), $block, true ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
2028 <?php echo wp_kses_post( $content ); ?>
2029 <input type="hidden" class="identify" value="<?php echo esc_attr( $data['page_id'] ); ?>" >
2030 </div>
2031 <?php
2032 }
2033
2034 $i++;
2035 }
2036 }
2037
2038 /**
2039 * Renders the HTML output for a given block with added styles based on the block's attributes and support.
2040 *
2041 * This function applies specific layout styles to a block based on its attributes such as 'layout' and 'blockGap'.
2042 * It adjusts the HTML output by appending a unique class for the block and wraps the modified content with necessary styles.
2043 * The function includes safeguards to skip serialization of the block's gap properties if unsupported and ensures that
2044 * CSS values are safe to use.
2045 *
2046 * @param string $block_content The HTML content of the block.
2047 * @param array $block The block's attributes and information.
2048 * @param array $style_only if true function returns only style.
2049 * @return string The HTML content of the block wrapped with a unique class and necessary styles.
2050 *
2051 * @since 2.3.0
2052 */
2053 public function render_block_html_or_styles( $block_content, $block, $style_only = false ) {
2054 $block_type = \WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] );
2055 if ( empty( $block_type ) ) {
2056 return;
2057 }
2058
2059 $block_gap = wp_get_global_settings( [ 'spacing', 'blockGap' ] );
2060 $has_block_gap_support = isset( $block_gap ) ? null !== $block_gap : false;
2061 $default_block_layout = _wp_array_get( $block_type->supports, [ '__experimentalLayout', 'default' ], [] );
2062 $used_layout = isset( $block['attrs']['layout'] ) ? $block['attrs']['layout'] : $default_block_layout;
2063
2064 $class_name = wp_unique_id( 'wp-container-' );
2065 $gap_value = _wp_array_get( $block, [ 'attrs', 'style', 'spacing', 'blockGap' ] );
2066
2067 // Skip if gap value contains unsupported characters. Regex for CSS value borrowed from `safecss_filter_attr`, and used here because we only want to match against the value, not the CSS attribute.
2068 if ( is_array( $gap_value ) ) {
2069 foreach ( $gap_value as $key => $value ) {
2070 $gap_value[ $key ] = $value && preg_match( '%[\\\(&=}]|/\*%', $value ) ? null : $value;
2071 }
2072 } else {
2073 $gap_value = $gap_value && preg_match( '%[\\\(&=}]|/\*%', $gap_value ) ? null : $gap_value;
2074 }
2075
2076 $fallback_gap_value = _wp_array_get( $block_type->supports, [ 'spacing', 'blockGap', '__experimentalDefault' ], '0.5em' );
2077
2078 // If a block's block.json skips serialization for spacing or spacing.blockGap, don't apply the user-defined value to the styles.
2079 $should_skip_gap_serialization = wp_should_skip_block_supports_serialization( $block_type, 'spacing', 'blockGap' );
2080 $style = wp_get_layout_style( ".$class_name", $used_layout, $has_block_gap_support, $gap_value, $should_skip_gap_serialization, $fallback_gap_value );
2081
2082 $content = preg_replace(
2083 '/' . preg_quote( 'class="', '/' ) . '/',
2084 'class="' . esc_attr( $class_name ) . ' ',
2085 $block_content,
2086 1
2087 );
2088
2089 if ( $style_only ) {
2090 return '<style>' . $style . '</style>';
2091 }
2092
2093 return $content;
2094 }
2095
2096 /**
2097 * Insert bundled product.
2098 *
2099 * @since 2.3.0
2100 * @return void
2101 */
2102 public function bundled_products() {
2103 global $wp_query;
2104 $query = $wp_query->query_vars;
2105 $option = 'default';
2106
2107 if ( ! array_key_exists( 'funnel_product_settings', $query ) ) {
2108 return;
2109 }
2110
2111 if ( ! empty( $query['funnel_product_settings'] ) ) {
2112 $option = $query['funnel_product_settings'];
2113 }
2114
2115 if ( 'default' === $option ) {
2116 return;
2117 }
2118
2119 add_action( 'sellkit_block_checkout_required_hidden_fields', function() use ( $option ) {
2120 ?>
2121 <input type="hidden" name="sellkit-bundle-products" value="<?php echo esc_attr( $option ); ?>" >
2122 <?php
2123 }, 10 );
2124
2125 $this->bundle_product_action( $option );
2126 }
2127
2128 /**
2129 * Display bundle products.
2130 *
2131 * @param string $option bundle products type.
2132 * @since 2.3.0
2133 * return void
2134 *
2135 * @SuppressWarnings(PHPMD.NPathComplexity)
2136 */
2137 public function bundle_product_action( $option ) {
2138 $fields_type = 'radio';
2139 $products = WC()->cart->get_cart();
2140 $default = null;
2141 $default_q = null;
2142 $readonly = 'readonly';
2143
2144 if ( 'allow-buyers' === $option ) {
2145 $fields_type = 'checkbox';
2146 $readonly = '';
2147 }
2148
2149 ob_start();
2150 ?>
2151 <section class="sellkit-checkout-bundled-products">
2152 <div class="sellkit-checkout-bundled-inner-wrap">
2153 <div class="sellkit-checkout-bundled-header heading"><?php echo esc_html__( 'Your Products', 'sellkit' ); ?></div>
2154 <table class="sellkit-checkout-bundled-products-table">
2155 <thead>
2156 <tr class="sellkit-checkout-bundled-products-head-row">
2157 <th class="sellkit-head-row-title"><?php echo esc_html__( 'Product', 'sellkit' ); ?></th>
2158 <th class="sellkit-head-row-quantity"><?php echo esc_html__( 'Quantity', 'sellkit' ); ?></th>
2159 <th class="sellkit-head-row-price"><?php echo esc_html__( 'Price', 'sellkit' ); ?></th>
2160 </tr>
2161 </thead>
2162 <tbody>
2163 <?php foreach ( $products as $cart_item_key => $cart_item ) : ?>
2164 <?php
2165 if ( in_array( $cart_item_key, $this->in_cart, true ) ) {
2166 continue;
2167 }
2168
2169 $_product = apply_filters( 'woocommerce_cart_item_product', $cart_item['data'], $cart_item, $cart_item_key );
2170 $default = $_product->get_id();
2171 $default_q = $cart_item['quantity'];
2172 $unique_id = 'bundle-unique-id-' . $default;
2173 $checked = '';
2174
2175 if ( 'checkbox' === $fields_type ) {
2176 $checked = 'checked';
2177 }
2178
2179 if ( 'radio' === $fields_type && array_key_last( $products ) === $cart_item_key ) {
2180 $checked = 'checked';
2181 }
2182 ?>
2183 <tr class="sellkit-checkout-bundled-products-item">
2184 <td class="sellkit-checkout-bundled-title">
2185 <input
2186 type="<?php echo esc_attr( $fields_type ); ?>"
2187 value="<?php echo esc_attr( $_product->get_id() ); ?>"
2188 name="sellkit-checkout-bundle-item"
2189 class="sellkit-checkout-bundle-item"
2190 id="<?php echo esc_attr( $unique_id ); ?>"
2191 <?php echo esc_html( $checked ); ?>
2192 >
2193 <label for="<?php echo esc_attr( $unique_id ); ?>">
2194 <?php echo esc_html( $_product->get_name() ); ?>
2195 </label>
2196 </td>
2197 <td class="sellkit-checkout-bundled-quantity">
2198 <input type="number" <?php echo esc_attr( $readonly ); ?> min="1" value="<?php echo esc_attr( $cart_item['quantity'] ); ?>" data-id="<?php echo esc_attr( $cart_item_key ); ?>" class="sellkit-checkout-single-bundle-item-quantity" >
2199 </td>
2200 <td class="sellkit-checkout-bundled-price">
2201 <?php echo apply_filters( 'woocommerce_cart_item_subtotal', WC()->cart->get_product_subtotal( $_product, $cart_item['quantity'] ), $cart_item, $cart_item_key ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
2202 </td>
2203 <tr>
2204 <?php endforeach; ?>
2205 </tbody>
2206 </table>
2207 </div>
2208 </section>
2209 <?php
2210 $bundle_html = ob_get_clean();
2211
2212 add_action( 'sellkit-bundled-products-position', function() use ( $bundle_html ) {
2213 echo $bundle_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
2214 } );
2215
2216 if ( wp_doing_ajax() ) {
2217 return;
2218 }
2219
2220 if ( 'radio' === $fields_type && null !== $default ) {
2221 WC()->cart->empty_cart();
2222 WC()->cart->add_to_cart( $default, $default_q );
2223 }
2224
2225 $this->make_changes_after_cart_item_edit( get_the_id() );
2226 }
2227
2228 /**
2229 * Rename Shipping text
2230 *
2231 * @param string $name title of shipping methods.
2232 * @return string
2233 * @since 2.3.0
2234 *
2235 * @SuppressWarnings(PHPMD.UnusedFormalParameter)
2236 */
2237 public function rename_shipping_text( $name ) {
2238 $post_id = get_the_ID();
2239 $attributes = $this->get_inner_block_attributes( 'checkout', 'checkout-cart-shipping', $post_id );
2240
2241 if ( ! isset( $attributes['shippingMethodsHeadingText'] ) ) {
2242 return $name;
2243 }
2244
2245 return '<div id="shipping_header_title" class="header heading">' . esc_html( $attributes['shippingMethodsHeadingText'] ) . '</div>';
2246 }
2247
2248 /**
2249 * Get inner block attributes.
2250 *
2251 * @param string $block_name Block name.
2252 * @param string $inner_block_name InnerBlock name.
2253 * @param string $post_id Post ID.
2254 * @since 2.3.0
2255 * return void
2256 */
2257 public function get_inner_block_attributes( $block_name, $inner_block_name, $post_id ) {
2258 if ( empty( $post_id ) ) {
2259 return;
2260 }
2261
2262 $post_content = get_post_field( 'post_content', $post_id );
2263 $blocks = parse_blocks( $post_content );
2264
2265 foreach ( $blocks as $block ) {
2266 if ( 'sellkit-blocks/' . $block_name === $block['blockName'] ) {
2267 foreach ( $block['innerBlocks'] as $inner_block ) {
2268 if ( 'sellkit-inner-blocks/' . $inner_block_name === $inner_block['blockName'] ) {
2269 return $inner_block['attrs'];
2270 }
2271 }
2272 }
2273 }
2274 }
2275
2276 /**
2277 * Assign user defined values to each field.
2278 *
2279 * @param array $default_fields : default woocommerce fields.
2280 * @param array $block_fields : fields get added by user from block option.
2281 * @param string $type : billing / shipping.
2282 * @return array
2283 * @since 2.3.0
2284 * @SuppressWarnings(PHPMD.NPathComplexity)
2285 */
2286 public static function assign_settings_per_field( $default_fields, $block_fields, $type ) {
2287 if ( empty( $block_fields ) ) {
2288 return $default_fields;
2289 }
2290
2291 $core_billing_fields = [
2292 'billing_first_name',
2293 'billing_last_name',
2294 'billing_company',
2295 'billing_address_1',
2296 'billing_address_2',
2297 'billing_city',
2298 'billing_postcode',
2299 'billing_country',
2300 'billing_state',
2301 'billing_email',
2302 'billing_phone',
2303 ];
2304
2305 // Unset unnecessary fields.
2306 $valid_roles = array_map( function( $field ) {
2307 return $field['role'];
2308 }, $block_fields );
2309
2310 foreach ( $default_fields as $key => $value ) {
2311 if ( in_array( $key, $core_billing_fields, true ) && ! in_array( $key, $valid_roles, true ) ) {
2312 unset( $default_fields[ $key ] );
2313 }
2314 }
2315
2316 // Assign user properties.
2317 foreach ( $block_fields as $key => $field ) {
2318 $default_fields[ $field['role'] ]['label'] = $field['label'] . ( ! $field['required'] ? ' ' . esc_html__( '(optional)', 'sellkit' ) : '' );
2319 $default_fields[ $field['role'] ]['class'] = explode( ' ', $field['customClass'] );
2320 $default_fields[ $field['role'] ]['class'][] = $field['width'];
2321 $default_fields[ $field['role'] ]['class'][] = 'sellkit-widget-checkout-fields';
2322 $default_fields[ $field['role'] ]['required'] = ( 'yes' === $field['required'] ) ? true : false;
2323 $default_fields[ $field['role'] ]['priority'] = ( $key + 1 ) * 10;
2324 $default_fields[ $field['role'] ]['local'] = true;
2325
2326 if ( $field['role'] === $type . '_phone' ) {
2327 $default_fields[ $field['role'] ]['type'] = 'tel';
2328 }
2329 }
2330
2331 foreach ( $default_fields as $key => $details ) {
2332 if ( ! array_key_exists( 'local', $details ) || false === $details['local'] ) {
2333
2334 $default_fields[ $key ]['priority'] = 500;
2335 }
2336 }
2337
2338 return $default_fields;
2339 }
2340
2341 /**
2342 * Register fields.
2343 *
2344 * @since 2.3.0
2345 */
2346 public static function register_fields() {
2347 sellkit()->load_files( [
2348 'block-editor/blocks/checkout/form-fields/base',
2349 'block-editor/blocks/checkout/form-fields/select',
2350 'block-editor/blocks/checkout/form-fields/text',
2351 'block-editor/blocks/checkout/form-fields/tel',
2352 'block-editor/blocks/checkout/form-fields/hidden',
2353 ] );
2354 }
2355
2356 /**
2357 * Load accept-reject-button block on frontend.
2358 *
2359 * @since 2.3.0
2360 */
2361 public function load_accept_reject_button_frontend() {
2362 global $post;
2363
2364 if ( empty( $post->post_content ) ) {
2365 return;
2366 }
2367
2368 $this->post_id = $post->ID;
2369
2370 $block = 'blocks/accept-reject-button';
2371
2372 $block_data = explode( '/', $block );
2373 $block_name = $block_data[1];
2374
2375 $class_name = str_replace( '-', ' ', $block_name );
2376 $class_name = str_replace( ' ', '_', ucwords( $class_name ) );
2377 $class_name = "Sellkit\blocks\Render\\{$class_name}";
2378 $class_path = 'block-editor/' . $block . '/index';
2379
2380 sellkit()->load_files( [
2381 $class_path,
2382 ] );
2383
2384 $new_class = new $class_name( $this->post_id );
2385 $new_class->register_block_meta();
2386
2387 $this->is_accept_reject_button_registered = true;
2388 }
2389
2390 /**
2391 * Render shipping methods.
2392 *
2393 * @since 2.6.0
2394 */
2395 private function shipping_methods() {
2396 $packages = WC()->shipping()->get_packages();
2397
2398 ob_start();
2399
2400 foreach ( $packages as $index => $package ) {
2401 $chosen_method = isset( WC()->session->chosen_shipping_methods[ $index ] )
2402 ? WC()->session->chosen_shipping_methods[ $index ]
2403 : '';
2404
2405 $chosen_method_2 = apply_filters( 'sellkit-block-shipping-methods-choosen-method', $chosen_method );
2406
2407 $package_name = apply_filters(
2408 'woocommerce_shipping_package_name',
2409 ( ( $index + 1 ) > 1 )
2410 // Translators: %d is the shipping package number.
2411 ? sprintf( _x( 'Shipping %d', 'shipping packages', 'sellkit' ), $index + 1 )
2412 : _x( 'Shipping', 'shipping packages', 'sellkit' ),
2413 $index,
2414 $package
2415 );
2416
2417 $available_methods = $package['rates'];
2418
2419 if ( $available_methods ) :
2420 ?>
2421 <table id="shipping_method" class="woocommerce-shipping-methods sellkit-shipping-methods-one-page sellkit-checkout-widget-divider">
2422 <?php foreach ( $available_methods as $method ) : ?>
2423 <tr class="sellkit-checkout-widget-divider">
2424 <td class="sellkit-shipping-method-t1 sellkit-checkout-widget-divider">
2425 <label class="wrp">
2426 <?php if ( count( $available_methods ) > 1 ) : ?>
2427 <input
2428 type="radio"
2429 name="shipping_method[<?php echo esc_attr( $index ); ?>]"
2430 data-index="<?php echo esc_attr( $index ); ?>"
2431 id="shipping_method_<?php echo esc_attr( $index ); ?>_<?php echo esc_attr( sanitize_title( $method->id ) ); ?>"
2432 value="<?php echo esc_attr( $method->id ); ?>"
2433 class="shipping_method"
2434 <?php checked( $method->id, $chosen_method ); ?>
2435 />
2436 <?php else : ?>
2437 <input
2438 type="radio"
2439 name="shipping_method[<?php echo esc_attr( $index ); ?>]"
2440 data-index="<?php echo esc_attr( $index ); ?>"
2441 id="shipping_method_<?php echo esc_attr( $index ); ?>_<?php echo esc_attr( sanitize_title( $method->id ) ); ?>"
2442 value="<?php echo esc_attr( $method->id ); ?>"
2443 class="shipping_method"
2444 />
2445 <?php endif; ?>
2446 <span class="checkmark"></span>
2447 <span class="labels"><?php echo esc_html( $method->label ); ?></span>
2448 </label>
2449 </td>
2450 <td class="sellkit-shipping-method-t3 sellkit-checkout-widget-divider">
2451 <?php
2452 add_filter( 'woocommerce_cart_shipping_method_full_label', array( $this, 'cart_shipping_method_full_label' ), 999, 2 );
2453 echo wc_cart_totals_shipping_method_label( $method ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
2454 ?>
2455 </td>
2456 </tr>
2457 <?php endforeach; ?>
2458 </table>
2459 <?php if ( $chosen_method !== $chosen_method_2 || ( is_array( $available_methods ) && 1 === count( $available_methods ) ) ) : ?>
2460 <script>
2461 jQuery( document ).ready( function() {
2462 jQuery( "input[value='<?php echo esc_js( $chosen_method_2 ); ?>']" ).prop( 'checked', true ).trigger( 'change' );
2463 } );
2464 </script>
2465 <?php endif; ?>
2466 <?php
2467 endif;
2468 }
2469 return ob_get_clean();
2470 }
2471
2472 /**
2473 * Filter shipping method label to include tax if applicable.
2474 *
2475 * @param string $label label of shipping method.
2476 * @param object $method shipping method.
2477 *
2478 * @since 2.6.0
2479 * @SuppressWarnings(PHPMD.UnusedFormalParameter)
2480 */
2481 public function cart_shipping_method_full_label( $label, $method ) {
2482 if ( WC()->cart->display_prices_including_tax() ) {
2483 return wc_price( $method->cost + $method->get_shipping_tax() );
2484 }
2485
2486 return wc_price( $method->cost );
2487 }
2488 }
2489