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

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