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
cartflows / classes / class-cartflows-dashboard-widget.php

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

349 lines 14.2 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=analytics' );
104 $upgrade_url = \Cartflows_Helper::get_upgrade_to_pro_link(
105 'pricing',
106 '',
107 array(
108 'utm_source' => 'plugin-page',
109 'utm_campaign' => 'dashboard-widget-card',
110 )
111 );
112 ?>
113 <div class="wcf-dash-widget">
114 <div class="wcf-dash-header">
115 <div class="wcf-dash-titles">
116 <p class="wcf-dash-heading"><?php esc_html_e( 'Your Funnel Performance 🚀', 'cartflows' ); ?></p>
117 <p class="wcf-dash-subheading">
118 <?php
119 echo wp_kses(
120 sprintf(
121 /* translators: %s: Bolded "last 30 days" timeframe. */
122 __( 'See how your funnels performed in the %s', 'cartflows' ),
123 '<strong>' . esc_html__( 'last 30 days', 'cartflows' ) . '</strong>'
124 ),
125 array( 'strong' => array() )
126 );
127 ?>
128 </p>
129 </div>
130 <a class="wcf-dash-report-link" href="<?php echo esc_url( $report_url ); ?>"><?php esc_html_e( 'View Report', 'cartflows' ); ?> &rarr;</a>
131 </div>
132
133 <div class="wcf-dash-cards">
134 <?php $this->render_card( __( 'Orders Generated', 'cartflows' ), $stats['total_orders'], false, false, false, $upgrade_url ); ?>
135 <?php $this->render_card( __( 'Total Revenue', 'cartflows' ), $stats['total_revenue_raw'], true, false, false, $upgrade_url ); ?>
136 <?php $this->render_card( __( 'Revenue from Order Bumps', 'cartflows' ), $stats['total_bump_revenue_raw'], true, $locked, true, $upgrade_url ); ?>
137 <?php $this->render_card( __( 'Revenue from Upsells', 'cartflows' ), $stats['total_offers_revenue_raw'], true, $locked, true, $upgrade_url ); ?>
138 </div>
139
140 <div class="wcf-dash-message">
141 <?php echo esc_html( $message ); ?>
142 </div>
143
144 <div class="wcf-dash-footer">
145 <?php if ( $locked ) : ?>
146 <a href="
147 <?php
148 echo esc_url(
149 \Cartflows_Helper::get_upgrade_to_pro_link(
150 'pricing',
151 '',
152 array(
153 'utm_source' => 'plugin-page',
154 'utm_campaign' => 'dashboard-widget-upgrade',
155 )
156 )
157 );
158 ?>
159 " target="_blank" rel="noopener noreferrer" class="button button-primary"><?php esc_html_e( 'Upgrade to Pro', 'cartflows' ); ?></a>
160 <?php endif; ?>
161 <div class="wcf-dash-footer-links">
162 <a href="
163 <?php
164 echo esc_url(
165 \Cartflows_Helper::get_kb_doc_link(
166 'https://cartflows.com/docs/',
167 array(
168 'utm_source' => 'plugin-page',
169 'utm_campaign' => 'dashboard-widget-help',
170 )
171 )
172 );
173 ?>
174 " target="_blank" rel="noopener noreferrer"><?php esc_html_e( 'Help & Tutorials', 'cartflows' ); ?></a>
175 </div>
176 </div>
177
178 <?php $this->render_inline_styles(); ?>
179 </div>
180 <?php
181 }
182
183 /**
184 * Get cached funnel stats for the last 30 days.
185 *
186 * @since 2.2.3
187 * @return array Normalised stats array.
188 *
189 * @phpstan-return array{total_orders: int, total_revenue_raw: float, total_bump_revenue_raw: float, total_offers_revenue_raw: float}
190 */
191 private function get_stats() {
192
193 $end = gmdate( 'Y-m-d' );
194 $start = gmdate( 'Y-m-d', strtotime( '-30 days' ) );
195 $currency = function_exists( 'get_woocommerce_currency' ) ? get_woocommerce_currency() : 'NA';
196
197 // Currency code is part of the key so symbol changes invalidate the cache.
198 $cache_key = 'cartflows_dashboard_widget_stats_' . $currency . '_' . $end;
199 $cached = get_transient( $cache_key );
200
201 if ( is_array( $cached ) && isset( $cached['total_orders'], $cached['total_revenue_raw'], $cached['total_bump_revenue_raw'], $cached['total_offers_revenue_raw'] ) ) {
202 return array(
203 'total_orders' => (int) $cached['total_orders'],
204 'total_revenue_raw' => (float) $cached['total_revenue_raw'],
205 'total_bump_revenue_raw' => (float) $cached['total_bump_revenue_raw'],
206 'total_offers_revenue_raw' => (float) $cached['total_offers_revenue_raw'],
207 );
208 }
209
210 $earnings = array();
211
212 if ( class_exists( '\CartflowsAdmin\AdminCore\Inc\AdminHelper' ) ) {
213 $earnings = \CartflowsAdmin\AdminCore\Inc\AdminHelper::get_earnings( $start, $end, '', 'home', '' );
214 }
215
216 if ( ! is_array( $earnings ) ) {
217 $earnings = array();
218 }
219
220 // Pro filter shape may omit the *_raw keys; fall back to numeric parse of formatted fields.
221 $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 );
222 $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 );
223
224 $stats = array(
225 'total_orders' => isset( $earnings['total_orders'] ) ? (int) $earnings['total_orders'] : 0,
226 '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 ),
227 'total_bump_revenue_raw' => $bump_raw,
228 'total_offers_revenue_raw' => $offers_raw,
229 );
230
231 set_transient( $cache_key, $stats, 6 * HOUR_IN_SECONDS );
232
233 return $stats;
234 }
235
236 /**
237 * Decide whether the PRO upsell cards should appear locked.
238 *
239 * @since 2.2.3
240 * @return bool True when running on free or unlicensed Pro.
241 */
242 private function show_locked_cards() {
243 return ! ( _is_cartflows_pro() && is_wcf_pro_plan() );
244 }
245
246 /**
247 * Render a single stat card.
248 *
249 * @since 2.2.3
250 * @param string $label Card label.
251 * @param int|float|string $raw_value Raw numeric value.
252 * @param bool $is_currency Whether the value should be formatted as currency.
253 * @param bool $is_locked Whether the card should display the locked state.
254 * @param bool $is_pro Whether this metric is PRO-only.
255 * @param string $upgrade_url Upgrade URL used when the card is locked.
256 * @return void
257 */
258 private function render_card( $label, $raw_value, $is_currency, $is_locked, $is_pro, $upgrade_url ) {
259
260 $numeric = (float) ( is_numeric( $raw_value ) ? $raw_value : 0 );
261 $display = $is_currency ? wc_price( $numeric ) : number_format_i18n( $numeric );
262
263 $class = 'wcf-dash-card';
264 if ( $is_locked ) {
265 $class .= ' is-locked';
266 }
267 if ( $is_pro ) {
268 $class .= ' is-pro';
269 }
270
271 // Locked card is rendered as a clickable upgrade surface; unlocked is a plain div.
272 if ( $is_locked ) {
273 ?>
274 <a class="<?php echo esc_attr( $class ); ?>" href="<?php echo esc_url( $upgrade_url ); ?>" target="_blank" rel="noopener noreferrer">
275 <?php
276 } else {
277 ?>
278 <div class="<?php echo esc_attr( $class ); ?>">
279 <?php
280 }
281 ?>
282 <span class="wcf-dash-card-label">
283 <span class="wcf-dash-card-label-text"><?php echo esc_html( $label ); ?></span>
284 </span>
285 <span class="wcf-dash-card-value-wrap">
286 <?php if ( $is_locked ) : ?>
287 <span class="wcf-dash-card-value wcf-dash-card-value-teaser" aria-hidden="true">-</span>
288 <span class="wcf-dash-card-unlock">
289 <svg width="11" height="11" viewBox="0 0 16 16" aria-hidden="true" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
290 <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"/>
291 </svg>
292 <span><?php esc_html_e( 'Unlock with Pro', 'cartflows' ); ?></span>
293 </span>
294 <?php else : ?>
295 <span class="wcf-dash-card-value">
296 <?php echo $is_currency ? wp_kses_post( $display ) : esc_html( $display ); ?>
297 </span>
298 <?php endif; ?>
299 </span>
300 <?php
301 echo $is_locked ? '</a>' : '</div>';
302 }
303
304 /**
305 * Render scoped inline styles for the widget.
306 *
307 * @since 2.2.3
308 * @return void
309 */
310 private function render_inline_styles() {
311 ?>
312 <style>
313 #cartflows_funnel_performance .wcf-dash-widget { position: relative; }
314 #cartflows_funnel_performance .wcf-dash-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 16px; }
315 #cartflows_funnel_performance .wcf-dash-titles { flex: 1; min-width: 0; }
316 #cartflows_funnel_performance .wcf-dash-heading { margin: 0 0 2px; font-size: 15px; font-weight: 600; color: #111827; line-height: 1.3; }
317 #cartflows_funnel_performance .wcf-dash-subheading { margin: 0; font-size: 13px; color: #6b7280; line-height: 1.4; }
318 #cartflows_funnel_performance .wcf-dash-report-link { font-size: 12px; font-weight: 500; text-decoration: none; color: var(--wp-admin-theme-color, #2271b1); }
319 #cartflows_funnel_performance .wcf-dash-report-link:hover { color: var(--wp-admin-theme-color-darker-10, #135e96); }
320 #cartflows_funnel_performance .wcf-dash-cards { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin-bottom: 14px; }
321 #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; }
322 #cartflows_funnel_performance .wcf-dash-card-label { display: inline-flex; align-items: center; font-size: 12px; font-weight: 600; color: #6b7280; }
323 #cartflows_funnel_performance .wcf-dash-card-label-text { line-height: 1.3; }
324 #cartflows_funnel_performance .wcf-dash-card-value-wrap { display: flex; flex-direction: column; gap: 6px; min-height: 30px; }
325 #cartflows_funnel_performance .wcf-dash-card-value { font-size: 22px; font-weight: 700; color: #111827; line-height: 1.15; letter-spacing: -0.01em; }
326 #cartflows_funnel_performance .wcf-dash-card-value-teaser { color: #9ca3af; user-select: none; pointer-events: none; }
327 #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); }
328 #cartflows_funnel_performance .wcf-dash-card-unlock svg { flex: 0 0 auto; }
329 #cartflows_funnel_performance a.wcf-dash-card { text-decoration: none; cursor: pointer; }
330 #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); }
331 #cartflows_funnel_performance a.wcf-dash-card:hover .wcf-dash-card-unlock { color: var(--wp-admin-theme-color-darker-10, #135e96); }
332 #cartflows_funnel_performance a.wcf-dash-card:focus-visible { outline: 2px solid var(--wp-admin-theme-color, #2271b1); outline-offset: 2px; }
333 #cartflows_funnel_performance .wcf-dash-message { margin-bottom: 14px; font-size: 13px; line-height: 1.5; color: #374151; }
334 #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; }
335 #cartflows_funnel_performance .wcf-dash-footer-links { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 14px; }
336 #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; }
337 #cartflows_funnel_performance .wcf-dash-footer-links a:hover { color: var(--wp-admin-theme-color-darker-10, #135e96); }
338 #cartflows_funnel_performance .wcf-dash-footer-links a + a::before { content: '·'; position: absolute; left: -10px; color: #d1d5db; font-weight: 700; }
339 @media ( max-width: 480px ) {
340 #cartflows_funnel_performance .wcf-dash-cards { grid-template-columns: minmax(0, 1fr); }
341 }
342 </style>
343 <?php
344 }
345
346 }
347
348 Cartflows_Dashboard_Widget::get_instance();
349