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

1,226 lines 35.4 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 = '';
316 if ( $cart_items[ $cart_item_key ] === end( $cart_items ) ) {
317 $extra_class = ' last-cart-item';
318 }
319 ?>
320 <tr class="<?php echo esc_attr( apply_filters( 'woocommerce_cart_item_class', 'cart_item', $cart_item, $cart_item_key ) ) . $extra_class; ?>">
321
322 <td class="product-name sellkit-one-page-checkout-product-name">
323 <?php
324 echo $img_html;
325
326 $fix_style = '';
327
328 if ( '' === $img_html ) {
329 $fix_style = 'margin-left: 0px;';
330 }
331 ?>
332
333 <div class="name-price" style="<?php echo esc_attr( $fix_style ); ?>">
334 <span style="display:inline-block">
335 <?php echo wp_kses_post( apply_filters( 'woocommerce_cart_item_name', $product->get_name(), $cart_item, $cart_item_key ) ) . '&nbsp;'; ?>
336 </span>
337 <span class="sellkit-checkout-variations">
338 <?php echo wc_get_formatted_cart_item_data( $cart_item ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
339 </span>
340 <?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'] ); ?>
341 <?php do_action( 'sellkit-checkout-editor-mode-quantity', $cart_item['quantity'] ); ?>
342 </div>
343 </td>
344 <td class="product-total sellkit-one-page-checkout-product-price">
345 <?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 ?>
346 </td>
347 </tr>
348 <?php
349 $cart_item = ob_get_clean();
350
351 echo apply_filters( 'sellkit-checkout-cart-item' , $cart_item );
352 //phpcs:enable
353 }
354
355 /**
356 * Modify fields after pressing place order button.
357 * we hooked here because some removed fields still get checked and have not removed by previous hooks.
358 *
359 * @SuppressWarnings(PHPMD.NPathComplexity)
360 * @param array $data checkout form data.
361 * @return array $data data of woocommerce checkout form.
362 * @since 1.1.0
363 */
364 public function modify_order_data_before_validate( $data ) {
365 $post_id = filter_input( INPUT_POST, 'post_id', FILTER_SANITIZE_NUMBER_INT );
366 $form_id = filter_input( INPUT_POST, 'form_id', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
367
368 if ( empty( $post_id ) && empty( $form_id ) ) {
369 return $data;
370 }
371
372 $settings = Helper::instance()->retrieve_checkout_widget_settings( $post_id, $form_id );
373
374 if ( empty( $settings ) ) {
375 return $data;
376 }
377
378 $widget_shipping_field = Helper::instance()->get_user_defined_fields_slug( $settings['shipping_list'], 'shipping_list_field' );
379 $widget_billing_field = Helper::instance()->get_user_defined_fields_slug( $settings['billing_list'], 'billing_list_field' );
380
381 $default_shipping_fields = Helper::instance()->shipping_fields();
382 $default_billing_fields = Helper::instance()->billing_fields();
383
384 // Unset removed shipping fields.
385 foreach ( $default_shipping_fields as $field => $details ) {
386 if ( ! in_array( $field, $widget_shipping_field, true ) ) {
387 unset( $data[ $field ] );
388 }
389 }
390
391 // Unset removed billing fields.
392 foreach ( $default_billing_fields as $field => $details ) {
393 if ( ! in_array( $field, $widget_billing_field, true ) && 'billing_email' !== $field ) {
394 unset( $data[ $field ] );
395 }
396 }
397
398 $mail = filter_input( INPUT_POST, 'billing_email', FILTER_SANITIZE_EMAIL );
399
400 if ( ! array_key_exists( 'billing_email', $data ) && ! empty( $mail ) ) {
401 $data['billing_email'] = $mail;
402 }
403
404 return $data;
405 }
406
407 /**
408 * Login user by ajax.
409 * sign user in by using form included in widget.
410 *
411 * @return void
412 * @since 1.1.0
413 */
414 public function auth_user() {
415 $email = filter_input( INPUT_POST, 'email', FILTER_SANITIZE_EMAIL );
416 $pass = filter_input( INPUT_POST, 'pass', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
417 $verify = filter_var( $email, FILTER_VALIDATE_EMAIL );
418
419 if ( ! $verify || empty( $email ) || empty( $pass ) ) {
420 wp_send_json_error( __( 'Email or password field is not valid.', 'sellkit' ) );
421 }
422
423 $result = wp_authenticate( $email, $pass );
424
425 if ( is_wp_error( $result ) ) {
426 wp_send_json_error( $result->get_error_message() );
427 }
428
429 wp_clear_auth_cookie();
430 wp_set_current_user( $result->ID );
431 wp_set_auth_cookie( $result->ID );
432
433 wp_send_json_success( $result );
434 }
435
436 /**
437 * Get widget settings in ajax calls using form and post id.
438 *
439 * @return void
440 * @since 1.1.0
441 */
442 private function widget_settings() {
443 $form_id = filter_input( INPUT_POST, 'form_id', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
444 $post_id = filter_input( INPUT_POST, 'post_id', FILTER_SANITIZE_NUMBER_INT );
445
446 if ( empty( $form_id ) && empty( $post_id ) ) {
447 return;
448 }
449
450 $widget_settings = Helper::instance()->retrieve_checkout_widget_settings( $post_id, $form_id );
451
452 return $widget_settings;
453 }
454
455 /**
456 * Validate user defined custom value.
457 *
458 * @return void
459 * @since 1.1.0
460 */
461 public function validate_user_defined_fields() {
462 $widget_settings = $this->widget_settings();
463
464 if ( empty( $widget_settings ) ) {
465 return;
466 }
467
468 $widget_shipping_fields = $widget_settings['shipping_list'];
469 $widget_billing_fields = $widget_settings['billing_list'];
470
471 Helper::instance()->validate_user_defined_fields( $widget_shipping_fields, $widget_billing_fields );
472 Helper::instance()->make_sure_to_convert_required_field_to_optional( $widget_shipping_fields, $widget_billing_fields );
473 }
474
475 /**
476 * Save user defined custom fields value to database.
477 *
478 * @param int $order_id id of woocommerce order.
479 * @return void
480 * @since 1.1.0
481 */
482 public function save_user_defined_fields( $order_id ) {
483 $widget_settings = $this->widget_settings();
484
485 if ( empty( $widget_settings ) ) {
486 return;
487 }
488
489 $widget_shipping_fields = $widget_settings['shipping_list'];
490 $widget_billing_fields = $widget_settings['billing_list'];
491
492 Helper::instance()->save_user_defined_fields( $widget_shipping_fields, $widget_billing_fields, $order_id );
493 }
494
495 /**
496 * Attach user defined shipping field values to order emails.
497 *
498 * @param string $address address.
499 * @param string $raw_address raw address.
500 * @param object $order woocommerce order object.
501 * @return string
502 * @since 1.1.0
503 */
504 public function attach_user_shipping_fields_to_order_email( $address, $raw_address, $order ) {
505 $order_id = $order->get_id();
506 $custom_fields = get_post_meta( $order_id, 'sellkit_checkout_widget_custom_field_of_order', true );
507
508 if ( empty( $custom_fields ) || ! is_array( $custom_fields ) ) {
509 return $address;
510 }
511
512 foreach ( $custom_fields as $field ) {
513 if ( 'yes' !== $field['show_in_email'] ) {
514 continue;
515 }
516
517 $key = $field['key_name'];
518 $value = get_post_meta( $order_id, $key, true );
519
520 if ( strpos( $key, 'shipping' ) !== false ) {
521 $address .= '<br>' . $value;
522 }
523 }
524
525 return $address;
526 }
527
528 /**
529 * Attach user defined billing field values to order emails.
530 *
531 * @param string $address address.
532 * @param string $raw_address raw address.
533 * @param object $order woocommerce order object.
534 * @return string
535 * @since 1.1.0
536 */
537 public function attach_user_billing_fields_to_order_email( $address, $raw_address, $order ) {
538 $order_id = $order->get_id();
539 $custom_fields = get_post_meta( $order_id, 'sellkit_checkout_widget_custom_field_of_order', true );
540
541 if ( empty( $custom_fields ) || ! is_array( $custom_fields ) ) {
542 return $address;
543 }
544
545 foreach ( $custom_fields as $field ) {
546 if ( 'yes' !== $field['show_in_email'] ) {
547 continue;
548 }
549
550 $key = $field['key_name'];
551 $value = get_post_meta( $order_id, $key, true );
552
553 if ( strpos( $key, 'billing' ) !== false ) {
554 $address .= '<br>' . $value;
555 }
556 }
557
558 return $address;
559 }
560
561 /**
562 * Place custom coupon form in checkout order-review based design.
563 * We have used same form in Local_Hooks. but this one is for ajax response.
564 *
565 * @see sellkit_Core\Raven\Modules\Checkout\Classes\Local_Hooks::coupon_form.
566 * @param array $settings widget settings.
567 * @return void
568 * @since 1.1.0
569 */
570 public function coupon_form( $settings ) {
571 if ( array_key_exists( 'show_coupon_field', $settings ) ) {
572 return;
573 }
574
575 echo '<tr class="coupon-form border-none"><td colspan="2">';
576 Local_Hooks::form( $settings );
577 echo '</td></tr>';
578
579 do_action( 'sellkit-checkout-after-coupon-form-ajax' );
580 }
581
582 /**
583 * Look for an email if exists or not.
584 * Is used for login process.
585 *
586 * @return void
587 * @since 1.1.0
588 */
589 public function search_for_email() {
590 $email = filter_input( INPUT_POST, 'email', FILTER_SANITIZE_EMAIL );
591 $valid = filter_var( $email, FILTER_VALIDATE_EMAIL );
592
593 if ( ! $valid || empty( $email ) ) {
594 wp_send_json_error();
595 }
596
597 $check = email_exists( $email );
598
599 if ( false === $check ) {
600 wp_send_json_error();
601 }
602
603 wp_send_json_success();
604 }
605
606 /**
607 * Look for an username if exists or not.
608 * Is used for register process.
609 *
610 * @return void
611 * @since 1.1.0
612 */
613 public function search_for_username() {
614 $username = filter_input( INPUT_POST, 'user', FILTER_SANITIZE_EMAIL );
615
616 if ( empty( $username ) ) {
617 wp_send_json_error();
618 }
619
620 $username = sanitize_user( $username );
621 $check = username_exists( $username );
622
623 if ( false !== $check ) {
624 wp_send_json_error();
625 }
626
627 wp_send_json_success();
628 }
629
630 /**
631 * Validate postcode via ajax.
632 *
633 * @since 1.1.0
634 * @return void
635 */
636 public function validate_postcode() {
637 $country = filter_input( INPUT_POST, 'country_code', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
638 $postcode = filter_input( INPUT_POST, 'post_code', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
639 $parent = filter_input( INPUT_POST, 'parent', FILTER_DEFAULT );
640
641 $postcode = wc_format_postcode( $postcode, $country );
642 $valid = \WC_Validation::is_postcode( $postcode, $country );
643
644 if ( ! $valid ) {
645 wp_send_json_error( $parent );
646 }
647
648 wp_send_json_success( $parent );
649 }
650
651 /**
652 * Validate phone number via ajax.
653 *
654 * @since 1.1.0
655 * @return void
656 */
657 public function validate_phone_number() {
658 $phone = filter_input( INPUT_POST, 'phone_number', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
659 $parent = filter_input( INPUT_POST, 'parent', FILTER_DEFAULT );
660
661 $valid = \WC_Validation::is_phone( $phone );
662
663 if ( ! $valid ) {
664 wp_send_json_error( $parent );
665 }
666
667 wp_send_json_success( $parent );
668 }
669
670 /**
671 * Simulate checkout page where our widget is present. using empty shortcode [sellkit_checkout_widget_simulated].
672 *
673 * @since 1.1.0
674 * @return void
675 */
676 public function simulate_our_widget_page_as_checkout() {
677 $page_id = get_the_ID();
678 $content = get_post_field( 'post_content', $page_id );
679
680 if ( false === strpos( $content, 'woocommerce_checkout' ) ) {
681 return;
682 }
683
684 add_filter( 'woocommerce_is_checkout', function() {
685 return true;
686 } );
687
688 if ( false === strpos( $content, 'sellkit_checkout_widget_simulated' ) ) {
689 return;
690 }
691
692 add_filter( 'theme_mod_jupiterx_jupiterx_checkout_cart_elements', function() {
693 return [];
694 }, 10 );
695 }
696
697 /**
698 * Integrate user country with our widget.
699 *
700 * @since 1.1.0
701 * @return void
702 */
703 public function set_customer_details_ajax() {
704 $country = filter_input( INPUT_POST, 'country', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
705 $state = filter_input( INPUT_POST, 'state', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
706 $shipping_country = filter_input( INPUT_POST, 'shipping_country', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
707 $shipping_state = filter_input( INPUT_POST, 'shipping_state', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
708
709 if ( ! empty( $country ) ) {
710 setcookie( 'sellkit-checkout-billing-cc', $country, time() + ( 86400 * 3 ), '/' );
711 setcookie( 'sellkit-checkout-billing-state', $state, time() + ( 86400 * 3 ), '/' );
712 }
713
714 if ( ! empty( $shipping_country ) ) {
715 setcookie( 'sellkit-checkout-shipping-cc', $shipping_country, time() + ( 86400 * 3 ), '/' );
716 setcookie( 'sellkit-checkout-shipping-state', $shipping_state, time() + ( 86400 * 3 ), '/' );
717 }
718
719 wp_send_json_success();
720 }
721
722 /**
723 * State lookup using postcode and country.
724 *
725 * @since 1.1.0
726 * @return void
727 */
728 public function sellkit_state_lookup_by_postcode() {
729 $country = filter_input( INPUT_POST, 'country_value', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
730 $postcode = filter_input( INPUT_POST, 'postcode_value', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
731 $api = 'https://api.zippopotam.us/' . $country . '/' . $postcode;
732
733 if ( empty( $country ) || empty( $postcode ) ) {
734 wp_send_json_error( esc_html__( 'Not valid inputs', 'sellkit' ) );
735 }
736
737 $response = wp_remote_get( $api );
738
739 if ( is_wp_error( $response ) || ! is_array( $response ) ) {
740 wp_send_json_error( esc_html__( 'Server error.', 'sellkit' ) );
741 }
742
743 if ( '{}' === $response['body'] ) {
744 // Country or postcode is not supported. it's kina an error but we show nothing in frontend.
745 wp_send_json_error( '' );
746 }
747
748 $body = json_decode( $response['body'], true );
749
750 wp_send_json_success( $body );
751 }
752
753 /**
754 * Modify cart contents using bundled products.
755 *
756 * @since 1.1.0
757 * @return void
758 */
759 public function sellkit_checkout_modify_cart_by_bundle_products() {
760 $qty = filter_input( INPUT_POST, 'qty', FILTER_SANITIZE_NUMBER_INT );
761 $id = filter_input( INPUT_POST, 'id', FILTER_SANITIZE_NUMBER_INT );
762 $type = filter_input( INPUT_POST, 'type', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
763 $checkout_id = filter_input( INPUT_POST, 'checkout_id', FILTER_SANITIZE_NUMBER_INT );
764 $modify = filter_input( INPUT_POST, 'modify', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
765 $key = filter_input( INPUT_POST, 'key', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
766
767 if ( ! empty( $key ) ) {
768 foreach ( WC()->cart->get_cart() as $item_key => $item ) {
769 if ( $item_key === $key ) {
770 WC()->cart->set_quantity( $item_key, $qty, true );
771 break; // stop the loop.
772 }
773 }
774
775 wp_send_json_success();
776 }
777
778 if ( 'radio' === $type ) {
779 $step_data = get_post_meta( $checkout_id, 'step_data', true );
780 $products = $step_data['data']['products']['list'];
781 $selected = $products[ $id ];
782 $real_quantity = ( empty( $selected['quantity'] ) ) ? 1 : $selected['quantity'];
783
784 if ( (int) $real_quantity !== (int) $qty ) {
785 wp_send_json_error( esc_html__( 'Oh do not be tricky.', 'sellkit' ) );
786 }
787 }
788
789 if ( 'radio' === $type ) {
790 WC()->cart->empty_cart();
791 WC()->cart->add_to_cart( $id, $qty );
792 }
793
794 if ( 'checkbox' === $type && 'add' === $modify ) {
795 WC()->cart->add_to_cart( $id, $qty );
796 }
797
798 if ( 'checkbox' === $type && 'remove' === $modify ) {
799 $post_type = get_post_type( $id );
800
801 // Maybe product is a variation.
802 if ( 'product_variation' === $post_type ) {
803 foreach ( WC()->cart->get_cart() as $item_key => $item ) {
804 if ( $item['variation_id'] === (int) $id ) {
805 WC()->cart->remove_cart_item( $item_key ); // we remove it.
806 break; // stop the loop.
807 }
808 }
809
810 self::make_changes_after_cart_item_edit( $checkout_id );
811
812 wp_send_json_success();
813 }
814
815 // Default product.
816 $product_cart_id = WC()->cart->generate_cart_id( $id );
817 $cart_item_key = WC()->cart->find_product_in_cart( $product_cart_id );
818
819 if ( $cart_item_key ) {
820 WC()->cart->remove_cart_item( $cart_item_key );
821 }
822 }
823
824 self::make_changes_after_cart_item_edit( $checkout_id );
825
826 wp_send_json_success();
827 }
828
829 /**
830 * Apply funnel discount.
831 * Will be called twice, first when page is loading , second during checkout ajax.
832 *
833 * @param object $cart cart object.
834 * @param int $checkout_id_ajax checkout id.
835 * @param string $source source of products, bump or default products to be checked.
836 * @param array $upsell_data upsell product data coming from popup.
837 * @since 1.2.8
838 */
839 public static function apply_discounted_prices( $cart = [], $checkout_id_ajax = null, $source = 'default', $upsell_data = [] ) {
840 $checkout_id = get_queried_object_id();
841
842 if ( wp_doing_ajax() ) {
843 $checkout_id = $checkout_id_ajax;
844 }
845
846 if ( empty( $checkout_id ) ) {
847 return;
848 }
849
850 $step_data = get_post_meta( $checkout_id, 'step_data', true );
851
852 if ( empty( $step_data ) ) {
853 return;
854 }
855
856 $products = [];
857
858 if (
859 'default' === $source &&
860 array_key_exists( 'data', $step_data ) &&
861 array_key_exists( 'list', $step_data['data']['products'] )
862 ) {
863 $products = $step_data['data']['products']['list'];
864 }
865
866 if ( 'bumps' === $source && ! empty( $step_data['bump'] ) ) {
867 $bumps = $step_data['bump'];
868
869 foreach ( $bumps as $bump ) {
870 if ( ! array_key_exists( 'products', $bump['data'] ) ) {
871 continue;
872 }
873
874 $key = array_key_first( $bump['data']['products']['list'] );
875 $products[ $key ] = $bump['data']['products']['list'][ $key ];
876 }
877 }
878
879 if ( 'upsell' === $source ) {
880 $products = $upsell_data['data']['products']['list'];
881 }
882
883 if ( empty( $products ) ) {
884 return;
885 }
886
887 $final_price = 0;
888
889 foreach ( $cart->get_cart() as $key => $details ) {
890 $item_id = $details['product_id'];
891 $price = false;
892
893 if ( ! empty( $details['variation_id'] ) ) {
894 $item_id = $details['variation_id'];
895 }
896
897 foreach ( $products as $product_id => $product_details ) {
898 if ( $item_id === $product_id ) {
899 $discount_type = $product_details['discountType'];
900 $discount_value = $product_details['discount'];
901
902 $price = Helper::calculate_discount( $item_id, $discount_type, $discount_value );
903 }
904 }
905
906 if ( false !== $price ) {
907 $details['data']->set_price( $price );
908 }
909
910 $final_price += $details['data']->get_price( $price ) * $details['quantity'];
911 }
912
913 self::modify_products_price_before_woo_apply_discounts( $final_price );
914 }
915
916 /**
917 * Modify cart subtotal before validating discounts.
918 *
919 * @since 1.2.8
920 * @param int $final_price cart new subtotal.
921 */
922 public static function modify_products_price_before_woo_apply_discounts( $final_price ) {
923 add_filter( 'woocommerce_coupon_validate_minimum_amount', function( $value, $coupon ) use ( $final_price ) {
924 return $coupon->get_minimum_amount() > $final_price;
925 }, 99, 2 );
926
927 add_filter( 'woocommerce_coupon_validate_maximum_amount', function( $value, $coupon ) use ( $final_price ) {
928 return $coupon->get_maximum_amount() < $final_price;
929 }, 99, 2 );
930 }
931
932 /**
933 * Fix shipping method price on checkout ajax state change.
934 *
935 * @param string $data checkout form data.
936 * @since 1.1.0
937 * @return void
938 */
939 public function sellkit_checkout_during_ajax( $data ) {
940 $data = explode( '&', $data );
941 $clear = [];
942
943 foreach ( $data as $key => $input ) {
944 $input = explode( '=', $input );
945 $clear[ $input[0] ] = $input[1];
946 }
947
948 if ( ! array_key_exists( 'form_id', $clear ) ) {
949 return;
950 }
951
952 $this->apply_template_replacement_during_ajax( $clear );
953
954 $_POST = Helper::instance()->assigning_default_ajax_shipping_fields( $_POST, $clear ); // phpcs:ignore
955
956 Local_Hooks::order_bump( $clear );
957
958 if ( array_key_exists( 'sellkit-bundle-products', $clear ) ) {
959 $option = $clear['sellkit-bundle-products'];
960
961 Local_Hooks::bundle_product_action( $option );
962 }
963
964 // Apply discounts.
965 if ( ! array_key_exists( 'sellkit_current_page_id', $clear ) ) {
966 return;
967 }
968
969 self::apply_discounted_prices( wc()->cart, $clear['sellkit_current_page_id'] );
970 self::apply_discounted_prices( wc()->cart, $clear['sellkit_current_page_id'], 'bumps' );
971 }
972
973 /**
974 * Template replacement during ajax call.
975 *
976 * @param array $data checkout form data.
977 * @since 1.2.1
978 */
979 private function apply_template_replacement_during_ajax( $data ) {
980 $widget_settings = Helper::instance()->retrieve_checkout_widget_settings( $data['post_id'], $data['form_id'] );
981
982 Local_Hooks::enable_disable_sections( $widget_settings, $data );
983
984 add_filter( 'wc_get_template', function( $located, $template_name ) {
985 $our_path = sellkit()->plugin_dir() . 'includes/elementor/modules/checkout/templates/';
986 $files = [
987 'review-order.php',
988 'payment-method.php',
989 'payment.php',
990 'form-checkout.php',
991 'cart-item-data.php',
992 'cart-shipping.php',
993 'terms.php',
994 ];
995 $template = str_replace( 'checkout/', '', $template_name );
996 $template = str_replace( 'cart/', '', $template );
997
998 if ( in_array( $template, $files, true ) ) {
999 $located = $our_path . $template;
1000 }
1001
1002 return $located;
1003 }, 10, 2 );
1004 }
1005
1006 /**
1007 * Modify default selected shipping method.
1008 *
1009 * @since 1.2.8
1010 * @param string $default default selected method.
1011 * @return string
1012 */
1013 public function sellkit_default_shipping_method( $default ) {
1014 $applied_coupons = WC()->cart->get_applied_coupons();
1015
1016 if ( count( $applied_coupons ) < 1 ) {
1017 return $default;
1018 }
1019
1020 foreach ( $applied_coupons as $coupon_code ) {
1021
1022 $coupon = new \WC_Coupon( $coupon_code );
1023
1024 if ( $coupon->get_free_shipping() ) {
1025
1026 if ( count( WC()->session->get( 'shipping_for_package_0' )['rates'] ) > 0 ) {
1027 // Loop through.
1028 foreach ( WC()->session->get( 'shipping_for_package_0' )['rates'] as $rate_id => $rate ) {
1029 // For free shipping.
1030 if ( 'free_shipping' === $rate->method_id ) {
1031 return $rate_id;
1032 }
1033 }
1034 }
1035 }
1036 }
1037
1038 return $default;
1039 }
1040
1041 /**
1042 * Recalculate checkout and cart prices.
1043 *
1044 * @since 1.2.8
1045 */
1046 public function before_cart_calculate() {
1047 // More than twice isn't necessary. this is called multiple times by WooCommerce.
1048 if ( did_action( 'woocommerce_before_calculate_totals' ) >= 2 && wp_doing_ajax() ) {
1049 return;
1050 }
1051
1052 // Gather checkout id.
1053 $checkout_id = get_queried_object_id();
1054
1055 if ( wp_doing_ajax() ) {
1056 // First try to catch id using our ajax call.
1057 $checkout_id = filter_input( INPUT_POST, 'related_checkout', FILTER_SANITIZE_NUMBER_INT );
1058
1059 // Second try to catch id using woocommerce ajax.
1060 if ( empty( $checkout_id ) ) {
1061 $checkout_id = filter_input( INPUT_POST, 'sellkit_current_page_id', FILTER_SANITIZE_NUMBER_INT );
1062 }
1063
1064 // Catch id after pressing place order button. none of above methods worked.
1065 if ( empty( $checkout_id ) ) {
1066 $data = filter_input( INPUT_POST, 'post_data', FILTER_DEFAULT );
1067
1068 parse_str( $data, $data );
1069
1070 if ( array_key_exists( 'sellkit_current_page_id', $data ) ) {
1071 $checkout_id = $data['sellkit_current_page_id'];
1072 }
1073 }
1074 }
1075
1076 // Empty id ? no action.
1077 if ( empty( $checkout_id ) ) {
1078 return;
1079 }
1080
1081 $checkout_id = (int) $checkout_id;
1082
1083 // Apply funnel prices.
1084 self::apply_discounted_prices( wc()->cart, $checkout_id );
1085 self::apply_discounted_prices( wc()->cart, $checkout_id, 'bumps' );
1086
1087 // We should re apply coupons after each price changes. to make sure everything is correct.
1088 $optimization_data = ! empty( $funnel_data['data']['optimization'] ) ? $funnel_data['data']['optimization'] : '';
1089
1090 if ( Checkout::apply_coupon_validation( $optimization_data ) ) {
1091 WC()->cart->remove_coupons();
1092
1093 foreach ( $optimization_data['auto_apply_coupons'] as $auto_apply_coupon ) {
1094 wc()->cart->add_discount( get_the_title( $auto_apply_coupon['value'] ) );
1095 }
1096
1097 wc_clear_notices();
1098 }
1099 }
1100
1101 /**
1102 * Check sellkit step through ajax.
1103 * And in order decide to show upsell or downsell steps through a popup.
1104 *
1105 * @since 1.6.2
1106 */
1107 public function call_funnel_popups() {
1108 $step = filter_input( INPUT_POST, 'step', FILTER_SANITIZE_NUMBER_INT );
1109 $funnel = new Sellkit_Funnel( $step );
1110 $next_step = $funnel->next_step_data;
1111 $popups = [ 'upsell', 'downsell' ];
1112
1113 if ( 'decision' === $funnel->next_step_data['type']['key'] ) {
1114 $this->take_care_of_decision_step( $funnel->next_step_data['page_id'] );
1115 return;
1116 }
1117
1118 if ( in_array( $funnel->next_step_data['type']['key'], $popups, true ) ) {
1119 wp_send_json_success( [
1120 'next_id' => $next_step['page_id'],
1121 'next_type' => $next_step['type']['key'],
1122 ] );
1123 }
1124 }
1125
1126 /**
1127 * Gets step id before the decision step and return result.
1128 *
1129 * @param int $step_id id of the step before decision step.
1130 * @since 1.6.2
1131 */
1132 private function take_care_of_decision_step( $step_id ) {
1133 $funnel = new Sellkit_Funnel( $step_id );
1134 $conditions = ! empty( $funnel->current_step_data['data']['conditions'] ) ? $funnel->current_step_data['data']['conditions'] : [];
1135 $is_valid = sellkit_conditions_validation( $conditions );
1136 $next_step = $funnel->next_no_step_data;
1137
1138 if ( $is_valid ) {
1139 $next_step = $funnel->next_step_data;
1140 }
1141
1142 wp_send_json_success( [
1143 'next_id' => $next_step['page_id'],
1144 'next_type' => $next_step['type']['key'],
1145 ] );
1146 }
1147
1148 /**
1149 * Perform upsell popup accept button.
1150 *
1151 * @since 1.6.2
1152 */
1153 public function perform_upsell_accept_button() {
1154 // Gather and validate information.
1155 $upsell_id = filter_input( INPUT_POST, 'upsell_id', FILTER_SANITIZE_NUMBER_INT );
1156 $checkout_id = filter_input( INPUT_POST, 'checkout_id', FILTER_SANITIZE_NUMBER_INT );
1157 $response = [];
1158
1159 if ( empty( $upsell_id ) ) {
1160 wp_send_json_error( esc_html__( 'Empty Upsell ID.', 'sellkit' ) );
1161 }
1162
1163 $upsell_data = get_post_meta( $upsell_id, 'step_data', true );
1164
1165 if ( empty( $upsell_data ) ) {
1166 wp_send_json_error( esc_html__( 'Empty Step Data.', 'sellkit' ) );
1167 }
1168
1169 // Add product to cart.
1170 $product_id = array_key_first( $upsell_data['data']['products']['list'] );
1171 $qty = $upsell_data['data']['products']['list'][ $product_id ]['quantity'];
1172
1173 if ( empty( $qty ) ) {
1174 $qty = 1;
1175 }
1176
1177 WC()->cart->add_to_cart( $product_id, $qty );
1178 // Take care of cart and applied discounts.
1179 self::apply_discounted_prices( WC()->cart, $checkout_id, 'upsell', $upsell_data );
1180 self::make_changes_after_cart_item_edit( $checkout_id );
1181 // Response.
1182 $funnel = new Sellkit_Funnel( $upsell_id );
1183 $next_step = $funnel->next_step_data;
1184
1185 if ( 'decision' === $next_step['type']['key'] ) {
1186 $this->take_care_of_decision_step( $next_step['page_id'] );
1187 return;
1188 }
1189
1190 $response = [
1191 'next_id' => $next_step['page_id'],
1192 'next_type' => $next_step['type']['key'],
1193 ];
1194
1195 wp_send_json_success( $response );
1196 }
1197
1198 /**
1199 * Perform upsell popup reject button.
1200 *
1201 * @since 1.6.2
1202 */
1203 public function perform_upsell_reject_button() {
1204 $upsell_id = filter_input( INPUT_POST, 'upsell_id', FILTER_SANITIZE_NUMBER_INT );
1205
1206 if ( empty( $upsell_id ) ) {
1207 wp_send_json_error( esc_html__( 'Empty Upsell ID.', 'sellkit' ) );
1208 }
1209
1210 $funnel = new Sellkit_Funnel( $upsell_id );
1211 $next_step = $funnel->next_no_step_data;
1212
1213 if ( 'decision' === $next_step['type']['key'] ) {
1214 $this->take_care_of_decision_step( $next_step['page_id'] );
1215 return;
1216 }
1217
1218 $response = [
1219 'next_id' => $next_step['page_id'],
1220 'next_type' => $next_step['type']['key'],
1221 ];
1222
1223 wp_send_json_success( $response );
1224 }
1225 }
1226