PluginProbe
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell / 3.13.3
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell v3.13.3
3.13.3 3.13.2 3.13.1 3.13.0 3.12.13 3.12.12 3.12.11 3.12.10 3.12.9 3.12.8 3.12.7 3.12.6 3.12.5 3.12.4 3.12.3 3.12.1 3.12.2 3.12.0 3.11.1 3.11.0 3.10.9 3.10.8 3.10.7 3.10.6 2.8.16 All 261 releases
wpfunnels / includes / core / rest-api / Controllers / DashboardController.php

DashboardController.php in WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell 3.13.3, at includes/core/rest-api/Controllers/DashboardController.php

652 lines 17.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3
4 namespace WPFunnels\Rest\Controllers;
5
6 use WPFunnels\Report\ReportGenerator;
7 use WPFunnels\Wpfnl_functions;
8
9 /**
10 *
11 *
12 * Class DashboardController
13 * @package WPFunnels\Rest\Controllers
14 * @since 3.2.0
15 */
16 class DashboardController extends Wpfnl_REST_Controller
17 {
18
19 /**
20 * Transient holding the last known answer to "is the onboarding checklist
21 * on show?", read by the admin page so it can decide whether to hold space
22 * for it while the real status loads.
23 *
24 * @var string
25 * @since 3.13.3
26 */
27 const ONBOARDING_VISIBILITY_KEY = 'wpfnl_onboarding_should_show';
28
29
30 /**
31 * Endpoint namespace.
32 *
33 * @var string
34 * @since 3.2.0
35 */
36 protected $namespace = 'wpfunnels/v1';
37
38
39 /**
40 * Route base.
41 *
42 * @var string
43 * @since 3.2.0
44 */
45 protected $rest_base = 'report';
46
47
48 /**
49 * Makes sure the current user has access to READ the settings APIs.
50 *
51 *
52 * @param $request
53 * @return \WP_Error|boolean
54 * @since 3.2.0
55 */
56 public function get_items_permissions_check($request)
57 {
58 if (!Wpfnl_functions::wpfnl_rest_check_manager_permissions('settings')) {
59 return new \WP_Error('wpfunnels_rest_cannot_edit', __('Sorry, you cannot list resources.', 'wpfnl'), array('status' => rest_authorization_required_code()));
60 }
61 return true;
62 }
63
64
65 /**
66 * Register rest routes
67 *
68 * @since 3.2.0
69 */
70 public function register_routes()
71 {
72 register_rest_route(
73 $this->namespace,
74 '/' . $this->rest_base . '/overview',
75 array(
76 array(
77 'methods' => \WP_REST_Server::READABLE,
78 'args' => $this->get_stats_args(),
79 'callback' => array($this, 'get_overview'),
80 'permission_callback' => array($this, 'get_items_permissions_check'),
81 ),
82 )
83 );
84
85 register_rest_route(
86 $this->namespace,
87 '/' . $this->rest_base . '/stats',
88 array(
89 array(
90 'methods' => \WP_REST_Server::READABLE,
91 'args' => $this->get_stats_args(),
92 'callback' => array($this, 'get_stats'),
93 'permission_callback' => array($this, 'get_items_permissions_check'),
94 ),
95 )
96 );
97
98 register_rest_route(
99 $this->namespace,
100 '/' . $this->rest_base . '/top-funnels',
101 array(
102 array(
103 'methods' => \WP_REST_Server::READABLE,
104 'args' => $this->get_stats_args(),
105 'callback' => array($this, 'get_top_funnels'),
106 'permission_callback' => array($this, 'get_items_permissions_check'),
107 ),
108 )
109 );
110
111 register_rest_route(
112 $this->namespace,
113 '/' . $this->rest_base . '/onboarding-status',
114 array(
115 array(
116 'methods' => \WP_REST_Server::READABLE,
117 'callback' => array($this, 'get_onboarding_status'),
118 'permission_callback' => array($this, 'get_items_permissions_check'),
119 ),
120 )
121 );
122
123 register_rest_route(
124 $this->namespace,
125 '/' . $this->rest_base . '/has-funnels',
126 array(
127 array(
128 'methods' => \WP_REST_Server::READABLE,
129 'callback' => array($this, 'check_has_funnels'),
130 'permission_callback' => array( $this, 'get_items_permissions_check' ),
131 ),
132 )
133 );
134
135 register_rest_route(
136 $this->namespace,
137 '/' . $this->rest_base . '/dismiss-onboarding',
138 array(
139 array(
140 'methods' => \WP_REST_Server::CREATABLE,
141 'callback' => array($this, 'dismiss_onboarding'),
142 'permission_callback' => array( $this, 'get_items_permissions_check' ),
143 ),
144 )
145 );
146
147 register_rest_route(
148 $this->namespace,
149 '/' . $this->rest_base . '/sync-cache',
150 array(
151 array(
152 'methods' => \WP_REST_Server::CREATABLE,
153 'callback' => array( $this, 'sync_cache' ),
154 'permission_callback' => array( $this, 'get_items_permissions_check' ),
155 ),
156 )
157 );
158 }
159
160 /**
161 * Get stats arguments
162 *
163 * @return mixed|void
164 * @since 3.2.0
165 */
166 public function get_stats_args()
167 {
168 return array(
169 'after' => array(
170 'type' => 'string',
171 'format' => 'date-time',
172 'validate_callback' => 'rest_validate_request_arg',
173 ),
174 'before' => array(
175 'type' => 'string',
176 'format' => 'date-time',
177 'validate_callback' => 'rest_validate_request_arg',
178 ),
179 'interval' => array(
180 'type' => 'string',
181 'default' => 'week',
182 'enum' => array(
183 'hour',
184 'day',
185 'week',
186 'month',
187 'quarter',
188 'year',
189 ),
190 ),
191 );
192 }
193
194
195 /**
196 * Cache payload version. Bump whenever the shape or the meaning of a cached
197 * report response changes, so entries written by an older build are ignored
198 * instead of served until they expire.
199 *
200 * @var string
201 * @since 3.13.0
202 */
203 const CACHE_VERSION = 'v2';
204
205
206 /**
207 * Build a cache key from a prefix and parameter array.
208 *
209 * @param string $prefix
210 * @param array $params
211 * @return string
212 *
213 * @since 3.9.6
214 */
215 protected function get_cache_key( $prefix, $params ) {
216 return 'wpfnl_dash_' . $prefix . '_' . self::CACHE_VERSION . '_' . md5( serialize( $params ) );
217 }
218
219
220 /**
221 * Delete all dashboard transients from the database.
222 * Called by the sync-cache endpoint or when cache must be invalidated.
223 *
224 * @since 3.9.6
225 */
226 public static function delete_dashboard_cache() {
227 global $wpdb;
228 $wpdb->query(
229 "DELETE FROM {$wpdb->options}
230 WHERE option_name LIKE '_transient_wpfnl_dash_%'
231 OR option_name LIKE '_transient_timeout_wpfnl_dash_%'"
232 );
233 }
234
235
236 /**
237 * Get overview data of funnels (with 24-hour server-side cache).
238 *
239 * @param \WP_REST_Request $request
240 * @return \WP_Error|\WP_REST_Response
241 * @throws \Exception
242 *
243 * @since 3.2.0
244 */
245 public function get_overview(\WP_REST_Request $request)
246 {
247 $params = $request->get_params();
248 $start_date = isset($params['after']) ? $params['after'] : $this->default_after()->format('Y-m-d H:i:s');
249 $end_date = isset($params['before']) ? $params['before'] : $this->default_before()->format('Y-m-d H:i:s');
250
251 $cache_key = $this->get_cache_key( 'ov', array( $start_date, $end_date ) );
252 $cached = get_transient( $cache_key );
253 if ( false !== $cached ) {
254 return rest_ensure_response( $cached );
255 }
256
257 $response = ReportGenerator::get_overview( $start_date, $end_date );
258 set_transient( $cache_key, $response, DAY_IN_SECONDS );
259
260 return rest_ensure_response( $response );
261 }
262
263
264 /**
265 * Get stats of the funnels with intervals period (with 24-hour server-side cache).
266 *
267 * @param \WP_REST_Request $request
268 * @return \WP_Error|\WP_REST_Response
269 * @throws \Exception
270 *
271 * @since 3.2.0
272 */
273 public function get_stats(\WP_REST_Request $request)
274 {
275 $params = $request->get_params();
276 $start_date = isset($params['after']) ? $params['after'] : $this->default_after()->format('Y-m-d H:i:s');
277 $end_date = isset($params['before']) ? $params['before'] : $this->default_before()->format('Y-m-d H:i:s');
278 $interval = isset($params['interval']) ? $params['interval'] : 'day';
279
280 $cache_key = $this->get_cache_key( 'st', array( $start_date, $end_date, $interval ) );
281 $cached = get_transient( $cache_key );
282 if ( false !== $cached ) {
283 return rest_ensure_response( $cached );
284 }
285
286 $response = ReportGenerator::get_stats( $start_date, $end_date, $interval );
287 set_transient( $cache_key, $response, DAY_IN_SECONDS );
288
289 return rest_ensure_response( $response );
290 }
291
292
293 /**
294 * Get 3 top performing funnels
295 *
296 * @param \WP_REST_Request $request
297 * @return \WP_Error|\WP_REST_Response
298 *
299 * @since 3.2.0
300 */
301 public function get_top_funnels(\WP_REST_Request $request)
302 {
303 $params = $request->get_params();
304 $start_date = isset( $params['after'] ) ? $params['after'] : $this->default_after()->format( 'Y-m-d H:i:s' );
305 $end_date = isset( $params['before'] ) ? $params['before'] : $this->default_before()->format( 'Y-m-d H:i:s' );
306
307 $response['status'] = true;
308 $response['data'] = ReportGenerator::get_top_funnels( $start_date, $end_date );
309 return rest_ensure_response( $response );
310 }
311
312
313 /**
314 * Remember whether the checklist was on show, for the next page load.
315 *
316 * Working this out costs several queries, so the admin page cannot ask
317 * before it prints — but it can read the last answer. The React dashboard
318 * uses it for one thing: whether to hold space for the checklist while the
319 * real status loads. Someone who is done with onboarding then sees no
320 * placeholder at all, instead of a skeleton that resolves to nothing.
321 *
322 * Short lived on purpose: it only has to outlive the gap between one page
323 * load and the next, and a stale answer costs at most one wrong placeholder.
324 *
325 * @param bool $is_visible Whether the checklist is on show.
326 *
327 * @return void
328 * @since 3.13.3
329 */
330 private function remember_onboarding_visibility( $is_visible )
331 {
332 set_transient( self::ONBOARDING_VISIBILITY_KEY, $is_visible ? 'yes' : 'no', 15 * MINUTE_IN_SECONDS );
333 }
334
335
336 /**
337 * Get onboarding status.
338 *
339 * Determines whether the onboarding section should be shown on the dashboard.
340 * The onboarding is hidden when ALL three conditions are met:
341 * 1. At least one funnel is published (live)
342 * 2. At least one funnel contains an order bump (upsell is optional/bonus)
343 * 3. At least one real order has been placed through any funnel
344 *
345 * Also returns individual step completion states for the checklist.
346 *
347 * @return \WP_REST_Response
348 * @since 3.9.5
349 */
350 public function get_onboarding_status()
351 {
352 // ── Check if onboarding has been dismissed ──
353 $is_dismissed = get_transient( 'wpfnl_onboarding_dismissed' );
354 if ( $is_dismissed ) {
355 $this->remember_onboarding_visibility( false );
356
357 return rest_ensure_response( array(
358 'success' => true,
359 'show_onboarding' => false,
360 'steps' => array(),
361 'completed_count' => 0,
362 'total_steps' => 0,
363 'progress_percent' => 0,
364 ) );
365 }
366
367 $is_wc_active = class_exists('WooCommerce');
368 $has_live_funnel = false;
369 $has_order_bump = false;
370 $has_upsell = false;
371 $has_real_order = false;
372 $has_funnel_created = false;
373 $has_store_checkout_created = false;
374
375 // Meta query to exclude store checkout funnels
376 $exclude_sc_meta = array(
377 'relation' => 'OR',
378 array(
379 'key' => '_wpfnl_funnel_type',
380 'compare' => 'NOT EXISTS',
381 ),
382 array(
383 'key' => '_wpfnl_funnel_type',
384 'value' => 'store_checkout',
385 'compare' => '!=',
386 ),
387 );
388
389 // ── Step 1: Check for published funnels (excluding store checkouts) ──
390 $funnels = get_posts(array(
391 'post_type' => WPFNL_FUNNELS_POST_TYPE,
392 'post_status' => 'publish',
393 'numberposts' => -1,
394 'fields' => 'ids',
395 'meta_query' => $exclude_sc_meta,
396 ));
397
398 if (!empty($funnels)) {
399 $has_funnel_created = true;
400 $has_live_funnel = true;
401 }
402
403 // Also check for draft funnels (counts as "created" but not "live")
404 if (!$has_funnel_created) {
405 $draft_funnels = get_posts(array(
406 'post_type' => WPFNL_FUNNELS_POST_TYPE,
407 'post_status' => array('publish', 'draft'),
408 'numberposts' => 1,
409 'fields' => 'ids',
410 'meta_query' => $exclude_sc_meta,
411 ));
412 $has_funnel_created = !empty($draft_funnels);
413 }
414
415 // ── Check for store checkout funnels separately ──
416 $sc_funnels = get_posts(array(
417 'post_type' => WPFNL_FUNNELS_POST_TYPE,
418 'post_status' => array('publish', 'draft'),
419 'numberposts' => 1,
420 'fields' => 'ids',
421 'meta_query' => array(
422 array(
423 'key' => '_wpfnl_funnel_type',
424 'value' => 'store_checkout',
425 ),
426 ),
427 ));
428 $has_store_checkout_created = !empty($sc_funnels);
429
430 // ── Step 2: Check for order bump & upsell across all published funnels ──
431 if (!empty($funnels)) {
432 foreach ($funnels as $funnel_id) {
433 $steps = Wpfnl_functions::get_steps($funnel_id);
434
435 if (!is_array($steps) || empty($steps)) {
436 continue;
437 }
438
439 foreach ($steps as $step) {
440 $step_id = isset($step['id']) ? $step['id'] : 0;
441 $step_type = isset($step['step_type']) ? $step['step_type'] : '';
442
443 // Check for order bump on checkout steps
444 if ('checkout' === $step_type && !$has_order_bump) {
445 $ob_settings = get_post_meta($step_id, 'order-bump-settings', true);
446 if (!empty($ob_settings) && is_array($ob_settings)) {
447 $is_multidimensional = Wpfnl_functions::check_array_is_multidimentional($ob_settings);
448 if ($is_multidimensional) {
449 foreach ($ob_settings as $ob) {
450 if (!empty($ob['product']) && !empty($ob['isEnabled'])) {
451 $has_order_bump = true;
452 break;
453 }
454 }
455 }
456 else {
457 // Single OB (legacy format)
458 if (!empty($ob_settings['product'])) {
459 $has_order_bump = true;
460 }
461 }
462 }
463 }
464
465 // Check for upsell step
466 if ('upsell' === $step_type) {
467 $has_upsell = true;
468 }
469 }
470
471 // If we already found both, no need to check more funnels
472 if ($has_order_bump && $has_upsell) {
473 break;
474 }
475 }
476 }
477
478 // ── Build the checklist deep links ──
479 // The checklist actions should drop the user into the place where the step
480 // actually gets done, rather than the funnel list: the order bump tab of the
481 // newest funnel's checkout step, and the canvas of that funnel for the upsell.
482 // Drafts count too — a funnel does not need to be live to be worked on.
483 $order_bump_url = '';
484 $funnel_builder_url = '';
485 $recent_funnels = get_posts(array(
486 'post_type' => WPFNL_FUNNELS_POST_TYPE,
487 'post_status' => array('publish', 'draft'),
488 'numberposts' => 10,
489 'orderby' => 'date',
490 'order' => 'DESC',
491 'fields' => 'ids',
492 'meta_query' => $exclude_sc_meta,
493 ));
494
495 if (!empty($recent_funnels)) {
496 // The upsell step is added on the canvas, so that is where its action goes.
497 $funnel_builder_url = admin_url(
498 sprintf('admin.php?page=edit_funnel&id=%d', $recent_funnels[0])
499 );
500 }
501
502 foreach ($recent_funnels as $recent_funnel_id) {
503 $recent_steps = Wpfnl_functions::get_steps($recent_funnel_id);
504
505 if (!is_array($recent_steps) || empty($recent_steps)) {
506 continue;
507 }
508
509 foreach ($recent_steps as $recent_step) {
510 $recent_step_type = isset($recent_step['step_type']) ? $recent_step['step_type'] : '';
511 $recent_step_id = isset($recent_step['id']) ? (int)$recent_step['id'] : 0;
512
513 if ('checkout' !== $recent_step_type || !$recent_step_id) {
514 continue;
515 }
516
517 // The hash is what tells the canvas which step settings drawer and
518 // tab to open once the funnel editor has finished loading.
519 $order_bump_url = admin_url(
520 sprintf(
521 'admin.php?page=edit_funnel&id=%d&step_id=%d#%d/settings/order-bump/',
522 $recent_funnel_id,
523 $recent_step_id,
524 $recent_step_id
525 )
526 );
527 break 2;
528 }
529 }
530
531 // ── Step 3: Check for real orders through funnels ──
532 if ($is_wc_active && !empty($funnels)) {
533 global $wpdb;
534 $stats_table = $wpdb->prefix . 'wpfnl_stats';
535
536 // Check if stats table exists
537 $table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $stats_table));
538 if ($table_exists) {
539 $order_count = $wpdb->get_var(
540 "SELECT COUNT(id) FROM {$stats_table} WHERE status IN ('completed', 'processing') LIMIT 1"
541 );
542 $has_real_order = (int)$order_count > 0;
543 }
544 }
545
546 // ── Determine if onboarding should be shown ──
547 // Hide onboarding only when ALL conditions are met:
548 // - At least 1 live funnel
549 // - Has an order bump (upsell is optional)
550 // - Has at least 1 real order
551 $should_hide_onboarding = $has_live_funnel && $has_order_bump && $has_real_order;
552
553 // ── Build checklist step completion data ──
554 $completed_count = 0;
555 $steps_status = array(
556 'store_checkout_created' => $has_store_checkout_created,
557 'funnel_created' => $has_funnel_created,
558 'order_bump_added' => $has_order_bump,
559 'upsell_added' => $has_upsell,
560 'test_order_placed' => $has_real_order,
561 'funnel_live' => $has_live_funnel,
562 );
563
564 foreach ($steps_status as $status) {
565 if ($status) {
566 $completed_count++;
567 }
568 }
569
570 $total_steps = count($steps_status);
571 $progress_percent = $total_steps > 0 ? round(($completed_count / $total_steps) * 100) : 0;
572
573 $this->remember_onboarding_visibility( !$should_hide_onboarding );
574
575 return rest_ensure_response(array(
576 'success' => true,
577 'show_onboarding' => !$should_hide_onboarding,
578 'steps' => $steps_status,
579 'completed_count' => $completed_count,
580 'total_steps' => $total_steps,
581 'progress_percent' => $progress_percent,
582 'order_bump_url' => $order_bump_url,
583 'funnel_builder_url' => $funnel_builder_url,
584 ));
585 }
586
587 /**
588 * Check if there are any funnels
589 *
590 * @param \WP_REST_Request $request
591 * @return \WP_REST_Response
592 *
593 * @since 3.9.5
594 */
595 public function check_has_funnels( \WP_REST_Request $request ) {
596 global $wpdb;
597
598 // Count only non-store-checkout funnels
599 $funnel_count = $wpdb->get_var(
600 $wpdb->prepare(
601 "SELECT COUNT(*) FROM {$wpdb->posts} p
602 LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = '_wpfnl_funnel_type'
603 WHERE p.post_type = %s
604 AND p.post_status IN ('publish', 'draft')
605 AND (pm.meta_value IS NULL OR pm.meta_value != 'store_checkout')",
606 'wpfunnels'
607 )
608 );
609
610 return rest_ensure_response( array(
611 'success' => true,
612 'has_funnels' => (int) $funnel_count > 0,
613 'funnel_count' => (int) $funnel_count
614 ) );
615 }
616
617 /**
618 * Clear all dashboard transient caches so the next request fetches fresh data.
619 *
620 * @param \WP_REST_Request $request
621 * @return \WP_REST_Response
622 *
623 * @since 3.9.6
624 */
625 public function sync_cache( \WP_REST_Request $request ) {
626 self::delete_dashboard_cache();
627 return rest_ensure_response( array(
628 'success' => true,
629 'message' => __( 'Dashboard cache cleared. Data will refresh on next load.', 'wpfnl' ),
630 ) );
631 }
632
633
634 /**
635 * Dismiss the onboarding checklist
636 *
637 * @param \WP_REST_Request $request
638 * @return \WP_REST_Response
639 *
640 * @since 3.9.5
641 */
642 public function dismiss_onboarding( \WP_REST_Request $request ) {
643 // Store dismissal in a transient for 30 days
644 set_transient( 'wpfnl_onboarding_dismissed', true, 30 * DAY_IN_SECONDS );
645
646 return rest_ensure_response( array(
647 'success' => true,
648 'message' => 'Onboarding dismissed successfully'
649 ) );
650 }
651 }
652