PluginProbe
SellKit – Funnel builder and checkout optimizer for WooCommerce to sell more, faster / 1.7.2
SellKit – Funnel builder and checkout optimizer for WooCommerce to sell more, faster v1.7.2
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 / elementor / modules / checkout / classes / global-hooks.php

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

1,304 lines 37.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Sellkit\Elementor\Modules\Checkout\Classes;
4
5 defined( 'ABSPATH' ) || exit;
6
7 use Sellkit\Elementor\Modules\Checkout\Classes\{ Local_Hooks, Helper };
8 use Sellkit\Funnel\Steps\Checkout;
9 use Sellkit_Funnel;
10
11 /**
12 * Global hooks.
13 *
14 * @SuppressWarnings(PHPMD.UnusedFormalParameter)
15 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
16 * @SuppressWarnings(PHPMD.TooManyMethods)
17 * @SuppressWarnings(PHPMD.NPathComplexity)
18 * @since 1.1.0
19 */
20 class Global_Hooks {
21 /**
22 * Create instance of class without construct.
23 *
24 * @since 1.1.0
25 * @return object
26 */
27 public static function instance() {
28 $class = new \ReflectionClass( __CLASS__ );
29 return $class->newInstanceWithoutConstructor();
30 }
31
32 /**
33 * Class construct.
34 * required actions that affects woocommerce global checkout shortcode / page.
35 *
36 * @since 1.1.0
37 */
38 public function __construct() {
39 // Create empty shortcode to simulate our widget page as checkout page.
40 add_shortcode( 'sellkit_checkout_widget_simulated', function() {
41 return '';
42 } );
43
44 add_action( 'wp_ajax_sellkit_checkout_ajax_handler', [ $this, 'ajax_handler' ] );
45 add_action( 'wp_ajax_nopriv_sellkit_checkout_ajax_handler', [ $this, 'ajax_handler' ] );
46
47 // Add shipping total to review order by custom hook.
48 add_action( 'sellkit-checkout-widget-display-shipping-price', [ $this, 'add_shipping_total_to_review_order' ] );
49
50 // Modify checkout order-review item.
51 add_action( 'sellkit-one-page-checkout-custom-order-item', [ $this, 'modify_checkout_order_item' ], 1, 4 );
52
53 // Modify sent data before processing & validate order.
54 add_filter( 'woocommerce_checkout_posted_data', [ $this, 'modify_order_data_before_validate' ], 10, 1 );
55
56 // Validate user defined fields.
57 add_action( 'woocommerce_checkout_process', [ $this, 'validate_user_defined_fields' ] );
58
59 // Save user defined fields.
60 add_action( 'woocommerce_checkout_update_order_meta', [ $this, 'save_user_defined_fields' ] );
61
62 // Add custom shipping field to email.
63 add_filter( 'woocommerce_order_get_formatted_shipping_address', [ $this, 'attach_user_shipping_fields_to_order_email' ], 10, 3 );
64
65 // Add custom billing field to email.
66 add_filter( 'woocommerce_order_get_formatted_billing_address', [ $this, 'attach_user_billing_fields_to_order_email' ], 10, 3 );
67
68 // Filter checkout ajax fragment.
69 add_filter( 'woocommerce_update_order_review_fragments', [ $this, 'checkout_fragment' ] );
70
71 // Simulate checkout page for our widget.
72 add_action( 'wp', [ $this, 'simulate_our_widget_page_as_checkout' ] );
73
74 // Fix shipping method price change on ajax.
75 // Manage bump order discounts.
76 // Template replacement.
77 add_action( 'woocommerce_checkout_update_order_review', [ $this, 'sellkit_checkout_during_ajax' ], 10, 1 );
78
79 add_filter( 'sellkit-shipping-methods-choosen-method', [ $this, 'sellkit_default_shipping_method' ], 10, 1 );
80
81 // Modify discounted products.
82 add_action( 'woocommerce_before_calculate_totals', [ $this, 'before_cart_calculate' ], 999 );
83 }
84
85 /**
86 * Handle ajax calls.
87 *
88 * @since 1.1.0
89 * @return void
90 */
91 public function ajax_handler() {
92 check_ajax_referer( 'sellkit_elementor', 'nonce' );
93
94 $sub_action = filter_input( INPUT_POST, 'sub_action', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
95
96 if ( method_exists( $this, $sub_action ) ) {
97 call_user_func( [ $this, $sub_action ] );
98 return;
99 }
100
101 wp_send_json_error();
102 }
103
104 /**
105 * Modify checkout ajax response HTML.
106 *
107 * @param array $response woocommerce checkout page ajax response.
108 * @return array
109 * @since 1.1.0
110 */
111 public function checkout_fragment( $response ) {
112 // Get posted data and identify if it's been sent by jupiter widget or not.
113 $checkout_form_data = filter_input( INPUT_POST, 'post_data', FILTER_DEFAULT );
114 $checkout_form_data = explode( '&', $checkout_form_data );
115 $posted_data = [];
116
117 foreach ( $checkout_form_data as $input ) {
118 $item = explode( '=', urldecode( $input ) );
119
120 $posted_data[ trim( $item[0] ) ] = $item[1];
121 }
122
123 // Identify if it's been sent by jupiter checkout widget or not.
124 if ( ! array_key_exists( 'form_id', $posted_data ) && ! array_key_exists( 'post_id', $posted_data ) ) {
125 // Default woocommerce checkout.
126 return $response;
127 }
128
129 // Get widget settings.
130 $widget_settings = Helper::instance()->retrieve_checkout_widget_settings( $posted_data['post_id'], $posted_data['form_id'] );
131
132 // Order review fragment.
133 ob_start();
134 woocommerce_order_review();
135 $order_review = ob_get_clean();
136
137 // Payment fragment.
138 ob_start();
139 woocommerce_checkout_payment();
140 $payment = ob_get_clean();
141
142 // Shipping method fragment.
143 ob_start();
144 echo '<section class="sellkit-one-page-shipping-methods">';
145 wc_cart_totals_shipping_html();
146 echo '</section>';
147 $shipping_method = ob_get_clean();
148
149 $response['.woocommerce-checkout-review-order-table'] = $order_review;
150 $response['.woocommerce-checkout-payment'] = $payment;
151 $response['.sellkit-one-page-shipping-methods'] = '';
152
153 if ( ! array_key_exists( 'show_shipping_method', $widget_settings ) ) {
154 $response['.sellkit-one-page-shipping-methods'] = $shipping_method;
155 }
156
157 return $response;
158 }
159
160 /**
161 * Apply coupon code using ajax.
162 *
163 * @return void
164 * @since 1.1.0
165 */
166 public function apply_coupon() {
167 $code = filter_input( INPUT_POST, 'code', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
168
169 if ( empty( $code ) ) {
170 wp_send_json_error();
171 }
172
173 $result = WC()->cart->add_discount( $code );
174 wp_send_json_success( $result );
175 }
176
177 /**
178 * Update cart item quantity in checkout page by ajax.
179 *
180 * @return void
181 * @since 1.1.0
182 */
183 public function change_cart_item_qty() {
184 $qty = filter_input( INPUT_POST, 'qty', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
185 $id = filter_input( INPUT_POST, 'id', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
186 $action = filter_input( INPUT_POST, 'mode', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
187 $funnel_id = filter_input( INPUT_POST, 'related_checkout', FILTER_SANITIZE_NUMBER_INT );
188
189 self::make_changes_after_cart_item_edit( $funnel_id );
190
191 if ( 'add' === $action ) {
192 WC()->cart->add_to_cart( $id, $qty );
193 self::make_changes_after_cart_item_edit( $funnel_id );
194 wp_send_json_success();
195 }
196
197 if ( 'remove' === $action ) {
198 $post_type = get_post_type( $id );
199
200 if ( 'product_variation' === $post_type ) {
201 foreach ( WC()->cart->get_cart() as $item_key => $item ) {
202 if ( $item['variation_id'] === (int) $id ) {
203 WC()->cart->remove_cart_item( $item_key ); // we remove it.
204 break; // stop the loop.
205 }
206 }
207
208 self::make_changes_after_cart_item_edit( $funnel_id );
209
210 wp_send_json_success();
211 }
212
213 $product_cart_id = WC()->cart->generate_cart_id( $id );
214 $cart_item_key = WC()->cart->find_product_in_cart( $product_cart_id );
215
216 if ( $cart_item_key ) {
217 WC()->cart->remove_cart_item( $cart_item_key );
218 }
219
220 self::make_changes_after_cart_item_edit( $funnel_id );
221
222 wp_send_json_success();
223 }
224
225 ( $qty > 0 ) ? WC()->cart->set_quantity( $id, $qty ) : WC()->cart->remove_cart_item( $id );
226
227 self::make_changes_after_cart_item_edit( $funnel_id );
228
229 wp_send_json_success();
230 }
231
232 /**
233 * Apply changes after editing cart items.
234 *
235 * @since 1.2.5
236 * @param int $funnel_id funnel step id.
237 */
238 public static function make_changes_after_cart_item_edit( $funnel_id ) {
239 if ( empty( $funnel_id ) ) {
240 return;
241 }
242
243 $funnel_data = get_post_meta( $funnel_id, 'step_data', true );
244 $optimization_data = ! empty( $funnel_data['data']['optimization'] ) ? $funnel_data['data']['optimization'] : '';
245
246 if ( empty( $optimization_data ) ) {
247 return;
248 }
249
250 // Help to apply funnel discount prices and coupons when changing product quantity.
251 self::apply_discounted_prices( wc()->cart, $funnel_id );
252 self::apply_discounted_prices( wc()->cart, $funnel_id, 'bumps' );
253
254 if ( Checkout::apply_coupon_validation( $optimization_data ) ) {
255 WC()->cart->remove_coupons();
256
257 foreach ( $optimization_data['auto_apply_coupons'] as $auto_apply_coupon ) {
258 wc()->cart->add_discount( get_the_title( $auto_apply_coupon['value'] ) );
259 }
260
261 wc_clear_notices();
262 }
263 }
264
265 /**
266 * Place shipping price based on design to checkout order-review.
267 *
268 * @return void
269 * @since 1.1.0
270 */
271 public function add_shipping_total_to_review_order() {
272 ?>
273 <tr class="sellkit-shipping-total">
274 <th><?php echo __( 'Shipping', 'sellkit' ); ?></th>
275 <td>
276 <?php
277 $price = WC()->cart->get_shipping_total();
278
279 if ( WC()->cart->display_prices_including_tax() ) {
280 $price = WC()->cart->get_shipping_total() + WC()->cart->shipping_tax_total;
281 }
282
283 echo wc_price( $price );
284 ?>
285 </td>
286 </tr>
287 <?php
288 }
289
290 /**
291 * Modify checkout review-order based on design.
292 *
293 * @return void
294 * @param string $row html string of cart row.
295 * @param object $product product object.
296 * @param array $cart_item cart item information.
297 * @param string $cart_item_key cart item unique key.
298 * @since 1.1.0
299 */
300 public function modify_checkout_order_item( $row, $product, $cart_item, $cart_item_key ) {
301 $cart_items = WC()->cart->get_cart();
302
303 //phpcs:disable
304 ob_start();
305 ?>
306 <div style="display: inline-block" class="sellkit-checkout-widget-item-image">
307 <?php echo $product->get_image(); ?>
308 </div>
309 <?php
310 $img_html = ob_get_clean();
311 $img_html = apply_filters( 'sellkit/includes/elementor/modules/checkout/product-image', $img_html, $product, $cart_item_key );
312
313 ob_start();
314
315 $extra_class = $product->get_type();
316 if ( $cart_items[ $cart_item_key ] === end( $cart_items ) ) {
317 $extra_class .= ' last-cart-item';
318 }
319
320 if ( 'variation' === $product->get_type() ) {
321 add_filter( 'woocommerce_cart_item_name', [ $this, 'modify_variation_title' ], 50, 2 );
322 add_filter( 'woocommerce_get_item_data', [ $this, 'modify_variation_items' ], 10, 2 );
323 }
324 ?>
325 <tr class="<?php echo esc_attr( apply_filters( 'woocommerce_cart_item_class', 'cart_item', $cart_item, $cart_item_key ) ) . ' ' . esc_attr( $extra_class ); ?>">
326
327 <td class="product-name sellkit-one-page-checkout-product-name">
328 <?php
329 echo $img_html;
330
331 $fix_style = '';
332
333 if ( '' === $img_html ) {
334 $fix_style = 'margin-left: 0px;';
335 }
336 ?>
337
338 <div class="name-price" style="<?php echo esc_attr( $fix_style ); ?>">
339 <span style="display:inline-block">
340 <?php echo wp_kses_post( apply_filters( 'woocommerce_cart_item_name', $product->get_name(), $cart_item, $cart_item_key ) ) . '&nbsp;'; ?>
341 </span>
342 <span class="sellkit-checkout-variations" id="sellkit-checkout-variations">
343 <?php echo wc_get_formatted_cart_item_data( $cart_item ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
344 </span>
345 <?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'] ); ?>
346 <?php do_action( 'sellkit-checkout-editor-mode-quantity', $cart_item['quantity'] ); ?>
347 </div>
348 </td>
349 <td class="product-total sellkit-one-page-checkout-product-price">
350 <?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 ?>
351 </td>
352 </tr>
353 <?php
354 $cart_item = ob_get_clean();
355
356 echo apply_filters( 'sellkit-checkout-cart-item' , $cart_item );
357 //phpcs:enable
358
359 if ( 'variation' === $product->get_type() ) {
360 remove_filter( 'woocommerce_cart_item_name', [ $this, 'modify_variation_title' ], 50 );
361 remove_filter( 'woocommerce_get_item_data', [ $this, 'modify_variation_items' ], 10 );
362 }
363 }
364
365 /**
366 * Remove variation attributes from variation product title.
367 *
368 * @param string $default default value.
369 * @param array $cart_item cart item data.
370 * @since 1.6.8
371 */
372 public function modify_variation_title( $default, $cart_item ) {
373 $product = $cart_item['product_id'];
374
375 return get_the_title( $product );
376 }
377
378 /**
379 * Display variation attributes separated from title even if those are less than 3.
380 *
381 * @param array $item_data attributes array.
382 * @param array $cart_item cart item data.
383 * @since 1.6.8
384 */
385 public function modify_variation_items( $item_data, $cart_item ) {
386 $product = new \WC_Product_Variation( $cart_item['variation_id'] );
387 $attributes = $product->get_attributes();
388
389 if ( count( $attributes ) > 2 ) {
390 return $item_data;
391 }
392
393 foreach ( $attributes as $key => $value ) {
394 $key = str_replace( 'pa_', '', $key );
395 $key = str_replace( '-', '', $key );
396 $key = str_replace( '_', '', $key );
397
398 $item_data[] = [
399 'key' => $key,
400 'value' => $value,
401 ];
402 }
403
404 return $item_data;
405 }
406
407 /**
408 * Modify fields after pressing place order button.
409 * we hooked here because some removed fields still get checked and have not removed by previous hooks.
410 *
411 * @SuppressWarnings(PHPMD.NPathComplexity)
412 * @param array $data checkout form data.
413 * @return array $data data of woocommerce checkout form.
414 * @since 1.1.0
415 */
416 public function modify_order_data_before_validate( $data ) {
417 $post_id = filter_input( INPUT_POST, 'post_id', FILTER_SANITIZE_NUMBER_INT );
418 $form_id = filter_input( INPUT_POST, 'form_id', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
419
420 if ( empty( $post_id ) && empty( $form_id ) ) {
421 return $data;
422 }
423
424 $settings = Helper::instance()->retrieve_checkout_widget_settings( $post_id, $form_id );
425
426 if ( empty( $settings ) ) {
427 return $data;
428 }
429
430 $widget_shipping_field = Helper::instance()->get_user_defined_fields_slug( $settings['shipping_list'], 'shipping_list_field' );
431 $widget_billing_field = Helper::instance()->get_user_defined_fields_slug( $settings['billing_list'], 'billing_list_field' );
432
433 $default_shipping_fields = Helper::instance()->shipping_fields();
434 $default_billing_fields = Helper::instance()->billing_fields();
435
436 // Unset removed shipping fields.
437 foreach ( $default_shipping_fields as $field => $details ) {
438 if ( ! in_array( $field, $widget_shipping_field, true ) ) {
439 unset( $data[ $field ] );
440 }
441 }
442
443 // Unset removed billing fields.
444 foreach ( $default_billing_fields as $field => $details ) {
445 if ( ! in_array( $field, $widget_billing_field, true ) && 'billing_email' !== $field ) {
446 unset( $data[ $field ] );
447 }
448 }
449
450 $mail = filter_input( INPUT_POST, 'billing_email', FILTER_SANITIZE_EMAIL );
451
452 if ( ! array_key_exists( 'billing_email', $data ) && ! empty( $mail ) ) {
453 $data['billing_email'] = $mail;
454 }
455
456 return $data;
457 }
458
459 /**
460 * Login user by ajax.
461 * sign user in by using form included in widget.
462 *
463 * @return void
464 * @since 1.1.0
465 */
466 public function auth_user() {
467 $email = filter_input( INPUT_POST, 'email', FILTER_SANITIZE_EMAIL );
468 $pass = filter_input( INPUT_POST, 'pass', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
469 $verify = filter_var( $email, FILTER_VALIDATE_EMAIL );
470
471 if ( ! $verify || empty( $email ) || empty( $pass ) ) {
472 wp_send_json_error( __( 'Email or password field is not valid.', 'sellkit' ) );
473 }
474
475 $result = wp_authenticate( $email, $pass );
476
477 if ( is_wp_error( $result ) ) {
478 wp_send_json_error( $result->get_error_message() );
479 }
480
481 wp_clear_auth_cookie();
482 wp_set_current_user( $result->ID );
483 wp_set_auth_cookie( $result->ID );
484
485 wp_send_json_success( $result );
486 }
487
488 /**
489 * Get widget settings in ajax calls using form and post id.
490 *
491 * @return void
492 * @since 1.1.0
493 */
494 private function widget_settings() {
495 $form_id = filter_input( INPUT_POST, 'form_id', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
496 $post_id = filter_input( INPUT_POST, 'post_id', FILTER_SANITIZE_NUMBER_INT );
497
498 if ( empty( $form_id ) && empty( $post_id ) ) {
499 return;
500 }
501
502 $widget_settings = Helper::instance()->retrieve_checkout_widget_settings( $post_id, $form_id );
503
504 return $widget_settings;
505 }
506
507 /**
508 * Validate user defined custom value.
509 *
510 * @return void
511 * @since 1.1.0
512 */
513 public function validate_user_defined_fields() {
514 $widget_settings = $this->widget_settings();
515
516 if ( empty( $widget_settings ) ) {
517 return;
518 }
519
520 $widget_shipping_fields = $widget_settings['shipping_list'];
521 $widget_billing_fields = $widget_settings['billing_list'];
522
523 Helper::instance()->validate_user_defined_fields( $widget_shipping_fields, $widget_billing_fields );
524 Helper::instance()->make_sure_to_convert_required_field_to_optional( $widget_shipping_fields, $widget_billing_fields );
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 1.1.0
533 */
534 public function save_user_defined_fields( $order_id ) {
535 $widget_settings = $this->widget_settings();
536
537 if ( empty( $widget_settings ) ) {
538 return;
539 }
540
541 $widget_shipping_fields = $widget_settings['shipping_list'];
542 $widget_billing_fields = $widget_settings['billing_list'];
543
544 Helper::instance()->save_user_defined_fields( $widget_shipping_fields, $widget_billing_fields, $order_id );
545 }
546
547 /**
548 * Attach user defined shipping field values to order emails.
549 *
550 * @param string $address address.
551 * @param string $raw_address raw address.
552 * @param object $order woocommerce order object.
553 * @return string
554 * @since 1.1.0
555 */
556 public function attach_user_shipping_fields_to_order_email( $address, $raw_address, $order ) {
557 $order_id = $order->get_id();
558 $custom_fields = get_post_meta( $order_id, 'sellkit_checkout_widget_custom_field_of_order', true );
559
560 if ( empty( $custom_fields ) || ! is_array( $custom_fields ) ) {
561 return $address;
562 }
563
564 foreach ( $custom_fields as $field ) {
565 if ( 'yes' !== $field['show_in_email'] ) {
566 continue;
567 }
568
569 $key = $field['key_name'];
570 $value = get_post_meta( $order_id, $key, true );
571
572 if ( strpos( $key, 'shipping' ) !== false ) {
573 $address .= '<br>' . $value;
574 }
575 }
576
577 return $address;
578 }
579
580 /**
581 * Attach user defined billing field values to order emails.
582 *
583 * @param string $address address.
584 * @param string $raw_address raw address.
585 * @param object $order woocommerce order object.
586 * @return string
587 * @since 1.1.0
588 */
589 public function attach_user_billing_fields_to_order_email( $address, $raw_address, $order ) {
590 $order_id = $order->get_id();
591 $custom_fields = get_post_meta( $order_id, 'sellkit_checkout_widget_custom_field_of_order', true );
592
593 if ( empty( $custom_fields ) || ! is_array( $custom_fields ) ) {
594 return $address;
595 }
596
597 foreach ( $custom_fields as $field ) {
598 if ( 'yes' !== $field['show_in_email'] ) {
599 continue;
600 }
601
602 $key = $field['key_name'];
603 $value = get_post_meta( $order_id, $key, true );
604
605 if ( strpos( $key, 'billing' ) !== false ) {
606 $address .= '<br>' . $value;
607 }
608 }
609
610 return $address;
611 }
612
613 /**
614 * Place custom coupon form in checkout order-review based design.
615 * We have used same form in Local_Hooks. but this one is for ajax response.
616 *
617 * @see sellkit_Core\Raven\Modules\Checkout\Classes\Local_Hooks::coupon_form.
618 * @param array $settings widget settings.
619 * @return void
620 * @since 1.1.0
621 */
622 public function coupon_form( $settings ) {
623 if ( array_key_exists( 'show_coupon_field', $settings ) ) {
624 return;
625 }
626
627 echo '<tr class="coupon-form border-none"><td colspan="2">';
628 Local_Hooks::form( $settings );
629 echo '</td></tr>';
630
631 do_action( 'sellkit-checkout-after-coupon-form-ajax' );
632 }
633
634 /**
635 * Look for an email if exists or not.
636 * Is used for login process.
637 *
638 * @return void
639 * @since 1.1.0
640 */
641 public function search_for_email() {
642 $email = filter_input( INPUT_POST, 'email', FILTER_SANITIZE_EMAIL );
643 $valid = filter_var( $email, FILTER_VALIDATE_EMAIL );
644
645 if ( ! $valid || empty( $email ) ) {
646 wp_send_json_error();
647 }
648
649 $check = email_exists( $email );
650
651 if ( false === $check ) {
652 wp_send_json_error();
653 }
654
655 wp_send_json_success();
656 }
657
658 /**
659 * Look for an username if exists or not.
660 * Is used for register process.
661 *
662 * @return void
663 * @since 1.1.0
664 */
665 public function search_for_username() {
666 $username = filter_input( INPUT_POST, 'user', FILTER_SANITIZE_EMAIL );
667
668 if ( empty( $username ) ) {
669 wp_send_json_error();
670 }
671
672 $username = sanitize_user( $username );
673 $check = username_exists( $username );
674
675 if ( false !== $check ) {
676 wp_send_json_error();
677 }
678
679 wp_send_json_success();
680 }
681
682 /**
683 * Validate postcode via ajax.
684 *
685 * @since 1.1.0
686 * @return void
687 */
688 public function validate_postcode() {
689 $country = filter_input( INPUT_POST, 'country_code', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
690 $postcode = filter_input( INPUT_POST, 'post_code', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
691 $parent = filter_input( INPUT_POST, 'parent', FILTER_DEFAULT );
692
693 $postcode = wc_format_postcode( $postcode, $country );
694 $valid = \WC_Validation::is_postcode( $postcode, $country );
695
696 if ( ! $valid ) {
697 wp_send_json_error( $parent );
698 }
699
700 wp_send_json_success( $parent );
701 }
702
703 /**
704 * Validate phone number via ajax.
705 *
706 * @since 1.1.0
707 * @return void
708 */
709 public function validate_phone_number() {
710 $phone = filter_input( INPUT_POST, 'phone_number', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
711 $parent = filter_input( INPUT_POST, 'parent', FILTER_DEFAULT );
712
713 $valid = \WC_Validation::is_phone( $phone );
714
715 if ( ! $valid ) {
716 wp_send_json_error( $parent );
717 }
718
719 wp_send_json_success( $parent );
720 }
721
722 /**
723 * Simulate checkout page where our widget is present. using empty shortcode [sellkit_checkout_widget_simulated].
724 *
725 * @since 1.1.0
726 * @return void
727 */
728 public function simulate_our_widget_page_as_checkout() {
729 $page_id = get_the_ID();
730 $content = get_post_field( 'post_content', $page_id );
731
732 if ( false === strpos( $content, 'woocommerce_checkout' ) ) {
733 return;
734 }
735
736 add_filter( 'woocommerce_is_checkout', function() {
737 return true;
738 } );
739
740 if ( false === strpos( $content, 'sellkit_checkout_widget_simulated' ) ) {
741 return;
742 }
743
744 add_filter( 'theme_mod_jupiterx_jupiterx_checkout_cart_elements', function() {
745 return [];
746 }, 10 );
747 }
748
749 /**
750 * Integrate user country with our widget.
751 *
752 * @since 1.1.0
753 * @return void
754 */
755 public function set_customer_details_ajax() {
756 $country = filter_input( INPUT_POST, 'country', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
757 $state = filter_input( INPUT_POST, 'state', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
758 $shipping_country = filter_input( INPUT_POST, 'shipping_country', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
759 $shipping_state = filter_input( INPUT_POST, 'shipping_state', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
760
761 if ( ! empty( $country ) ) {
762 setcookie( 'sellkit-checkout-billing-cc', $country, time() + ( 86400 * 3 ), '/' );
763 setcookie( 'sellkit-checkout-billing-state', $state, time() + ( 86400 * 3 ), '/' );
764 }
765
766 if ( ! empty( $shipping_country ) ) {
767 setcookie( 'sellkit-checkout-shipping-cc', $shipping_country, time() + ( 86400 * 3 ), '/' );
768 setcookie( 'sellkit-checkout-shipping-state', $shipping_state, time() + ( 86400 * 3 ), '/' );
769 }
770
771 wp_send_json_success();
772 }
773
774 /**
775 * State lookup using postcode and country.
776 *
777 * @since 1.1.0
778 * @return void
779 */
780 public function sellkit_state_lookup_by_postcode() {
781 $country = filter_input( INPUT_POST, 'country_value', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
782 $postcode = filter_input( INPUT_POST, 'postcode_value', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
783 $api = 'https://api.zippopotam.us/' . $country . '/' . $postcode;
784
785 if ( empty( $country ) || empty( $postcode ) ) {
786 wp_send_json_error( esc_html__( 'Not valid inputs', 'sellkit' ) );
787 }
788
789 $response = wp_remote_get( $api );
790
791 if ( is_wp_error( $response ) || ! is_array( $response ) ) {
792 wp_send_json_error( esc_html__( 'Server error.', 'sellkit' ) );
793 }
794
795 if ( '{}' === $response['body'] ) {
796 // Country or postcode is not supported. it's kina an error but we show nothing in frontend.
797 wp_send_json_error( '' );
798 }
799
800 $body = json_decode( $response['body'], true );
801
802 wp_send_json_success( $body );
803 }
804
805 /**
806 * Modify cart contents using bundled products.
807 *
808 * @since 1.1.0
809 * @return void
810 */
811 public function sellkit_checkout_modify_cart_by_bundle_products() {
812 $qty = filter_input( INPUT_POST, 'qty', FILTER_SANITIZE_NUMBER_INT );
813 $id = filter_input( INPUT_POST, 'id', FILTER_SANITIZE_NUMBER_INT );
814 $type = filter_input( INPUT_POST, 'type', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
815 $checkout_id = filter_input( INPUT_POST, 'checkout_id', FILTER_SANITIZE_NUMBER_INT );
816 $modify = filter_input( INPUT_POST, 'modify', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
817 $key = filter_input( INPUT_POST, 'key', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
818
819 if ( ! empty( $key ) ) {
820 foreach ( WC()->cart->get_cart() as $item_key => $item ) {
821 if ( $item_key === $key ) {
822 WC()->cart->set_quantity( $item_key, $qty, true );
823 break; // stop the loop.
824 }
825 }
826
827 wp_send_json_success();
828 }
829
830 if ( 'radio' === $type ) {
831 $step_data = get_post_meta( $checkout_id, 'step_data', true );
832 $products = $step_data['data']['products']['list'];
833 $selected = $products[ $id ];
834 $real_quantity = ( empty( $selected['quantity'] ) ) ? 1 : $selected['quantity'];
835
836 if ( (int) $real_quantity !== (int) $qty ) {
837 wp_send_json_error( esc_html__( 'Oh do not be tricky.', 'sellkit' ) );
838 }
839 }
840
841 if ( 'radio' === $type ) {
842 WC()->cart->empty_cart();
843 WC()->cart->add_to_cart( $id, $qty );
844 }
845
846 if ( 'checkbox' === $type && 'add' === $modify ) {
847 WC()->cart->add_to_cart( $id, $qty );
848 }
849
850 if ( 'checkbox' === $type && 'remove' === $modify ) {
851 $post_type = get_post_type( $id );
852
853 // Maybe product is a variation.
854 if ( 'product_variation' === $post_type ) {
855 foreach ( WC()->cart->get_cart() as $item_key => $item ) {
856 if ( $item['variation_id'] === (int) $id ) {
857 WC()->cart->remove_cart_item( $item_key ); // we remove it.
858 break; // stop the loop.
859 }
860 }
861
862 self::make_changes_after_cart_item_edit( $checkout_id );
863
864 wp_send_json_success();
865 }
866
867 // Default product.
868 $product_cart_id = WC()->cart->generate_cart_id( $id );
869 $cart_item_key = WC()->cart->find_product_in_cart( $product_cart_id );
870
871 if ( $cart_item_key ) {
872 WC()->cart->remove_cart_item( $cart_item_key );
873 }
874 }
875
876 self::make_changes_after_cart_item_edit( $checkout_id );
877
878 wp_send_json_success();
879 }
880
881 /**
882 * Apply funnel discount.
883 * Will be called twice, first when page is loading , second during checkout ajax.
884 *
885 * @param object $cart cart object.
886 * @param int $checkout_id_ajax checkout id.
887 * @param string $source source of products, bump or default products to be checked.
888 * @param array $upsell_data upsell product data coming from popup.
889 * @since 1.2.8
890 */
891 public static function apply_discounted_prices( $cart = [], $checkout_id_ajax = null, $source = 'default', $upsell_data = [] ) {
892 $checkout_id = get_queried_object_id();
893
894 if ( wp_doing_ajax() ) {
895 $checkout_id = $checkout_id_ajax;
896 }
897
898 if ( empty( $checkout_id ) ) {
899 return;
900 }
901
902 $step_data = get_post_meta( $checkout_id, 'step_data', true );
903
904 if ( empty( $step_data ) ) {
905 return;
906 }
907
908 $products = [];
909
910 if (
911 'default' === $source &&
912 array_key_exists( 'data', $step_data ) &&
913 array_key_exists( 'list', $step_data['data']['products'] )
914 ) {
915 $products = $step_data['data']['products']['list'];
916 }
917
918 if ( 'bumps' === $source && ! empty( $step_data['bump'] ) ) {
919 $bumps = $step_data['bump'];
920
921 foreach ( $bumps as $bump ) {
922 if ( ! array_key_exists( 'products', $bump['data'] ) ) {
923 continue;
924 }
925
926 $key = array_key_first( $bump['data']['products']['list'] );
927 $products[ $key ] = $bump['data']['products']['list'][ $key ];
928 }
929 }
930
931 if ( 'upsell' === $source ) {
932 $products = $upsell_data['data']['products']['list'];
933 }
934
935 if ( empty( $products ) ) {
936 return;
937 }
938
939 $final_price = 0;
940
941 foreach ( $cart->get_cart() as $key => $details ) {
942 $item_id = $details['product_id'];
943 $price = false;
944
945 if ( ! empty( $details['variation_id'] ) ) {
946 $item_id = $details['variation_id'];
947 }
948
949 foreach ( $products as $product_id => $product_details ) {
950 if ( $item_id === $product_id ) {
951 $discount_type = $product_details['discountType'];
952 $discount_value = $product_details['discount'];
953
954 $price = Helper::calculate_discount( $item_id, $discount_type, $discount_value );
955 }
956 }
957
958 if ( false !== $price ) {
959 $details['data']->set_price( $price );
960 }
961
962 $final_price += $details['data']->get_price( $price ) * $details['quantity'];
963 }
964
965 self::modify_products_price_before_woo_apply_discounts( $final_price );
966 }
967
968 /**
969 * Modify cart subtotal before validating discounts.
970 *
971 * @since 1.2.8
972 * @param int $final_price cart new subtotal.
973 */
974 public static function modify_products_price_before_woo_apply_discounts( $final_price ) {
975 add_filter( 'woocommerce_coupon_validate_minimum_amount', function( $value, $coupon ) use ( $final_price ) {
976 return $coupon->get_minimum_amount() > $final_price;
977 }, 99, 2 );
978
979 add_filter( 'woocommerce_coupon_validate_maximum_amount', function( $value, $coupon ) use ( $final_price ) {
980 return $coupon->get_maximum_amount() < $final_price;
981 }, 99, 2 );
982 }
983
984 /**
985 * Fix shipping method price on checkout ajax state change.
986 *
987 * @param string $data checkout form data.
988 * @since 1.1.0
989 * @return void
990 */
991 public function sellkit_checkout_during_ajax( $data ) {
992 $data = explode( '&', $data );
993 $clear = [];
994
995 foreach ( $data as $key => $input ) {
996 $input = explode( '=', $input );
997 $clear[ $input[0] ] = $input[1];
998 }
999
1000 if ( ! array_key_exists( 'form_id', $clear ) ) {
1001 return;
1002 }
1003
1004 $this->apply_template_replacement_during_ajax( $clear );
1005
1006 $_POST = Helper::instance()->assigning_default_ajax_shipping_fields( $_POST, $clear ); // phpcs:ignore
1007
1008 Local_Hooks::order_bump( $clear );
1009
1010 if ( array_key_exists( 'sellkit-bundle-products', $clear ) ) {
1011 $option = $clear['sellkit-bundle-products'];
1012
1013 Local_Hooks::bundle_product_action( $option );
1014 }
1015
1016 // Apply discounts.
1017 if ( ! array_key_exists( 'sellkit_current_page_id', $clear ) ) {
1018 return;
1019 }
1020
1021 self::apply_discounted_prices( wc()->cart, $clear['sellkit_current_page_id'] );
1022 self::apply_discounted_prices( wc()->cart, $clear['sellkit_current_page_id'], 'bumps' );
1023 }
1024
1025 /**
1026 * Template replacement during ajax call.
1027 *
1028 * @param array $data checkout form data.
1029 * @since 1.2.1
1030 */
1031 private function apply_template_replacement_during_ajax( $data ) {
1032 $widget_settings = Helper::instance()->retrieve_checkout_widget_settings( $data['post_id'], $data['form_id'] );
1033
1034 Local_Hooks::enable_disable_sections( $widget_settings, $data );
1035
1036 add_filter( 'wc_get_template', function( $located, $template_name ) {
1037 $our_path = sellkit()->plugin_dir() . 'includes/elementor/modules/checkout/templates/';
1038 $files = [
1039 'review-order.php',
1040 'payment-method.php',
1041 'payment.php',
1042 'form-checkout.php',
1043 'cart-item-data.php',
1044 'cart-shipping.php',
1045 'terms.php',
1046 ];
1047 $template = str_replace( 'checkout/', '', $template_name );
1048 $template = str_replace( 'cart/', '', $template );
1049
1050 if ( in_array( $template, $files, true ) ) {
1051 $located = $our_path . $template;
1052 }
1053
1054 return $located;
1055 }, 10, 2 );
1056 }
1057
1058 /**
1059 * Modify default selected shipping method.
1060 *
1061 * @since 1.2.8
1062 * @param string $default default selected method.
1063 * @return string
1064 */
1065 public function sellkit_default_shipping_method( $default ) {
1066 $applied_coupons = WC()->cart->get_applied_coupons();
1067
1068 if ( count( $applied_coupons ) < 1 ) {
1069 return $default;
1070 }
1071
1072 foreach ( $applied_coupons as $coupon_code ) {
1073
1074 $coupon = new \WC_Coupon( $coupon_code );
1075
1076 if ( $coupon->get_free_shipping() ) {
1077
1078 if ( count( WC()->session->get( 'shipping_for_package_0' )['rates'] ) > 0 ) {
1079 // Loop through.
1080 foreach ( WC()->session->get( 'shipping_for_package_0' )['rates'] as $rate_id => $rate ) {
1081 // For free shipping.
1082 if ( 'free_shipping' === $rate->method_id ) {
1083 return $rate_id;
1084 }
1085 }
1086 }
1087 }
1088 }
1089
1090 return $default;
1091 }
1092
1093 /**
1094 * Recalculate checkout and cart prices.
1095 *
1096 * @since 1.2.8
1097 */
1098 public function before_cart_calculate() {
1099 // More than twice isn't necessary. this is called multiple times by WooCommerce.
1100 if ( did_action( 'woocommerce_before_calculate_totals' ) >= 2 && wp_doing_ajax() ) {
1101 return;
1102 }
1103
1104 // Gather checkout id.
1105 $checkout_id = get_queried_object_id();
1106
1107 if ( wp_doing_ajax() ) {
1108 // First try to catch id using our ajax call.
1109 $checkout_id = filter_input( INPUT_POST, 'related_checkout', FILTER_SANITIZE_NUMBER_INT );
1110
1111 // Second try to catch id using woocommerce ajax.
1112 if ( empty( $checkout_id ) ) {
1113 $checkout_id = filter_input( INPUT_POST, 'sellkit_current_page_id', FILTER_SANITIZE_NUMBER_INT );
1114 }
1115
1116 // Catch id after pressing place order button. none of above methods worked.
1117 if ( empty( $checkout_id ) ) {
1118 $data = filter_input( INPUT_POST, 'post_data', FILTER_DEFAULT );
1119
1120 parse_str( $data, $data );
1121
1122 if ( array_key_exists( 'sellkit_current_page_id', $data ) ) {
1123 $checkout_id = $data['sellkit_current_page_id'];
1124 }
1125 }
1126 }
1127
1128 // Empty id ? no action.
1129 if ( empty( $checkout_id ) ) {
1130 return;
1131 }
1132
1133 $checkout_id = (int) $checkout_id;
1134
1135 // Apply funnel prices.
1136 self::apply_discounted_prices( wc()->cart, $checkout_id );
1137 self::apply_discounted_prices( wc()->cart, $checkout_id, 'bumps' );
1138
1139 // We should re apply coupons after each price changes. to make sure everything is correct.
1140 $optimization_data = ! empty( $funnel_data['data']['optimization'] ) ? $funnel_data['data']['optimization'] : '';
1141
1142 if ( Checkout::apply_coupon_validation( $optimization_data ) ) {
1143 WC()->cart->remove_coupons();
1144
1145 foreach ( $optimization_data['auto_apply_coupons'] as $auto_apply_coupon ) {
1146 wc()->cart->add_discount( get_the_title( $auto_apply_coupon['value'] ) );
1147 }
1148
1149 wc_clear_notices();
1150 }
1151 }
1152
1153 /**
1154 * Check sellkit step through ajax.
1155 * And in order decide to show upsell or downsell steps through a popup.
1156 *
1157 * @since 1.6.2
1158 */
1159 public function call_funnel_popups() {
1160 $step = filter_input( INPUT_POST, 'step', FILTER_SANITIZE_NUMBER_INT );
1161 $funnel = new Sellkit_Funnel( $step );
1162 $next_step = $funnel->next_step_data;
1163 $popups = [ 'upsell', 'downsell' ];
1164
1165 if ( 'decision' === $funnel->next_step_data['type']['key'] ) {
1166 $this->take_care_of_decision_step( $funnel->next_step_data['page_id'] );
1167 return;
1168 }
1169
1170 if ( in_array( $funnel->next_step_data['type']['key'], $popups, true ) ) {
1171 wp_send_json_success( [
1172 'next_id' => $next_step['page_id'],
1173 'next_type' => $next_step['type']['key'],
1174 ] );
1175 }
1176 }
1177
1178 /**
1179 * Gets step id before the decision step and return result.
1180 *
1181 * @param int $step_id id of the step before decision step.
1182 * @since 1.6.2
1183 */
1184 private function take_care_of_decision_step( $step_id ) {
1185 $funnel = new Sellkit_Funnel( $step_id );
1186 $conditions = ! empty( $funnel->current_step_data['data']['conditions'] ) ? $funnel->current_step_data['data']['conditions'] : [];
1187 $is_valid = sellkit_conditions_validation( $conditions );
1188 $next_step = $funnel->next_no_step_data;
1189
1190 if ( $is_valid ) {
1191 $next_step = $funnel->next_step_data;
1192 }
1193
1194 if ( 'decision' === $next_step['type']['key'] ) {
1195 $this->take_care_of_decision_step( $next_step['page_id'] );
1196
1197 return;
1198 }
1199
1200 wp_send_json_success( [
1201 'next_id' => $next_step['page_id'],
1202 'next_type' => $next_step['type']['key'],
1203 ] );
1204 }
1205
1206 /**
1207 * Perform upsell popup accept button.
1208 *
1209 * @since 1.6.2
1210 */
1211 public function perform_upsell_accept_button() {
1212 // Gather and validate information.
1213 $upsell_id = filter_input( INPUT_POST, 'upsell_id', FILTER_SANITIZE_NUMBER_INT );
1214 $checkout_id = filter_input( INPUT_POST, 'checkout_id', FILTER_SANITIZE_NUMBER_INT );
1215 $response = [];
1216
1217 if ( empty( $upsell_id ) ) {
1218 wp_send_json_error( esc_html__( 'Empty Upsell ID.', 'sellkit' ) );
1219 }
1220
1221 $upsell_data = get_post_meta( $upsell_id, 'step_data', true );
1222
1223 if ( empty( $upsell_data ) ) {
1224 wp_send_json_error( esc_html__( 'Empty Step Data.', 'sellkit' ) );
1225 }
1226
1227 // Add product to cart.
1228 $product_id = array_key_first( $upsell_data['data']['products']['list'] );
1229 $qty = $upsell_data['data']['products']['list'][ $product_id ]['quantity'];
1230
1231 if ( empty( $qty ) ) {
1232 $qty = 1;
1233 }
1234
1235 WC()->cart->add_to_cart( $product_id, $qty );
1236 // Take care of cart and applied discounts.
1237 self::apply_discounted_prices( WC()->cart, $checkout_id, 'upsell', $upsell_data );
1238 self::make_changes_after_cart_item_edit( $checkout_id );
1239 // Response.
1240 $funnel = new Sellkit_Funnel( $upsell_id );
1241 $next_step = $funnel->next_step_data;
1242
1243 // Step with empty data, will be redirected to thankyou page.
1244 if ( empty( $next_step ) ) {
1245 wp_send_json_success(
1246 [
1247 'next_id' => $funnel->end_node_step_data['page_id'],
1248 'next_type' => $funnel->end_node_step_data['type']['key'],
1249 ]
1250 );
1251 }
1252
1253 if ( 'decision' === $next_step['type']['key'] ) {
1254 $this->take_care_of_decision_step( $next_step['page_id'] );
1255 return;
1256 }
1257
1258 $response = [
1259 'next_id' => $next_step['page_id'],
1260 'next_type' => $next_step['type']['key'],
1261 ];
1262
1263 wp_send_json_success( $response );
1264 }
1265
1266 /**
1267 * Perform upsell popup reject button.
1268 *
1269 * @since 1.6.2
1270 */
1271 public function perform_upsell_reject_button() {
1272 $upsell_id = filter_input( INPUT_POST, 'upsell_id', FILTER_SANITIZE_NUMBER_INT );
1273
1274 if ( empty( $upsell_id ) ) {
1275 wp_send_json_error( esc_html__( 'Empty Upsell ID.', 'sellkit' ) );
1276 }
1277
1278 $funnel = new Sellkit_Funnel( $upsell_id );
1279 $next_step = $funnel->next_no_step_data;
1280
1281 // Step with empty data, will be redirected to thankyou page.
1282 if ( empty( $next_step ) ) {
1283 wp_send_json_success(
1284 [
1285 'next_id' => $funnel->end_node_step_data['page_id'],
1286 'next_type' => $funnel->end_node_step_data['type']['key'],
1287 ]
1288 );
1289 }
1290
1291 if ( 'decision' === $next_step['type']['key'] ) {
1292 $this->take_care_of_decision_step( $next_step['page_id'] );
1293 return;
1294 }
1295
1296 $response = [
1297 'next_id' => $next_step['page_id'],
1298 'next_type' => $next_step['type']['key'],
1299 ];
1300
1301 wp_send_json_success( $response );
1302 }
1303 }
1304