PluginProbe
CartFlows – Funnel Builder & Checkout Plugin for WooCommerce / 3.1.1
CartFlows – Funnel Builder & Checkout Plugin for WooCommerce v3.1.1
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
cartflows / classes / class-cartflows-admin-notices.php

class-cartflows-admin-notices.php in CartFlows – Funnel Builder & Checkout Plugin for WooCommerce 3.1.1, at classes/class-cartflows-admin-notices.php

767 lines 27.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * CartFlows Admin Notices.
4 *
5 * @package CartFlows
6 */
7
8 // Exit if accessed directly.
9 if ( ! defined( 'ABSPATH' ) ) {
10 exit;
11 }
12 /**
13 * Class Cartflows_Admin_Notices.
14 */
15 class Cartflows_Admin_Notices {
16
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 /**
32 * Instance
33 *
34 * @access private
35 * @var object Class object.
36 * @since 1.0.0
37 */
38 private static $instance;
39
40 /**
41 * Initiator
42 *
43 * @since 1.0.0
44 * @return object initialized object of class.
45 */
46 public static function get_instance() {
47 if ( ! isset( self::$instance ) ) {
48 self::$instance = new self();
49 }
50 return self::$instance;
51 }
52
53 /**
54 * Constructor
55 *
56 * @since 1.0.0
57 */
58 public function __construct() {
59 // Add the notices script.
60 add_action( 'admin_enqueue_scripts', array( $this, 'notices_scripts' ) );
61
62 // Group the admin notices actions.
63 $this->register_admin_notices();
64
65 // Group the ajax action callbacks.
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 );
79 }
80
81 /**
82 * Registers admin notices for CartFlows.
83 *
84 * Hooks the methods responsible for displaying admin and NPS notices
85 * to appropriate WordPress admin actions.
86 *
87 * @since 2.1.17
88 * @return void
89 */
90 public function register_admin_notices() {
91 add_action( 'admin_head', array( $this, 'show_admin_notices' ) );
92
93 add_action( 'admin_footer', array( $this, 'show_nps_notice' ), 999 );
94 }
95
96 /**
97 * Registers AJAX callbacks for various CartFlows admin notices.
98 *
99 * This method hooks AJAX actions to their corresponding handler functions,
100 * allowing notices (such as Gutenberg, weekly report email, and custom offer notices)
101 * to be dismissed or acknowledged via AJAX requests in the WordPress admin area.
102 *
103 * @since 2.1.17
104 * @return void
105 */
106 public function register_ajax_callbacks() {
107 add_action( 'wp_ajax_cartflows_ignore_gutenberg_notice', array( $this, 'ignore_gb_notice' ) );
108 add_action( 'wp_ajax_cartflows_disable_weekly_report_email_notice', array( $this, 'disable_weekly_report_email_notice' ) );
109 add_action( 'wp_ajax_cartflows_snooze_script_migration', array( $this, 'snooze_script_migration_notice' ) );
110 add_action( 'wp_ajax_cartflows_dismiss_script_migration_complete_notice', array( $this, 'dismiss_script_migration_complete_notice' ) );
111 add_action( 'wp_ajax_cartflows_dismiss_new_ui_notice', array( $this, 'dismiss_new_ui_notice' ) );
112 }
113
114 /**
115 * Persist the "Switch to New UI" dismissal for the current user.
116 *
117 * Writes the same `notice-dismissed` user_meta key that Astra_Notices uses
118 * so the WP admin_notices banner and the React-shell banner stay in sync —
119 * dismissing either one hides both on the next load.
120 *
121 * @since 2.2.4
122 * @return void
123 */
124 public function dismiss_new_ui_notice() {
125
126 if ( ! current_user_can( 'cartflows_manage_settings' ) ) {
127 wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'cartflows' ) ) );
128 }
129
130 if ( ! check_ajax_referer( 'cartflows_dismiss_new_ui_notice', 'security', false ) ) {
131 wp_send_json_error( array( 'message' => __( 'Security check failed.', 'cartflows' ) ) );
132 }
133
134 update_user_meta( get_current_user_id(), 'cartflows-switch-to-new-ui-notice', 'notice-dismissed' );
135
136 wp_send_json_success();
137 }
138
139 /**
140 * Show the weekly email Notice
141 *
142 * @return void
143 */
144 public function show_weekly_report_email_settings_notice() {
145
146 if ( ! $this->allowed_screen_for_notices() ) {
147 return;
148 }
149
150 $is_show_notice = get_option( 'cartflows_show_weekly_report_email_notice', 'no' );
151
152 if ( 'yes' === $is_show_notice && current_user_can( 'manage_options' ) ) {
153
154 $setting_url = admin_url( 'admin.php?page=cartflows&path=settings#other_settings' );
155
156 /* translators: %1$s Software Title, %2$s Plugin, %3$s Anchor opening tag, %4$s Anchor closing tag, %5$s Software Title. */
157 $message = sprintf( __( '%1$sCartFlows:%2$s We just introduced an awesome new feature, weekly store revenue reports via email. Now you can see how many revenue we are generating for your store each week, without having to log into your website. You can set the email address for these email from %3$shere.%4$s', 'cartflows' ), '<strong>', '</strong>', '<a class="wcf-redirect-to-settings" target="_blank" href=" ' . esc_url( $setting_url ) . ' ">', '</a>' );
158 $output = '<div class="wcf-notice weekly-report-email-notice wcf-dismissible-notice notice notice-info is-dismissible">';
159 $output .= '<p>' . $message . '</p>';
160 $output .= '</div>';
161
162 echo wp_kses_post( $output );
163 }
164 }
165
166 /**
167 * Disable the weekly email Notice
168 *
169 * @return void
170 */
171 public function disable_weekly_report_email_notice() {
172
173 if ( ! current_user_can( 'manage_options' ) ) {
174 return;
175 }
176
177 check_ajax_referer( 'cartflows-disable-weekly-report-email-notice', 'security' );
178 delete_option( 'cartflows_show_weekly_report_email_notice' );
179 // Track weekly report notice dismissed event.
180 Cartflows_Helper::set_analytics_flag( 'weekly_report_notice_dismissed' );
181 wp_send_json_success();
182 }
183
184 /**
185 * Show the custom script migration notice.
186 *
187 * Renders an admin notice prompting users to migrate custom script data
188 * from the old textarea format to the new CodeMirror editor format.
189 *
190 * @since 2.2.2
191 * @return void
192 */
193 public function show_script_migration_notice() {
194
195 if ( ! $this->allowed_screen_for_notices() ) {
196 return;
197 }
198
199 if ( ! current_user_can( 'manage_options' ) ) {
200 return;
201 }
202
203 if ( ! Cartflows_Update::get_instance()->should_show_migration_notice() ) {
204 return;
205 }
206
207 $output = '<div class="wcf-notice script-migration-notice wcf-dismissible-notice notice notice-warning">';
208 $output .= '<p>';
209 $output .= '<strong>' . esc_html__( 'CartFlows: Custom Script Migration', 'cartflows' ) . '</strong><br>';
210 $output .= esc_html__( 'CartFlows now has dedicated JavaScript and CSS editors for custom scripts in your funnels and steps. Click "Migrate Data" to move your existing scripts to the new editors, or choose "Remind Me Later" to be notified again in 7 days.', 'cartflows' );
211 $output .= '</p>';
212 $output .= '<p>';
213 $output .= '<button class="button button-primary wcf-migrate-scripts-btn" data-action="migrate_scripts">' . esc_html__( 'Migrate Data', 'cartflows' ) . '</button>';
214 $output .= '&nbsp;';
215 $output .= '<button class="button button-secondary wcf-snooze-migration-btn" data-action="remind_later">' . esc_html__( 'Remind Me Later', 'cartflows' ) . '</button>';
216 $output .= '</p>';
217 $output .= '</div>';
218
219 echo wp_kses_post( $output );
220 }
221
222 /**
223 * Handle snooze (remind me later) AJAX request for the script migration notice.
224 *
225 * Increments the skip counter and updates the skip timestamp.
226 * After 2 skips, sets the status to 'accepted' to stop showing the notice.
227 *
228 * @since 2.2.2
229 * @return void
230 */
231 public function snooze_script_migration_notice() {
232
233 if ( ! check_ajax_referer( 'cartflows-snooze-script-migration', 'security', false ) ) {
234 wp_send_json_error( array( 'message' => __( 'Invalid nonce.', 'cartflows' ) ) );
235 }
236
237 if ( ! current_user_can( 'manage_options' ) ) {
238 wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'cartflows' ) ) );
239 }
240
241 $update = Cartflows_Update::get_instance();
242
243 if ( $update instanceof Cartflows_Update ) {
244 $update->set_script_migration_status( 'skipped' );
245 $update->increment_migration_skip_count();
246
247 // After 2 skips, stop showing the notice permanently.
248 if ( $update->get_migration_skip_count() >= 2 ) {
249 $update->set_script_migration_status( 'accepted' );
250 }
251
252 wp_send_json_success( array( 'message' => __( 'You will be reminded in 7 days.', 'cartflows' ) ) );
253 }
254
255 wp_send_json_error( array( 'message' => __( 'Unable to update migration status.', 'cartflows' ) ) );
256 }
257
258 /**
259 * After save of permalinks.
260 */
261 public function notices_scripts() {
262
263 if ( ! $this->allowed_screen_for_notices() || ! current_user_can( 'cartflows_manage_flows_steps' ) ) {
264 return;
265 }
266
267 wp_enqueue_style( 'cartflows-custom-notices', CARTFLOWS_URL . 'admin/assets/css/notices.css', array(), CARTFLOWS_VER );
268
269 wp_enqueue_script( 'cartflows-notices', CARTFLOWS_URL . 'admin/assets/js/ui-notice.js', array( 'jquery' ), CARTFLOWS_VER, true );
270
271 $localize_vars = array(
272 'ignore_gb_notice' => wp_create_nonce( 'cartflows-ignore-gutenberg-notice' ),
273 'dismiss_weekly_report_email_notice' => wp_create_nonce( 'cartflows-disable-weekly-report-email-notice' ),
274 'snooze_script_migration' => wp_create_nonce( 'cartflows-snooze-script-migration' ),
275 'migrate_custom_scripts' => wp_create_nonce( 'cartflows_migrate_custom_scripts' ),
276 'dismiss_migration_complete_notice' => wp_create_nonce( 'cartflows-dismiss-script-migration-complete-notice' ),
277 );
278
279 wp_localize_script( 'cartflows-notices', 'cartflows_notices', $localize_vars );
280 }
281
282 /**
283 * After save of permalinks.
284 */
285 public function show_admin_notices() {
286
287 if ( ! $this->allowed_screen_for_notices() || ! current_user_can( 'cartflows_manage_flows_steps' ) ) {
288 return;
289 }
290
291 global $wp_version;
292
293 if ( version_compare( $wp_version, '5.0', '>=' ) && is_plugin_active( 'gutenberg/gutenberg.php' ) ) {
294 add_action( 'admin_notices', array( $this, 'gutenberg_plugin_deactivate_notice' ) );
295 }
296
297 add_action( 'admin_notices', array( $this, 'show_weekly_report_email_settings_notice' ) );
298 add_action( 'admin_notices', array( $this, 'show_script_migration_notice' ) );
299 add_action( 'admin_notices', array( $this, 'show_script_migration_complete_notice' ) );
300
301 $image_path = esc_url( CARTFLOWS_URL . 'assets/images/cartflows-logo-small.jpg' );
302 Astra_Notices::add_notice(
303 array(
304 'id' => 'cartflows-5-start-notice',
305 'type' => 'info',
306 'class' => 'cartflows-5-star cartflows-admin-notice',
307 'show_if' => $this->should_show_review_notice(),
308 /* translators: %1$s white label plugin name and %2$s deactivation link */
309 'message' => sprintf(
310 '<div class="notice-image" style="display: block;">
311 <img src="%1$s" class="custom-logo" alt="CartFlows Icon" itemprop="logo" style="max-width: 90px; border-radius: 50px;"></div>
312 <div class="notice-content">
313 <div class="notice-heading">
314 %2$s
315 </div>
316 <div class="notice-description">
317 %3$s
318 </div>
319 <div class="astra-review-notice-container">
320 <a href="%4$s" class="astra-notice-close astra-review-notice button-primary" target="_blank">
321 <span class="dashicons dashicons-yes"></span>
322 %5$s
323 </a>
324
325 <a href="#" data-repeat-notice-after="%6$s" class="astra-notice-close astra-review-notice">
326 <span class="dashicons dashicons-calendar"></span>
327 %7$s
328 </a>
329
330 <a href="#" class="astra-notice-close astra-review-notice">
331 <span class="dashicons dashicons-smiley"></span>
332 <u>%8$s</u>
333 </a>
334 </div>
335 </div>',
336 $image_path,
337 __( 'Hi there! You recently used CartFlows to build a sales funnel &mdash; Thanks a ton!', 'cartflows' ),
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' ),
339 'https://wordpress.org/support/plugin/cartflows/reviews/#new-post',
340 __( 'Ok, you deserve it', 'cartflows' ),
341 MONTH_IN_SECONDS,
342 __( 'Nope, maybe later', 'cartflows' ),
343 __( 'I already did', 'cartflows' )
344 ),
345 'repeat-notice-after' => MONTH_IN_SECONDS,
346 'display-notice-after' => ( 2 * WEEK_IN_SECONDS ), // Display notice after 2 weeks.
347 )
348 );
349
350 // CartFlows 3.0 — soft notice inviting legacy users to opt into the new admin UI.
351 // Registered through Astra_Notices so it inherits the standard notice
352 // design, dismiss handling and per-user storage the rest of CartFlows uses.
353 if ( $this->is_new_ui_notice_visible() ) {
354 $this->enqueue_new_ui_notice_assets();
355
356 Astra_Notices::add_notice(
357 array(
358 'id' => 'cartflows-switch-to-new-ui-notice',
359 'type' => 'info',
360 'class' => 'cartflows-switch-ui-notice cartflows-admin-notice',
361 'show_if' => true,
362 'is_dismissible' => true,
363 'priority' => 5,
364 'capability' => 'cartflows_manage_settings',
365 'message' => sprintf(
366 '<div class="notice-image" style="display: block;">
367 <img src="%1$s" class="custom-logo" alt="CartFlows Icon" itemprop="logo" style="max-width: 90px; border-radius: 50px;"></div>
368 <div class="notice-content">
369 <div class="notice-heading">%2$s</div>
370 <div class="notice-description">%3$s</div>
371 <div class="astra-review-notice-container">
372 <button type="button" class="astra-review-notice button-primary" data-wcf-action="switch">
373 <span class="dashicons dashicons-yes"></span>
374 %4$s
375 </button>
376 <a href="#" class="astra-notice-close astra-review-notice">
377 <span class="dashicons dashicons-no-alt"></span>
378 %5$s
379 </a>
380 </div>
381 </div>',
382 esc_url( CARTFLOWS_URL . 'assets/images/cartflows-logo-small.jpg' ),
383 esc_html__( 'Your CartFlows got a fresh new look ✨', 'cartflows' ),
384 esc_html__( 'Enjoy a faster, cleaner dashboard designed to help you work smarter. Not ready? You can always switch back from Settings → Advanced.', 'cartflows' ),
385 esc_html__( 'Try the New Experience', 'cartflows' ),
386 esc_html__( 'Maybe Later', 'cartflows' )
387 ),
388 )
389 );
390 }
391 }
392
393 /**
394 * Render CartFlows NPS Survey Notice.
395 *
396 * @since 2.1.6
397 * @return void
398 */
399 public function show_nps_notice() {
400
401 Nps_Survey::show_nps_notice(
402 'nps-survey-cartflows',
403 array(
404 'show_if' => $this->should_display_nps_survey_notice(),
405 'dismiss_timespan' => 2 * WEEK_IN_SECONDS,
406 'display_after' => 0,
407 'plugin_slug' => 'cartflows',
408 'show_on_screens' => array( 'edit-cartflows_flow', 'toplevel_page_cartflows' ),
409 'message' => array(
410
411 // Step 1 i.e rating input.
412 'logo' => esc_url( CARTFLOWS_URL . 'admin-core/assets/images/cartflows-icon.svg' ),
413 'plugin_name' => __( 'CartFlows', 'cartflows' ),
414 'nps_rating_message' => __( 'How likely are you to recommend #pluginname to your friends or colleagues?', 'cartflows' ),
415
416 // Step 2A i.e. positive.
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' ),
418 'plugin_rating_link' => esc_url( 'https://wordpress.org/support/plugin/cartflows/reviews/#new-post' ),
419
420 // Step 2B i.e. negative.
421 'plugin_rating_title' => __( 'Thank you for your feedback', 'cartflows' ),
422 'plugin_rating_content' => __( 'We value your input. How can we improve your experience?', 'cartflows' ),
423 ),
424 )
425 );
426 }
427
428 /**
429 * Show Deactivate gutenberg plugin notice.
430 *
431 * @since 1.1.19
432 *
433 * @return void
434 */
435 public function gutenberg_plugin_deactivate_notice() {
436
437 $ignore_notice = get_option( 'wcf_ignore_gutenberg_notice', false );
438
439 if ( 'yes' !== $ignore_notice ) {
440 printf(
441 '<div class="notice notice-error wcf_notice_gutenberg_plugin is-dismissible"><p>%s</p>%s</div>',
442 wp_kses_post(
443 sprintf(
444 /* translators: %1$s: HTML, %2$s: HTML */
445 __( 'Heads up! The Gutenberg plugin is not recommended on production sites as it may contain non-final features that cause compatibility issues with CartFlows and other plugins. %1$s Please deactivate the Gutenberg plugin %2$s to ensure the proper functioning of your website.', 'cartflows' ),
446 '<strong>',
447 '</strong>'
448 )
449 ),
450 ''
451 );
452 }
453 }
454
455 /**
456 * Ignore admin notice.
457 */
458 public function ignore_gb_notice() {
459
460 if ( ! current_user_can( 'cartflows_manage_flows_steps' ) ) {
461 return;
462 }
463
464 check_ajax_referer( 'cartflows-ignore-gutenberg-notice', 'security' );
465
466 update_option( 'wcf_ignore_gutenberg_notice', 'yes' );
467 }
468
469 /**
470 * Show the custom script migration complete notice.
471 *
472 * Renders an admin notice informing users that their custom scripts
473 * have been successfully migrated to the new dedicated code editors.
474 *
475 * @since 2.2.2
476 * @return void
477 */
478 public function show_script_migration_complete_notice() {
479
480 if ( ! $this->allowed_screen_for_notices() ) {
481 return;
482 }
483
484 if ( ! current_user_can( 'manage_options' ) ) {
485 return;
486 }
487
488 if ( 'completed' !== \CartFlows_Helper::get_script_migration_status() ) {
489 return;
490 }
491
492 if ( 'yes' === get_option( 'cartflows_script_migration_complete_notice_dismissed', 'no' ) ) {
493 return;
494 }
495
496 $output = '<div class="wcf-notice script-migration-complete-notice wcf-dismissible-notice notice notice-success is-dismissible">';
497 $output .= '<p>';
498 $output .= '<strong>' . esc_html__( 'CartFlows: Custom Script Migration Complete', 'cartflows' ) . '</strong><br>';
499 $output .= esc_html__( 'Your scripts have been successfully migrated to the dedicated JavaScript and CSS editors.', 'cartflows' );
500 $output .= '</p>';
501 $output .= '</div>';
502
503 echo wp_kses_post( $output );
504 }
505
506 /**
507 * Handle dismiss AJAX request for the script migration complete notice.
508 *
509 * Permanently hides the migration complete notice once the user dismisses it.
510 *
511 * @since 2.2.2
512 * @return void
513 */
514 public function dismiss_script_migration_complete_notice() {
515
516 if ( ! current_user_can( 'manage_options' ) ) {
517 wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'cartflows' ) ) );
518 }
519
520 check_ajax_referer( 'cartflows-dismiss-script-migration-complete-notice', 'security' );
521
522 update_option( 'cartflows_script_migration_complete_notice_dismissed', 'yes' );
523
524 wp_send_json_success();
525 }
526
527 /**
528 * Check allowed screen for notices.
529 *
530 * @since 1.0.0
531 *
532 * @param array $exclude_page_ids Optional. Array of screen IDs to exclude from displaying notices.
533 * @return bool True if the notice should be displayed, false otherwise.
534 */
535 public function allowed_screen_for_notices( $exclude_page_ids = array() ) {
536
537 $screen = get_current_screen();
538 $screen_id = $screen ? $screen->id : '';
539 $allowed_screens = array(
540 'dashboard',
541 'plugins',
542 );
543
544 // Exclude any page ids passed in $exclude_page_ids from $allowed_screens.
545 if ( ! empty( $exclude_page_ids ) && is_array( $exclude_page_ids ) ) {
546 $allowed_screens = array_diff( $allowed_screens, $exclude_page_ids );
547 }
548
549 if ( in_array( $screen_id, $allowed_screens, true ) ) {
550 return true;
551 }
552
553 return false;
554 }
555
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 /**
601 * Whether the "Switch to New UI" promo should be shown to the current user.
602 *
603 * Centralises the gating logic so the WP `admin_notices` render and the
604 * React-shell banner both honour the exact same conditions.
605 *
606 * @since 2.2.4
607 * @return bool
608 */
609 public function is_new_ui_notice_visible() {
610
611 if ( ! current_user_can( 'cartflows_manage_settings' ) ) {
612 return false;
613 }
614
615 // Only relevant when the legacy admin is the active UI. The constant is
616 // defined by the loader and accepts the 'enable'/'disable' string forms
617 // the toggle stores, so don't read the option directly here.
618 if ( ! defined( 'CARTFLOWS_LEGACY_ADMIN' ) || ! CARTFLOWS_LEGACY_ADMIN ) {
619 return false;
620 }
621
622 // Astra_Notices' own astra-notice-close JS writes 'notice-dismissed' to
623 // this user_meta key when the user dismisses the notice.
624 if ( 'notice-dismissed' === get_user_meta( get_current_user_id(), 'cartflows-switch-to-new-ui-notice', true ) ) {
625 return false;
626 }
627
628 return true;
629 }
630
631 /**
632 * Enqueue the vanilla JS that wires the Switch button of the "Switch to
633 * New UI" notice. Called from `show_admin_notices()` so the asset only
634 * loads on screens where the Astra notice actually renders.
635 *
636 * @since 2.2.4
637 * @return void
638 */
639 public function enqueue_new_ui_notice_assets() {
640
641 $handle = 'wcf-new-ui-notice';
642
643 if ( wp_script_is( $handle, 'enqueued' ) ) {
644 return;
645 }
646
647 wp_enqueue_script(
648 $handle,
649 CARTFLOWS_URL . 'admin-legacy-core/assets/js/new-ui-notice.js',
650 array(),
651 CARTFLOWS_VER,
652 true
653 );
654
655 wp_localize_script(
656 $handle,
657 'wcfNewUiNotice',
658 array(
659 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
660 'switchAction' => 'cartflows_switch_to_new_ui',
661 'switchNonce' => wp_create_nonce( 'cartflows_switch_to_new_ui' ),
662 )
663 );
664 }
665
666 /**
667 * Hook callback for the WooCommerce checkout-completion actions.
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 *
741 * @since 2.1.6
742 * @since 2.2.5 Fixed audience logic to include power users (>1 funnel).
743 * @return bool
744 */
745 public function should_display_nps_survey_notice() {
746
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 $is_first_funnel_imported = (bool) get_option( 'wcf_first_flow_imported', false );
750 $total_funnels = $this->get_published_flow_count();
751
752 /**
753 * Show the notice in any of these conditions:
754 * 1. User finished the onboarding wizard AND imported the store checkout funnel.
755 * 2. User imported their first funnel AND has at least one published funnel.
756 * 3. User has at least one published funnel (built manually).
757 *
758 * Note: condition 3 intentionally has NO upper bound on funnel count,
759 * so power users with many funnels still see the survey.
760 */
761 return ( true === $is_store_checkout_imported && true === $onboarding_completed )
762 || ( true === $is_first_funnel_imported && ! empty( $total_funnels ) && 1 >= $total_funnels );
763 }
764 }
765
766 Cartflows_Admin_Notices::get_instance();
767