PluginProbe
SellKit – Funnel builder and checkout optimizer for WooCommerce to sell more, faster / 1.5.8
SellKit – Funnel builder and checkout optimizer for WooCommerce to sell more, faster v1.5.8
2.7.0 2.6.0 trunk 1.1.0 1.1.4 1.2.1 1.2.2 1.2.3 1.2.5 1.2.9 1.3.1 1.3.2 1.5.0 1.5.1 1.5.4 1.5.7 1.5.8 1.5.9 1.6.2 1.6.5 1.6.8 1.7.2 1.7.4 1.7.5 1.7.9 All 43 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.8, at includes/elementor/modules/checkout/classes/global-hooks.php

1,095 lines 31.7 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 (
857 'default' === $source &&
858 array_key_exists( 'data', $step_data ) &&
859 array_key_exists( 'list', $step_data['data']['products'] )
860 ) {
861 $products = $step_data['data']['products']['list'];
862 }
863
864 if ( 'bumps' === $source && ! empty( $step_data['bump'] ) ) {
865 $bumps = $step_data['bump'];
866
867 foreach ( $bumps as $bump ) {
868 if ( ! array_key_exists( 'products', $bump['data'] ) ) {
869 continue;
870 }
871
872 $key = array_key_first( $bump['data']['products']['list'] );
873 $products[ $key ] = $bump['data']['products']['list'][ $key ];
874 }
875 }
876
877 if ( empty( $products ) ) {
878 return;
879 }
880
881 $final_price = 0;
882
883 foreach ( $cart->get_cart() as $key => $details ) {
884 $item_id = $details['product_id'];
885 $price = false;
886
887 if ( ! empty( $details['variation_id'] ) ) {
888 $item_id = $details['variation_id'];
889 }
890
891 foreach ( $products as $product_id => $product_details ) {
892 if ( $item_id === $product_id ) {
893 $discount_type = $product_details['discountType'];
894 $discount_value = $product_details['discount'];
895
896 $price = Helper::calculate_discount( $item_id, $discount_type, $discount_value );
897 }
898 }
899
900 if ( false !== $price ) {
901 $details['data']->set_price( $price );
902 }
903
904 $final_price += $details['data']->get_price( $price ) * $details['quantity'];
905 }
906
907 self::modify_products_price_before_woo_apply_discounts( $final_price );
908 }
909
910 /**
911 * Modify cart subtotal before validating discounts.
912 *
913 * @since 1.2.8
914 * @param int $final_price cart new subtotal.
915 */
916 public static function modify_products_price_before_woo_apply_discounts( $final_price ) {
917 add_filter( 'woocommerce_coupon_validate_minimum_amount', function( $value, $coupon ) use ( $final_price ) {
918 return $coupon->get_minimum_amount() > $final_price;
919 }, 99, 2 );
920
921 add_filter( 'woocommerce_coupon_validate_maximum_amount', function( $value, $coupon ) use ( $final_price ) {
922 return $coupon->get_maximum_amount() < $final_price;
923 }, 99, 2 );
924 }
925
926 /**
927 * Fix shipping method price on checkout ajax state change.
928 *
929 * @param string $data checkout form data.
930 * @since 1.1.0
931 * @return void
932 */
933 public function sellkit_checkout_during_ajax( $data ) {
934 $data = explode( '&', $data );
935 $clear = [];
936
937 foreach ( $data as $key => $input ) {
938 $input = explode( '=', $input );
939 $clear[ $input[0] ] = $input[1];
940 }
941
942 if ( ! array_key_exists( 'form_id', $clear ) ) {
943 return;
944 }
945
946 $this->apply_template_replacement_during_ajax( $clear );
947
948 $_POST = Helper::instance()->assigning_default_ajax_shipping_fields( $_POST, $clear ); // phpcs:ignore
949
950 Local_Hooks::order_bump( $clear );
951
952 if ( array_key_exists( 'sellkit-bundle-products', $clear ) ) {
953 $option = $clear['sellkit-bundle-products'];
954
955 Local_Hooks::bundle_product_action( $option );
956 }
957
958 // Apply discounts.
959 if ( ! array_key_exists( 'sellkit_current_page_id', $clear ) ) {
960 return;
961 }
962
963 self::apply_discounted_prices( wc()->cart, $clear['sellkit_current_page_id'] );
964 self::apply_discounted_prices( wc()->cart, $clear['sellkit_current_page_id'], 'bumps' );
965 }
966
967 /**
968 * Template replacement during ajax call.
969 *
970 * @param array $data checkout form data.
971 * @since 1.2.1
972 */
973 private function apply_template_replacement_during_ajax( $data ) {
974 $widget_settings = Helper::instance()->retrieve_checkout_widget_settings( $data['post_id'], $data['form_id'] );
975
976 Local_Hooks::enable_disable_sections( $widget_settings, $data );
977
978 add_filter( 'wc_get_template', function( $located, $template_name ) {
979 $our_path = sellkit()->plugin_dir() . 'includes/elementor/modules/checkout/templates/';
980 $files = [
981 'review-order.php',
982 'payment-method.php',
983 'payment.php',
984 'form-checkout.php',
985 'cart-item-data.php',
986 'cart-shipping.php',
987 'terms.php',
988 ];
989 $template = str_replace( 'checkout/', '', $template_name );
990 $template = str_replace( 'cart/', '', $template );
991
992 if ( in_array( $template, $files, true ) ) {
993 $located = $our_path . $template;
994 }
995
996 return $located;
997 }, 10, 2 );
998 }
999
1000 /**
1001 * Modify default selected shipping method.
1002 *
1003 * @since 1.2.8
1004 * @param string $default default selected method.
1005 * @return string
1006 */
1007 public function sellkit_default_shipping_method( $default ) {
1008 $applied_coupons = WC()->cart->get_applied_coupons();
1009
1010 if ( count( $applied_coupons ) < 1 ) {
1011 return $default;
1012 }
1013
1014 foreach ( $applied_coupons as $coupon_code ) {
1015
1016 $coupon = new \WC_Coupon( $coupon_code );
1017
1018 if ( $coupon->get_free_shipping() ) {
1019
1020 if ( count( WC()->session->get( 'shipping_for_package_0' )['rates'] ) > 0 ) {
1021 // Loop through.
1022 foreach ( WC()->session->get( 'shipping_for_package_0' )['rates'] as $rate_id => $rate ) {
1023 // For free shipping.
1024 if ( 'free_shipping' === $rate->method_id ) {
1025 return $rate_id;
1026 }
1027 }
1028 }
1029 }
1030 }
1031
1032 return $default;
1033 }
1034
1035 /**
1036 * Recalculate checkout and cart prices.
1037 *
1038 * @since 1.2.8
1039 */
1040 public function before_cart_calculate() {
1041 // More than twice isn't necessary. this is called multiple times by WooCommerce.
1042 if ( did_action( 'woocommerce_before_calculate_totals' ) >= 2 && wp_doing_ajax() ) {
1043 return;
1044 }
1045
1046 // Gather checkout id.
1047 $checkout_id = get_queried_object_id();
1048
1049 if ( wp_doing_ajax() ) {
1050 // First try to catch id using our ajax call.
1051 $checkout_id = filter_input( INPUT_POST, 'related_checkout', FILTER_SANITIZE_NUMBER_INT );
1052
1053 // Second try to catch id using woocommerce ajax.
1054 if ( empty( $checkout_id ) ) {
1055 $checkout_id = filter_input( INPUT_POST, 'sellkit_current_page_id', FILTER_SANITIZE_NUMBER_INT );
1056 }
1057
1058 // Catch id after pressing place order button. none of above methods worked.
1059 if ( empty( $checkout_id ) ) {
1060 $data = filter_input( INPUT_POST, 'post_data', FILTER_DEFAULT );
1061
1062 parse_str( $data, $data );
1063
1064 if ( array_key_exists( 'sellkit_current_page_id', $data ) ) {
1065 $checkout_id = $data['sellkit_current_page_id'];
1066 }
1067 }
1068 }
1069
1070 // Empty id ? no action.
1071 if ( empty( $checkout_id ) ) {
1072 return;
1073 }
1074
1075 $checkout_id = (int) $checkout_id;
1076
1077 // Apply funnel prices.
1078 self::apply_discounted_prices( wc()->cart, $checkout_id );
1079 self::apply_discounted_prices( wc()->cart, $checkout_id, 'bumps' );
1080
1081 // We should re apply coupons after each price changes. to make sure everything is correct.
1082 $optimization_data = ! empty( $funnel_data['data']['optimization'] ) ? $funnel_data['data']['optimization'] : '';
1083
1084 if ( Checkout::apply_coupon_validation( $optimization_data ) ) {
1085 WC()->cart->remove_coupons();
1086
1087 foreach ( $optimization_data['auto_apply_coupons'] as $auto_apply_coupon ) {
1088 wc()->cart->add_discount( get_the_title( $auto_apply_coupon['value'] ) );
1089 }
1090
1091 wc_clear_notices();
1092 }
1093 }
1094 }
1095