rest_authorization_required_code())); } return true; } /** * Register rest routes * * @since 3.2.0 */ public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base . '/overview', array( array( 'methods' => \WP_REST_Server::READABLE, 'args' => $this->get_stats_args(), 'callback' => array($this, 'get_overview'), 'permission_callback' => array($this, 'get_items_permissions_check'), ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/stats', array( array( 'methods' => \WP_REST_Server::READABLE, 'args' => $this->get_stats_args(), 'callback' => array($this, 'get_stats'), 'permission_callback' => array($this, 'get_items_permissions_check'), ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/top-funnels', array( array( 'methods' => \WP_REST_Server::READABLE, 'args' => $this->get_stats_args(), 'callback' => array($this, 'get_top_funnels'), 'permission_callback' => array($this, 'get_items_permissions_check'), ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/onboarding-status', array( array( 'methods' => \WP_REST_Server::READABLE, 'callback' => array($this, 'get_onboarding_status'), 'permission_callback' => array($this, 'get_items_permissions_check'), ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/has-funnels', array( array( 'methods' => \WP_REST_Server::READABLE, 'callback' => array($this, 'check_has_funnels'), 'permission_callback' => array( $this, 'get_items_permissions_check' ), ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/dismiss-onboarding', array( array( 'methods' => \WP_REST_Server::CREATABLE, 'callback' => array($this, 'dismiss_onboarding'), 'permission_callback' => array( $this, 'get_items_permissions_check' ), ), ) ); register_rest_route( $this->namespace, '/' . $this->rest_base . '/sync-cache', array( array( 'methods' => \WP_REST_Server::CREATABLE, 'callback' => array( $this, 'sync_cache' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), ), ) ); } /** * Get stats arguments * * @return mixed|void * @since 3.2.0 */ public function get_stats_args() { return array( 'after' => array( 'type' => 'string', 'format' => 'date-time', 'validate_callback' => 'rest_validate_request_arg', ), 'before' => array( 'type' => 'string', 'format' => 'date-time', 'validate_callback' => 'rest_validate_request_arg', ), 'interval' => array( 'type' => 'string', 'default' => 'week', 'enum' => array( 'hour', 'day', 'week', 'month', 'quarter', 'year', ), ), ); } /** * Cache payload version. Bump whenever the shape or the meaning of a cached * report response changes, so entries written by an older build are ignored * instead of served until they expire. * * @var string * @since 3.13.0 */ const CACHE_VERSION = 'v2'; /** * Build a cache key from a prefix and parameter array. * * @param string $prefix * @param array $params * @return string * * @since 3.9.6 */ protected function get_cache_key( $prefix, $params ) { return 'wpfnl_dash_' . $prefix . '_' . self::CACHE_VERSION . '_' . md5( serialize( $params ) ); } /** * Delete all dashboard transients from the database. * Called by the sync-cache endpoint or when cache must be invalidated. * * @since 3.9.6 */ public static function delete_dashboard_cache() { global $wpdb; $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_wpfnl_dash_%' OR option_name LIKE '_transient_timeout_wpfnl_dash_%'" ); } /** * Get overview data of funnels (with 24-hour server-side cache). * * @param \WP_REST_Request $request * @return \WP_Error|\WP_REST_Response * @throws \Exception * * @since 3.2.0 */ public function get_overview(\WP_REST_Request $request) { $params = $request->get_params(); $start_date = isset($params['after']) ? $params['after'] : $this->default_after()->format('Y-m-d H:i:s'); $end_date = isset($params['before']) ? $params['before'] : $this->default_before()->format('Y-m-d H:i:s'); $cache_key = $this->get_cache_key( 'ov', array( $start_date, $end_date ) ); $cached = get_transient( $cache_key ); if ( false !== $cached ) { return rest_ensure_response( $cached ); } $response = ReportGenerator::get_overview( $start_date, $end_date ); set_transient( $cache_key, $response, DAY_IN_SECONDS ); return rest_ensure_response( $response ); } /** * Get stats of the funnels with intervals period (with 24-hour server-side cache). * * @param \WP_REST_Request $request * @return \WP_Error|\WP_REST_Response * @throws \Exception * * @since 3.2.0 */ public function get_stats(\WP_REST_Request $request) { $params = $request->get_params(); $start_date = isset($params['after']) ? $params['after'] : $this->default_after()->format('Y-m-d H:i:s'); $end_date = isset($params['before']) ? $params['before'] : $this->default_before()->format('Y-m-d H:i:s'); $interval = isset($params['interval']) ? $params['interval'] : 'day'; $cache_key = $this->get_cache_key( 'st', array( $start_date, $end_date, $interval ) ); $cached = get_transient( $cache_key ); if ( false !== $cached ) { return rest_ensure_response( $cached ); } $response = ReportGenerator::get_stats( $start_date, $end_date, $interval ); set_transient( $cache_key, $response, DAY_IN_SECONDS ); return rest_ensure_response( $response ); } /** * Get 3 top performing funnels * * @param \WP_REST_Request $request * @return \WP_Error|\WP_REST_Response * * @since 3.2.0 */ public function get_top_funnels(\WP_REST_Request $request) { $params = $request->get_params(); $start_date = isset( $params['after'] ) ? $params['after'] : $this->default_after()->format( 'Y-m-d H:i:s' ); $end_date = isset( $params['before'] ) ? $params['before'] : $this->default_before()->format( 'Y-m-d H:i:s' ); $response['status'] = true; $response['data'] = ReportGenerator::get_top_funnels( $start_date, $end_date ); return rest_ensure_response( $response ); } /** * Get onboarding status. * * Determines whether the onboarding section should be shown on the dashboard. * The onboarding is hidden when ALL three conditions are met: * 1. At least one funnel is published (live) * 2. At least one funnel contains an order bump (upsell is optional/bonus) * 3. At least one real order has been placed through any funnel * * Also returns individual step completion states for the checklist. * * @return \WP_REST_Response * @since 3.9.5 */ public function get_onboarding_status() { // ── Check if onboarding has been dismissed ── $is_dismissed = get_transient( 'wpfnl_onboarding_dismissed' ); if ( $is_dismissed ) { return rest_ensure_response( array( 'success' => true, 'show_onboarding' => false, 'steps' => array(), 'completed_count' => 0, 'total_steps' => 0, 'progress_percent' => 0, ) ); } $is_wc_active = class_exists('WooCommerce'); $has_live_funnel = false; $has_order_bump = false; $has_upsell = false; $has_real_order = false; $has_funnel_created = false; $has_store_checkout_created = false; // Meta query to exclude store checkout funnels $exclude_sc_meta = array( 'relation' => 'OR', array( 'key' => '_wpfnl_funnel_type', 'compare' => 'NOT EXISTS', ), array( 'key' => '_wpfnl_funnel_type', 'value' => 'store_checkout', 'compare' => '!=', ), ); // ── Step 1: Check for published funnels (excluding store checkouts) ── $funnels = get_posts(array( 'post_type' => WPFNL_FUNNELS_POST_TYPE, 'post_status' => 'publish', 'numberposts' => -1, 'fields' => 'ids', 'meta_query' => $exclude_sc_meta, )); if (!empty($funnels)) { $has_funnel_created = true; $has_live_funnel = true; } // Also check for draft funnels (counts as "created" but not "live") if (!$has_funnel_created) { $draft_funnels = get_posts(array( 'post_type' => WPFNL_FUNNELS_POST_TYPE, 'post_status' => array('publish', 'draft'), 'numberposts' => 1, 'fields' => 'ids', 'meta_query' => $exclude_sc_meta, )); $has_funnel_created = !empty($draft_funnels); } // ── Check for store checkout funnels separately ── $sc_funnels = get_posts(array( 'post_type' => WPFNL_FUNNELS_POST_TYPE, 'post_status' => array('publish', 'draft'), 'numberposts' => 1, 'fields' => 'ids', 'meta_query' => array( array( 'key' => '_wpfnl_funnel_type', 'value' => 'store_checkout', ), ), )); $has_store_checkout_created = !empty($sc_funnels); // ── Step 2: Check for order bump & upsell across all published funnels ── if (!empty($funnels)) { foreach ($funnels as $funnel_id) { $steps = Wpfnl_functions::get_steps($funnel_id); if (!is_array($steps) || empty($steps)) { continue; } foreach ($steps as $step) { $step_id = isset($step['id']) ? $step['id'] : 0; $step_type = isset($step['step_type']) ? $step['step_type'] : ''; // Check for order bump on checkout steps if ('checkout' === $step_type && !$has_order_bump) { $ob_settings = get_post_meta($step_id, 'order-bump-settings', true); if (!empty($ob_settings) && is_array($ob_settings)) { $is_multidimensional = Wpfnl_functions::check_array_is_multidimentional($ob_settings); if ($is_multidimensional) { foreach ($ob_settings as $ob) { if (!empty($ob['product']) && !empty($ob['isEnabled'])) { $has_order_bump = true; break; } } } else { // Single OB (legacy format) if (!empty($ob_settings['product'])) { $has_order_bump = true; } } } } // Check for upsell step if ('upsell' === $step_type) { $has_upsell = true; } } // If we already found both, no need to check more funnels if ($has_order_bump && $has_upsell) { break; } } } // ── Build the checklist deep links ── // The checklist actions should drop the user into the place where the step // actually gets done, rather than the funnel list: the order bump tab of the // newest funnel's checkout step, and the canvas of that funnel for the upsell. // Drafts count too — a funnel does not need to be live to be worked on. $order_bump_url = ''; $funnel_builder_url = ''; $recent_funnels = get_posts(array( 'post_type' => WPFNL_FUNNELS_POST_TYPE, 'post_status' => array('publish', 'draft'), 'numberposts' => 10, 'orderby' => 'date', 'order' => 'DESC', 'fields' => 'ids', 'meta_query' => $exclude_sc_meta, )); if (!empty($recent_funnels)) { // The upsell step is added on the canvas, so that is where its action goes. $funnel_builder_url = admin_url( sprintf('admin.php?page=edit_funnel&id=%d', $recent_funnels[0]) ); } foreach ($recent_funnels as $recent_funnel_id) { $recent_steps = Wpfnl_functions::get_steps($recent_funnel_id); if (!is_array($recent_steps) || empty($recent_steps)) { continue; } foreach ($recent_steps as $recent_step) { $recent_step_type = isset($recent_step['step_type']) ? $recent_step['step_type'] : ''; $recent_step_id = isset($recent_step['id']) ? (int)$recent_step['id'] : 0; if ('checkout' !== $recent_step_type || !$recent_step_id) { continue; } // The hash is what tells the canvas which step settings drawer and // tab to open once the funnel editor has finished loading. $order_bump_url = admin_url( sprintf( 'admin.php?page=edit_funnel&id=%d&step_id=%d#%d/settings/order-bump/', $recent_funnel_id, $recent_step_id, $recent_step_id ) ); break 2; } } // ── Step 3: Check for real orders through funnels ── if ($is_wc_active && !empty($funnels)) { global $wpdb; $stats_table = $wpdb->prefix . 'wpfnl_stats'; // Check if stats table exists $table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $stats_table)); if ($table_exists) { $order_count = $wpdb->get_var( "SELECT COUNT(id) FROM {$stats_table} WHERE status IN ('completed', 'processing') LIMIT 1" ); $has_real_order = (int)$order_count > 0; } } // ── Determine if onboarding should be shown ── // Hide onboarding only when ALL conditions are met: // - At least 1 live funnel // - Has an order bump (upsell is optional) // - Has at least 1 real order $should_hide_onboarding = $has_live_funnel && $has_order_bump && $has_real_order; // ── Build checklist step completion data ── $completed_count = 0; $steps_status = array( 'store_checkout_created' => $has_store_checkout_created, 'funnel_created' => $has_funnel_created, 'order_bump_added' => $has_order_bump, 'upsell_added' => $has_upsell, 'test_order_placed' => $has_real_order, 'funnel_live' => $has_live_funnel, ); foreach ($steps_status as $status) { if ($status) { $completed_count++; } } $total_steps = count($steps_status); $progress_percent = $total_steps > 0 ? round(($completed_count / $total_steps) * 100) : 0; return rest_ensure_response(array( 'success' => true, 'show_onboarding' => !$should_hide_onboarding, 'steps' => $steps_status, 'completed_count' => $completed_count, 'total_steps' => $total_steps, 'progress_percent' => $progress_percent, 'order_bump_url' => $order_bump_url, 'funnel_builder_url' => $funnel_builder_url, )); } /** * Check if there are any funnels * * @param \WP_REST_Request $request * @return \WP_REST_Response * * @since 3.9.5 */ public function check_has_funnels( \WP_REST_Request $request ) { global $wpdb; // Count only non-store-checkout funnels $funnel_count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->posts} p LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = '_wpfnl_funnel_type' WHERE p.post_type = %s AND p.post_status IN ('publish', 'draft') AND (pm.meta_value IS NULL OR pm.meta_value != 'store_checkout')", 'wpfunnels' ) ); return rest_ensure_response( array( 'success' => true, 'has_funnels' => (int) $funnel_count > 0, 'funnel_count' => (int) $funnel_count ) ); } /** * Clear all dashboard transient caches so the next request fetches fresh data. * * @param \WP_REST_Request $request * @return \WP_REST_Response * * @since 3.9.6 */ public function sync_cache( \WP_REST_Request $request ) { self::delete_dashboard_cache(); return rest_ensure_response( array( 'success' => true, 'message' => __( 'Dashboard cache cleared. Data will refresh on next load.', 'wpfnl' ), ) ); } /** * Dismiss the onboarding checklist * * @param \WP_REST_Request $request * @return \WP_REST_Response * * @since 3.9.5 */ public function dismiss_onboarding( \WP_REST_Request $request ) { // Store dismissal in a transient for 30 days set_transient( 'wpfnl_onboarding_dismissed', true, 30 * DAY_IN_SECONDS ); return rest_ensure_response( array( 'success' => true, 'message' => 'Onboarding dismissed successfully' ) ); } }