| @@ -5,8 +5,9 @@ | ||
| 5 | 5 | use Sellkit\Funnel\Analytics\Data_Updater; |
| 6 | 6 | use Sellkit\Funnel\Contacts\Base_Contacts; |
| 7 | 7 | use Sellkit\Database; |
| 8 | 8 | use Sellkit_Funnel; |
| 9 | +use Sellkit\Global_Checkout\Checkout as Global_Checkout; | |
| 9 | 10 | |
| 10 | 11 | defined( 'ABSPATH' ) || die(); |
| 11 | 12 | |
| 12 | 13 | /** |
| @@ -39,11 +40,52 @@ | ||
| 39 | 40 | |
| 40 | 41 | add_action( 'woocommerce_checkout_create_order', [ $this, 'save_order_custom_meta_data' ], 10 ); |
| 41 | 42 | add_action( 'woocommerce_after_order_notes', [ $this, 'custom_checkout_hidden_fields' ], 10 ); |
| 42 | 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 | + } | |
| 43 | 52 | } |
| 44 | 53 | |
| 45 | 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 | + /** | |
| 46 | 88 | * Adding items to the checkout page. |
| 47 | 89 | * |
| 48 | 90 | * @since 1.1.0 |
| 49 | 91 | * @SuppressWarnings(PHPMD.NPathComplexity) |
| @@ -53,15 +95,29 @@ | ||
| 53 | 95 | // These variables should be used in checkout builder widget. |
| 54 | 96 | $funnel_data = $this->funnel->current_step_data; |
| 55 | 97 | $optimization_data = ! empty( $funnel_data['data']['optimization'] ) ? $funnel_data['data']['optimization'] : ''; |
| 56 | 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'; | |
| 57 | 100 | |
| 58 | - 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 ) { | |
| 59 | 114 | $product_settings = ! empty( $funnel_data['data']['products']['settings'] ) ? $funnel_data['data']['products']['settings'] : ''; |
| 60 | 115 | $bump_data = ! empty( $funnel_data['bump'] ) ? $funnel_data['bump'] : ''; |
| 61 | 116 | |
| 62 | 117 | if ( ! empty( $product_settings ) ) { |
| 63 | 118 | set_query_var( 'funnel_product_settings', $product_settings ); |
| 119 | + set_query_var( 'funnel_reset_cart', $reset_cart ); | |
| 64 | 120 | } |
| 65 | 121 | |
| 66 | 122 | if ( ! empty( $bump_data ) ) { |
| 67 | 123 | $bump_data = $this->get_valid_bumps( $bump_data ); |
| @@ -66,9 +122,13 @@ | ||
| 66 | 122 | if ( ! empty( $bump_data ) ) { |
| 67 | 123 | $bump_data = $this->get_valid_bumps( $bump_data ); |
| 68 | 124 | |
| 69 | 125 | set_query_var( 'bump_data', $bump_data ); |
| 126 | + } else { | |
| 127 | + set_query_var( 'bump_data', [] ); | |
| 70 | 128 | } |
| 129 | + | |
| 130 | + set_query_var( 'funnel_reset_cart', $reset_cart ); | |
| 71 | 131 | } ); |
| 72 | 132 | |
| 73 | 133 | if ( |
| 74 | 134 | ! empty( $optimization_data['expiration_date'] ) && |
| @@ -99,9 +159,11 @@ | ||
| 99 | 159 | |
| 100 | 160 | $action = sellkit_htmlspecialchars( INPUT_GET, 'wc-ajax' ); |
| 101 | 161 | |
| 102 | 162 | if ( 'update_order_review' !== $action && 'checkout' !== $action ) { |
| 103 | - wc()->cart->empty_cart(); | |
| 163 | + if ( 'true' === $reset_cart || true === $reset_cart ) { | |
| 164 | + wc()->cart->empty_cart(); | |
| 165 | + } | |
| 104 | 166 | |
| 105 | 167 | foreach ( $funnel_products as $product_id => $product ) { |
| 106 | 168 | if ( empty( $product ) ) { |
| 107 | 169 | continue; |
| @@ -106,11 +168,14 @@ | ||
| 106 | 168 | if ( empty( $product ) ) { |
| 107 | 169 | continue; |
| 108 | 170 | } |
| 109 | 171 | |
| 110 | - $quantity = ! empty( $product['quantity'] ) ? $product['quantity'] : 1; | |
| 172 | + $quantity = ! empty( $product['quantity'] ) ? $product['quantity'] : 1; | |
| 173 | + $product_cart_id = WC()->cart->generate_cart_id( $product_id ); | |
| 111 | 174 | |
| 112 | - wc()->cart->add_to_cart( $product_id, $quantity ); | |
| 175 | + if ( ! WC()->cart->find_product_in_cart( $product_cart_id ) ) { | |
| 176 | + WC()->cart->add_to_cart( $product_id, $quantity ); | |
| 177 | + } | |
| 113 | 178 | } |
| 114 | 179 | } |
| 115 | 180 | |
| 116 | 181 | if ( empty( $optimization_data ) ) { |
| @@ -118,8 +183,12 @@ | ||
| 118 | 183 | } |
| 119 | 184 | |
| 120 | 185 | if ( self::apply_coupon_validation( $optimization_data ) ) { |
| 121 | 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 | + } | |
| 122 | 191 | wc()->cart->add_discount( get_the_title( $auto_apply_coupon['value'] ) ); |
| 123 | 192 | } |
| 124 | 193 | |
| 125 | 194 | wc_clear_notices(); |
| @@ -169,12 +238,50 @@ | ||
| 169 | 238 | * |
| 170 | 239 | * @since 1.1.0 |
| 171 | 240 | */ |
| 172 | 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 | + | |
| 173 | 271 | woocommerce_form_field( 'sellkit_current_page_id', [ |
| 174 | 272 | 'type' => 'hidden', |
| 175 | 273 | 'class' => [ 'hidden form-row-wide' ], |
| 176 | - ], 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' ); | |
| 177 | 284 | } |
| 178 | 285 | |
| 179 | 286 | /** |
| 180 | 287 | * Check If a coupon should be applied. |
| @@ -276,8 +383,10 @@ | ||
| 276 | 383 | * @return void |
| 277 | 384 | * |
| 278 | 385 | * @since 1.5.0 |
| 279 | 386 | * @access private |
| 387 | + * | |
| 388 | + * @SuppressWarnings(PHPMD.NPathComplexity) | |
| 280 | 389 | */ |
| 281 | 390 | private function check_bump_acceptance( $order ) { |
| 282 | 391 | if ( |
| 283 | 392 | empty( $this->funnel->funnel_id ) || |
| @@ -291,9 +400,11 @@ | ||
| 291 | 400 | if ( empty( $bump['data']['products'] ) ) { |
| 292 | 401 | continue; |
| 293 | 402 | } |
| 294 | 403 | |
| 295 | - $bump_product_ids = array_merge( $bump_product_ids, array_keys( $bump['data']['products']['list'] ) ); | |
| 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 ) ); | |
| 296 | 407 | } |
| 297 | 408 | |
| 298 | 409 | $bump_exists_in_order = false; |
| 299 | 410 | |
| @@ -313,10 +424,12 @@ | ||
| 313 | 424 | $database = new Database(); |
| 314 | 425 | $old_values = []; |
| 315 | 426 | |
| 316 | 427 | // Getting old data. |
| 317 | - $result = $database->get( 'funnel_contact', [ 'id' => $_SESSION['entered_funnel_id'] ] ); | |
| 428 | + $contact_id = isset( $_SESSION['entered_funnel_id'] ) ? absint( $_SESSION['entered_funnel_id'] ) : 0; | |
| 318 | 429 | |
| 430 | + $result = $database->get( 'funnel_contact', [ 'id' => $contact_id ] ); | |
| 431 | + | |
| 319 | 432 | if ( ! empty( $result[0]['order_bump'] ) ) { |
| 320 | 433 | $old_values = unserialize( $result[0]['order_bump'] ); // phpcs:ignore |
| 321 | 434 | } |
| 322 | 435 | |
| @@ -324,9 +437,9 @@ | ||
| 324 | 437 | |
| 325 | 438 | $database->update( |
| 326 | 439 | 'funnel_contact', |
| 327 | 440 | [ 'order_bump' => $new_values ], |
| 328 | - [ 'id' => $_SESSION['entered_funnel_id'] ] | |
| 441 | + [ 'id' => $contact_id ] | |
| 329 | 442 | ); |
| 330 | 443 | } |
| 331 | 444 | |
| 332 | 445 | /** |
| @@ -334,8 +447,12 @@ | ||
| 334 | 447 | * |
| 335 | 448 | * @since 1.1.0 |
| 336 | 449 | */ |
| 337 | 450 | public function update_funnel_data() { |
| 451 | + if ( is_admin() ) { | |
| 452 | + return; | |
| 453 | + } | |
| 454 | + | |
| 338 | 455 | $action = sellkit_htmlspecialchars( INPUT_GET, 'wc-ajax' ); |
| 339 | 456 | |
| 340 | 457 | if ( 'update_order_review' === $action ) { |
| 341 | 458 | $post_data = sellkit_htmlspecialchars( INPUT_POST, 'post_data' ); |
| @@ -341,10 +458,53 @@ | ||
| 341 | 458 | $post_data = sellkit_htmlspecialchars( INPUT_POST, 'post_data' ); |
| 342 | 459 | |
| 343 | 460 | $this->funnel = new Sellkit_Funnel( $this->get_current_page_id_by_post_data( $post_data ) ); |
| 344 | 461 | |
| 462 | + if ( empty( $this->funnel->funnel_id ) ) { | |
| 463 | + $this->global_checkout_funnel_integration(); | |
| 464 | + } | |
| 465 | + | |
| 345 | 466 | return; |
| 346 | 467 | } |
| 347 | 468 | |
| 348 | 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 | + } | |
| 349 | 509 | } |
| 350 | 510 | } |