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 / block-editor / blocks / checkout / helper.php

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

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