| @@ -20,8 +20,46 @@ | ||
| 20 | 20 | protected $current_ob_settings; |
| 21 | 21 | protected $selected_ob; |
| 22 | 22 | protected $checkout_id; |
| 23 | 23 | |
| 24 | + /** | |
| 25 | + * Counts how many times each (checkout_id, bump key) pair has been | |
| 26 | + * rendered on this request. Some checkout layouts (e.g. the express | |
| 27 | + * checkout's two-column form) call `woocommerce_order_review()` more than | |
| 28 | + * once, which fires the order bump position hooks more than once too, so | |
| 29 | + * the same bump's markup ends up on the page more than once. Every | |
| 30 | + * occurrence still needs to work (it may be the one the customer actually | |
| 31 | + * sees), so instead of suppressing the repeats we give each render its | |
| 32 | + * own instance number and use it to keep the checkbox's `id`/`for` pair | |
| 33 | + * unique per occurrence -- see $render_instance below. | |
| 34 | + * | |
| 35 | + * @var array | |
| 36 | + */ | |
| 37 | + protected static $render_counts = array(); | |
| 38 | + | |
| 39 | + /** | |
| 40 | + * The 0-based occurrence number of the bump currently being rendered, | |
| 41 | + * for the (checkout_id, key) pair being processed. Templates use this to | |
| 42 | + * suffix the checkbox's id/for attributes so duplicate renders on the | |
| 43 | + * same page never collide -- `data-key` (still just $key) is what JS | |
| 44 | + * uses to keep every occurrence of the same bump in sync. | |
| 45 | + * | |
| 46 | + * @var int | |
| 47 | + */ | |
| 48 | + public $render_instance = 0; | |
| 49 | + | |
| 50 | + /** | |
| 51 | + * @param int|string $checkout_id | |
| 52 | + * @param int|string $key | |
| 53 | + * | |
| 54 | + * @return void | |
| 55 | + */ | |
| 56 | + private function mark_bump_render_instance( $checkout_id, $key ) { | |
| 57 | + $cache_key = $checkout_id . ':' . $key; | |
| 58 | + $this->render_instance = isset( self::$render_counts[ $cache_key ] ) ? self::$render_counts[ $cache_key ] : 0; | |
| 59 | + self::$render_counts[ $cache_key ] = $this->render_instance + 1; | |
| 60 | + } | |
| 61 | + | |
| 24 | 62 | public function __construct() { |
| 25 | 63 | |
| 26 | 64 | add_action( 'woocommerce_checkout_after_order_review', array( $this, 'add_order_bump_hidden_fields' ), 99 ); |
| 27 | 65 | add_action( 'wpfunnels/elementor_render_order_bump', array( $this, 'load_elementor_actions' ), 9999, 2 ); |
| @@ -27,8 +65,9 @@ | ||
| 27 | 65 | add_action( 'wpfunnels/elementor_render_order_bump', array( $this, 'load_elementor_actions' ), 9999, 2 ); |
| 28 | 66 | add_action( 'wpfunnels/bricks_render_order_bump', array( $this, 'load_elementor_actions' ), 9999, 2 ); |
| 29 | 67 | add_action( 'wpfunnels/gb_render_order_bump_ajax', array( $this, 'load_gb_actions' ), 10, 2 ); |
| 30 | 68 | add_action( 'wpfunnels/before_checkout_form', array( $this, 'load_actions' ), 10, 2 ); |
| 69 | + add_action( 'woocommerce_checkout_update_order_review', array( $this, 'load_ajax_actions' ), 5 ); | |
| 31 | 70 | |
| 32 | 71 | } |
| 33 | 72 | |
| 34 | 73 | |
| @@ -40,9 +79,10 @@ | ||
| 40 | 79 | |
| 41 | 80 | $this->ob_settings = $checkout_meta->get_checkout_meta_value(get_the_ID(), 'order-bump-settings', wpfnl()->meta->get_default_order_bump_meta()); |
| 42 | 81 | foreach( $this->ob_settings as $key=>$settings ){ |
| 43 | 82 | if ( $settings['isEnabled'] ) { |
| 44 | - echo '<input type="hidden" name="_wpfunnels_order_bump_product_'.$key.'" value="">'; | |
| 83 | + $in_cart = isset( $settings['product'] ) && $this->is_order_bump_in_cart( $settings['product'] ); | |
| 84 | + echo '<input type="hidden" name="_wpfunnels_order_bump_product_'.$key.'" value="'.( $in_cart ? esc_attr( $settings['product'] ) : '' ).'">'; | |
| 45 | 85 | echo '<input type="hidden" name="_wpfunnels_order_bump_clicked" value="">'; |
| 46 | 86 | } |
| 47 | 87 | } |
| 48 | 88 | } |
| @@ -47,8 +87,33 @@ | ||
| 47 | 87 | } |
| 48 | 88 | } |
| 49 | 89 | |
| 50 | 90 | |
| 91 | + /** | |
| 92 | + * Load action hooks during the update_order_review AJAX request. | |
| 93 | + * | |
| 94 | + * `wpfunnels/before_checkout_form` only fires while the full checkout form is | |
| 95 | + * rendered, so on an AJAX refresh nothing would be listening on | |
| 96 | + * `wpfunnels/after_order_total`. An `after-order` bump would then be dropped from | |
| 97 | + * the refreshed `.wpfnl-after-order-total` fragment and disappear from the page. | |
| 98 | + * | |
| 99 | + * @return void | |
| 100 | + */ | |
| 101 | + public function load_ajax_actions() { | |
| 102 | + $checkout_id = Wpfnl_functions::get_checkout_id_from_post( $_POST ); // phpcs:ignore WordPress.Security.NonceVerification.Missing | |
| 103 | + | |
| 104 | + if ( ! $checkout_id ) { | |
| 105 | + return; | |
| 106 | + } | |
| 107 | + | |
| 108 | + // render_order_bump_after_order() falls back to this property when $post is | |
| 109 | + // unavailable, which is always the case during this AJAX request. | |
| 110 | + $this->checkout_id = (int) $checkout_id; | |
| 111 | + | |
| 112 | + $this->load_actions( $checkout_id ); | |
| 113 | + } | |
| 114 | + | |
| 115 | + | |
| 51 | 116 | public function load_actions( $checkout_id, $settings = array() ) { |
| 52 | 117 | if( !function_exists( 'is_plugin_active' ) ) { |
| 53 | 118 | require_once( ABSPATH . '/wp-admin/includes/plugin.php' ); |
| 54 | 119 | if( is_plugin_active( 'elementor/elementor.php' ) ) { |
| @@ -209,8 +274,35 @@ | ||
| 209 | 274 | } |
| 210 | 275 | |
| 211 | 276 | |
| 212 | 277 | /** |
| 278 | + * Check if an order bump product is already present in the cart so the | |
| 279 | + * checkbox can be rendered as checked after a page reload. | |
| 280 | + * | |
| 281 | + * @param int|string $product_id product/variation id stored in the order bump settings | |
| 282 | + * | |
| 283 | + * @return bool | |
| 284 | + */ | |
| 285 | + public function is_order_bump_in_cart( $product_id ) { | |
| 286 | + if ( ! Wpfnl_functions::is_wc_active() || ! WC()->cart ) { | |
| 287 | + return false; | |
| 288 | + } | |
| 289 | + | |
| 290 | + foreach ( WC()->cart->get_cart() as $cart_item ) { | |
| 291 | + if ( empty( $cart_item['wpfnl_order_bump'] ) ) { | |
| 292 | + continue; | |
| 293 | + } | |
| 294 | + | |
| 295 | + if ( $cart_item['product_id'] == $product_id || ( ! empty( $cart_item['variation_id'] ) && $cart_item['variation_id'] == $product_id ) ) { | |
| 296 | + return true; | |
| 297 | + } | |
| 298 | + } | |
| 299 | + | |
| 300 | + return false; | |
| 301 | + } | |
| 302 | + | |
| 303 | + | |
| 304 | + /** | |
| 213 | 305 | * Render popup style for order bump in elementor |
| 214 | 306 | * builder preview |
| 215 | 307 | * |
| 216 | 308 | * @since 2.0.3 |
| @@ -260,8 +352,10 @@ | ||
| 260 | 352 | foreach( $this->ob_settings as $key=> $settings ){ |
| 261 | 353 | |
| 262 | 354 | if( $settings['isEnabled'] && $settings['position'] == 'popup'){ |
| 263 | 355 | |
| 356 | + $this->mark_bump_render_instance( $checkout_id, $key ); | |
| 357 | + | |
| 264 | 358 | if( $settings['isEnabled'] === 'no' ){ |
| 265 | 359 | continue; |
| 266 | 360 | } |
| 267 | 361 | |
| @@ -341,8 +435,9 @@ | ||
| 341 | 435 | |
| 342 | 436 | foreach( $this->ob_settings as $key=> $settings ){ |
| 343 | 437 | |
| 344 | 438 | if( $settings['isEnabled'] && $settings['position'] == 'before-checkout'){ |
| 439 | + $this->mark_bump_render_instance( $checkout_id, $key ); | |
| 345 | 440 | if( $settings['isEnabled'] === 'no' ){ |
| 346 | 441 | continue; |
| 347 | 442 | } |
| 348 | 443 | |
| @@ -421,8 +516,9 @@ | ||
| 421 | 516 | $type = get_post_meta($funnel_id,'_wpfnl_funnel_type',true); |
| 422 | 517 | foreach( $this->ob_settings as $key=> $settings ){ |
| 423 | 518 | |
| 424 | 519 | if( $settings['isEnabled'] && $settings['position'] == 'before-lms-order'){ |
| 520 | + $this->mark_bump_render_instance( $checkout_id, $key ); | |
| 425 | 521 | if( $settings['isEnabled'] === 'no' ){ |
| 426 | 522 | continue; |
| 427 | 523 | } |
| 428 | 524 | |
| @@ -501,8 +597,9 @@ | ||
| 501 | 597 | $type = get_post_meta($funnel_id,'_wpfnl_funnel_type',true); |
| 502 | 598 | foreach( $this->ob_settings as $key=> $settings ){ |
| 503 | 599 | |
| 504 | 600 | if( $settings['isEnabled'] && $settings['position'] == 'after-lms-order'){ |
| 601 | + $this->mark_bump_render_instance( $checkout_id, $key ); | |
| 505 | 602 | if( $settings['isEnabled'] === 'no' ){ |
| 506 | 603 | continue; |
| 507 | 604 | } |
| 508 | 605 | |
| @@ -581,8 +678,9 @@ | ||
| 581 | 678 | |
| 582 | 679 | foreach( $this->ob_settings as $key=> $settings ){ |
| 583 | 680 | |
| 584 | 681 | if( $settings['isEnabled'] && $settings['position'] == 'before-payment'){ |
| 682 | + $this->mark_bump_render_instance( $checkout_id, $key ); | |
| 585 | 683 | if( $settings['isEnabled'] === 'no' ){ |
| 586 | 684 | continue; |
| 587 | 685 | } |
| 588 | 686 | |
| @@ -662,8 +760,9 @@ | ||
| 662 | 760 | |
| 663 | 761 | foreach( $this->ob_settings as $key=> $settings ){ |
| 664 | 762 | |
| 665 | 763 | if( $settings['isEnabled'] && $settings['position'] == 'after-payment'){ |
| 764 | + $this->mark_bump_render_instance( $checkout_id, $key ); | |
| 666 | 765 | if( $settings['isEnabled'] === 'no' ){ |
| 667 | 766 | continue; |
| 668 | 767 | } |
| 669 | 768 | |
| @@ -743,8 +842,9 @@ | ||
| 743 | 842 | $type = get_post_meta( $funnel_id, '_wpfnl_funnel_type', true ); |
| 744 | 843 | |
| 745 | 844 | foreach( $this->ob_settings as $key=> $settings ){ |
| 746 | 845 | if( $settings['isEnabled'] && $settings['position'] == 'after-customer-details'){ |
| 846 | + $this->mark_bump_render_instance( $checkout_id, $key ); | |
| 747 | 847 | if( $settings['isEnabled'] === 'no' ){ |
| 748 | 848 | continue; |
| 749 | 849 | } |
| 750 | 850 | // Check conditional rules before displaying |
| @@ -824,8 +924,9 @@ | ||
| 824 | 924 | foreach( $this->ob_settings as $key=> $settings ){ |
| 825 | 925 | |
| 826 | 926 | if( $settings['isEnabled'] && $settings['position'] == 'after-order'){ |
| 827 | 927 | |
| 928 | + $this->mark_bump_render_instance( $checkout_id, $key ); | |
| 828 | 929 | |
| 829 | 930 | if( $settings['isEnabled'] === 'no' ){ |
| 830 | 931 | continue; |
| 831 | 932 | } |
| @@ -909,8 +1010,9 @@ | ||
| 909 | 1010 | |
| 910 | 1011 | foreach( $this->ob_settings as $key=> $settings ){ |
| 911 | 1012 | |
| 912 | 1013 | if( $settings['isEnabled'] && $settings['position'] == 'before-order'){ |
| 1014 | + $this->mark_bump_render_instance( $checkout_id, $key ); | |
| 913 | 1015 | if( $settings['isEnabled'] === 'no' ){ |
| 914 | 1016 | continue; |
| 915 | 1017 | } |
| 916 | 1018 | |