PluginProbe
CartFlows – Funnel Builder & Checkout Plugin for WooCommerce / 3.3.0
CartFlows – Funnel Builder & Checkout Plugin for WooCommerce v3.3.0
3.3.0 3.2.1 3.2.0 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.1 trunk 1.0.4 1.1.0 1.1.0.1 1.1.1 1.1.10 1.1.11 1.1.12 1.1.13 1.1.14 1.1.15 1.1.16 1.1.17 1.1.18 1.1.19 1.1.2 All 162 releases
← All changes | classes/class-cartflows-admin-notices.php +161 -16 3.0.1 → 3.3.0 View file →
@@ -14,8 +14,22 @@
14 14 */
15 15 class Cartflows_Admin_Notices {
16 16
17 17 /**
18 + * Option key that stores whether the site has received at least one
19 + * WooCommerce order through a CartFlows funnel.
20 + *
21 + * This is the success milestone used to gate the WordPress.org review
22 + * notice — we only ask for a review after a user has seen real value
23 + * from the plugin (revenue through a funnel), not after an arbitrary
24 + * time-since-install delay.
25 + *
26 + * @since 2.2.5
27 + * @var string
28 + */
29 + const FIRST_FUNNEL_ORDER_OPTION = 'cartflows_first_funnel_order_received';
30 +
31 + /**
18 32 * Instance
19 33 *
20 34 * @access private
21 35 * @var object Class object.
@@ -49,8 +63,20 @@
49 63 $this->register_admin_notices();
50 64
51 65 // Group the ajax action callbacks.
52 66 $this->register_ajax_callbacks();
67 +
68 + // Track first successful CartFlows funnel order to unlock the review prompt.
69 + //
70 + // `woocommerce_new_order` fires *before* CartFlows saves `_wcf_flow_id`
71 + // (the checkout module persists it on `woocommerce_checkout_update_order_meta`,
72 + // which runs later in the same request). We therefore listen on hooks that
73 + // run *after* the flow meta is on the order:
74 + //
75 + // - `woocommerce_checkout_order_processed` — classic shortcode checkout.
76 + // - `woocommerce_store_api_checkout_order_processed` — Blocks-based checkout.
77 + add_action( 'woocommerce_checkout_order_processed', array( $this, 'maybe_flag_first_funnel_order' ), 10, 3 );
78 + add_action( 'woocommerce_store_api_checkout_order_processed', array( $this, 'maybe_flag_first_funnel_order' ), 10, 1 );
53 79 }
54 80
55 81 /**
56 82 * Registers admin notices for CartFlows.
@@ -87,9 +113,9 @@
87 113
88 114 /**
89 115 * Persist the "Switch to New UI" dismissal for the current user.
90 116 *
91 - * Writes the same `notice-dismissed` user_meta key that Astra_Notices uses
117 + * Writes the same `notice-dismissed` user_meta key that BSF_Admin_Notices uses
92 118 * so the WP admin_notices banner and the React-shell banner stay in sync —
93 119 * dismissing either one hides both on the next load.
94 120 *
95 121 * @since 2.2.4
@@ -277,9 +303,9 @@
277 303 array(
278 304 'id' => 'cartflows-5-start-notice',
279 305 'type' => 'info',
280 306 'class' => 'cartflows-5-star cartflows-admin-notice',
281 - 'show_if' => true,
307 + 'show_if' => $this->should_show_review_notice(),
282 308 /* translators: %1$s white label plugin name and %2$s deactivation link */
283 309 'message' => sprintf(
284 310 '<div class="notice-image" style="display: block;">
285 311 <img src="%1$s" class="custom-logo" alt="CartFlows Icon" itemprop="logo" style="max-width: 90px; border-radius: 50px;"></div>
@@ -302,9 +328,9 @@
302 328 </a>
303 329
304 330 <a href="#" class="astra-notice-close astra-review-notice">
305 331 <span class="dashicons dashicons-smiley"></span>
306 - <u>%8$s</u>
332 + %8$s
307 333 </a>
308 334 </div>
309 335 </div>',
310 336 $image_path,
@@ -309,9 +335,9 @@
309 335 </div>',
310 336 $image_path,
311 337 __( 'Hi there! You recently used CartFlows to build a sales funnel &mdash; Thanks a ton!', 'cartflows' ),
312 338 __( 'It would be awesome if you could leave us a 5-star review-it helps us grow and guide others in choosing CartFlows!', 'cartflows' ),
313 - 'https://wordpress.org/support/plugin/cartflows/reviews/?filter=5#new-post',
339 + 'https://wordpress.org/support/plugin/cartflows/reviews/#new-post',
314 340 __( 'Ok, you deserve it', 'cartflows' ),
315 341 MONTH_IN_SECONDS,
316 342 __( 'Nope, maybe later', 'cartflows' ),
317 343 __( 'I already did', 'cartflows' )
@@ -321,9 +347,9 @@
321 347 )
322 348 );
323 349
324 350 // CartFlows 3.0 — soft notice inviting legacy users to opt into the new admin UI.
325 - // Registered through Astra_Notices so it inherits the standard notice
351 + // Registered through BSF_Admin_Notices so it inherits the standard notice
326 352 // design, dismiss handling and per-user storage the rest of CartFlows uses.
327 353 if ( $this->is_new_ui_notice_visible() ) {
328 354 $this->enqueue_new_ui_notice_assets();
329 355
@@ -388,9 +414,9 @@
388 414 'nps_rating_message' => __( 'How likely are you to recommend #pluginname to your friends or colleagues?', 'cartflows' ),
389 415
390 416 // Step 2A i.e. positive.
391 417 'feedback_content' => __( 'Could you please do us a favor and give us a 5-star rating on WordPress? It would help others choose CartFlows with confidence. Thank you!', 'cartflows' ),
392 - 'plugin_rating_link' => esc_url( 'https://wordpress.org/support/plugin/cartflows/reviews/?filter=5#new-post' ),
418 + 'plugin_rating_link' => esc_url( 'https://wordpress.org/support/plugin/cartflows/reviews/#new-post' ),
393 419
394 420 // Step 2B i.e. negative.
395 421 'plugin_rating_title' => __( 'Thank you for your feedback', 'cartflows' ),
396 422 'plugin_rating_content' => __( 'We value your input. How can we improve your experience?', 'cartflows' ),
@@ -527,8 +553,52 @@
527 553 return false;
528 554 }
529 555
530 556 /**
557 + * Determine whether the WordPress.org 5-star review notice should be shown.
558 + *
559 + * Gates the review ask on a real success milestone (first WooCommerce
560 + * order processed through a CartFlows funnel) rather than an arbitrary
561 + * time-since-install delay. This mirrors the usage-milestone pattern
562 + * Elementor uses (>10 pages built) and sharpens the ask like FunnelKit's
563 + * support-triggered outreach — both high-ROI strategies validated in the
564 + * April 2026 WordPress.org review competitive analysis.
565 + *
566 + * The check is lightweight (one published-post count + one option read)
567 + * and fires only on allowed admin screens, so the overhead is negligible.
568 + *
569 + * @since 2.2.5
570 + * @return bool True when the user has reached the success milestone.
571 + */
572 + public function should_show_review_notice() {
573 +
574 + $total_funnels = $this->get_published_flow_count();
575 + $first_order_received = (bool) get_option( self::FIRST_FUNNEL_ORDER_OPTION, false );
576 +
577 + $should_show = ( $total_funnels >= 1 ) && $first_order_received;
578 +
579 + /**
580 + * Filters whether the WordPress.org 5-star review notice should be shown.
581 + *
582 + * Allows site owners, agencies, and white-label distributors to override
583 + * the default gating logic. Return false to suppress the notice entirely,
584 + * or true to force-show it (e.g. in staging for QA).
585 + *
586 + * @since 2.2.5
587 + *
588 + * @param bool $should_show Whether the notice should be shown.
589 + * @param int $total_funnels Number of published CartFlows flows.
590 + * @param bool $first_order_received Whether the first funnel order flag is set.
591 + */
592 + return (bool) apply_filters(
593 + 'cartflows_show_review_notice',
594 + $should_show,
595 + $total_funnels,
596 + $first_order_received
597 + );
598 + }
599 +
600 + /**
531 601 * Whether the "Switch to New UI" promo should be shown to the current user.
532 602 *
533 603 * Centralises the gating logic so the WP `admin_notices` render and the
534 604 * React-shell banner both honour the exact same conditions.
@@ -548,9 +618,9 @@
548 618 if ( ! defined( 'CARTFLOWS_LEGACY_ADMIN' ) || ! CARTFLOWS_LEGACY_ADMIN ) {
549 619 return false;
550 620 }
551 621
552 - // Astra_Notices' own astra-notice-close JS writes 'notice-dismissed' to
622 + // BSF_Admin_Notices' own astra-notice-close JS writes 'notice-dismissed' to
553 623 // this user_meta key when the user dismisses the notice.
554 624 if ( 'notice-dismissed' === get_user_meta( get_current_user_id(), 'cartflows-switch-to-new-ui-notice', true ) ) {
555 625 return false;
556 626 }
@@ -593,26 +663,101 @@
593 663 );
594 664 }
595 665
596 666 /**
597 - * Check if the user has completed the onboarding, skipped the onboarding on ready step, and the store checkout is imported.
667 + * Hook callback for the WooCommerce checkout-completion actions.
598 668 *
669 + * When an order is successfully processed through a CartFlows funnel,
670 + * persist a single option flag so the review notice can unlock. The
671 + * option acts as an idempotent one-way switch — once set, it is never
672 + * unset or re-evaluated on subsequent orders, keeping the hook cost at
673 + * effectively zero after the first qualifying order.
674 + *
675 + * @since 2.2.5
676 + *
677 + * @param int|WC_Order $order_or_id Order ID (classic checkout) or WC_Order
678 + * (Blocks Store API passes the object).
679 + * @param mixed $unused Unused — present so this method can also
680 + * attach to `woocommerce_checkout_order_processed`,
681 + * which passes `( $order_id, $posted_data, $order )`.
682 + * @param WC_Order|null $order Order object (third arg from
683 + * `woocommerce_checkout_order_processed`).
684 + * @return void
685 + */
686 + public function maybe_flag_first_funnel_order( $order_or_id, $unused = null, $order = null ) {
687 +
688 + // Cheapest possible short-circuit: once flagged, do no further work.
689 + if ( (bool) get_option( self::FIRST_FUNNEL_ORDER_OPTION, false ) ) {
690 + return;
691 + }
692 +
693 + // Normalise the various hook signatures into a single WC_Order instance.
694 + if ( $order_or_id instanceof WC_Order ) {
695 + $order = $order_or_id;
696 + } elseif ( ! $order instanceof WC_Order && function_exists( 'wc_get_order' ) ) {
697 + $order = wc_get_order( $order_or_id );
698 + }
699 +
700 + if ( ! $order instanceof WC_Order ) {
701 + return;
702 + }
703 +
704 + $flow_meta = $order->get_meta( '_wcf_flow_id' );
705 + $flow_id = is_scalar( $flow_meta ) ? absint( $flow_meta ) : 0;
706 +
707 + if ( $flow_id > 0 ) {
708 + update_option( self::FIRST_FUNNEL_ORDER_OPTION, 'yes', false );
709 + }
710 + }
711 +
712 + /**
713 + * Count of published CartFlows flows.
714 + *
715 + * Extracted into its own method so the gating/NPS checks share a single
716 + * implementation that is easy to stub in unit tests.
717 + *
718 + * @since 2.2.5
719 + * @return int
720 + */
721 + protected function get_published_flow_count() {
722 +
723 + if ( ! defined( 'CARTFLOWS_FLOW_POST_TYPE' ) ) {
724 + return 0;
725 + }
726 +
727 + $counts = wp_count_posts( CARTFLOWS_FLOW_POST_TYPE );
728 +
729 + return isset( $counts->publish ) ? (int) $counts->publish : 0;
730 + }
731 +
732 + /**
733 + * Check if the user is eligible to see the NPS survey.
734 + *
735 + * Previous behaviour used `1 >= $total_funnels` which EXCLUDED engaged
736 + * users with more than one published funnel — the exact audience we most
737 + * want feedback from. This revision inverts that logic: once a user has
738 + * reached post-onboarding engagement (store checkout set up OR at least
739 + * one published funnel), they are eligible regardless of funnel count.
740 + *
599 741 * @since 2.1.6
742 + * @since 2.2.5 Fixed audience logic to include power users (>1 funnel).
600 743 * @return bool
601 744 */
602 745 public function should_display_nps_survey_notice() {
603 746
604 - $is_store_checkout_imported = (bool) get_option( '_cartflows_wizard_store_checkout_set', false ); // Must be true.
605 - $onboarding_completed = (bool) get_option( 'wcf_setup_complete', false ); // Must be true.
606 - $is_first_funnel_imported = (bool) get_option( 'wcf_first_flow_imported', false ); // Must be true.
607 - $total_funnels = intval( wp_count_posts( CARTFLOWS_FLOW_POST_TYPE )->publish ); // Must be greater than or equal to 1.
747 + $is_store_checkout_imported = (bool) get_option( '_cartflows_wizard_store_checkout_set', false );
748 + $onboarding_completed = (bool) get_option( 'wcf_setup_complete', false );
749 + $total_funnels = $this->get_published_flow_count();
608 750
609 751 /**
610 - * Show the notice in two conditions.
611 - * 1. If completed the onboarding steps/process of plugin and sets their first store checkout funnel successfully.
612 - * 2. If sets up the first funnel manually and makes it live.
752 + * Show the notice in either of these conditions:
753 + * 1. User finished the onboarding wizard AND imported the store checkout funnel.
754 + * 2. User has at least one published funnel — imported or built by hand. This has
755 + * NO upper bound on funnel count, so power users still see the survey, and it
756 + * subsumes the old "imported their first funnel" branch.
613 757 */
614 - return ( true === $is_store_checkout_imported && true === $onboarding_completed ) || ( true === $is_first_funnel_imported && ! empty( $total_funnels ) && 1 >= $total_funnels ) || ( ! empty( $total_funnels ) && 1 >= $total_funnels );
758 + return ( true === $is_store_checkout_imported && true === $onboarding_completed )
759 + || ! empty( $total_funnels );
615 760 }
616 761 }
617 762
618 763 Cartflows_Admin_Notices::get_instance();