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

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