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 / elementor / modules / checkout / classes / global-hooks.php

global-hooks.php in SellKit – Funnel builder and checkout optimizer for WooCommerce to sell more, faster 2.8.0, at includes/elementor/modules/checkout/classes/global-hooks.php

1,639 lines 49.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Sellkit\Elementor\Modules\Checkout\Classes;
4
5 defined( 'ABSPATH' ) || exit;
6
7 use Sellkit\Elementor\Modules\Checkout\Classes\{ Local_Hooks, Helper };
8 use Sellkit\Funnel\Analytics\Data_Updater;
9 use Sellkit\Funnel\Contacts\Base_Contacts;
10 use Sellkit\Funnel\Steps\Checkout;
11 use Sellkit_Funnel;
12
13 /**
14 * Global hooks.
15 *
16 * @SuppressWarnings(PHPMD.UnusedFormalParameter)
17 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
18 * @SuppressWarnings(PHPMD.TooManyMethods)
19 * @SuppressWarnings(PHPMD.NPathComplexity)
20 * @SuppressWarnings(PHPMD.ExcessiveClassLength)
21 * @since 1.1.0
22 */
23 class Global_Hooks {
24 /**
25 * Helper id during checkout process.
26 *
27 * @var int
28 */
29 private $helper_id = 0;
30
31 /**
32 * Create instance of class without construct.
33 *
34 * @since 1.1.0
35 * @return object
36 */
37 public static function instance() {
38 $class = new \ReflectionClass( __CLASS__ );
39 return $class->newInstanceWithoutConstructor();
40 }
41
42 /**
43 * Class construct.
44 * required actions that affects woocommerce global checkout shortcode / page.
45 *
46 * @since 1.1.0
47 */
48 public function __construct() {
49 // Create empty shortcode to simulate our widget page as checkout page.
50 add_shortcode( 'sellkit_checkout_widget_simulated', function() {
51 return '';
52 } );
53
54 add_action( 'wp_ajax_sellkit_checkout_ajax_handler', [ $this, 'ajax_handler' ] );
55 add_action( 'wp_ajax_nopriv_sellkit_checkout_ajax_handler', [ $this, 'ajax_handler' ] );
56
57 // Add shipping total to review order by custom hook.
58 add_action( 'sellkit-checkout-widget-display-shipping-price', [ $this, 'add_shipping_total_to_review_order' ] );
59
60 // Modify checkout order-review item.
61 add_action( 'sellkit-one-page-checkout-custom-order-item', [ $this, 'modify_checkout_order_item' ], 1, 4 );
62
63 // Modify sent data before processing & validate order.
64 add_filter( 'woocommerce_checkout_posted_data', [ $this, 'modify_order_data_before_validate' ], 10, 1 );
65
66 // Validate user defined fields.
67 add_action( 'woocommerce_checkout_process', [ $this, 'validate_user_defined_fields' ] );
68
69 // Save user defined fields.
70 add_action( 'woocommerce_checkout_update_order_meta', [ $this, 'save_user_defined_fields' ] );
71
72 // Display user defined fields in order edit page.
73 add_action( 'woocommerce_admin_order_data_after_billing_address', [ $this, 'add_item_to_order_details' ], 10, 1 );
74 add_action( 'woocommerce_admin_order_data_after_shipping_address', [ $this, 'add_item_to_order_details' ], 10, 1 );
75
76 // Add custom shipping field to email.
77 add_filter( 'woocommerce_order_get_formatted_shipping_address', [ $this, 'attach_user_shipping_fields_to_order_email' ], 10, 3 );
78
79 // Add custom billing field to email.
80 add_filter( 'woocommerce_order_get_formatted_billing_address', [ $this, 'attach_user_billing_fields_to_order_email' ], 10, 3 );
81
82 // Filter checkout ajax fragment.
83 add_filter( 'woocommerce_update_order_review_fragments', [ $this, 'checkout_fragment' ] );
84
85 // Simulate checkout page for our widget.
86 add_action( 'wp', [ $this, 'simulate_our_widget_page_as_checkout' ] );
87
88 // Fix shipping method price change on ajax.
89 // Manage bump order discounts.
90 // Template replacement.
91 add_action( 'woocommerce_checkout_update_order_review', [ $this, 'sellkit_checkout_during_ajax' ], 10, 1 );
92
93 add_filter( 'sellkit-shipping-methods-choosen-method', [ $this, 'sellkit_default_shipping_method' ], 10, 1 );
94
95 // Modify discounted products.
96 add_action( 'woocommerce_before_calculate_totals', [ $this, 'before_cart_calculate' ], 999 );
97
98 // Allow viewing of Checkout page in the Editor with an empty cart.
99 $action = isset( $_REQUEST['action'] ) ? filter_var( $_REQUEST['action'], FILTER_SANITIZE_FULL_SPECIAL_CHARS ) : ''; // phpcs:ignore
100 $elementor_preview = isset( $_REQUEST['elementor-preview'] ) ? filter_var( $_REQUEST['elementor-preview'], FILTER_SANITIZE_FULL_SPECIAL_CHARS ) : ''; // phpcs:ignore
101
102 if (
103 ( 'elementor' === $action && is_admin() ) // Elementor Editor.
104 || 'elementor_ajax' === $action // Elementor Editor Preview - Ajax Render Widget.
105 || ! empty( $elementor_preview ) // Elementor Editor Preview.
106 ) {
107 add_filter( 'woocommerce_checkout_redirect_empty_cart', '__return_false', 5 );
108 }
109 }
110
111 /**
112 * Handle ajax calls.
113 *
114 * @since 1.1.0
115 * @return void
116 */
117 public function ajax_handler() {
118 check_ajax_referer( 'sellkit_elementor', 'nonce' );
119
120 $sub_action = filter_input( INPUT_POST, 'sub_action', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
121
122 if ( method_exists( $this, $sub_action ) ) {
123 call_user_func( [ $this, $sub_action ] );
124 return;
125 }
126
127 wp_send_json_error();
128 }
129
130 /**
131 * Modify checkout ajax response HTML.
132 *
133 * @param array $response woocommerce checkout page ajax response.
134 * @return array
135 * @since 1.1.0
136 */
137 public function checkout_fragment( $response ) {
138 // Get posted data and identify if it's been sent by jupiter widget or not.
139 $checkout_form_data = filter_input( INPUT_POST, 'post_data', FILTER_DEFAULT );
140 $checkout_form_data = explode( '&', $checkout_form_data );
141 $posted_data = [];
142
143 foreach ( $checkout_form_data as $input ) {
144 $item = explode( '=', urldecode( $input ) );
145
146 $posted_data[ trim( $item[0] ) ] = $item[1];
147 }
148
149 // Identify if it's been sent by jupiter checkout widget or not.
150 if ( ! array_key_exists( 'form_id', $posted_data ) && ! array_key_exists( 'post_id', $posted_data ) ) {
151 // Default woocommerce checkout.
152 return $response;
153 }
154
155 // Get widget settings.
156 $widget_settings = Helper::instance()->retrieve_checkout_widget_settings( $posted_data['post_id'], $posted_data['form_id'] );
157
158 // Order review fragment.
159 ob_start();
160 woocommerce_order_review();
161 $order_review = ob_get_clean();
162
163 // Payment fragment.
164 ob_start();
165 woocommerce_checkout_payment();
166 $payment = ob_get_clean();
167
168 // Shipping method fragment.
169 ob_start();
170 echo '<section class="sellkit-one-page-shipping-methods">';
171 wc_cart_totals_shipping_html();
172 echo '</section>';
173 $shipping_method = ob_get_clean();
174
175 $response['.woocommerce-checkout-review-order-table'] = $order_review;
176 $response['.woocommerce-checkout-payment'] = $payment;
177 $response['.sellkit-one-page-shipping-methods'] = '';
178
179 if ( ! array_key_exists( 'show_shipping_method', $widget_settings ) ) {
180 $response['.sellkit-one-page-shipping-methods'] = $shipping_method;
181 }
182
183 return $response;
184 }
185
186 /**
187 * Apply coupon code using ajax.
188 *
189 * @return void
190 * @since 1.1.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 1.1.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 self::make_changes_after_cart_item_edit( $funnel_id );
216
217 if ( 'add' === $action ) {
218 WC()->cart->add_to_cart( $id, $qty );
219 self::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 self::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 self::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 self::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 1.2.5
262 * @param int $funnel_id funnel step id.
263 */
264 public static 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 $auto_apply_coupon_cookie_value = null;
277
278 if ( isset( $_COOKIE['sellkit_rejected_coupon'] ) ) {
279 // Get the value of the cookie.
280 $auto_apply_coupon_cookie_value = sanitize_text_field( wp_unslash( $_COOKIE['sellkit_rejected_coupon'] ) );
281 $auto_apply_coupon_cookie_value = explode( ',', $auto_apply_coupon_cookie_value );
282 }
283
284 // Help to apply funnel discount prices and coupons when changing product quantity.
285 self::apply_discounted_prices( wc()->cart, $funnel_id );
286 self::apply_discounted_prices( wc()->cart, $funnel_id, 'bumps' );
287
288 if ( Checkout::apply_coupon_validation( $optimization_data ) ) {
289 WC()->cart->remove_coupons();
290
291 foreach ( $optimization_data['auto_apply_coupons'] as $auto_apply_coupon ) {
292 if ( ! empty( $auto_apply_coupon_cookie_value ) && in_array( $auto_apply_coupon['label'], $auto_apply_coupon_cookie_value, true ) ) {
293 continue;
294 }
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 1.1.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 wp_kses_post( wc_price( $price ) );
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 1.1.0
336 */
337 public function modify_checkout_order_item( $row, $product, $cart_item, $cart_item_key ) {
338 $cart_items = WC()->cart->get_cart();
339
340 //phpcs:disable
341 ob_start();
342 ?>
343 <div style="display: inline-block" class="sellkit-checkout-widget-item-image">
344 <?php echo $product->get_image(); ?>
345 </div>
346 <?php
347 $img_html = ob_get_clean();
348 $img_html = apply_filters( 'sellkit/includes/elementor/modules/checkout/product-image', $img_html, $product, $cart_item_key );
349
350 ob_start();
351
352 $extra_class = $product->get_type();
353 if ( $cart_items[ $cart_item_key ] === end( $cart_items ) ) {
354 $extra_class .= ' last-cart-item';
355 }
356
357 if ( 'variation' === $product->get_type() ) {
358 add_filter( 'woocommerce_cart_item_name', [ $this, 'modify_variation_title' ], 50, 2 );
359 add_filter( 'woocommerce_get_item_data', [ $this, 'modify_variation_items' ], 10, 2 );
360 }
361 ?>
362 <tr class="<?php echo esc_attr( apply_filters( 'woocommerce_cart_item_class', 'cart_item', $cart_item, $cart_item_key ) ) . ' ' . esc_attr( $extra_class ); ?>">
363
364 <td class="product-name sellkit-one-page-checkout-product-name">
365 <?php
366 echo $img_html;
367
368 $fix_style = '';
369
370 if ( '' === $img_html ) {
371 $fix_style = 'margin-left: 0px;';
372 }
373 ?>
374
375 <div class="name-price" style="<?php echo esc_attr( $fix_style ); ?>">
376 <span style="display:inline-block">
377 <?php echo wp_kses_post( apply_filters( 'woocommerce_cart_item_name', $product->get_name(), $cart_item, $cart_item_key ) ) . '&nbsp;'; ?>
378 </span>
379 <span class="sellkit-checkout-variations" id="sellkit-checkout-variations">
380 <?php echo wc_get_formatted_cart_item_data( $cart_item ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
381 </span>
382 <?php echo apply_filters( 'sellkit-checkout-widget-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'] ); ?>
383 <?php do_action( 'sellkit-checkout-editor-mode-quantity', $cart_item['quantity'] ); ?>
384 </div>
385 </td>
386 <td class="product-total sellkit-one-page-checkout-product-price">
387 <?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 ?>
388 </td>
389 </tr>
390 <?php
391 $cart_item = ob_get_clean();
392
393 echo apply_filters( 'sellkit-checkout-cart-item' , $cart_item );
394 //phpcs:enable
395
396 if ( 'variation' === $product->get_type() ) {
397 remove_filter( 'woocommerce_cart_item_name', [ $this, 'modify_variation_title' ], 50 );
398 remove_filter( 'woocommerce_get_item_data', [ $this, 'modify_variation_items' ], 10 );
399 }
400 }
401
402 /**
403 * Remove variation attributes from variation product title.
404 *
405 * @param string $default default value.
406 * @param array $cart_item cart item data.
407 * @since 1.6.8
408 */
409 public function modify_variation_title( $default, $cart_item ) {
410 $product = $cart_item['product_id'];
411
412 return get_the_title( $product );
413 }
414
415 /**
416 * Display variation attributes separated from title even if those are less than 3.
417 *
418 * @param array $item_data attributes array.
419 * @param array $cart_item cart item data.
420 * @since 1.6.8
421 */
422 public function modify_variation_items( $item_data, $cart_item ) {
423 $product = new \WC_Product_Variation( $cart_item['variation_id'] );
424 $attributes = $product->get_attributes();
425
426 if ( count( $attributes ) > 2 ) {
427 return $item_data;
428 }
429
430 foreach ( $attributes as $key => $value ) {
431 $key = str_replace( 'pa_', '', $key );
432 $key = str_replace( '-', '', $key );
433 $key = str_replace( '_', '', $key );
434
435 $item_data[] = [
436 'key' => $key,
437 'value' => $value,
438 ];
439 }
440
441 return $item_data;
442 }
443
444 /**
445 * Modify fields after pressing place order button.
446 * we hooked here because some removed fields still get checked and have not removed by previous hooks.
447 *
448 * @SuppressWarnings(PHPMD.NPathComplexity)
449 * @param array $data checkout form data.
450 * @return array $data data of woocommerce checkout form.
451 * @since 1.1.0
452 */
453 public function modify_order_data_before_validate( $data ) {
454 $post_id = filter_input( INPUT_POST, 'post_id', FILTER_SANITIZE_NUMBER_INT );
455 $form_id = filter_input( INPUT_POST, 'form_id', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
456
457 if ( empty( $post_id ) && empty( $form_id ) ) {
458 return $data;
459 }
460
461 $settings = Helper::instance()->retrieve_checkout_widget_settings( $post_id, $form_id );
462
463 if ( empty( $settings ) ) {
464 return $data;
465 }
466
467 $widget_shipping_field = Helper::instance()->get_user_defined_fields_slug( $settings['shipping_list'], 'shipping_list_field' );
468 $widget_billing_field = Helper::instance()->get_user_defined_fields_slug( $settings['billing_list'], 'billing_list_field' );
469
470 $default_shipping_fields = Helper::instance()->shipping_fields();
471 $default_billing_fields = Helper::instance()->billing_fields();
472
473 // Unset removed shipping fields.
474 foreach ( $default_shipping_fields as $field => $details ) {
475 if ( ! in_array( $field, $widget_shipping_field, true ) ) {
476 unset( $data[ $field ] );
477 }
478 }
479
480 // Unset removed billing fields.
481 foreach ( $default_billing_fields as $field => $details ) {
482 if ( ! in_array( $field, $widget_billing_field, true ) && 'billing_email' !== $field ) {
483 unset( $data[ $field ] );
484 }
485 }
486
487 $mail = filter_input( INPUT_POST, 'billing_email', FILTER_SANITIZE_EMAIL );
488
489 if ( ! array_key_exists( 'billing_email', $data ) && ! empty( $mail ) ) {
490 $data['billing_email'] = $mail;
491 }
492
493 return $data;
494 }
495
496 /**
497 * Login user by ajax.
498 * sign user in by using form included in widget.
499 *
500 * @return void
501 * @since 1.1.0
502 */
503 public function auth_user() {
504 $email = filter_input( INPUT_POST, 'email', FILTER_SANITIZE_EMAIL );
505 $pass = filter_input( INPUT_POST, 'pass', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
506 $verify = filter_var( $email, FILTER_VALIDATE_EMAIL );
507
508 if ( ! $verify || empty( $email ) || empty( $pass ) ) {
509 wp_send_json_error( __( 'Email or password field is not valid.', 'sellkit' ) );
510 }
511
512 $result = wp_authenticate( $email, $pass );
513
514 if ( is_wp_error( $result ) ) {
515 wp_send_json_error( $result->get_error_message() );
516 }
517
518 wp_clear_auth_cookie();
519 wp_set_current_user( $result->ID );
520 wp_set_auth_cookie( $result->ID );
521
522 wp_send_json_success( $result );
523 }
524
525 /**
526 * Get widget settings in ajax calls using form and post id.
527 *
528 * @return void
529 * @since 1.1.0
530 */
531 private function widget_settings() {
532 $form_id = filter_input( INPUT_POST, 'form_id', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
533 $post_id = filter_input( INPUT_POST, 'post_id', FILTER_SANITIZE_NUMBER_INT );
534
535 if ( empty( $form_id ) && empty( $post_id ) ) {
536 return;
537 }
538
539 $widget_settings = Helper::instance()->retrieve_checkout_widget_settings( $post_id, $form_id );
540
541 return $widget_settings;
542 }
543
544 /**
545 * Validate user defined custom value.
546 *
547 * @return void
548 * @since 1.1.0
549 */
550 public function validate_user_defined_fields() {
551 $widget_settings = $this->widget_settings();
552
553 if ( empty( $widget_settings ) ) {
554 return;
555 }
556
557 $widget_shipping_fields = $widget_settings['shipping_list'];
558 $widget_billing_fields = $widget_settings['billing_list'];
559
560 Helper::instance()->validate_user_defined_fields( $widget_shipping_fields, $widget_billing_fields );
561 Helper::instance()->make_sure_to_convert_required_field_to_optional( $widget_shipping_fields, $widget_billing_fields );
562 }
563
564 /**
565 * Save user defined custom fields value to database.
566 *
567 * @param int $order_id id of woocommerce order.
568 * @return void
569 * @since 1.1.0
570 */
571 public function save_user_defined_fields( $order_id ) {
572 $widget_settings = $this->widget_settings();
573
574 if ( empty( $widget_settings ) ) {
575 return;
576 }
577
578 $widget_shipping_fields = $widget_settings['shipping_list'];
579 $widget_billing_fields = $widget_settings['billing_list'];
580
581 Helper::instance()->save_user_defined_fields( $widget_shipping_fields, $widget_billing_fields, $order_id );
582 }
583
584 /**
585 * Attach user defined shipping field values to order emails.
586 *
587 * @param string $address address.
588 * @param string $raw_address raw address.
589 * @param object $order woocommerce order object.
590 * @return string
591 * @since 1.1.0
592 */
593 public function attach_user_shipping_fields_to_order_email( $address, $raw_address, $order ) {
594 $order_id = $order->get_id();
595 $custom_fields = get_post_meta( $order_id, 'sellkit_checkout_widget_custom_field_of_order', true );
596
597 if ( empty( $custom_fields ) || ! is_array( $custom_fields ) ) {
598 return $address;
599 }
600
601 foreach ( $custom_fields as $field ) {
602 if ( 'yes' !== $field['show_in_email'] ) {
603 continue;
604 }
605
606 $key = $field['key_name'];
607 $value = get_post_meta( $order_id, $key, true );
608
609 if ( strpos( $key, 'shipping' ) !== false ) {
610 $address .= '<br>' . $value;
611 }
612 }
613
614 return $address;
615 }
616
617 /**
618 * Attach user defined billing field values to order emails.
619 *
620 * @param string $address address.
621 * @param string $raw_address raw address.
622 * @param object $order woocommerce order object.
623 * @return string
624 * @since 1.1.0
625 */
626 public function attach_user_billing_fields_to_order_email( $address, $raw_address, $order ) {
627 $order_id = $order->get_id();
628 $custom_fields = get_post_meta( $order_id, 'sellkit_checkout_widget_custom_field_of_order', true );
629
630 if ( empty( $custom_fields ) || ! is_array( $custom_fields ) ) {
631 return $address;
632 }
633
634 foreach ( $custom_fields as $field ) {
635 if ( 'yes' !== $field['show_in_email'] ) {
636 continue;
637 }
638
639 $key = $field['key_name'];
640 $value = get_post_meta( $order_id, $key, true );
641
642 if ( strpos( $key, 'billing' ) !== false ) {
643 $address .= '<br>' . $value;
644 }
645 }
646
647 return $address;
648 }
649
650 /**
651 * Place custom coupon form in checkout order-review based design.
652 * We have used same form in Local_Hooks. but this one is for ajax response.
653 *
654 * @see sellkit_Core\Raven\Modules\Checkout\Classes\Local_Hooks::coupon_form.
655 * @param array $settings widget settings.
656 * @return void
657 * @since 1.1.0
658 */
659 public function coupon_form( $settings ) {
660 if ( array_key_exists( 'show_coupon_field', $settings ) ) {
661 return;
662 }
663
664 echo '<tr class="coupon-form border-none"><td colspan="2">';
665 Local_Hooks::form( $settings );
666 echo '</td></tr>';
667
668 do_action( 'sellkit-checkout-after-coupon-form-ajax' );
669 }
670
671 /**
672 * Look for an email if exists or not.
673 * Is used for login process.
674 *
675 * @return void
676 * @since 1.1.0
677 */
678 public function search_for_email() {
679 $email = filter_input( INPUT_POST, 'email', FILTER_SANITIZE_EMAIL );
680 $valid = filter_var( $email, FILTER_VALIDATE_EMAIL );
681
682 if ( ! $valid || empty( $email ) ) {
683 wp_send_json_error();
684 }
685
686 $check = email_exists( $email );
687
688 if ( false === $check ) {
689 wp_send_json_error();
690 }
691
692 wp_send_json_success();
693 }
694
695 /**
696 * Look for an username if exists or not.
697 * Is used for register process.
698 *
699 * @return void
700 * @since 1.1.0
701 */
702 public function search_for_username() {
703 $username = filter_input( INPUT_POST, 'user', FILTER_SANITIZE_EMAIL );
704
705 if ( empty( $username ) ) {
706 wp_send_json_error();
707 }
708
709 $username = sanitize_user( $username );
710 $check = username_exists( $username );
711
712 if ( false !== $check ) {
713 wp_send_json_error();
714 }
715
716 wp_send_json_success();
717 }
718
719 /**
720 * Validate postcode via ajax.
721 *
722 * @since 1.1.0
723 * @return void
724 */
725 public function validate_postcode() {
726 $country = filter_input( INPUT_POST, 'country_code', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
727 $postcode = filter_input( INPUT_POST, 'post_code', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
728 $parent = filter_input( INPUT_POST, 'parent', FILTER_DEFAULT );
729
730 $postcode = wc_format_postcode( $postcode, $country );
731 $valid = \WC_Validation::is_postcode( $postcode, $country );
732
733 if ( ! $valid ) {
734 wp_send_json_error( $parent );
735 }
736
737 wp_send_json_success( $parent );
738 }
739
740 /**
741 * Validate phone number via ajax.
742 *
743 * @since 1.1.0
744 * @return void
745 */
746 public function validate_phone_number() {
747 $phone = filter_input( INPUT_POST, 'phone_number', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
748 $parent = filter_input( INPUT_POST, 'parent', FILTER_DEFAULT );
749
750 $valid = \WC_Validation::is_phone( $phone );
751
752 if ( ! $valid ) {
753 wp_send_json_error( $parent );
754 }
755
756 wp_send_json_success( $parent );
757 }
758
759 /**
760 * Simulate checkout page where our widget is present. using empty shortcode [sellkit_checkout_widget_simulated].
761 *
762 * @since 1.1.0
763 * @return void
764 */
765 public function simulate_our_widget_page_as_checkout() {
766 $page_id = get_the_ID();
767 $content = get_post_field( 'post_content', $page_id );
768
769 if ( false === strpos( $content, 'woocommerce_checkout' ) ) {
770 return;
771 }
772
773 add_filter( 'woocommerce_is_checkout', function() {
774 return true;
775 } );
776
777 if ( false === strpos( $content, 'sellkit_checkout_widget_simulated' ) ) {
778 return;
779 }
780
781 add_filter( 'theme_mod_jupiterx_jupiterx_checkout_cart_elements', function() {
782 return [];
783 }, 10 );
784 }
785
786 /**
787 * Integrate user country with our widget.
788 *
789 * @since 1.1.0
790 * @return void
791 */
792 public function set_customer_details_ajax() {
793 $country = filter_input( INPUT_POST, 'country', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
794 $state = filter_input( INPUT_POST, 'state', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
795 $shipping_country = filter_input( INPUT_POST, 'shipping_country', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
796 $shipping_state = filter_input( INPUT_POST, 'shipping_state', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
797
798 if ( ! empty( $country ) ) {
799 setcookie( 'sellkit-checkout-billing-cc', $country, time() + ( 86400 * 3 ), '/' );
800 setcookie( 'sellkit-checkout-billing-state', $state, time() + ( 86400 * 3 ), '/' );
801 }
802
803 if ( ! empty( $shipping_country ) ) {
804 setcookie( 'sellkit-checkout-shipping-cc', $shipping_country, time() + ( 86400 * 3 ), '/' );
805 setcookie( 'sellkit-checkout-shipping-state', $shipping_state, time() + ( 86400 * 3 ), '/' );
806 }
807
808 wp_send_json_success();
809 }
810
811 /**
812 * State lookup using postcode and country.
813 *
814 * @since 1.1.0
815 * @return void
816 */
817 public function sellkit_state_lookup_by_postcode() {
818 $country = filter_input( INPUT_POST, 'country_value', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
819 $postcode = filter_input( INPUT_POST, 'postcode_value', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
820 $api = 'https://api.zippopotam.us/' . $country . '/' . $postcode;
821
822 if ( empty( $country ) || empty( $postcode ) ) {
823 wp_send_json_error( esc_html__( 'Not valid inputs', 'sellkit' ) );
824 }
825
826 $response = wp_remote_get( $api );
827
828 if ( is_wp_error( $response ) || ! is_array( $response ) ) {
829 wp_send_json_error( esc_html__( 'Server error.', 'sellkit' ) );
830 }
831
832 if ( '{}' === $response['body'] ) {
833 // Country or postcode is not supported. it's kina an error but we show nothing in frontend.
834 wp_send_json_error( '' );
835 }
836
837 $body = json_decode( $response['body'], true );
838
839 wp_send_json_success( $body );
840 }
841
842 /**
843 * Modify cart contents using bundled products.
844 *
845 * @since 1.1.0
846 * @return void
847 * @SuppressWarnings(PHPMD.CyclomaticComplexity)
848 */
849 public function sellkit_checkout_modify_cart_by_bundle_products() {
850 $qty = filter_input( INPUT_POST, 'qty', FILTER_SANITIZE_NUMBER_INT );
851 $id = filter_input( INPUT_POST, 'id', FILTER_SANITIZE_NUMBER_INT );
852 $type = filter_input( INPUT_POST, 'type', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
853 $checkout_id = filter_input( INPUT_POST, 'checkout_id', FILTER_SANITIZE_NUMBER_INT );
854 $modify = filter_input( INPUT_POST, 'modify', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
855 $key = filter_input( INPUT_POST, 'key', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
856
857 if ( ! empty( $key ) ) {
858 foreach ( WC()->cart->get_cart() as $item_key => $item ) {
859 if ( $item_key === $key ) {
860 WC()->cart->set_quantity( $item_key, $qty, true );
861 break; // stop the loop.
862 }
863 }
864
865 wp_send_json_success();
866 }
867
868 if ( 'radio' === $type ) {
869 $step_data = get_post_meta( $checkout_id, 'step_data', true );
870 $products = $step_data['data']['products']['list'];
871 $reset_cart = isset( $step_data['data']['products']['reset_cart'] ) ? $step_data['data']['products']['reset_cart'] : 'true';
872
873 if ( 'true' === $reset_cart ) {
874 foreach ( WC()->cart->get_cart() as $item ) {
875 if ( empty( $item['product_id'] ) ) {
876 continue;
877 }
878
879 if ( array_key_exists( $item['product_id'], $products ) ) {
880 continue;
881 }
882
883 $products[ $item['product_id'] ] = [
884 'quantity' => $item['quantity'],
885 'discount' => '',
886 'discountType' => 'fixed',
887 ];
888 }
889 }
890
891 $selected = ! empty( $products[ $id ] ) ? $products[ $id ] : [];
892 $real_quantity = ( empty( $selected['quantity'] ) ) ? 1 : $selected['quantity'];
893
894 if ( (int) $real_quantity !== (int) $qty ) {
895 wp_send_json_error( esc_html__( 'Oh do not be tricky.', 'sellkit' ) );
896 }
897 }
898
899 if ( 'radio' === $type ) {
900 WC()->cart->empty_cart();
901 WC()->cart->add_to_cart( $id, $qty );
902 }
903
904 if ( 'checkbox' === $type && 'add' === $modify ) {
905 WC()->cart->add_to_cart( $id, $qty );
906 }
907
908 if ( 'checkbox' === $type && 'remove' === $modify ) {
909 $post_type = get_post_type( $id );
910
911 // Maybe product is a variation.
912 if ( 'product_variation' === $post_type ) {
913 foreach ( WC()->cart->get_cart() as $item_key => $item ) {
914 if ( $item['variation_id'] === (int) $id ) {
915 WC()->cart->remove_cart_item( $item_key ); // we remove it.
916 break; // stop the loop.
917 }
918 }
919
920 self::make_changes_after_cart_item_edit( $checkout_id );
921
922 wp_send_json_success();
923 }
924
925 // Default product.
926 $product_cart_id = WC()->cart->generate_cart_id( $id );
927 $cart_item_key = WC()->cart->find_product_in_cart( $product_cart_id );
928
929 if ( $cart_item_key ) {
930 WC()->cart->remove_cart_item( $cart_item_key );
931 }
932 }
933
934 self::make_changes_after_cart_item_edit( $checkout_id );
935
936 wp_send_json_success();
937 }
938
939 /**
940 * Apply funnel discount.
941 * Will be called twice, first when page is loading , second during checkout ajax.
942 *
943 * @param object $cart cart object.
944 * @param int $checkout_id_ajax checkout id.
945 * @param string $source source of products, bump or default products to be checked.
946 * @param array $upsell_data upsell product data coming from popup.
947 * @since 1.2.8
948 * @SuppressWarnings(PHPMD.CyclomaticComplexity)
949 */
950 public static function apply_discounted_prices( $cart = [], $checkout_id_ajax = null, $source = 'default', $upsell_data = [] ) {
951 $checkout_id = get_queried_object_id();
952
953 if ( wp_doing_ajax() ) {
954 $checkout_id = $checkout_id_ajax;
955 }
956
957 if ( empty( $checkout_id ) ) {
958 return;
959 }
960
961 $step_data = get_post_meta( $checkout_id, 'step_data', true );
962
963 if ( empty( $step_data ) ) {
964 return;
965 }
966
967 $products = [];
968
969 if (
970 'default' === $source &&
971 array_key_exists( 'data', $step_data ) &&
972 ( ! empty( $step_data['data']['products'] ) && array_key_exists( 'list', $step_data['data']['products'] ) )
973 ) {
974 $products = $step_data['data']['products']['list'];
975 }
976
977 if ( 'bumps' === $source && ! empty( $step_data['bump'] ) ) {
978 $bumps = $step_data['bump'];
979
980 foreach ( $bumps as $bump ) {
981 if ( ! array_key_exists( 'products', $bump['data'] ) ) {
982 continue;
983 }
984
985 $key = ! empty( $bump['data']['products']['list'] ) ? array_key_first( $bump['data']['products']['list'] ) : null;
986
987 if ( empty( $key ) ) {
988 continue;
989 }
990
991 $products[ $key ] = $bump['data']['products']['list'][ $key ];
992 }
993 }
994
995 if ( 'upsell' === $source ) {
996 $products = $upsell_data['data']['products']['list'];
997 }
998
999 if ( empty( $products ) ) {
1000 return;
1001 }
1002
1003 $final_price = 0;
1004
1005 foreach ( $cart->get_cart() as $key => $details ) {
1006 $item_id = $details['product_id'];
1007 $price = false;
1008
1009 if ( ! empty( $details['variation_id'] ) ) {
1010 $item_id = $details['variation_id'];
1011 }
1012
1013 foreach ( $products as $product_id => $product_details ) {
1014 if ( $item_id === $product_id ) {
1015 $discount_type = isset( $product_details['discountType'] ) ? $product_details['discountType'] : '';
1016 $discount_value = isset( $product_details['discount'] ) ? $product_details['discount'] : '';
1017
1018 $price = Helper::calculate_discount( $item_id, $discount_type, $discount_value );
1019 }
1020 }
1021
1022 if ( false !== $price ) {
1023 $details['data']->set_price( $price );
1024 }
1025
1026 $final_price += $details['data']->get_price() * $details['quantity'];
1027 }
1028
1029 self::modify_products_price_before_woo_apply_discounts( $final_price );
1030 }
1031
1032 /**
1033 * Modify cart subtotal before validating discounts.
1034 *
1035 * @since 1.2.8
1036 * @param int $final_price cart new subtotal.
1037 */
1038 public static function modify_products_price_before_woo_apply_discounts( $final_price ) {
1039 add_filter( 'woocommerce_coupon_validate_minimum_amount', function( $value, $coupon ) use ( $final_price ) {
1040 return $coupon->get_minimum_amount() > $final_price;
1041 }, 99, 2 );
1042
1043 add_filter( 'woocommerce_coupon_validate_maximum_amount', function( $value, $coupon ) use ( $final_price ) {
1044 return $coupon->get_maximum_amount() < $final_price;
1045 }, 99, 2 );
1046 }
1047
1048 /**
1049 * Fix shipping method price on checkout ajax state change.
1050 *
1051 * @param string $data checkout form data.
1052 * @since 1.1.0
1053 * @return void
1054 */
1055 public function sellkit_checkout_during_ajax( $data ) {
1056 $data = explode( '&', $data );
1057 $clear = [];
1058
1059 foreach ( $data as $key => $input ) {
1060 $input = explode( '=', $input );
1061 $clear[ $input[0] ] = $input[1];
1062 }
1063
1064 if ( ! array_key_exists( 'form_id', $clear ) ) {
1065 return;
1066 }
1067
1068 $this->apply_template_replacement_during_ajax( $clear );
1069
1070 $_POST = Helper::instance()->assigning_default_ajax_shipping_fields( $_POST, $clear ); // phpcs:ignore
1071
1072 Local_Hooks::order_bump( $clear );
1073
1074 if ( array_key_exists( 'sellkit-bundle-products', $clear ) ) {
1075 $option = $clear['sellkit-bundle-products'];
1076
1077 Local_Hooks::bundle_product_action( $option );
1078 }
1079
1080 // Apply discounts.
1081 if ( ! array_key_exists( 'sellkit_current_page_id', $clear ) ) {
1082 return;
1083 }
1084
1085 self::apply_discounted_prices( wc()->cart, $clear['sellkit_current_page_id'] );
1086 self::apply_discounted_prices( wc()->cart, $clear['sellkit_current_page_id'], 'bumps' );
1087 }
1088
1089 /**
1090 * Template replacement during ajax call.
1091 *
1092 * @param array $data checkout form data.
1093 * @since 1.2.1
1094 */
1095 private function apply_template_replacement_during_ajax( $data ) {
1096 $widget_settings = Helper::instance()->retrieve_checkout_widget_settings( $data['post_id'], $data['form_id'] );
1097
1098 Local_Hooks::enable_disable_sections( $widget_settings, $data );
1099
1100 add_filter( 'wc_get_template', function( $located, $template_name ) {
1101 $our_path = sellkit()->plugin_dir() . 'includes/elementor/modules/checkout/templates/';
1102 $files = [
1103 'review-order.php',
1104 'payment-method.php',
1105 'payment.php',
1106 'form-checkout.php',
1107 'cart-item-data.php',
1108 'cart-shipping.php',
1109 'terms.php',
1110 ];
1111 $template = str_replace( 'checkout/', '', $template_name );
1112 $template = str_replace( 'cart/', '', $template );
1113
1114 if ( in_array( $template, $files, true ) ) {
1115 $located = $our_path . $template;
1116 }
1117
1118 return $located;
1119 }, 10, 2 );
1120 }
1121
1122 /**
1123 * Modify default selected shipping method.
1124 *
1125 * @since 1.2.8
1126 * @param string $default default selected method.
1127 * @return string
1128 */
1129 public function sellkit_default_shipping_method( $default ) {
1130 $applied_coupons = WC()->cart->get_applied_coupons();
1131
1132 if ( count( $applied_coupons ) < 1 ) {
1133 return $default;
1134 }
1135
1136 foreach ( $applied_coupons as $coupon_code ) {
1137
1138 $coupon = new \WC_Coupon( $coupon_code );
1139
1140 if ( $coupon->get_free_shipping() ) {
1141
1142 if ( count( WC()->session->get( 'shipping_for_package_0' )['rates'] ) > 0 ) {
1143 // Loop through.
1144 foreach ( WC()->session->get( 'shipping_for_package_0' )['rates'] as $rate_id => $rate ) {
1145 // For free shipping.
1146 if ( 'free_shipping' === $rate->method_id ) {
1147 return $rate_id;
1148 }
1149 }
1150 }
1151 }
1152 }
1153
1154 return $default;
1155 }
1156
1157 /**
1158 * Recalculate checkout and cart prices.
1159 *
1160 * @since 1.2.8
1161 * @SuppressWarnings(PHPMD.CyclomaticComplexity)
1162 */
1163 public function before_cart_calculate() {
1164 // More than twice isn't necessary. this is called multiple times by WooCommerce.
1165 if ( did_action( 'woocommerce_before_calculate_totals' ) >= 2 && wp_doing_ajax() ) {
1166 return;
1167 }
1168
1169 $checkout_id = self::get_valid_checkout_id();
1170
1171 if ( 0 === $checkout_id ) {
1172 return;
1173 }
1174
1175 // Apply funnel prices.
1176 self::apply_discounted_prices( wc()->cart, $checkout_id );
1177 self::apply_discounted_prices( wc()->cart, $checkout_id, 'bumps' );
1178
1179 // Apply upsell discount.
1180 $sellkit_upsell_ids = null;
1181 if ( isset( $_POST['woocommerce-process-checkout-nonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['woocommerce-process-checkout-nonce'] ) ), 'woocommerce-process_checkout' ) ) {
1182 $sellkit_upsell_ids = isset( $_POST['sellkit_upsell_ids'] ) ? sanitize_textarea_field( wp_unslash( $_POST['sellkit_upsell_ids'] ) ) : null;
1183 }
1184
1185 if ( ! empty( $sellkit_upsell_ids ) ) {
1186 $upsell_ids = explode( ',', $sellkit_upsell_ids );
1187 $list = self::upsell_downsell_ids_of_funnel( $checkout_id );
1188
1189 if ( ! empty( $list ) ) {
1190 foreach ( $upsell_ids as $id ) {
1191
1192 if ( in_array( $id, $list, true ) ) {
1193 self::applay_upsell_downsell_discount( $id );
1194 }
1195 }
1196 }
1197 }
1198
1199 $auto_apply_coupon_cookie_value = null;
1200
1201 if ( isset( $_COOKIE['sellkit_rejected_coupon'] ) ) {
1202 // Get the value of the cookie.
1203 $auto_apply_coupon_cookie_value = sanitize_text_field( wp_unslash( $_COOKIE['sellkit_rejected_coupon'] ) );
1204 $auto_apply_coupon_cookie_value = explode( ',', $auto_apply_coupon_cookie_value );
1205 }
1206
1207 // We should re apply coupons after each price changes. to make sure everything is correct.
1208 $optimization_data = ! empty( $funnel_data['data']['optimization'] ) ? $funnel_data['data']['optimization'] : '';
1209
1210 if ( Checkout::apply_coupon_validation( $optimization_data ) ) {
1211 WC()->cart->remove_coupons();
1212
1213 foreach ( $optimization_data['auto_apply_coupons'] as $auto_apply_coupon ) {
1214 if ( ! empty( $auto_apply_coupon_cookie_value ) && in_array( $auto_apply_coupon['label'], $auto_apply_coupon_cookie_value, true ) ) {
1215 continue;
1216 }
1217 wc()->cart->add_discount( get_the_title( $auto_apply_coupon['value'] ) );
1218 }
1219
1220 wc_clear_notices();
1221 }
1222 }
1223
1224 /**
1225 * Extract upsell and downsell ides from funnel.
1226 *
1227 * @since 2.8.0
1228 *
1229 * @param int $checkout_id id of checkout.
1230 *
1231 * @return array
1232 */
1233 private static function upsell_downsell_ids_of_funnel( $checkout_id ) {
1234 $step_data = get_post_meta( $checkout_id, 'step_data', true );
1235 $funnel_id = $step_data['funnel_id'];
1236 $nodes = get_post_meta( $funnel_id, 'nodes', true );
1237 $ids = [];
1238
1239 foreach ( $nodes as $node ) {
1240 if ( isset( $node['type'] ) && in_array( $node['type']['key'], [ 'upsell', 'downsell' ], true ) ) {
1241 $ids[] = $node['page_id'];
1242 }
1243 }
1244
1245 return $ids;
1246 }
1247
1248 /**
1249 * Apply discount to upsell or downsell.
1250 *
1251 * @param int $id id of checkout.
1252 *
1253 * @since 2.8.0
1254 */
1255 private static function applay_upsell_downsell_discount( $id ) {
1256 $upsell_data = get_post_meta( $id, 'step_data', true );
1257 $product = $upsell_data['data']['products']['list'];
1258
1259 foreach ( $product as $key => $value ) {
1260 foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
1261 $product = $cart_item['data'];
1262 $product_id = $product->get_id();
1263
1264 if ( $key === $product_id ) {
1265 $discount_type = isset( $value['discountType'] ) ? $value['discountType'] : '';
1266 $discount_value = isset( $value['discount'] ) ? $value['discount'] : '';
1267
1268 $price = Helper::calculate_discount( $product_id, $discount_type, $discount_value );
1269 $product->set_price( $price );
1270 }
1271 }
1272 }
1273 }
1274
1275 /**
1276 * Get the checkout step associated with the current visitor.
1277 *
1278 * @since 2.8.0
1279 *
1280 * @return int
1281 */
1282 private static function get_valid_checkout_id() {
1283 $checkout_id = get_queried_object_id();
1284
1285 if ( wp_doing_ajax() ) {
1286 $checkout_id = filter_input( INPUT_POST, 'related_checkout', FILTER_SANITIZE_NUMBER_INT );
1287
1288 if ( empty( $checkout_id ) ) {
1289 $checkout_id = filter_input( INPUT_POST, 'sellkit_current_page_id', FILTER_SANITIZE_NUMBER_INT );
1290 }
1291
1292 if ( empty( $checkout_id ) ) {
1293 $data = filter_input( INPUT_POST, 'post_data', FILTER_DEFAULT );
1294
1295 if ( ! empty( $data ) ) {
1296 parse_str( $data, $data );
1297 }
1298
1299 if ( is_array( $data ) && array_key_exists( 'sellkit_current_page_id', $data ) ) {
1300 $checkout_id = $data['sellkit_current_page_id'];
1301 }
1302 }
1303 }
1304
1305 $checkout_id = absint( $checkout_id );
1306 $step_data = get_post_meta( $checkout_id, 'step_data', true );
1307
1308 if (
1309 0 === $checkout_id ||
1310 'publish' !== get_post_status( $checkout_id ) ||
1311 ! is_array( $step_data ) ||
1312 ! isset( $step_data['type']['key'] ) ||
1313 'checkout' !== $step_data['type']['key']
1314 ) {
1315 return 0;
1316 }
1317
1318 if ( wp_doing_ajax() && ! function_exists( 'WC' ) ) {
1319 return 0;
1320 }
1321
1322 return $checkout_id;
1323 }
1324
1325 /**
1326 * Check sellkit step through ajax.
1327 * And in order decide to show upsell or downsell steps through a popup.
1328 *
1329 * @since 1.6.2
1330 */
1331 public function call_funnel_popups() {
1332 $step = filter_input( INPUT_POST, 'step', FILTER_SANITIZE_NUMBER_INT );
1333 $funnel = new Sellkit_Funnel( $step );
1334 $next_step = $funnel->next_step_data;
1335 $popups = [ 'upsell', 'downsell' ];
1336
1337 $funnel->next_step_data['type'] = (array) $funnel->next_step_data['type'];
1338
1339 if ( 'decision' === $funnel->next_step_data['type']['key'] ) {
1340 $page_id = isset( $funnel->next_step_data['page_id'] ) ? apply_filters( 'wpml_object_id', $funnel->next_step_data['page_id'], 'sellkit_step', true ) : 0; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML API.
1341
1342 $this->helper_id = $step;
1343 $this->take_care_of_decision_step( $page_id, $funnel->funnel_id );
1344 return;
1345 }
1346
1347 if ( in_array( $funnel->next_step_data['type']['key'], $popups, true ) ) {
1348
1349 $next_step = $this->is_set_upsell_product( $next_step );
1350
1351 wp_send_json_success( [
1352 'next_id' => apply_filters( 'wpml_object_id', $next_step['page_id'], 'sellkit_step', true ), // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML API.
1353 'next_type' => $next_step['type']['key'],
1354 ] );
1355 }
1356 }
1357
1358 /**
1359 * Check the upsell or downsell have product or not, ignore upsell/downsell step if product doesn't set.
1360 *
1361 * @param array $next_step Next step data.
1362 * @since 2.6.0
1363 */
1364 private function is_set_upsell_product( $next_step ) {
1365 if ( isset( $next_step['type']['key'] ) && ! in_array( $next_step['type']['key'], [ 'upsell', 'downsell' ], true ) ) {
1366 return $next_step;
1367 }
1368
1369 $products = isset( $next_step['data'] ) ? $next_step['data']['products'] : [];
1370 $list = isset( $products['list'] ) ? $products['list'] : [];
1371
1372 if ( empty( $list ) ) {
1373 $funnel = isset( $next_step['page_id'] ) ? new Sellkit_Funnel( $next_step['page_id'] ) : null;
1374 $next_step = ! empty( $funnel ) ? $funnel->next_no_step_data : [];
1375
1376 if ( empty( $next_step ) && ! empty( $funnel ) ) {
1377 return $funnel->end_node_step_data;
1378 }
1379
1380 if ( isset( $next_step['type']['key'] ) && in_array( $next_step['type']['key'], [ 'upsell', 'downsell' ], true ) ) {
1381
1382 $next_step = $this->is_set_upsell_product( $next_step );
1383 }
1384 }
1385
1386 return $next_step;
1387 }
1388
1389 /**
1390 * Gets step id before the decision step and return result.
1391 *
1392 * @param int $step_id id of the step before decision step.
1393 * @param int $funnel_id id of the funnel.
1394 * @since 1.6.2
1395 */
1396 private function take_care_of_decision_step( $step_id, $funnel_id = null ) {
1397 // Failed decision step due to no page id should be checked a little different.
1398 if ( empty( $step_id ) && ! empty( $funnel_id ) ) {
1399 $this->check_decision_step_with_no_page_id();
1400 }
1401
1402 $funnel = new Sellkit_Funnel( $step_id );
1403 $conditions = ! empty( $funnel->current_step_data['data']['conditions'] ) ? $funnel->current_step_data['data']['conditions'] : [];
1404 $is_valid = sellkit_conditions_validation( $conditions );
1405 $next_step = $funnel->next_no_step_data;
1406
1407 if ( $is_valid ) {
1408 $next_step = $funnel->next_step_data;
1409 }
1410
1411 $next_step = $this->is_set_upsell_product( $next_step );
1412 $next_step['type'] = (array) $next_step['type'];
1413
1414 if ( 'decision' === $next_step['type']['key'] ) {
1415 $this->take_care_of_decision_step( apply_filters( 'wpml_object_id', $next_step['page_id'], 'sellkit_step', true ) ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML API.
1416
1417 return;
1418 }
1419
1420 wp_send_json_success( [
1421 'next_id' => apply_filters( 'wpml_object_id', $next_step['page_id'], 'sellkit_step', true ), // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML API.
1422 'next_type' => $next_step['type']['key'],
1423 ] );
1424 }
1425
1426 /**
1427 * Decision next step directly using funnel data.
1428 *
1429 * @since 1.8.6
1430 */
1431 private function check_decision_step_with_no_page_id() {
1432 $funnel = new Sellkit_Funnel( $this->helper_id );
1433 $decicion_data = $funnel->next_step_data;
1434 $conditions = $decicion_data['data']['conditions'];
1435 $funnel_data = get_post_meta( $funnel->funnel_id, 'nodes', true );
1436 $next_no = 'none' !== $decicion_data['targets'][1]['nodeId'] ? $funnel_data[ $decicion_data['targets'][1]['nodeId'] ] : $funnel->end_node_step_data;
1437 $next_yes = 'none' !== $decicion_data['targets'][0]['nodeId'] ? $funnel_data[ $decicion_data['targets'][0]['nodeId'] ] : $funnel->end_node_step_data;
1438 $is_valid = sellkit_conditions_validation( $conditions );
1439 $next_step = $next_no;
1440
1441 if ( $is_valid ) {
1442 $next_step = $next_yes;
1443 }
1444
1445 $next_step = $this->is_set_upsell_product( $next_step );
1446
1447 wp_send_json_success( [
1448 'next_id' => apply_filters( 'wpml_object_id', $next_step['page_id'], 'sellkit_step', true ), // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML API.
1449 'next_type' => $next_step['type']['key'],
1450 ] );
1451 }
1452
1453 /**
1454 * Perform upsell popup accept button.
1455 *
1456 * @since 1.6.2
1457 */
1458 public function perform_upsell_accept_button() {
1459 // Gather and validate information.
1460 $upsell_id = filter_input( INPUT_POST, 'upsell_id', FILTER_SANITIZE_NUMBER_INT );
1461 $checkout_id = filter_input( INPUT_POST, 'checkout_id', FILTER_SANITIZE_NUMBER_INT );
1462 $response = [];
1463
1464 if ( empty( $upsell_id ) ) {
1465 wp_send_json_error( esc_html__( 'Empty Upsell ID.', 'sellkit' ) );
1466 }
1467
1468 $upsell_data = get_post_meta( $upsell_id, 'step_data', true );
1469
1470 if ( empty( $upsell_data ) ) {
1471 wp_send_json_error( esc_html__( 'Empty Step Data.', 'sellkit' ) );
1472 }
1473
1474 // Add product to cart.
1475 $product_id = '';
1476 $qty = '';
1477
1478 if (
1479 is_array( $upsell_data['data'] ) &&
1480 array_key_exists( 'products', $upsell_data['data'] ) &&
1481 ! empty( $upsell_data['data']['products']['list'] )
1482 ) {
1483 $product_id = array_key_first( $upsell_data['data']['products']['list'] );
1484 $qty = ! empty( $upsell_data['data']['products']['list'][ $product_id ] ) ? $upsell_data['data']['products']['list'][ $product_id ]['quantity'] : 0;
1485 }
1486
1487 if ( empty( $qty ) ) {
1488 $qty = 1;
1489 }
1490
1491 $added_product_hash = '';
1492
1493 if ( ! in_array( $product_id, array_column( WC()->cart->get_cart(), 'product_id' ), true ) ) {
1494 $added_product_hash = WC()->cart->add_to_cart( $product_id, $qty );
1495
1496 // Take care of cart and applied discounts.
1497 self::apply_discounted_prices( WC()->cart, $checkout_id, 'upsell', $upsell_data );
1498 self::make_changes_after_cart_item_edit( $checkout_id );
1499 }
1500
1501 // Response.
1502 $funnel = new Sellkit_Funnel( $upsell_id );
1503 $next_step = $funnel->next_step_data;
1504 $next_step = $this->is_set_upsell_product( $next_step );
1505 $next_step['type'] = ! empty( $next_step['type'] ) ? (array) $next_step['type'] : null;
1506
1507 if ( 'upsell' === $funnel->current_step_data['type']['key'] && isset( WC()->cart->cart_contents[ $added_product_hash ] ) ) {
1508 $analytics_updater = new Data_Updater();
1509 $total = empty( WC()->cart->cart_contents[ $added_product_hash ]['line_total'] ) ? 0 : WC()->cart->cart_contents[ $added_product_hash ]['line_total'];
1510
1511 $analytics_updater->set_funnel_id( $funnel->funnel_id );
1512 $analytics_updater->add_new_upsell_revenue_log( $total );
1513 }
1514
1515 $contact_data = [
1516 'key' => $funnel->current_step_data['type']['key'],
1517 'page_id' => $funnel->current_step_data['page_id'],
1518 ];
1519
1520 Base_Contacts::step_is_passed( $contact_data );
1521
1522 // Step with empty data, will be redirected to thankyou page.
1523 if ( empty( $next_step ) || empty( $next_step['type'] ) ) {
1524 wp_send_json_success(
1525 [
1526 'next_id' => $funnel->end_node_step_data['page_id'],
1527 'next_type' => $funnel->end_node_step_data['type']['key'],
1528 'upsell_ids' => $upsell_id,
1529 ]
1530 );
1531 }
1532
1533 if ( 'decision' === $next_step['type']['key'] ) {
1534 $this->take_care_of_decision_step( $next_step['page_id'] );
1535 return;
1536 }
1537
1538 $response = [
1539 'next_id' => apply_filters( 'wpml_object_id', $next_step['page_id'], 'sellkit_step', true ), // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML API.
1540 'next_type' => $next_step['type']['key'],
1541 'upsell_ids' => $upsell_id,
1542 ];
1543
1544 wp_send_json_success( $response );
1545 }
1546
1547 /**
1548 * Perform upsell popup reject button.
1549 *
1550 * @since 1.6.2
1551 */
1552 public function perform_upsell_reject_button() {
1553 $upsell_id = filter_input( INPUT_POST, 'upsell_id', FILTER_SANITIZE_NUMBER_INT );
1554
1555 if ( empty( $upsell_id ) ) {
1556 wp_send_json_error( esc_html__( 'Empty Upsell ID.', 'sellkit' ) );
1557 }
1558
1559 $funnel = new Sellkit_Funnel( $upsell_id );
1560 $next_step = $funnel->next_no_step_data;
1561 $next_step = $this->is_set_upsell_product( $next_step );
1562
1563 if ( $next_step ) {
1564 $next_step['type'] = (array) $next_step['type'];
1565 }
1566
1567 // Step with empty data, will be redirected to thankyou page.
1568 if ( empty( $next_step ) || empty( $next_step['type'] ) ) {
1569 wp_send_json_success(
1570 [
1571 'next_id' => $funnel->end_node_step_data['page_id'],
1572 'next_type' => $funnel->end_node_step_data['type']['key'],
1573 ]
1574 );
1575 }
1576
1577 if ( 'decision' === $next_step['type']['key'] ) {
1578 $this->take_care_of_decision_step( $next_step['page_id'] );
1579 return;
1580 }
1581
1582 $response = [
1583 'next_id' => apply_filters( 'wpml_object_id', $next_step['page_id'], 'sellkit_step', true ), // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML API.
1584 'next_type' => $next_step['type']['key'],
1585 ];
1586
1587 wp_send_json_success( $response );
1588 }
1589
1590 /**
1591 * Add new item to the WooCommerce order details on the edit page.
1592 *
1593 * @param object $order woocommerce order object.
1594 * @since 1.8.9
1595 */
1596 public function add_item_to_order_details( $order ) {
1597 $order_id = $order->get_id();
1598 $custom_fields = get_post_meta( $order_id, 'sellkit_checkout_widget_custom_field_of_order', true );
1599 $current_action = current_action();
1600
1601 if ( empty( $custom_fields ) || ! is_array( $custom_fields ) ) {
1602 return;
1603 }
1604
1605 foreach ( $custom_fields as $field ) {
1606 $key_name = $field['key_name'];
1607 $label = $field['label'];
1608
1609 // For old users data.
1610 if ( empty( $label ) ) {
1611 $label = $key_name;
1612 $label = str_replace( '_', ' ', $label );
1613 $label = str_replace( '-', ' ', $label );
1614 $label = ucwords( $label );
1615 }
1616
1617 if (
1618 'woocommerce_admin_order_data_after_shipping_address' === $current_action &&
1619 'shipping' === substr( $key_name, 0, 8 )
1620 ) {
1621 ?>
1622 <h3><?php echo esc_html( $label ); ?></h3>
1623 <div><?php echo esc_html( get_post_meta( $order_id, $key_name, true ) ); ?></div>
1624 <?php
1625 }
1626
1627 if (
1628 'woocommerce_admin_order_data_after_billing_address' === $current_action &&
1629 'billing' === substr( $key_name, 0, 7 )
1630 ) {
1631 ?>
1632 <h3><?php echo esc_html( $label ); ?></h3>
1633 <div><?php echo esc_html( get_post_meta( $order_id, $key_name, true ) ); ?></div>
1634 <?php
1635 }
1636 }
1637 }
1638 }
1639