| @@ -2,9 +2,12 @@ | ||
| 2 | 2 | |
| 3 | 3 | namespace Sellkit\Funnel\Steps; |
| 4 | 4 | |
| 5 | 5 | use Sellkit\Funnel\Analytics\Data_Updater; |
| 6 | +use Sellkit\Funnel\Contacts\Base_Contacts; | |
| 7 | +use Sellkit\Database; | |
| 6 | 8 | use Sellkit_Funnel; |
| 9 | +use Sellkit\Global_Checkout\Checkout as Global_Checkout; | |
| 7 | 10 | |
| 8 | 11 | defined( 'ABSPATH' ) || die(); |
| 9 | 12 | |
| 10 | 13 | /** |
| @@ -9,8 +12,9 @@ | ||
| 9 | 12 | |
| 10 | 13 | /** |
| 11 | 14 | * Class Sellkit_Checkout. |
| 12 | 15 | * |
| 16 | + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) | |
| 13 | 17 | * @since 1.1.0 |
| 14 | 18 | */ |
| 15 | 19 | class Checkout { |
| 16 | 20 | |
| @@ -36,11 +40,52 @@ | ||
| 36 | 40 | |
| 37 | 41 | add_action( 'woocommerce_checkout_create_order', [ $this, 'save_order_custom_meta_data' ], 10 ); |
| 38 | 42 | add_action( 'woocommerce_after_order_notes', [ $this, 'custom_checkout_hidden_fields' ], 10 ); |
| 39 | 43 | add_action( 'wc_ajax_update_order_review', [ $this, 'do_actions' ] ); |
| 44 | + | |
| 45 | + // Persist the current checkout step id in the WC session so express-checkout | |
| 46 | + // AJAX requests (which do not submit the sellkit_current_page_id hidden field) | |
| 47 | + // can still resolve the funnel context and save sellkit_funnel_next_step_data | |
| 48 | + // on the created order. | |
| 49 | + if ( ! wp_doing_ajax() && ! is_admin() ) { | |
| 50 | + add_action( 'wp', [ $this, 'store_checkout_step_in_session' ], 20 ); | |
| 51 | + } | |
| 40 | 52 | } |
| 41 | 53 | |
| 42 | 54 | /** |
| 55 | + * Store the current checkout step id in the WC session when rendering an actual | |
| 56 | + * SellKit funnel checkout step page. The value is later consumed by | |
| 57 | + * Sellkit_Funnel::get_page_id() during AJAX requests (e.g. Stripe express | |
| 58 | + * checkout) that lack the sellkit_current_page_id POST field. | |
| 59 | + * | |
| 60 | + * A timestamp is stored alongside the id so the fallback can be ignored if it | |
| 61 | + * is stale, avoiding cross-contamination between unrelated checkouts. | |
| 62 | + * | |
| 63 | + * @since 2.3.3 | |
| 64 | + * @return void | |
| 65 | + */ | |
| 66 | + public function store_checkout_step_in_session() { | |
| 67 | + if ( ! function_exists( 'WC' ) || ! is_object( WC()->session ) ) { | |
| 68 | + return; | |
| 69 | + } | |
| 70 | + | |
| 71 | + $post_id = get_the_ID(); | |
| 72 | + | |
| 73 | + if ( empty( $post_id ) || 'sellkit_step' !== get_post_type( $post_id ) ) { | |
| 74 | + return; | |
| 75 | + } | |
| 76 | + | |
| 77 | + $step_data = get_post_meta( $post_id, 'step_data', true ); | |
| 78 | + | |
| 79 | + if ( empty( $step_data['type']['key'] ) || 'checkout' !== $step_data['type']['key'] ) { | |
| 80 | + return; | |
| 81 | + } | |
| 82 | + | |
| 83 | + WC()->session->set( 'sellkit_current_page_id', (int) $post_id ); | |
| 84 | + WC()->session->set( 'sellkit_current_page_id_time', time() ); | |
| 85 | + } | |
| 86 | + | |
| 87 | + /** | |
| 43 | 88 | * Adding items to the checkout page. |
| 44 | 89 | * |
| 45 | 90 | * @since 1.1.0 |
| 46 | 91 | * @SuppressWarnings(PHPMD.NPathComplexity) |
| @@ -50,15 +95,29 @@ | ||
| 50 | 95 | // These variables should be used in checkout builder widget. |
| 51 | 96 | $funnel_data = $this->funnel->current_step_data; |
| 52 | 97 | $optimization_data = ! empty( $funnel_data['data']['optimization'] ) ? $funnel_data['data']['optimization'] : ''; |
| 53 | 98 | $redirect_url = ! empty( $optimization_data['closed_checkout_redirect_url'] ) ? $optimization_data['closed_checkout_redirect_url'] : site_url(); |
| 99 | + $reset_cart = isset( $funnel_data['data']['products']['reset_cart'] ) ? $funnel_data['data']['products']['reset_cart'] : 'true'; | |
| 54 | 100 | |
| 55 | - add_action( 'template_redirect', function () use ( $funnel_data ) { | |
| 101 | + $auto_apply_coupon_cookie_value = null; | |
| 102 | + | |
| 103 | + if ( function_exists( 'sellkit_pro' ) && ! sellkit_pro()->is_active_sellkit_pro ) { | |
| 104 | + $optimization_data = null; | |
| 105 | + } | |
| 106 | + | |
| 107 | + if ( isset( $_COOKIE['sellkit_rejected_coupon'] ) ) { | |
| 108 | + // Get the value of the cookie. | |
| 109 | + $auto_apply_coupon_cookie_value = sanitize_text_field( wp_unslash( $_COOKIE['sellkit_rejected_coupon'] ) ); | |
| 110 | + $auto_apply_coupon_cookie_value = explode( ',', $auto_apply_coupon_cookie_value ); | |
| 111 | + } | |
| 112 | + | |
| 113 | + add_action( 'template_redirect', function () use ( $funnel_data, $reset_cart ) { | |
| 56 | 114 | $product_settings = ! empty( $funnel_data['data']['products']['settings'] ) ? $funnel_data['data']['products']['settings'] : ''; |
| 57 | 115 | $bump_data = ! empty( $funnel_data['bump'] ) ? $funnel_data['bump'] : ''; |
| 58 | 116 | |
| 59 | 117 | if ( ! empty( $product_settings ) ) { |
| 60 | 118 | set_query_var( 'funnel_product_settings', $product_settings ); |
| 119 | + set_query_var( 'funnel_reset_cart', $reset_cart ); | |
| 61 | 120 | } |
| 62 | 121 | |
| 63 | 122 | if ( ! empty( $bump_data ) ) { |
| 64 | 123 | $bump_data = $this->get_valid_bumps( $bump_data ); |
| @@ -63,9 +122,13 @@ | ||
| 63 | 122 | if ( ! empty( $bump_data ) ) { |
| 64 | 123 | $bump_data = $this->get_valid_bumps( $bump_data ); |
| 65 | 124 | |
| 66 | 125 | set_query_var( 'bump_data', $bump_data ); |
| 126 | + } else { | |
| 127 | + set_query_var( 'bump_data', [] ); | |
| 67 | 128 | } |
| 129 | + | |
| 130 | + set_query_var( 'funnel_reset_cart', $reset_cart ); | |
| 68 | 131 | } ); |
| 69 | 132 | |
| 70 | 133 | if ( |
| 71 | 134 | ! empty( $optimization_data['expiration_date'] ) && |
| @@ -72,9 +135,9 @@ | ||
| 72 | 135 | ! empty( $optimization_data['expire_checkout_coupons_switch'] ) && |
| 73 | 136 | time() > $optimization_data['expiration_date'] && |
| 74 | 137 | 'on' === $optimization_data['expire_checkout_coupons_switch'] |
| 75 | 138 | ) { |
| 76 | - wp_redirect( $redirect_url ); // phpcs:ignore | |
| 139 | + wp_redirect( $this->add_http_to_url( $redirect_url ) ); // phpcs:ignore | |
| 77 | 140 | exit; |
| 78 | 141 | } |
| 79 | 142 | |
| 80 | 143 | if ( empty( $funnel_data ) ) { |
| @@ -89,32 +152,31 @@ | ||
| 89 | 152 | } |
| 90 | 153 | |
| 91 | 154 | $funnel_products = $funnel_data['data']['products']['list']; |
| 92 | 155 | |
| 156 | + if ( ! sellkit()->has_valid_dependencies() ) { | |
| 157 | + return; | |
| 158 | + } | |
| 159 | + | |
| 93 | 160 | $action = sellkit_htmlspecialchars( INPUT_GET, 'wc-ajax' ); |
| 94 | 161 | |
| 95 | 162 | if ( 'update_order_review' !== $action && 'checkout' !== $action ) { |
| 96 | - wc()->cart->empty_cart(); | |
| 163 | + if ( 'true' === $reset_cart || true === $reset_cart ) { | |
| 164 | + wc()->cart->empty_cart(); | |
| 165 | + } | |
| 97 | 166 | |
| 98 | 167 | foreach ( $funnel_products as $product_id => $product ) { |
| 99 | - $quantity = ! empty( $product['quantity'] ) ? $product['quantity'] : 1; | |
| 168 | + if ( empty( $product ) ) { | |
| 169 | + continue; | |
| 170 | + } | |
| 100 | 171 | |
| 101 | - wc()->cart->add_to_cart( $product_id, $quantity ); | |
| 102 | - } | |
| 103 | - } | |
| 172 | + $quantity = ! empty( $product['quantity'] ) ? $product['quantity'] : 1; | |
| 173 | + $product_cart_id = WC()->cart->generate_cart_id( $product_id ); | |
| 104 | 174 | |
| 105 | - foreach ( wc()->cart->get_cart() as $cart_item ) { | |
| 106 | - $price = $cart_item['data']->get_regular_price(); | |
| 107 | - $product_id = $cart_item['product_id']; | |
| 108 | - | |
| 109 | - if ( empty( $funnel_products[ $product_id ] ) && empty( $funnel_products[ $cart_item['variation_id'] ] ) ) { | |
| 110 | - continue; | |
| 175 | + if ( ! WC()->cart->find_product_in_cart( $product_cart_id ) ) { | |
| 176 | + WC()->cart->add_to_cart( $product_id, $quantity ); | |
| 177 | + } | |
| 111 | 178 | } |
| 112 | - | |
| 113 | - $discount_info = ! empty( $funnel_products[ $cart_item['variation_id'] ] ) ? $funnel_products[ $cart_item['variation_id'] ] : $funnel_products[ $product_id ]; | |
| 114 | - $discount_value = $this->calculate_discount( $price, $discount_info['discount'], $discount_info['discountType'] ); | |
| 115 | - | |
| 116 | - $cart_item['data']->set_price( floatval( $price ) - ( floatval( $discount_value ) ) ); | |
| 117 | 179 | } |
| 118 | 180 | |
| 119 | 181 | if ( empty( $optimization_data ) ) { |
| 120 | 182 | return; |
| @@ -121,8 +183,12 @@ | ||
| 121 | 183 | } |
| 122 | 184 | |
| 123 | 185 | if ( self::apply_coupon_validation( $optimization_data ) ) { |
| 124 | 186 | foreach ( $optimization_data['auto_apply_coupons'] as $auto_apply_coupon ) { |
| 187 | + | |
| 188 | + if ( ! empty( $auto_apply_coupon_cookie_value ) && in_array( $auto_apply_coupon['label'], $auto_apply_coupon_cookie_value, true ) ) { | |
| 189 | + continue; | |
| 190 | + } | |
| 125 | 191 | wc()->cart->add_discount( get_the_title( $auto_apply_coupon['value'] ) ); |
| 126 | 192 | } |
| 127 | 193 | |
| 128 | 194 | wc_clear_notices(); |
| @@ -129,8 +195,23 @@ | ||
| 129 | 195 | } |
| 130 | 196 | } |
| 131 | 197 | |
| 132 | 198 | /** |
| 199 | + * Adds http if it is needed. | |
| 200 | + * | |
| 201 | + * @since 1.5.0 | |
| 202 | + * @param string $url Entered url. | |
| 203 | + * @return string|boolean | |
| 204 | + */ | |
| 205 | + private function add_http_to_url( $url ) { | |
| 206 | + if ( ! preg_match( '~^(?:f|ht)tps?://~i', $url ) ) { | |
| 207 | + return 'http://' . $url; | |
| 208 | + } | |
| 209 | + | |
| 210 | + return $url; | |
| 211 | + } | |
| 212 | + | |
| 213 | + /** | |
| 133 | 214 | * Saving the funnel data to the order. |
| 134 | 215 | * |
| 135 | 216 | * @since 1.1.0 |
| 136 | 217 | * @param object $order Order object. |
| @@ -139,8 +220,11 @@ | ||
| 139 | 220 | if ( empty( $this->funnel->next_step_data ) ) { |
| 140 | 221 | return; |
| 141 | 222 | } |
| 142 | 223 | |
| 224 | + Base_Contacts::step_is_passed(); | |
| 225 | + $this->check_bump_acceptance( $order ); | |
| 226 | + | |
| 143 | 227 | $analytics_updater = new Data_Updater(); |
| 144 | 228 | |
| 145 | 229 | $analytics_updater->set_funnel_id( $this->funnel->funnel_id ); |
| 146 | 230 | $analytics_updater->add_new_order_log( $order ); |
| @@ -145,8 +229,9 @@ | ||
| 145 | 229 | $analytics_updater->set_funnel_id( $this->funnel->funnel_id ); |
| 146 | 230 | $analytics_updater->add_new_order_log( $order ); |
| 147 | 231 | |
| 148 | 232 | $order->update_meta_data( 'sellkit_funnel_next_step_data', $this->funnel->next_step_data ); |
| 233 | + $order->update_meta_data( 'sellkit_funnel_id', $this->funnel->funnel_id ); | |
| 149 | 234 | } |
| 150 | 235 | |
| 151 | 236 | /** |
| 152 | 237 | * Adding page id field to the inputs. |
| @@ -153,12 +238,50 @@ | ||
| 153 | 238 | * |
| 154 | 239 | * @since 1.1.0 |
| 155 | 240 | */ |
| 156 | 241 | public function custom_checkout_hidden_fields() { |
| 242 | + $id = get_the_ID(); | |
| 243 | + $global_checkout_id = get_option( Global_Checkout::SELLKIT_GLOBAL_CHECKOUT_OPTION, 0 ); | |
| 244 | + $default_checkout_id = wc_get_page_id( 'checkout' ); | |
| 245 | + | |
| 246 | + if ( ! empty( $default_checkout_id ) ) { | |
| 247 | + $default_checkout_id = apply_filters( 'wpml_object_id', $default_checkout_id, 'page', true ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML API. | |
| 248 | + } | |
| 249 | + | |
| 250 | + if ( | |
| 251 | + (int) $global_checkout_id > 0 && | |
| 252 | + 'publish' === get_post_status( $global_checkout_id ) && | |
| 253 | + $id === $default_checkout_id | |
| 254 | + ) { | |
| 255 | + $steps = get_post_meta( $global_checkout_id, 'nodes', true ); | |
| 256 | + $checkout_id = 0; | |
| 257 | + | |
| 258 | + foreach ( $steps as $step ) { | |
| 259 | + $step['type'] = (array) $step['type']; | |
| 260 | + | |
| 261 | + if ( 'checkout' === $step['type']['key'] ) { | |
| 262 | + $checkout_id = apply_filters( 'wpml_object_id', $step['page_id'], 'sellkit_step', true ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML API. | |
| 263 | + } | |
| 264 | + } | |
| 265 | + | |
| 266 | + if ( (int) $checkout_id > 0 ) { | |
| 267 | + $id = $checkout_id; | |
| 268 | + } | |
| 269 | + } | |
| 270 | + | |
| 157 | 271 | woocommerce_form_field( 'sellkit_current_page_id', [ |
| 158 | 272 | 'type' => 'hidden', |
| 159 | 273 | 'class' => [ 'hidden form-row-wide' ], |
| 160 | - ], get_the_ID() ); | |
| 274 | + ], $id ); | |
| 275 | + | |
| 276 | + // This field added for passing upsell discount price to backend. | |
| 277 | + woocommerce_form_field( 'sellkit_product_prices', [ | |
| 278 | + 'type' => 'hidden', | |
| 279 | + 'class' => [ 'hidden form-row-wide' ], | |
| 280 | + 'autocomplete' => 'off', | |
| 281 | + 'autocorrect' => 'off', | |
| 282 | + 'autocapitalize' => 'off', | |
| 283 | + ], '0' ); | |
| 161 | 284 | } |
| 162 | 285 | |
| 163 | 286 | /** |
| 164 | 287 | * Check If a coupon should be applied. |
| @@ -193,13 +316,17 @@ | ||
| 193 | 316 | * @param string $type Type. |
| 194 | 317 | * @return false|float|int|mixed |
| 195 | 318 | */ |
| 196 | 319 | public function calculate_discount( $total, $value, $type ) { |
| 197 | - if ( 'fixed' === $type ) { | |
| 320 | + if ( empty( $value ) || 1 > $value ) { | |
| 321 | + return 0; | |
| 322 | + } | |
| 323 | + | |
| 324 | + if ( strpos( $type, 'fixed' ) !== false ) { | |
| 198 | 325 | return $value; |
| 199 | 326 | } |
| 200 | 327 | |
| 201 | - if ( 'percentage' === $type ) { | |
| 328 | + if ( strpos( $type, 'percentage' ) !== false ) { | |
| 202 | 329 | return ( floatval( $total ) * floatval( $value ) ) / 100; |
| 203 | 330 | } |
| 204 | 331 | |
| 205 | 332 | return 0; |
| @@ -249,13 +376,83 @@ | ||
| 249 | 376 | return $valid_bumps; |
| 250 | 377 | } |
| 251 | 378 | |
| 252 | 379 | /** |
| 380 | + * Checks if bump is accepted during checkout or not and updates table accordingly. | |
| 381 | + * | |
| 382 | + * @param object $order WooCommerce order object. | |
| 383 | + * @return void | |
| 384 | + * | |
| 385 | + * @since 1.5.0 | |
| 386 | + * @access private | |
| 387 | + * | |
| 388 | + * @SuppressWarnings(PHPMD.NPathComplexity) | |
| 389 | + */ | |
| 390 | + private function check_bump_acceptance( $order ) { | |
| 391 | + if ( | |
| 392 | + empty( $this->funnel->funnel_id ) || | |
| 393 | + empty( $this->funnel->current_step_data['bump'] ) | |
| 394 | + ) { | |
| 395 | + return; | |
| 396 | + } | |
| 397 | + | |
| 398 | + $bump_product_ids = []; | |
| 399 | + foreach ( $this->funnel->current_step_data['bump'] as $bump ) { | |
| 400 | + if ( empty( $bump['data']['products'] ) ) { | |
| 401 | + continue; | |
| 402 | + } | |
| 403 | + | |
| 404 | + $product_list = ! empty( $bump['data']['products']['list'] ) ? $bump['data']['products']['list'] : []; | |
| 405 | + | |
| 406 | + $bump_product_ids = array_merge( $bump_product_ids, array_keys( $product_list ) ); | |
| 407 | + } | |
| 408 | + | |
| 409 | + $bump_exists_in_order = false; | |
| 410 | + | |
| 411 | + foreach ( $order->get_items() as $item ) { | |
| 412 | + $product_id = $item->get_changes()['product_id']; | |
| 413 | + | |
| 414 | + if ( in_array( $product_id, $bump_product_ids, true ) ) { | |
| 415 | + $bump_exists_in_order = true; | |
| 416 | + break; | |
| 417 | + } | |
| 418 | + } | |
| 419 | + | |
| 420 | + if ( ! $bump_exists_in_order ) { | |
| 421 | + return; | |
| 422 | + } | |
| 423 | + | |
| 424 | + $database = new Database(); | |
| 425 | + $old_values = []; | |
| 426 | + | |
| 427 | + // Getting old data. | |
| 428 | + $contact_id = isset( $_SESSION['entered_funnel_id'] ) ? absint( $_SESSION['entered_funnel_id'] ) : 0; | |
| 429 | + | |
| 430 | + $result = $database->get( 'funnel_contact', [ 'id' => $contact_id ] ); | |
| 431 | + | |
| 432 | + if ( ! empty( $result[0]['order_bump'] ) ) { | |
| 433 | + $old_values = unserialize( $result[0]['order_bump'] ); // phpcs:ignore | |
| 434 | + } | |
| 435 | + | |
| 436 | + $new_values = array_merge( $old_values, [ $this->funnel->current_step_data['page_id'] ] ); | |
| 437 | + | |
| 438 | + $database->update( | |
| 439 | + 'funnel_contact', | |
| 440 | + [ 'order_bump' => $new_values ], | |
| 441 | + [ 'id' => $contact_id ] | |
| 442 | + ); | |
| 443 | + } | |
| 444 | + | |
| 445 | + /** | |
| 253 | 446 | * Updates funnel data. |
| 254 | 447 | * |
| 255 | 448 | * @since 1.1.0 |
| 256 | 449 | */ |
| 257 | 450 | public function update_funnel_data() { |
| 451 | + if ( is_admin() ) { | |
| 452 | + return; | |
| 453 | + } | |
| 454 | + | |
| 258 | 455 | $action = sellkit_htmlspecialchars( INPUT_GET, 'wc-ajax' ); |
| 259 | 456 | |
| 260 | 457 | if ( 'update_order_review' === $action ) { |
| 261 | 458 | $post_data = sellkit_htmlspecialchars( INPUT_POST, 'post_data' ); |
| @@ -261,10 +458,53 @@ | ||
| 261 | 458 | $post_data = sellkit_htmlspecialchars( INPUT_POST, 'post_data' ); |
| 262 | 459 | |
| 263 | 460 | $this->funnel = new Sellkit_Funnel( $this->get_current_page_id_by_post_data( $post_data ) ); |
| 264 | 461 | |
| 462 | + if ( empty( $this->funnel->funnel_id ) ) { | |
| 463 | + $this->global_checkout_funnel_integration(); | |
| 464 | + } | |
| 465 | + | |
| 265 | 466 | return; |
| 266 | 467 | } |
| 267 | 468 | |
| 268 | 469 | $this->funnel = Sellkit_Funnel::get_instance(); |
| 470 | + | |
| 471 | + if ( empty( $this->funnel->funnel_id ) ) { | |
| 472 | + $this->global_checkout_funnel_integration(); | |
| 473 | + } | |
| 474 | + } | |
| 475 | + | |
| 476 | + /** | |
| 477 | + * Setup global checkout as a native sellkit funnel, so every functions works with no issue. | |
| 478 | + * detect funnel id in the global checkout. | |
| 479 | + * | |
| 480 | + * @since 1.7.4 | |
| 481 | + */ | |
| 482 | + public function global_checkout_funnel_integration() { | |
| 483 | + $global_checkout_id = get_option( Global_Checkout::SELLKIT_GLOBAL_CHECKOUT_OPTION, 0 ); | |
| 484 | + | |
| 485 | + if ( $global_checkout_id > 0 && 'publish' === get_post_status( $global_checkout_id ) ) { | |
| 486 | + $steps = get_post_meta( $global_checkout_id, 'nodes', true ); | |
| 487 | + $checkout_id = 0; | |
| 488 | + | |
| 489 | + if ( ! is_array( $steps ) ) { | |
| 490 | + return; | |
| 491 | + } | |
| 492 | + | |
| 493 | + foreach ( $steps as $step ) { | |
| 494 | + $step['type'] = (array) $step['type']; | |
| 495 | + | |
| 496 | + if ( 'checkout' === $step['type']['key'] ) { | |
| 497 | + $checkout_id = $step['page_id']; | |
| 498 | + } | |
| 499 | + } | |
| 500 | + | |
| 501 | + if ( (int) $checkout_id > 0 ) { | |
| 502 | + $this->funnel = new Sellkit_Funnel( $checkout_id ); | |
| 503 | + | |
| 504 | + add_filter( 'sellkit_global_checkout_activated', function() { | |
| 505 | + return true; | |
| 506 | + } ); | |
| 507 | + } | |
| 508 | + } | |
| 269 | 509 | } |
| 270 | 510 | } |