PluginProbe
NotificationX – FOMO, Live Sales Notification, WooCommerce Sales Popup, GDPR, Social Proof, Announcement Banner & Floating Notification Bar / 3.3.0
NotificationX – FOMO, Live Sales Notification, WooCommerce Sales Popup, GDPR, Social Proof, Announcement Banner & Floating Notification Bar v3.3.0
3.3.1 3.3.0 3.2.14 3.2.13 3.2.12 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 trunk 0.2.5.5 0.2.5.6 0.2.5.7 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.2.0 1.2.1 All 156 releases
notificationx / includes / Types / WooCommerceCartPeek.php

WooCommerceCartPeek.php in NotificationX – FOMO, Live Sales Notification, WooCommerce Sales Popup, GDPR, Social Proof, Announcement Banner & Floating Notification Bar 3.3.0, at includes/Types/WooCommerceCartPeek.php

424 lines 18.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Cart Peek Type
5 *
6 * @package NotificationX\Types
7 */
8
9 namespace NotificationX\Types;
10
11 use NotificationX\Core\Rules;
12 use NotificationX\Extensions\GlobalFields;
13 use NotificationX\GetInstance;
14
15 /**
16 * Cart Peek Type — Conversions-family feature card that surfaces the live
17 * count of shoppers who currently have a product in cart. Pro-only, WooCommerce
18 * module. Sits next to Growth Alert 🚀 (Inline) in the Notification Type grid.
19 *
20 * Design note: Cart Peek reuses the existing WooCommerce "Sales" conversion
21 * theme styling so it ships with polished designs and zero frontend rebuild.
22 * The theme keys declared here (`woocommerce_cart_peek_*`) are remapped to the
23 * matching `woocommerce_sales_*` classes at render time by the Pro extension
24 * (see WooCommerceCartPeek::remap_theme()), so the compiled Sales CSS/React
25 * renders them. Theme grid previews reuse the Sales conversion preview images.
26 *
27 * @method static WooCommerceCartPeek get_instance($args = null)
28 */
29 class WooCommerceCartPeek extends Types {
30 /**
31 * Instance of Admin
32 *
33 * @var Admin
34 */
35 use GetInstance;
36 public $priority = 51;
37 public $is_pro = true;
38 public $themes = [];
39 public $res_themes = [];
40 public $module = ['modules_woocommerce'];
41 public $id = 'woocommerce_cart_peek';
42 public $default_source = 'woocommerce_cart_peek';
43 public $default_theme = 'woocommerce_cart_peek_conv-theme-fourteen';
44 public $link_type = 'product_page';
45
46 /**
47 * Initially Invoked when initialized.
48 */
49 public function __construct() {
50 parent::__construct();
51 }
52
53 public function init() {
54 parent::init();
55 $this->title = __('Cart Peek 🛒', 'notificationx');
56 $this->dashboard_title = __('Cart Peek', 'notificationx');
57
58 // Cart Peek's message reads "N shoppers have this in their cart" — it has
59 // no purchase verb. The second text slot reuses the shared content field
60 // whose field-level default ("recently purchased") re-fills the emptied
61 // value in the builder, which then renders on the front end and looks
62 // wrong. Keep that slot blank by default (only when it is still the
63 // inherited sales verb; a verb the merchant typed themselves is kept),
64 // enforced on both save and the builder preview.
65 add_filter( "nx_save_post_{$this->default_source}", [ $this, 'blank_default_verb_post' ], 20, 1 );
66 add_filter( "nx_preview_settings_{$this->default_source}", [ $this, 'blank_default_verb_settings' ], 20, 1 );
67 // On read: an existing notification saved without the field (Cart Peek
68 // does not expose it) has no second_param at all, so the front end falls
69 // back to the remapped Sales design's "just purchased" default. Pin it to
70 // empty whenever a Cart Peek record is read so the front end renders no
71 // verb.
72 add_filter( "nx_get_post_{$this->default_source}", [ $this, 'blank_default_verb_settings' ], 20, 1 );
73 // At front-end render (after the theme is remapped to the Sales design).
74 add_filter( 'nx_filtered_post', [ $this, 'blank_default_verb_filtered_post' ], 20, 1 );
75
76 // Hide the free-text "verb" (second_param) field from the Content tab for
77 // Cart Peek ONLY. Cart Peek's message is count / product / time — it has no
78 // verb slot in its template, so anything typed there never renders and only
79 // confuses. Every other notification type still shows the field.
80 add_filter( 'nx_notification_template', [ $this, 'hide_verb_field' ], 20, 1 );
81
82 // Default param selection for each theme's text rows.
83 // row 1 (first_param) = tag_cart_peek_count -> "N shoppers have this in their cart"
84 // row 2 (third_param) = tag_product_title -> the product name
85 // row 3 (fourth_param) = tag_time
86 $common_fields = [
87 'first_param' => 'tag_cart_peek_count',
88 'custom_first_param' => __('A few shoppers have this in their cart', 'notificationx'),
89 'second_param' => '',
90 'third_param' => 'tag_product_title',
91 'custom_third_param' => __('this product', 'notificationx'),
92 'fourth_param' => 'tag_time',
93 'custom_fourth_param' => __('right now', 'notificationx'),
94 ];
95
96 // Preview-image base URL + an mtime cache-buster, so regenerated preview
97 // artwork is picked up without a hard refresh (and never served stale
98 // from the browser cache at the same filename).
99 $cp = NOTIFICATIONX_ADMIN_URL . 'images/extensions/themes/cart-peek/';
100 $cpv = '?ver=' . ( @filemtime( NOTIFICATIONX_ASSETS_PATH . 'admin/images/extensions/themes/cart-peek/cp-14.png' ) ?: NOTIFICATIONX_VERSION );
101
102 // Desktop themes — reuse the Sales conversion + sales-count card designs.
103 // The count-card themes (fourteen/sixteen) read best for a "N people have
104 // this in their cart" message, so they lead the grid.
105 $this->themes = [
106 'conv-theme-fourteen' => [
107 'source' => $cp . 'cp-14.png' . $cpv,
108 'image_shape' => 'rounded',
109 'template' => $common_fields,
110 'defaults' => [
111 'link_button' => true,
112 'link_button_text' => __('View product', 'notificationx'),
113 ],
114 ],
115 'conv-theme-sixteen' => [
116 'source' => $cp . 'cp-16.png' . $cpv,
117 'image_shape' => 'rounded',
118 'template' => $common_fields,
119 'defaults' => [
120 'link_button' => true,
121 'link_button_text' => __('View product', 'notificationx'),
122 ],
123 ],
124 'theme-one' => [
125 'source' => $cp . 'cp-2.png' . $cpv,
126 'image_shape' => 'square',
127 'template' => $common_fields,
128 ],
129 'theme-two' => [
130 'source' => $cp . 'cp-1.png' . $cpv,
131 'image_shape' => 'square',
132 'template' => $common_fields,
133 ],
134 'theme-three' => [
135 'source' => $cp . 'cp-3.png' . $cpv,
136 'image_shape' => 'square',
137 'template' => $common_fields,
138 ],
139 'conv-theme-twelve' => [
140 'source' => $cp . 'cp-12.png' . $cpv,
141 'image_shape' => 'circle',
142 'template' => $common_fields,
143 'defaults' => [
144 'link_button' => true,
145 'link_button_text' => __('View product', 'notificationx'),
146 ],
147 ],
148 ];
149
150 // Mobile themes.
151 $this->res_themes = [
152 'res-theme-nine' => [
153 'source' => $cp . 'cp-res-9.png' . $cpv,
154 '_template' => 'cart_peek_count_template',
155 ],
156 'res-theme-one' => [
157 'source' => $cp . 'cp-res-1.png' . $cpv,
158 '_template' => 'cart_peek_template',
159 ],
160 'res-theme-two' => [
161 'source' => $cp . 'cp-res-2.png' . $cpv,
162 '_template' => 'cart_peek_template',
163 ],
164 ];
165
166 // Builder param dropdowns per template. `_themes` lists the full theme
167 // keys (type-id prefixed) that use each template.
168 $this->templates = [
169 'cart_peek_count_template' => [
170 'first_param' => ['tag_cart_peek_count' => __('Cart Count', 'notificationx')],
171 'third_param' => ['tag_product_title' => __('Product Title', 'notificationx')],
172 'fourth_param' => ['tag_time' => __('Definite Time', 'notificationx')],
173 '_themes' => [
174 'woocommerce_cart_peek_conv-theme-fourteen',
175 'woocommerce_cart_peek_conv-theme-sixteen',
176 ],
177 ],
178 'cart_peek_template' => [
179 'first_param' => ['tag_cart_peek_count' => __('Cart Count', 'notificationx')],
180 'third_param' => ['tag_product_title' => __('Product Title', 'notificationx')],
181 'fourth_param' => ['tag_time' => __('Definite Time', 'notificationx')],
182 '_themes' => [
183 'woocommerce_cart_peek_theme-one',
184 'woocommerce_cart_peek_theme-two',
185 'woocommerce_cart_peek_theme-three',
186 'woocommerce_cart_peek_conv-theme-twelve',
187 'woocommerce_cart_peek_res-theme-two',
188 ],
189 ],
190 ];
191
192 $this->popup = [
193 "denyButtonText" => __("<a href='https://notificationx.com/docs/how-to-create-a-cart-peek-notification/' target='_blank'>More Info</a>", "notificationx"),
194 "confirmButtonText" => __("<a href='https://notificationx.com/#pricing' target='_blank'>Upgrade to PRO</a>", "notificationx"),
195 // phpcs:disable PluginCheck.CodeAnalysis.Offloading.OffloadedContent -- False positive for this context: remote documentation/pricing links in admin help text, not offloaded plugin assets.
196 "html" => __('
197 <span>Show the live count of shoppers who currently have a product in their cart to spark urgency and boost conversions.</span>
198 ', 'notificationx')
199 // phpcs:enable PluginCheck.CodeAnalysis.Offloading.OffloadedContent
200 ];
201 }
202
203 /**
204 * The inherited sales verbs that do not belong on a Cart Peek message.
205 * Anything else in the slot is treated as merchant-authored and kept.
206 *
207 * @param string $value
208 * @return bool
209 */
210 protected function is_inherited_verb( $value ) {
211 return in_array(
212 trim( (string) $value ),
213 [ '', __( 'recently purchased', 'notificationx' ), __( 'just purchased', 'notificationx' ) ],
214 true
215 );
216 }
217
218 /**
219 * True when the first row still holds an inherited Sales default rather than
220 * Cart Peek's own headline. Every Cart Peek theme uses `tag_cart_peek_count`
221 * for row 1 ("N shoppers have this in their cart") — the builder offers no
222 * other option — so a `tag_name` (or empty) value can only be a default that
223 * leaked in from the Sales schema and must be normalised back to the count.
224 *
225 * @param string $value
226 * @return bool
227 */
228 protected function is_inherited_first_param( $value ) {
229 return in_array( trim( (string) $value ), [ '', 'tag_name' ], true );
230 }
231
232 /**
233 * Blank the "verb" slot when it still holds the inherited Sales default
234 * (or is unset). Covers BOTH the flat `second_param` and the nested
235 * `notification-template.second_param` — the front end composes the message
236 * from the nested copy, so both must be cleared. Merchant-authored text is
237 * kept.
238 *
239 * @param array $arr
240 * @return array
241 */
242 protected function blank_verb( $arr ) {
243 if ( ! is_array( $arr ) ) {
244 return $arr;
245 }
246 if ( ! isset( $arr['second_param'] ) || $this->is_inherited_verb( $arr['second_param'] ) ) {
247 $arr['second_param'] = '';
248 }
249 if ( isset( $arr['notification-template'] ) && is_array( $arr['notification-template'] )
250 && ( ! isset( $arr['notification-template']['second_param'] ) || $this->is_inherited_verb( $arr['notification-template']['second_param'] ) ) ) {
251 $arr['notification-template']['second_param'] = '';
252 }
253 // Force row 1 back to the live-count headline when it still carries a
254 // leaked Sales default (`tag_name`/empty). The default Cart Peek theme
255 // (conv-theme-fourteen) can otherwise save `first_param => tag_name`,
256 // which renders "Someone <product>" instead of the shopper count. Covers
257 // both the flat and the nested (front-end-composed) copies.
258 if ( ! isset( $arr['first_param'] ) || $this->is_inherited_first_param( $arr['first_param'] ) ) {
259 $arr['first_param'] = 'tag_cart_peek_count';
260 }
261 if ( isset( $arr['notification-template'] ) && is_array( $arr['notification-template'] )
262 && ( ! isset( $arr['notification-template']['first_param'] ) || $this->is_inherited_first_param( $arr['notification-template']['first_param'] ) ) ) {
263 $arr['notification-template']['first_param'] = 'tag_cart_peek_count';
264 }
265 return $arr;
266 }
267
268 /**
269 * On save. Wired to nx_save_post_{source}; the params live in $post['data'].
270 *
271 * @param array $post
272 * @return array
273 */
274 public function blank_default_verb_post( $post ) {
275 if ( isset( $post['data'] ) && is_array( $post['data'] ) ) {
276 $post['data'] = $this->blank_verb( $post['data'] );
277 }
278 return $post;
279 }
280
281 /**
282 * Builder preview + every read of a Cart Peek record
283 * (nx_preview_settings_{source} / nx_get_post_{source}).
284 *
285 * @param array $settings
286 * @return array
287 */
288 public function blank_default_verb_settings( $settings ) {
289 return $this->blank_verb( $settings );
290 }
291
292 /**
293 * Front-end render filter (nx_filtered_post fires for every source, so gate
294 * on this one). This is the path the live popup reads.
295 *
296 * @param array $post
297 * @return array
298 */
299 public function blank_default_verb_filtered_post( $post ) {
300 if ( isset( $post['source'] ) && $this->default_source === $post['source'] ) {
301 $post = $this->blank_verb( $post );
302 }
303 return $post;
304 }
305
306 /**
307 * Hooked to nx_notification_template. Hides the shared free-text `second_param`
308 * ("verb") field for Cart Peek only, via a source rule the builder evaluates
309 * client-side — every other source keeps the field. Cart Peek's template does
310 * not render second_param, so leaving it editable only invited confusion.
311 *
312 * @param array $fields notification-template group sub-fields.
313 * @return array
314 */
315 public function hide_verb_field( $fields ) {
316 if ( isset( $fields['second_param'] ) ) {
317 // Show only when the source is NOT Cart Peek (i.e. hide for Cart Peek).
318 $fields['second_param']['rules'] = Rules::is( 'source', $this->default_source, true );
319 }
320 return $fields;
321 }
322
323 /**
324 * Hooked to nx_before_metabox_load. Adds the Cart Peek-only "Cart
325 * Confirmation Popup" controls to the Customize tab.
326 *
327 * @return void
328 */
329 public function init_fields() {
330 parent::init_fields();
331 add_filter( 'nx_customize_fields', array( $this, 'cart_confirm_fields' ), 20 );
332 }
333
334 /**
335 * Builder fields for the shopper-facing cart confirmation popup — a small
336 * "Continue shopping / Go to checkout" prompt shown to the shopper right
337 * after they add a product to the cart (behaviour lives in the Pro
338 * extension). Scoped to the Cart Peek type only.
339 *
340 * @param array $fields
341 * @return array
342 */
343 public function cart_confirm_fields( $fields ) {
344 $fields[] = [
345 'label' => __( 'Cart Confirmation Popup', 'notificationx' ),
346 'name' => 'cart_confirm_section',
347 'type' => 'section',
348 'priority' => 12,
349 'rules' => Rules::is( 'type', 'woocommerce_cart_peek' ),
350 'fields' => [
351 [
352 'label' => __( 'Cart Confirmation Popup', 'notificationx' ),
353 'name' => 'cart_confirm_enable',
354 'type' => 'toggle',
355 'default' => false,
356 'is_pro' => true,
357 'description' => __( 'Show the shopper a "Continue shopping / Go to checkout" popup right after they add a product to the cart.', 'notificationx' ),
358 ],
359 [
360 'label' => __( 'Popup Heading', 'notificationx' ),
361 'name' => 'cart_confirm_heading',
362 'type' => 'text',
363 'default' => __( 'Leaving so soon?', 'notificationx' ),
364 'rules' => Rules::is( 'cart_confirm_enable', true ),
365 ],
366 [
367 'label' => __( 'Continue Shopping Button', 'notificationx' ),
368 'name' => 'cart_confirm_continue_text',
369 'type' => 'text',
370 'default' => __( 'Continue shopping', 'notificationx' ),
371 'rules' => Rules::is( 'cart_confirm_enable', true ),
372 ],
373 [
374 'label' => __( 'Checkout Button', 'notificationx' ),
375 'name' => 'cart_confirm_checkout_text',
376 'type' => 'text',
377 'default' => __( 'Go to checkout', 'notificationx' ),
378 'rules' => Rules::is( 'cart_confirm_enable', true ),
379 ],
380 [
381 'label' => __( 'Multiple Products Display', 'notificationx' ),
382 'name' => 'cart_confirm_multi_display',
383 'type' => 'select',
384 'default' => 'count',
385 'description' => __( 'How to show the message when the cart has more than one product.', 'notificationx' ),
386 'options' => GlobalFields::get_instance()->normalize_fields( [
387 'count' => __( 'Show count — e.g. "Product A and 2 more products"', 'notificationx' ),
388 'list' => __( 'List product names — e.g. "Product A, Product B, Product C"', 'notificationx' ),
389 'total' => __( 'Show total items — e.g. "3 items"', 'notificationx' ),
390 ] ),
391 'rules' => Rules::is( 'cart_confirm_enable', true ),
392 ],
393 ],
394 ];
395 return $fields;
396 }
397
398 /**
399 * Bound to `nx_can_entry_{$id}` by the WooCommerce base extension (which
400 * assumes the type exposes this, like Sales/Conversions do). Cart Peek does
401 * not offer product-exclude controls, so every entry passes through.
402 *
403 * @param bool $result
404 * @param array $entry
405 * @param array $settings
406 * @return bool
407 */
408 public function nx_can_entry( $result, $entry, $settings ) {
409 return $result;
410 }
411
412 /**
413 * Bound to `nx_filtered_data_{$id}` by the WooCommerce Pro base extension.
414 * No product-exclude filtering for Cart Peek — return data untouched.
415 *
416 * @param array $data
417 * @param array $settings
418 * @return array
419 */
420 public function show_exclude_product( $data, $settings ) {
421 return $data;
422 }
423 }
424