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-dashboard-widget.php

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

336 lines 14.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * CartFlows Dashboard Widget.
4 *
5 * Renders a "Funnel Performance" dashboard widget summarising
6 * the last 30 days of orders and revenue, with locked PRO upsell
7 * cards for free users.
8 *
9 * @package CartFlows
10 */
11
12 // Exit if accessed directly.
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit;
15 }
16
17 /**
18 * Class Cartflows_Dashboard_Widget.
19 */
20 class Cartflows_Dashboard_Widget {
21
22 /**
23 * Instance.
24 *
25 * @access private
26 * @var self|null Class instance.
27 * @since 2.2.3
28 */
29 private static $instance;
30
31 /**
32 * Initiator.
33 *
34 * @since 2.2.3
35 * @return self Initialised object of class.
36 */
37 public static function get_instance() {
38 if ( ! isset( self::$instance ) ) {
39 self::$instance = new self();
40 }
41 return self::$instance;
42 }
43
44 /**
45 * Constructor.
46 *
47 * @since 2.2.3
48 */
49 public function __construct() {
50 add_action( 'wp_dashboard_setup', array( $this, 'register_widget' ) );
51 }
52
53 /**
54 * Register the dashboard widget.
55 *
56 * @since 2.2.3
57 * @return void
58 */
59 public function register_widget() {
60
61 if ( ! current_user_can( 'manage_options' ) ) {
62 return;
63 }
64
65 // No meaningful stats without WooCommerce; avoid fatal from wc_price() in renderers.
66 if ( ! wcf()->is_woo_active ) {
67 return;
68 }
69
70 wp_add_dashboard_widget(
71 'cartflows_funnel_performance',
72 __( 'CartFlows — Funnel Performance', 'cartflows' ),
73 array( $this, 'render_widget' )
74 );
75 }
76
77 /**
78 * Render the dashboard widget body.
79 *
80 * @since 2.2.3
81 * @return void
82 */
83 public function render_widget() {
84
85 $stats = $this->get_stats();
86 $locked = $this->show_locked_cards();
87
88 // Decide message copy based on activity and licence state.
89 if ( $locked ) {
90 if ( 0 === (int) $stats['total_orders'] && (float) $stats['total_revenue_raw'] <= 0 ) {
91 $message = __( 'Your funnels are ready to generate revenue. Start boosting sales instantly with order bumps and upsells in CartFlows Pro.', 'cartflows' );
92 } elseif ( (int) $stats['total_orders'] > 0 ) {
93 /* translators: %s: Total revenue formatted as currency. */
94 $message = sprintf( __( "You've made %s — now scale it with upsells & order bumps.", 'cartflows' ), wp_strip_all_tags( str_replace( '&nbsp;', ' ', wc_price( (float) $stats['total_revenue_raw'] ) ) ) );
95 } else {
96 $message = __( 'Want to boost funnel revenue by 30%+? Unlock order bumps, upsells, and advanced funnel features with CartFlows Pro.', 'cartflows' );
97 }
98 } else {
99 /* translators: %s: Total revenue formatted as currency. */
100 $message = sprintf( __( "You've earned %s with CartFlows in the last 30 days.", 'cartflows' ), wp_strip_all_tags( str_replace( '&nbsp;', ' ', wc_price( (float) $stats['total_revenue_raw'] ) ) ) );
101 }
102
103 $report_url = admin_url( 'admin.php?page=cartflows&path=home' );
104 $upgrade_url = $this->get_utm_url( 'https://cartflows.com/pricing/', 'dashboard-widget-card' );
105 ?>
106 <div class="wcf-dash-widget">
107 <div class="wcf-dash-header">
108 <div class="wcf-dash-titles">
109 <p class="wcf-dash-heading"><?php esc_html_e( 'Your Funnel Performance 🚀', 'cartflows' ); ?></p>
110 <p class="wcf-dash-subheading">
111 <?php
112 echo wp_kses(
113 sprintf(
114 /* translators: %s: Bolded "last 30 days" timeframe. */
115 __( 'See how your funnels performed in the %s', 'cartflows' ),
116 '<strong>' . esc_html__( 'last 30 days', 'cartflows' ) . '</strong>'
117 ),
118 array( 'strong' => array() )
119 );
120 ?>
121 </p>
122 </div>
123 <a class="wcf-dash-report-link" href="<?php echo esc_url( $report_url ); ?>"><?php esc_html_e( 'View Report', 'cartflows' ); ?> &rarr;</a>
124 </div>
125
126 <div class="wcf-dash-cards">
127 <?php $this->render_card( __( 'Orders Generated', 'cartflows' ), $stats['total_orders'], false, false, false, $upgrade_url ); ?>
128 <?php $this->render_card( __( 'Total Revenue', 'cartflows' ), $stats['total_revenue_raw'], true, false, false, $upgrade_url ); ?>
129 <?php $this->render_card( __( 'Revenue from Order Bumps', 'cartflows' ), $stats['total_bump_revenue_raw'], true, $locked, true, $upgrade_url ); ?>
130 <?php $this->render_card( __( 'Revenue from Upsells', 'cartflows' ), $stats['total_offers_revenue_raw'], true, $locked, true, $upgrade_url ); ?>
131 </div>
132
133 <div class="wcf-dash-message">
134 <?php echo esc_html( $message ); ?>
135 </div>
136
137 <div class="wcf-dash-footer">
138 <?php if ( $locked ) : ?>
139 <a href="<?php echo esc_url( $this->get_utm_url( 'https://cartflows.com/pricing/', 'dashboard-widget-upgrade' ) ); ?>" target="_blank" rel="noopener noreferrer" class="button button-primary"><?php esc_html_e( 'Upgrade to Pro', 'cartflows' ); ?></a>
140 <?php endif; ?>
141 <div class="wcf-dash-footer-links">
142 <a href="<?php echo esc_url( $this->get_utm_url( 'https://cartflows.com/docs/', 'dashboard-widget-help' ) ); ?>" target="_blank" rel="noopener noreferrer"><?php esc_html_e( 'Help & Tutorials', 'cartflows' ); ?></a>
143 </div>
144 </div>
145
146 <?php $this->render_inline_styles(); ?>
147 </div>
148 <?php
149 }
150
151 /**
152 * Get cached funnel stats for the last 30 days.
153 *
154 * @since 2.2.3
155 * @return array Normalised stats array.
156 *
157 * @phpstan-return array{total_orders: int, total_revenue_raw: float, total_bump_revenue_raw: float, total_offers_revenue_raw: float}
158 */
159 private function get_stats() {
160
161 $end = gmdate( 'Y-m-d' );
162 $start = gmdate( 'Y-m-d', strtotime( '-30 days' ) );
163 $currency = function_exists( 'get_woocommerce_currency' ) ? get_woocommerce_currency() : 'NA';
164
165 // Currency code is part of the key so symbol changes invalidate the cache.
166 $cache_key = 'cartflows_dashboard_widget_stats_' . $currency . '_' . $end;
167 $cached = get_transient( $cache_key );
168
169 if ( is_array( $cached ) && isset( $cached['total_orders'], $cached['total_revenue_raw'], $cached['total_bump_revenue_raw'], $cached['total_offers_revenue_raw'] ) ) {
170 return array(
171 'total_orders' => (int) $cached['total_orders'],
172 'total_revenue_raw' => (float) $cached['total_revenue_raw'],
173 'total_bump_revenue_raw' => (float) $cached['total_bump_revenue_raw'],
174 'total_offers_revenue_raw' => (float) $cached['total_offers_revenue_raw'],
175 );
176 }
177
178 $earnings = array();
179
180 if ( class_exists( '\CartflowsAdmin\AdminCore\Inc\AdminHelper' ) ) {
181 $earnings = \CartflowsAdmin\AdminCore\Inc\AdminHelper::get_earnings( $start, $end, '', 'home', '' );
182 }
183
184 if ( ! is_array( $earnings ) ) {
185 $earnings = array();
186 }
187
188 // Pro filter shape may omit the *_raw keys; fall back to numeric parse of formatted fields.
189 $bump_raw = isset( $earnings['total_bump_revenue_raw'] ) ? (float) $earnings['total_bump_revenue_raw'] : ( isset( $earnings['total_bump_revenue'] ) && is_numeric( $earnings['total_bump_revenue'] ) ? (float) $earnings['total_bump_revenue'] : 0.0 );
190 $offers_raw = isset( $earnings['total_offers_revenue_raw'] ) ? (float) $earnings['total_offers_revenue_raw'] : ( isset( $earnings['total_offers_revenue'] ) && is_numeric( $earnings['total_offers_revenue'] ) ? (float) $earnings['total_offers_revenue'] : 0.0 );
191
192 $stats = array(
193 'total_orders' => isset( $earnings['total_orders'] ) ? (int) $earnings['total_orders'] : 0,
194 'total_revenue_raw' => isset( $earnings['total_revenue_raw'] ) ? (float) $earnings['total_revenue_raw'] : ( isset( $earnings['total_revenue'] ) && is_numeric( $earnings['total_revenue'] ) ? (float) $earnings['total_revenue'] : 0.0 ),
195 'total_bump_revenue_raw' => $bump_raw,
196 'total_offers_revenue_raw' => $offers_raw,
197 );
198
199 set_transient( $cache_key, $stats, 6 * HOUR_IN_SECONDS );
200
201 return $stats;
202 }
203
204 /**
205 * Decide whether the PRO upsell cards should appear locked.
206 *
207 * @since 2.2.3
208 * @return bool True when running on free or unlicensed Pro.
209 */
210 private function show_locked_cards() {
211 return ! ( _is_cartflows_pro() && is_wcf_pro_plan() );
212 }
213
214 /**
215 * Build a CartFlows.com URL with UTM parameters appended.
216 *
217 * @since 2.2.3
218 * @param string $base Destination base URL.
219 * @param string $campaign UTM campaign slug.
220 * @return string Full URL with UTM query parameters.
221 */
222 private function get_utm_url( $base, $campaign ) {
223 return add_query_arg(
224 array(
225 'utm_source' => 'plugin-page',
226 'utm_medium' => 'free-cartflows',
227 'utm_campaign' => $campaign,
228 ),
229 $base
230 );
231 }
232
233 /**
234 * Render a single stat card.
235 *
236 * @since 2.2.3
237 * @param string $label Card label.
238 * @param int|float|string $raw_value Raw numeric value.
239 * @param bool $is_currency Whether the value should be formatted as currency.
240 * @param bool $is_locked Whether the card should display the locked state.
241 * @param bool $is_pro Whether this metric is PRO-only.
242 * @param string $upgrade_url Upgrade URL used when the card is locked.
243 * @return void
244 */
245 private function render_card( $label, $raw_value, $is_currency, $is_locked, $is_pro, $upgrade_url ) {
246
247 $numeric = (float) ( is_numeric( $raw_value ) ? $raw_value : 0 );
248 $display = $is_currency ? wc_price( $numeric ) : number_format_i18n( $numeric );
249
250 $class = 'wcf-dash-card';
251 if ( $is_locked ) {
252 $class .= ' is-locked';
253 }
254 if ( $is_pro ) {
255 $class .= ' is-pro';
256 }
257
258 // Locked card is rendered as a clickable upgrade surface; unlocked is a plain div.
259 if ( $is_locked ) {
260 ?>
261 <a class="<?php echo esc_attr( $class ); ?>" href="<?php echo esc_url( $upgrade_url ); ?>" target="_blank" rel="noopener noreferrer">
262 <?php
263 } else {
264 ?>
265 <div class="<?php echo esc_attr( $class ); ?>">
266 <?php
267 }
268 ?>
269 <span class="wcf-dash-card-label">
270 <span class="wcf-dash-card-label-text"><?php echo esc_html( $label ); ?></span>
271 </span>
272 <span class="wcf-dash-card-value-wrap">
273 <?php if ( $is_locked ) : ?>
274 <span class="wcf-dash-card-value wcf-dash-card-value-teaser" aria-hidden="true">-</span>
275 <span class="wcf-dash-card-unlock">
276 <svg width="11" height="11" viewBox="0 0 16 16" aria-hidden="true" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
277 <path d="M8 1a3 3 0 0 0-3 3v3H4a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V8a1 1 0 0 0-1-1h-1V4a3 3 0 0 0-3-3zm-2 6V4a2 2 0 1 1 4 0v3H6z"/>
278 </svg>
279 <span><?php esc_html_e( 'Unlock with Pro', 'cartflows' ); ?></span>
280 </span>
281 <?php else : ?>
282 <span class="wcf-dash-card-value">
283 <?php echo $is_currency ? wp_kses_post( $display ) : esc_html( $display ); ?>
284 </span>
285 <?php endif; ?>
286 </span>
287 <?php
288 echo $is_locked ? '</a>' : '</div>';
289 }
290
291 /**
292 * Render scoped inline styles for the widget.
293 *
294 * @since 2.2.3
295 * @return void
296 */
297 private function render_inline_styles() {
298 ?>
299 <style>
300 #cartflows_funnel_performance .wcf-dash-widget { position: relative; }
301 #cartflows_funnel_performance .wcf-dash-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 16px; }
302 #cartflows_funnel_performance .wcf-dash-titles { flex: 1; min-width: 0; }
303 #cartflows_funnel_performance .wcf-dash-heading { margin: 0 0 2px; font-size: 15px; font-weight: 600; color: #111827; line-height: 1.3; }
304 #cartflows_funnel_performance .wcf-dash-subheading { margin: 0; font-size: 13px; color: #6b7280; line-height: 1.4; }
305 #cartflows_funnel_performance .wcf-dash-report-link { font-size: 12px; font-weight: 500; text-decoration: none; color: var(--wp-admin-theme-color, #2271b1); }
306 #cartflows_funnel_performance .wcf-dash-report-link:hover { color: var(--wp-admin-theme-color-darker-10, #135e96); }
307 #cartflows_funnel_performance .wcf-dash-cards { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin-bottom: 14px; }
308 #cartflows_funnel_performance .wcf-dash-card { position: relative; background: #fff; border: 1px solid #e5e7eb; border-radius: 8px; padding: 16px; display: flex; flex-direction: column; gap: 10px; box-shadow: 0 1px 2px rgba(15, 23, 42, .04); transition: transform .15s ease, box-shadow .15s ease, border-color .15s ease; }
309 #cartflows_funnel_performance .wcf-dash-card-label { display: inline-flex; align-items: center; font-size: 12px; font-weight: 600; color: #6b7280; }
310 #cartflows_funnel_performance .wcf-dash-card-label-text { line-height: 1.3; }
311 #cartflows_funnel_performance .wcf-dash-card-value-wrap { display: flex; flex-direction: column; gap: 6px; min-height: 30px; }
312 #cartflows_funnel_performance .wcf-dash-card-value { font-size: 22px; font-weight: 700; color: #111827; line-height: 1.15; letter-spacing: -0.01em; }
313 #cartflows_funnel_performance .wcf-dash-card-value-teaser { color: #9ca3af; user-select: none; pointer-events: none; }
314 #cartflows_funnel_performance .wcf-dash-card-unlock { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; font-weight: 600; color: var(--wp-admin-theme-color, #2271b1); }
315 #cartflows_funnel_performance .wcf-dash-card-unlock svg { flex: 0 0 auto; }
316 #cartflows_funnel_performance a.wcf-dash-card { text-decoration: none; cursor: pointer; }
317 #cartflows_funnel_performance a.wcf-dash-card:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(15, 23, 42, .08); border-color: var(--wp-admin-theme-color, #2271b1); }
318 #cartflows_funnel_performance a.wcf-dash-card:hover .wcf-dash-card-unlock { color: var(--wp-admin-theme-color-darker-10, #135e96); }
319 #cartflows_funnel_performance a.wcf-dash-card:focus-visible { outline: 2px solid var(--wp-admin-theme-color, #2271b1); outline-offset: 2px; }
320 #cartflows_funnel_performance .wcf-dash-message { margin-bottom: 14px; font-size: 13px; line-height: 1.5; color: #374151; }
321 #cartflows_funnel_performance .wcf-dash-footer { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; padding-top: 14px; border-top: 1px solid #e5e7eb; }
322 #cartflows_funnel_performance .wcf-dash-footer-links { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 14px; }
323 #cartflows_funnel_performance .wcf-dash-footer-links a { position: relative; font-size: 13px; font-weight: 500; text-decoration: none; color: var(--wp-admin-theme-color, #2271b1); transition: color .15s ease; }
324 #cartflows_funnel_performance .wcf-dash-footer-links a:hover { color: var(--wp-admin-theme-color-darker-10, #135e96); }
325 #cartflows_funnel_performance .wcf-dash-footer-links a + a::before { content: '·'; position: absolute; left: -10px; color: #d1d5db; font-weight: 700; }
326 @media ( max-width: 480px ) {
327 #cartflows_funnel_performance .wcf-dash-cards { grid-template-columns: minmax(0, 1fr); }
328 }
329 </style>
330 <?php
331 }
332
333 }
334
335 Cartflows_Dashboard_Widget::get_instance();
336