newInstanceWithoutConstructor(); } /** * Class construct. * required actions that affects woocommerce global checkout shortcode / page. * * @since 1.1.0 */ public function __construct() { // Create empty shortcode to simulate our widget page as checkout page. add_shortcode( 'sellkit_checkout_widget_simulated', function() { return ''; } ); add_action( 'wp_ajax_sellkit_checkout_ajax_handler', [ $this, 'ajax_handler' ] ); add_action( 'wp_ajax_nopriv_sellkit_checkout_ajax_handler', [ $this, 'ajax_handler' ] ); // Add shipping total to review order by custom hook. add_action( 'sellkit-checkout-widget-display-shipping-price', [ $this, 'add_shipping_total_to_review_order' ] ); // Modify checkout order-review item. add_action( 'sellkit-one-page-checkout-custom-order-item', [ $this, 'modify_checkout_order_item' ], 1, 4 ); // Modify sent data before processing & validate order. add_filter( 'woocommerce_checkout_posted_data', [ $this, 'modify_order_data_before_validate' ], 10, 1 ); // Validate user defined fields. add_action( 'woocommerce_checkout_process', [ $this, 'validate_user_defined_fields' ] ); // Save user defined fields. add_action( 'woocommerce_checkout_update_order_meta', [ $this, 'save_user_defined_fields' ] ); // Display user defined fields in order edit page. add_action( 'woocommerce_admin_order_data_after_billing_address', [ $this, 'add_item_to_order_details' ], 10, 1 ); add_action( 'woocommerce_admin_order_data_after_shipping_address', [ $this, 'add_item_to_order_details' ], 10, 1 ); // Add custom shipping field to email. add_filter( 'woocommerce_order_get_formatted_shipping_address', [ $this, 'attach_user_shipping_fields_to_order_email' ], 10, 3 ); // Add custom billing field to email. add_filter( 'woocommerce_order_get_formatted_billing_address', [ $this, 'attach_user_billing_fields_to_order_email' ], 10, 3 ); // Filter checkout ajax fragment. add_filter( 'woocommerce_update_order_review_fragments', [ $this, 'checkout_fragment' ] ); // Simulate checkout page for our widget. add_action( 'wp', [ $this, 'simulate_our_widget_page_as_checkout' ] ); // Fix shipping method price change on ajax. // Manage bump order discounts. // Template replacement. add_action( 'woocommerce_checkout_update_order_review', [ $this, 'sellkit_checkout_during_ajax' ], 10, 1 ); add_filter( 'sellkit-shipping-methods-choosen-method', [ $this, 'sellkit_default_shipping_method' ], 10, 1 ); // Modify discounted products. add_action( 'woocommerce_before_calculate_totals', [ $this, 'before_cart_calculate' ], 999 ); // Allow viewing of Checkout page in the Editor with an empty cart. $action = isset( $_REQUEST['action'] ) ? filter_var( $_REQUEST['action'], FILTER_SANITIZE_FULL_SPECIAL_CHARS ) : ''; // phpcs:ignore $elementor_preview = isset( $_REQUEST['elementor-preview'] ) ? filter_var( $_REQUEST['elementor-preview'], FILTER_SANITIZE_FULL_SPECIAL_CHARS ) : ''; // phpcs:ignore if ( ( 'elementor' === $action && is_admin() ) // Elementor Editor. || 'elementor_ajax' === $action // Elementor Editor Preview - Ajax Render Widget. || ! empty( $elementor_preview ) // Elementor Editor Preview. ) { add_filter( 'woocommerce_checkout_redirect_empty_cart', '__return_false', 5 ); } } /** * Handle ajax calls. * * @since 1.1.0 * @return void */ public function ajax_handler() { check_ajax_referer( 'sellkit_elementor', 'nonce' ); $sub_action = filter_input( INPUT_POST, 'sub_action', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); if ( method_exists( $this, $sub_action ) ) { call_user_func( [ $this, $sub_action ] ); return; } wp_send_json_error(); } /** * Modify checkout ajax response HTML. * * @param array $response woocommerce checkout page ajax response. * @return array * @since 1.1.0 */ public function checkout_fragment( $response ) { // Get posted data and identify if it's been sent by jupiter widget or not. $checkout_form_data = filter_input( INPUT_POST, 'post_data', FILTER_DEFAULT ); $checkout_form_data = explode( '&', $checkout_form_data ); $posted_data = []; foreach ( $checkout_form_data as $input ) { $item = explode( '=', urldecode( $input ) ); $posted_data[ trim( $item[0] ) ] = $item[1]; } // Identify if it's been sent by jupiter checkout widget or not. if ( ! array_key_exists( 'form_id', $posted_data ) && ! array_key_exists( 'post_id', $posted_data ) ) { // Default woocommerce checkout. return $response; } // Get widget settings. $widget_settings = Helper::instance()->retrieve_checkout_widget_settings( $posted_data['post_id'], $posted_data['form_id'] ); // Order review fragment. ob_start(); woocommerce_order_review(); $order_review = ob_get_clean(); // Payment fragment. ob_start(); woocommerce_checkout_payment(); $payment = ob_get_clean(); // Shipping method fragment. ob_start(); echo '
'; wc_cart_totals_shipping_html(); echo '
'; $shipping_method = ob_get_clean(); $response['.woocommerce-checkout-review-order-table'] = $order_review; $response['.woocommerce-checkout-payment'] = $payment; $response['.sellkit-one-page-shipping-methods'] = ''; if ( ! array_key_exists( 'show_shipping_method', $widget_settings ) ) { $response['.sellkit-one-page-shipping-methods'] = $shipping_method; } return $response; } /** * Apply coupon code using ajax. * * @return void * @since 1.1.0 */ public function apply_coupon() { $code = filter_input( INPUT_POST, 'code', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); if ( empty( $code ) ) { wp_send_json_error(); } $result = WC()->cart->add_discount( $code ); wp_send_json_success( $result ); } /** * Update cart item quantity in checkout page by ajax. * * @return void * @since 1.1.0 */ public function change_cart_item_qty() { $qty = filter_input( INPUT_POST, 'qty', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); $id = filter_input( INPUT_POST, 'id', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); $action = filter_input( INPUT_POST, 'mode', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); $funnel_id = filter_input( INPUT_POST, 'related_checkout', FILTER_SANITIZE_NUMBER_INT ); self::make_changes_after_cart_item_edit( $funnel_id ); if ( 'add' === $action ) { WC()->cart->add_to_cart( $id, $qty ); self::make_changes_after_cart_item_edit( $funnel_id ); wp_send_json_success(); } if ( 'remove' === $action ) { $post_type = get_post_type( $id ); if ( 'product_variation' === $post_type ) { foreach ( WC()->cart->get_cart() as $item_key => $item ) { if ( $item['variation_id'] === (int) $id ) { WC()->cart->remove_cart_item( $item_key ); // we remove it. break; // stop the loop. } } self::make_changes_after_cart_item_edit( $funnel_id ); wp_send_json_success(); } $product_cart_id = WC()->cart->generate_cart_id( $id ); $cart_item_key = WC()->cart->find_product_in_cart( $product_cart_id ); if ( $cart_item_key ) { WC()->cart->remove_cart_item( $cart_item_key ); } self::make_changes_after_cart_item_edit( $funnel_id ); wp_send_json_success(); } ( $qty > 0 ) ? WC()->cart->set_quantity( $id, $qty ) : WC()->cart->remove_cart_item( $id ); self::make_changes_after_cart_item_edit( $funnel_id ); wp_send_json_success(); } /** * Apply changes after editing cart items. * * @since 1.2.5 * @param int $funnel_id funnel step id. */ public static function make_changes_after_cart_item_edit( $funnel_id ) { if ( empty( $funnel_id ) ) { return; } $funnel_data = get_post_meta( $funnel_id, 'step_data', true ); $optimization_data = ! empty( $funnel_data['data']['optimization'] ) ? $funnel_data['data']['optimization'] : ''; if ( empty( $optimization_data ) ) { return; } $auto_apply_coupon_cookie_value = null; if ( isset( $_COOKIE['sellkit_rejected_coupon'] ) ) { // Get the value of the cookie. $auto_apply_coupon_cookie_value = sanitize_text_field( wp_unslash( $_COOKIE['sellkit_rejected_coupon'] ) ); $auto_apply_coupon_cookie_value = explode( ',', $auto_apply_coupon_cookie_value ); } // Help to apply funnel discount prices and coupons when changing product quantity. self::apply_discounted_prices( wc()->cart, $funnel_id ); self::apply_discounted_prices( wc()->cart, $funnel_id, 'bumps' ); if ( Checkout::apply_coupon_validation( $optimization_data ) ) { WC()->cart->remove_coupons(); foreach ( $optimization_data['auto_apply_coupons'] as $auto_apply_coupon ) { if ( ! empty( $auto_apply_coupon_cookie_value ) && in_array( $auto_apply_coupon['label'], $auto_apply_coupon_cookie_value, true ) ) { continue; } wc()->cart->add_discount( get_the_title( $auto_apply_coupon['value'] ) ); } wc_clear_notices(); } } /** * Place shipping price based on design to checkout order-review. * * @return void * @since 1.1.0 */ public function add_shipping_total_to_review_order() { ?> cart->get_shipping_total(); if ( WC()->cart->display_prices_including_tax() ) { $price = WC()->cart->get_shipping_total() + WC()->cart->shipping_tax_total; } echo wp_kses_post( wc_price( $price ) ); ?> cart->get_cart(); //phpcs:disable ob_start(); ?>
get_image(); ?>
get_type(); if ( $cart_items[ $cart_item_key ] === end( $cart_items ) ) { $extra_class .= ' last-cart-item'; } if ( 'variation' === $product->get_type() ) { add_filter( 'woocommerce_cart_item_name', [ $this, 'modify_variation_title' ], 50, 2 ); add_filter( 'woocommerce_get_item_data', [ $this, 'modify_variation_items' ], 10, 2 ); } ?>
get_name(), $cart_item, $cart_item_key ) ) . ' '; ?> ', $cart_item['quantity'] ); ?>
cart->get_product_subtotal( $product, $cart_item['quantity'] ), $cart_item, $cart_item_key ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> get_type() ) { remove_filter( 'woocommerce_cart_item_name', [ $this, 'modify_variation_title' ], 50 ); remove_filter( 'woocommerce_get_item_data', [ $this, 'modify_variation_items' ], 10 ); } } /** * Remove variation attributes from variation product title. * * @param string $default default value. * @param array $cart_item cart item data. * @since 1.6.8 */ public function modify_variation_title( $default, $cart_item ) { $product = $cart_item['product_id']; return get_the_title( $product ); } /** * Display variation attributes separated from title even if those are less than 3. * * @param array $item_data attributes array. * @param array $cart_item cart item data. * @since 1.6.8 */ public function modify_variation_items( $item_data, $cart_item ) { $product = new \WC_Product_Variation( $cart_item['variation_id'] ); $attributes = $product->get_attributes(); if ( count( $attributes ) > 2 ) { return $item_data; } foreach ( $attributes as $key => $value ) { $key = str_replace( 'pa_', '', $key ); $key = str_replace( '-', '', $key ); $key = str_replace( '_', '', $key ); $item_data[] = [ 'key' => $key, 'value' => $value, ]; } return $item_data; } /** * Modify fields after pressing place order button. * we hooked here because some removed fields still get checked and have not removed by previous hooks. * * @SuppressWarnings(PHPMD.NPathComplexity) * @param array $data checkout form data. * @return array $data data of woocommerce checkout form. * @since 1.1.0 */ public function modify_order_data_before_validate( $data ) { $post_id = filter_input( INPUT_POST, 'post_id', FILTER_SANITIZE_NUMBER_INT ); $form_id = filter_input( INPUT_POST, 'form_id', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); if ( empty( $post_id ) && empty( $form_id ) ) { return $data; } $settings = Helper::instance()->retrieve_checkout_widget_settings( $post_id, $form_id ); if ( empty( $settings ) ) { return $data; } $widget_shipping_field = Helper::instance()->get_user_defined_fields_slug( $settings['shipping_list'], 'shipping_list_field' ); $widget_billing_field = Helper::instance()->get_user_defined_fields_slug( $settings['billing_list'], 'billing_list_field' ); $default_shipping_fields = Helper::instance()->shipping_fields(); $default_billing_fields = Helper::instance()->billing_fields(); // Unset removed shipping fields. foreach ( $default_shipping_fields as $field => $details ) { if ( ! in_array( $field, $widget_shipping_field, true ) ) { unset( $data[ $field ] ); } } // Unset removed billing fields. foreach ( $default_billing_fields as $field => $details ) { if ( ! in_array( $field, $widget_billing_field, true ) && 'billing_email' !== $field ) { unset( $data[ $field ] ); } } $mail = filter_input( INPUT_POST, 'billing_email', FILTER_SANITIZE_EMAIL ); if ( ! array_key_exists( 'billing_email', $data ) && ! empty( $mail ) ) { $data['billing_email'] = $mail; } return $data; } /** * Login user by ajax. * sign user in by using form included in widget. * * @return void * @since 1.1.0 */ public function auth_user() { $email = filter_input( INPUT_POST, 'email', FILTER_SANITIZE_EMAIL ); $pass = filter_input( INPUT_POST, 'pass', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); $verify = filter_var( $email, FILTER_VALIDATE_EMAIL ); if ( ! $verify || empty( $email ) || empty( $pass ) ) { wp_send_json_error( __( 'Email or password field is not valid.', 'sellkit' ) ); } $result = wp_authenticate( $email, $pass ); if ( is_wp_error( $result ) ) { wp_send_json_error( $result->get_error_message() ); } wp_clear_auth_cookie(); wp_set_current_user( $result->ID ); wp_set_auth_cookie( $result->ID ); wp_send_json_success( $result ); } /** * Get widget settings in ajax calls using form and post id. * * @return void * @since 1.1.0 */ private function widget_settings() { $form_id = filter_input( INPUT_POST, 'form_id', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); $post_id = filter_input( INPUT_POST, 'post_id', FILTER_SANITIZE_NUMBER_INT ); if ( empty( $form_id ) && empty( $post_id ) ) { return; } $widget_settings = Helper::instance()->retrieve_checkout_widget_settings( $post_id, $form_id ); return $widget_settings; } /** * Validate user defined custom value. * * @return void * @since 1.1.0 */ public function validate_user_defined_fields() { $widget_settings = $this->widget_settings(); if ( empty( $widget_settings ) ) { return; } $widget_shipping_fields = $widget_settings['shipping_list']; $widget_billing_fields = $widget_settings['billing_list']; Helper::instance()->validate_user_defined_fields( $widget_shipping_fields, $widget_billing_fields ); Helper::instance()->make_sure_to_convert_required_field_to_optional( $widget_shipping_fields, $widget_billing_fields ); } /** * Save user defined custom fields value to database. * * @param int $order_id id of woocommerce order. * @return void * @since 1.1.0 */ public function save_user_defined_fields( $order_id ) { $widget_settings = $this->widget_settings(); if ( empty( $widget_settings ) ) { return; } $widget_shipping_fields = $widget_settings['shipping_list']; $widget_billing_fields = $widget_settings['billing_list']; Helper::instance()->save_user_defined_fields( $widget_shipping_fields, $widget_billing_fields, $order_id ); } /** * Attach user defined shipping field values to order emails. * * @param string $address address. * @param string $raw_address raw address. * @param object $order woocommerce order object. * @return string * @since 1.1.0 */ public function attach_user_shipping_fields_to_order_email( $address, $raw_address, $order ) { $order_id = $order->get_id(); $custom_fields = get_post_meta( $order_id, 'sellkit_checkout_widget_custom_field_of_order', true ); if ( empty( $custom_fields ) || ! is_array( $custom_fields ) ) { return $address; } foreach ( $custom_fields as $field ) { if ( 'yes' !== $field['show_in_email'] ) { continue; } $key = $field['key_name']; $value = get_post_meta( $order_id, $key, true ); if ( strpos( $key, 'shipping' ) !== false ) { $address .= '
' . $value; } } return $address; } /** * Attach user defined billing field values to order emails. * * @param string $address address. * @param string $raw_address raw address. * @param object $order woocommerce order object. * @return string * @since 1.1.0 */ public function attach_user_billing_fields_to_order_email( $address, $raw_address, $order ) { $order_id = $order->get_id(); $custom_fields = get_post_meta( $order_id, 'sellkit_checkout_widget_custom_field_of_order', true ); if ( empty( $custom_fields ) || ! is_array( $custom_fields ) ) { return $address; } foreach ( $custom_fields as $field ) { if ( 'yes' !== $field['show_in_email'] ) { continue; } $key = $field['key_name']; $value = get_post_meta( $order_id, $key, true ); if ( strpos( $key, 'billing' ) !== false ) { $address .= '
' . $value; } } return $address; } /** * Place custom coupon form in checkout order-review based design. * We have used same form in Local_Hooks. but this one is for ajax response. * * @see sellkit_Core\Raven\Modules\Checkout\Classes\Local_Hooks::coupon_form. * @param array $settings widget settings. * @return void * @since 1.1.0 */ public function coupon_form( $settings ) { if ( array_key_exists( 'show_coupon_field', $settings ) ) { return; } echo ''; Local_Hooks::form( $settings ); echo ''; do_action( 'sellkit-checkout-after-coupon-form-ajax' ); } /** * Look for an email if exists or not. * Is used for login process. * * @return void * @since 1.1.0 */ public function search_for_email() { $email = filter_input( INPUT_POST, 'email', FILTER_SANITIZE_EMAIL ); $valid = filter_var( $email, FILTER_VALIDATE_EMAIL ); if ( ! $valid || empty( $email ) ) { wp_send_json_error(); } $check = email_exists( $email ); if ( false === $check ) { wp_send_json_error(); } wp_send_json_success(); } /** * Look for an username if exists or not. * Is used for register process. * * @return void * @since 1.1.0 */ public function search_for_username() { $username = filter_input( INPUT_POST, 'user', FILTER_SANITIZE_EMAIL ); if ( empty( $username ) ) { wp_send_json_error(); } $username = sanitize_user( $username ); $check = username_exists( $username ); if ( false !== $check ) { wp_send_json_error(); } wp_send_json_success(); } /** * Validate postcode via ajax. * * @since 1.1.0 * @return void */ public function validate_postcode() { $country = filter_input( INPUT_POST, 'country_code', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); $postcode = filter_input( INPUT_POST, 'post_code', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); $parent = filter_input( INPUT_POST, 'parent', FILTER_DEFAULT ); $postcode = wc_format_postcode( $postcode, $country ); $valid = \WC_Validation::is_postcode( $postcode, $country ); if ( ! $valid ) { wp_send_json_error( $parent ); } wp_send_json_success( $parent ); } /** * Validate phone number via ajax. * * @since 1.1.0 * @return void */ public function validate_phone_number() { $phone = filter_input( INPUT_POST, 'phone_number', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); $parent = filter_input( INPUT_POST, 'parent', FILTER_DEFAULT ); $valid = \WC_Validation::is_phone( $phone ); if ( ! $valid ) { wp_send_json_error( $parent ); } wp_send_json_success( $parent ); } /** * Simulate checkout page where our widget is present. using empty shortcode [sellkit_checkout_widget_simulated]. * * @since 1.1.0 * @return void */ public function simulate_our_widget_page_as_checkout() { $page_id = get_the_ID(); $content = get_post_field( 'post_content', $page_id ); if ( false === strpos( $content, 'woocommerce_checkout' ) ) { return; } add_filter( 'woocommerce_is_checkout', function() { return true; } ); if ( false === strpos( $content, 'sellkit_checkout_widget_simulated' ) ) { return; } add_filter( 'theme_mod_jupiterx_jupiterx_checkout_cart_elements', function() { return []; }, 10 ); } /** * Integrate user country with our widget. * * @since 1.1.0 * @return void */ public function set_customer_details_ajax() { $country = filter_input( INPUT_POST, 'country', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); $state = filter_input( INPUT_POST, 'state', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); $shipping_country = filter_input( INPUT_POST, 'shipping_country', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); $shipping_state = filter_input( INPUT_POST, 'shipping_state', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); if ( ! empty( $country ) ) { setcookie( 'sellkit-checkout-billing-cc', $country, time() + ( 86400 * 3 ), '/' ); setcookie( 'sellkit-checkout-billing-state', $state, time() + ( 86400 * 3 ), '/' ); } if ( ! empty( $shipping_country ) ) { setcookie( 'sellkit-checkout-shipping-cc', $shipping_country, time() + ( 86400 * 3 ), '/' ); setcookie( 'sellkit-checkout-shipping-state', $shipping_state, time() + ( 86400 * 3 ), '/' ); } wp_send_json_success(); } /** * State lookup using postcode and country. * * @since 1.1.0 * @return void */ public function sellkit_state_lookup_by_postcode() { $country = filter_input( INPUT_POST, 'country_value', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); $postcode = filter_input( INPUT_POST, 'postcode_value', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); $api = 'https://api.zippopotam.us/' . $country . '/' . $postcode; if ( empty( $country ) || empty( $postcode ) ) { wp_send_json_error( esc_html__( 'Not valid inputs', 'sellkit' ) ); } $response = wp_remote_get( $api ); if ( is_wp_error( $response ) || ! is_array( $response ) ) { wp_send_json_error( esc_html__( 'Server error.', 'sellkit' ) ); } if ( '{}' === $response['body'] ) { // Country or postcode is not supported. it's kina an error but we show nothing in frontend. wp_send_json_error( '' ); } $body = json_decode( $response['body'], true ); wp_send_json_success( $body ); } /** * Modify cart contents using bundled products. * * @since 1.1.0 * @return void * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function sellkit_checkout_modify_cart_by_bundle_products() { $qty = filter_input( INPUT_POST, 'qty', FILTER_SANITIZE_NUMBER_INT ); $id = filter_input( INPUT_POST, 'id', FILTER_SANITIZE_NUMBER_INT ); $type = filter_input( INPUT_POST, 'type', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); $checkout_id = filter_input( INPUT_POST, 'checkout_id', FILTER_SANITIZE_NUMBER_INT ); $modify = filter_input( INPUT_POST, 'modify', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); $key = filter_input( INPUT_POST, 'key', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); if ( ! empty( $key ) ) { foreach ( WC()->cart->get_cart() as $item_key => $item ) { if ( $item_key === $key ) { WC()->cart->set_quantity( $item_key, $qty, true ); break; // stop the loop. } } wp_send_json_success(); } if ( 'radio' === $type ) { $step_data = get_post_meta( $checkout_id, 'step_data', true ); $products = $step_data['data']['products']['list']; $reset_cart = isset( $step_data['data']['products']['reset_cart'] ) ? $step_data['data']['products']['reset_cart'] : 'true'; if ( 'true' === $reset_cart ) { foreach ( WC()->cart->get_cart() as $item ) { if ( empty( $item['product_id'] ) ) { continue; } if ( array_key_exists( $item['product_id'], $products ) ) { continue; } $products[ $item['product_id'] ] = [ 'quantity' => $item['quantity'], 'discount' => '', 'discountType' => 'fixed', ]; } } $selected = ! empty( $products[ $id ] ) ? $products[ $id ] : []; $real_quantity = ( empty( $selected['quantity'] ) ) ? 1 : $selected['quantity']; if ( (int) $real_quantity !== (int) $qty ) { wp_send_json_error( esc_html__( 'Oh do not be tricky.', 'sellkit' ) ); } } if ( 'radio' === $type ) { WC()->cart->empty_cart(); WC()->cart->add_to_cart( $id, $qty ); } if ( 'checkbox' === $type && 'add' === $modify ) { WC()->cart->add_to_cart( $id, $qty ); } if ( 'checkbox' === $type && 'remove' === $modify ) { $post_type = get_post_type( $id ); // Maybe product is a variation. if ( 'product_variation' === $post_type ) { foreach ( WC()->cart->get_cart() as $item_key => $item ) { if ( $item['variation_id'] === (int) $id ) { WC()->cart->remove_cart_item( $item_key ); // we remove it. break; // stop the loop. } } self::make_changes_after_cart_item_edit( $checkout_id ); wp_send_json_success(); } // Default product. $product_cart_id = WC()->cart->generate_cart_id( $id ); $cart_item_key = WC()->cart->find_product_in_cart( $product_cart_id ); if ( $cart_item_key ) { WC()->cart->remove_cart_item( $cart_item_key ); } } self::make_changes_after_cart_item_edit( $checkout_id ); wp_send_json_success(); } /** * Apply funnel discount. * Will be called twice, first when page is loading , second during checkout ajax. * * @param object $cart cart object. * @param int $checkout_id_ajax checkout id. * @param string $source source of products, bump or default products to be checked. * @param array $upsell_data upsell product data coming from popup. * @since 1.2.8 * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public static function apply_discounted_prices( $cart = [], $checkout_id_ajax = null, $source = 'default', $upsell_data = [] ) { $checkout_id = get_queried_object_id(); if ( wp_doing_ajax() ) { $checkout_id = $checkout_id_ajax; } if ( empty( $checkout_id ) ) { return; } $step_data = get_post_meta( $checkout_id, 'step_data', true ); if ( empty( $step_data ) ) { return; } $products = []; if ( 'default' === $source && array_key_exists( 'data', $step_data ) && ( ! empty( $step_data['data']['products'] ) && array_key_exists( 'list', $step_data['data']['products'] ) ) ) { $products = $step_data['data']['products']['list']; } if ( 'bumps' === $source && ! empty( $step_data['bump'] ) ) { $bumps = $step_data['bump']; foreach ( $bumps as $bump ) { if ( ! array_key_exists( 'products', $bump['data'] ) ) { continue; } $key = ! empty( $bump['data']['products']['list'] ) ? array_key_first( $bump['data']['products']['list'] ) : null; if ( empty( $key ) ) { continue; } $products[ $key ] = $bump['data']['products']['list'][ $key ]; } } if ( 'upsell' === $source ) { $products = $upsell_data['data']['products']['list']; } if ( empty( $products ) ) { return; } $final_price = 0; foreach ( $cart->get_cart() as $key => $details ) { $item_id = $details['product_id']; $price = false; if ( ! empty( $details['variation_id'] ) ) { $item_id = $details['variation_id']; } foreach ( $products as $product_id => $product_details ) { if ( $item_id === $product_id ) { $discount_type = isset( $product_details['discountType'] ) ? $product_details['discountType'] : ''; $discount_value = isset( $product_details['discount'] ) ? $product_details['discount'] : ''; $price = Helper::calculate_discount( $item_id, $discount_type, $discount_value ); if ( 'upsell' === $source ) { self::$sellkit_upsell_products[ $product_id ] = $price; } } } if ( false !== $price ) { $details['data']->set_price( $price ); } $final_price += $details['data']->get_price() * $details['quantity']; } self::modify_products_price_before_woo_apply_discounts( $final_price ); } /** * Modify cart subtotal before validating discounts. * * @since 1.2.8 * @param int $final_price cart new subtotal. */ public static function modify_products_price_before_woo_apply_discounts( $final_price ) { add_filter( 'woocommerce_coupon_validate_minimum_amount', function( $value, $coupon ) use ( $final_price ) { return $coupon->get_minimum_amount() > $final_price; }, 99, 2 ); add_filter( 'woocommerce_coupon_validate_maximum_amount', function( $value, $coupon ) use ( $final_price ) { return $coupon->get_maximum_amount() < $final_price; }, 99, 2 ); } /** * Fix shipping method price on checkout ajax state change. * * @param string $data checkout form data. * @since 1.1.0 * @return void */ public function sellkit_checkout_during_ajax( $data ) { $data = explode( '&', $data ); $clear = []; foreach ( $data as $key => $input ) { $input = explode( '=', $input ); $clear[ $input[0] ] = $input[1]; } if ( ! array_key_exists( 'form_id', $clear ) ) { return; } $this->apply_template_replacement_during_ajax( $clear ); $_POST = Helper::instance()->assigning_default_ajax_shipping_fields( $_POST, $clear ); // phpcs:ignore Local_Hooks::order_bump( $clear ); if ( array_key_exists( 'sellkit-bundle-products', $clear ) ) { $option = $clear['sellkit-bundle-products']; Local_Hooks::bundle_product_action( $option ); } // Apply discounts. if ( ! array_key_exists( 'sellkit_current_page_id', $clear ) ) { return; } self::apply_discounted_prices( wc()->cart, $clear['sellkit_current_page_id'] ); self::apply_discounted_prices( wc()->cart, $clear['sellkit_current_page_id'], 'bumps' ); } /** * Template replacement during ajax call. * * @param array $data checkout form data. * @since 1.2.1 */ private function apply_template_replacement_during_ajax( $data ) { $widget_settings = Helper::instance()->retrieve_checkout_widget_settings( $data['post_id'], $data['form_id'] ); Local_Hooks::enable_disable_sections( $widget_settings, $data ); add_filter( 'wc_get_template', function( $located, $template_name ) { $our_path = sellkit()->plugin_dir() . 'includes/elementor/modules/checkout/templates/'; $files = [ 'review-order.php', 'payment-method.php', 'payment.php', 'form-checkout.php', 'cart-item-data.php', 'cart-shipping.php', 'terms.php', ]; $template = str_replace( 'checkout/', '', $template_name ); $template = str_replace( 'cart/', '', $template ); if ( in_array( $template, $files, true ) ) { $located = $our_path . $template; } return $located; }, 10, 2 ); } /** * Modify default selected shipping method. * * @since 1.2.8 * @param string $default default selected method. * @return string */ public function sellkit_default_shipping_method( $default ) { $applied_coupons = WC()->cart->get_applied_coupons(); if ( count( $applied_coupons ) < 1 ) { return $default; } foreach ( $applied_coupons as $coupon_code ) { $coupon = new \WC_Coupon( $coupon_code ); if ( $coupon->get_free_shipping() ) { if ( count( WC()->session->get( 'shipping_for_package_0' )['rates'] ) > 0 ) { // Loop through. foreach ( WC()->session->get( 'shipping_for_package_0' )['rates'] as $rate_id => $rate ) { // For free shipping. if ( 'free_shipping' === $rate->method_id ) { return $rate_id; } } } } } return $default; } /** * Recalculate checkout and cart prices. * * @since 1.2.8 * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function before_cart_calculate() { // More than twice isn't necessary. this is called multiple times by WooCommerce. if ( did_action( 'woocommerce_before_calculate_totals' ) >= 2 && wp_doing_ajax() ) { return; } // Gather checkout id. $checkout_id = get_queried_object_id(); if ( wp_doing_ajax() ) { // First try to catch id using our ajax call. $checkout_id = filter_input( INPUT_POST, 'related_checkout', FILTER_SANITIZE_NUMBER_INT ); // Second try to catch id using woocommerce ajax. if ( empty( $checkout_id ) ) { $checkout_id = filter_input( INPUT_POST, 'sellkit_current_page_id', FILTER_SANITIZE_NUMBER_INT ); } // Catch id after pressing place order button. none of above methods worked. if ( empty( $checkout_id ) ) { $data = filter_input( INPUT_POST, 'post_data', FILTER_DEFAULT ); if ( ! empty( $data ) ) { parse_str( $data, $data ); } if ( is_array( $data ) && array_key_exists( 'sellkit_current_page_id', $data ) ) { $checkout_id = $data['sellkit_current_page_id']; } } } // Empty id ? no action. if ( empty( $checkout_id ) ) { return; } $checkout_id = (int) $checkout_id; // Apply funnel prices. self::apply_discounted_prices( wc()->cart, $checkout_id ); self::apply_discounted_prices( wc()->cart, $checkout_id, 'bumps' ); // Apply upsell discount. $sellkit_upsell_prices = null; if ( isset( $_POST['woocommerce-process-checkout-nonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['woocommerce-process-checkout-nonce'] ) ), 'woocommerce-process_checkout' ) ) { $sellkit_upsell_prices = isset( $_POST['sellkit_product_prices'] ) ? sanitize_textarea_field( wp_unslash( $_POST['sellkit_product_prices'] ) ) : null; } $upsell_product_ids; if ( isset( $sellkit_upsell_prices ) && ! empty( $sellkit_upsell_prices ) ) { $sellkit_upsell_prices = json_decode( $sellkit_upsell_prices, true ); $upsell_product_ids = array_keys( $sellkit_upsell_prices ); } if ( isset( $upsell_product_ids ) ) { foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { $product = $cart_item['data']; $product_name = $product->get_name(); $product_id = $product->get_id(); if ( in_array( $product_id, $upsell_product_ids, true ) ) { $upsell_price = $sellkit_upsell_prices[ $product_id ]; $product->set_price( $upsell_price ); } $price = $product->get_price(); } } $auto_apply_coupon_cookie_value = null; if ( isset( $_COOKIE['sellkit_rejected_coupon'] ) ) { // Get the value of the cookie. $auto_apply_coupon_cookie_value = sanitize_text_field( wp_unslash( $_COOKIE['sellkit_rejected_coupon'] ) ); $auto_apply_coupon_cookie_value = explode( ',', $auto_apply_coupon_cookie_value ); } // We should re apply coupons after each price changes. to make sure everything is correct. $optimization_data = ! empty( $funnel_data['data']['optimization'] ) ? $funnel_data['data']['optimization'] : ''; if ( Checkout::apply_coupon_validation( $optimization_data ) ) { WC()->cart->remove_coupons(); foreach ( $optimization_data['auto_apply_coupons'] as $auto_apply_coupon ) { if ( ! empty( $auto_apply_coupon_cookie_value ) && in_array( $auto_apply_coupon['label'], $auto_apply_coupon_cookie_value, true ) ) { continue; } wc()->cart->add_discount( get_the_title( $auto_apply_coupon['value'] ) ); } wc_clear_notices(); } } /** * Check sellkit step through ajax. * And in order decide to show upsell or downsell steps through a popup. * * @since 1.6.2 */ public function call_funnel_popups() { $step = filter_input( INPUT_POST, 'step', FILTER_SANITIZE_NUMBER_INT ); $funnel = new Sellkit_Funnel( $step ); $next_step = $funnel->next_step_data; $popups = [ 'upsell', 'downsell' ]; $funnel->next_step_data['type'] = (array) $funnel->next_step_data['type']; if ( 'decision' === $funnel->next_step_data['type']['key'] ) { $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. $this->helper_id = $step; $this->take_care_of_decision_step( $page_id, $funnel->funnel_id ); return; } if ( in_array( $funnel->next_step_data['type']['key'], $popups, true ) ) { $next_step = $this->is_set_upsell_product( $next_step ); wp_send_json_success( [ 'next_id' => apply_filters( 'wpml_object_id', $next_step['page_id'], 'sellkit_step', true ), // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML API. 'next_type' => $next_step['type']['key'], ] ); } } /** * Check the upsell or downsell have product or not, ignore upsell/downsell step if product doesn't set. * * @param array $next_step Next step data. * @since 2.6.0 */ private function is_set_upsell_product( $next_step ) { if ( isset( $next_step['type']['key'] ) && ! in_array( $next_step['type']['key'], [ 'upsell', 'downsell' ], true ) ) { return $next_step; } $products = isset( $next_step['data'] ) ? $next_step['data']['products'] : []; $list = isset( $products['list'] ) ? $products['list'] : []; if ( empty( $list ) ) { $funnel = isset( $next_step['page_id'] ) ? new Sellkit_Funnel( $next_step['page_id'] ) : null; $next_step = ! empty( $funnel ) ? $funnel->next_no_step_data : []; if ( empty( $next_step ) && ! empty( $funnel ) ) { return $funnel->end_node_step_data; } if ( isset( $next_step['type']['key'] ) && in_array( $next_step['type']['key'], [ 'upsell', 'downsell' ], true ) ) { $next_step = $this->is_set_upsell_product( $next_step ); } } return $next_step; } /** * Gets step id before the decision step and return result. * * @param int $step_id id of the step before decision step. * @param int $funnel_id id of the funnel. * @since 1.6.2 */ private function take_care_of_decision_step( $step_id, $funnel_id = null ) { // Failed decision step due to no page id should be checked a little different. if ( empty( $step_id ) && ! empty( $funnel_id ) ) { $this->check_decision_step_with_no_page_id(); } $funnel = new Sellkit_Funnel( $step_id ); $conditions = ! empty( $funnel->current_step_data['data']['conditions'] ) ? $funnel->current_step_data['data']['conditions'] : []; $is_valid = sellkit_conditions_validation( $conditions ); $next_step = $funnel->next_no_step_data; if ( $is_valid ) { $next_step = $funnel->next_step_data; } $next_step = $this->is_set_upsell_product( $next_step ); $next_step['type'] = (array) $next_step['type']; if ( 'decision' === $next_step['type']['key'] ) { $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. return; } wp_send_json_success( [ 'next_id' => apply_filters( 'wpml_object_id', $next_step['page_id'], 'sellkit_step', true ), // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML API. 'next_type' => $next_step['type']['key'], ] ); } /** * Decision next step directly using funnel data. * * @since 1.8.6 */ private function check_decision_step_with_no_page_id() { $funnel = new Sellkit_Funnel( $this->helper_id ); $decicion_data = $funnel->next_step_data; $conditions = $decicion_data['data']['conditions']; $funnel_data = get_post_meta( $funnel->funnel_id, 'nodes', true ); $next_no = 'none' !== $decicion_data['targets'][1]['nodeId'] ? $funnel_data[ $decicion_data['targets'][1]['nodeId'] ] : $funnel->end_node_step_data; $next_yes = 'none' !== $decicion_data['targets'][0]['nodeId'] ? $funnel_data[ $decicion_data['targets'][0]['nodeId'] ] : $funnel->end_node_step_data; $is_valid = sellkit_conditions_validation( $conditions ); $next_step = $next_no; if ( $is_valid ) { $next_step = $next_yes; } $next_step = $this->is_set_upsell_product( $next_step ); wp_send_json_success( [ 'next_id' => apply_filters( 'wpml_object_id', $next_step['page_id'], 'sellkit_step', true ), // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML API. 'next_type' => $next_step['type']['key'], ] ); } /** * Perform upsell popup accept button. * * @since 1.6.2 */ public function perform_upsell_accept_button() { // Gather and validate information. $upsell_id = filter_input( INPUT_POST, 'upsell_id', FILTER_SANITIZE_NUMBER_INT ); $checkout_id = filter_input( INPUT_POST, 'checkout_id', FILTER_SANITIZE_NUMBER_INT ); $response = []; if ( empty( $upsell_id ) ) { wp_send_json_error( esc_html__( 'Empty Upsell ID.', 'sellkit' ) ); } $upsell_data = get_post_meta( $upsell_id, 'step_data', true ); if ( empty( $upsell_data ) ) { wp_send_json_error( esc_html__( 'Empty Step Data.', 'sellkit' ) ); } // Add product to cart. $product_id = ''; $qty = ''; if ( is_array( $upsell_data['data'] ) && array_key_exists( 'products', $upsell_data['data'] ) && ! empty( $upsell_data['data']['products']['list'] ) ) { $product_id = array_key_first( $upsell_data['data']['products']['list'] ); $qty = ! empty( $upsell_data['data']['products']['list'][ $product_id ] ) ? $upsell_data['data']['products']['list'][ $product_id ]['quantity'] : 0; } if ( empty( $qty ) ) { $qty = 1; } $added_product_hash = ''; if ( ! in_array( $product_id, array_column( WC()->cart->get_cart(), 'product_id' ), true ) ) { $added_product_hash = WC()->cart->add_to_cart( $product_id, $qty ); // Take care of cart and applied discounts. self::apply_discounted_prices( WC()->cart, $checkout_id, 'upsell', $upsell_data ); self::make_changes_after_cart_item_edit( $checkout_id ); } // Response. $funnel = new Sellkit_Funnel( $upsell_id ); $next_step = $funnel->next_step_data; $next_step = $this->is_set_upsell_product( $next_step ); $next_step['type'] = ! empty( $next_step['type'] ) ? (array) $next_step['type'] : null; if ( 'upsell' === $funnel->current_step_data['type']['key'] && isset( WC()->cart->cart_contents[ $added_product_hash ] ) ) { $analytics_updater = new Data_Updater(); $total = empty( WC()->cart->cart_contents[ $added_product_hash ]['line_total'] ) ? 0 : WC()->cart->cart_contents[ $added_product_hash ]['line_total']; $analytics_updater->set_funnel_id( $funnel->funnel_id ); $analytics_updater->add_new_upsell_revenue_log( $total ); } $contact_data = [ 'key' => $funnel->current_step_data['type']['key'], 'page_id' => $funnel->current_step_data['page_id'], ]; Base_Contacts::step_is_passed( $contact_data ); // Step with empty data, will be redirected to thankyou page. if ( empty( $next_step ) || empty( $next_step['type'] ) ) { wp_send_json_success( [ 'next_id' => $funnel->end_node_step_data['page_id'], 'next_type' => $funnel->end_node_step_data['type']['key'], 'upsell_prices' => wp_json_encode( self::$sellkit_upsell_products ), ] ); } if ( 'decision' === $next_step['type']['key'] ) { $this->take_care_of_decision_step( $next_step['page_id'] ); return; } $response = [ 'next_id' => apply_filters( 'wpml_object_id', $next_step['page_id'], 'sellkit_step', true ), // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML API. 'next_type' => $next_step['type']['key'], 'upsell_prices' => wp_json_encode( self::$sellkit_upsell_products ), ]; wp_send_json_success( $response ); } /** * Perform upsell popup reject button. * * @since 1.6.2 */ public function perform_upsell_reject_button() { $upsell_id = filter_input( INPUT_POST, 'upsell_id', FILTER_SANITIZE_NUMBER_INT ); if ( empty( $upsell_id ) ) { wp_send_json_error( esc_html__( 'Empty Upsell ID.', 'sellkit' ) ); } $funnel = new Sellkit_Funnel( $upsell_id ); $next_step = $funnel->next_no_step_data; $next_step = $this->is_set_upsell_product( $next_step ); if ( $next_step ) { $next_step['type'] = (array) $next_step['type']; } // Step with empty data, will be redirected to thankyou page. if ( empty( $next_step ) || empty( $next_step['type'] ) ) { wp_send_json_success( [ 'next_id' => $funnel->end_node_step_data['page_id'], 'next_type' => $funnel->end_node_step_data['type']['key'], ] ); } if ( 'decision' === $next_step['type']['key'] ) { $this->take_care_of_decision_step( $next_step['page_id'] ); return; } $response = [ 'next_id' => apply_filters( 'wpml_object_id', $next_step['page_id'], 'sellkit_step', true ), // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML API. 'next_type' => $next_step['type']['key'], ]; wp_send_json_success( $response ); } /** * Add new item to the WooCommerce order details on the edit page. * * @param object $order woocommerce order object. * @since 1.8.9 */ public function add_item_to_order_details( $order ) { $order_id = $order->get_id(); $custom_fields = get_post_meta( $order_id, 'sellkit_checkout_widget_custom_field_of_order', true ); $current_action = current_action(); if ( empty( $custom_fields ) || ! is_array( $custom_fields ) ) { return; } foreach ( $custom_fields as $field ) { $key_name = $field['key_name']; $label = $field['label']; // For old users data. if ( empty( $label ) ) { $label = $key_name; $label = str_replace( '_', ' ', $label ); $label = str_replace( '-', ' ', $label ); $label = ucwords( $label ); } if ( 'woocommerce_admin_order_data_after_shipping_address' === $current_action && 'shipping' === substr( $key_name, 0, 8 ) ) { ?>