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

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