title = __('Cart Peek 🛒', 'notificationx');
$this->dashboard_title = __('Cart Peek', 'notificationx');
// Cart Peek's message reads "N shoppers have this in their cart" — it has
// no purchase verb. The second text slot reuses the shared content field
// whose field-level default ("recently purchased") re-fills the emptied
// value in the builder, which then renders on the front end and looks
// wrong. Keep that slot blank by default (only when it is still the
// inherited sales verb; a verb the merchant typed themselves is kept),
// enforced on both save and the builder preview.
add_filter( "nx_save_post_{$this->default_source}", [ $this, 'blank_default_verb_post' ], 20, 1 );
add_filter( "nx_preview_settings_{$this->default_source}", [ $this, 'blank_default_verb_settings' ], 20, 1 );
// On read: an existing notification saved without the field (Cart Peek
// does not expose it) has no second_param at all, so the front end falls
// back to the remapped Sales design's "just purchased" default. Pin it to
// empty whenever a Cart Peek record is read so the front end renders no
// verb.
add_filter( "nx_get_post_{$this->default_source}", [ $this, 'blank_default_verb_settings' ], 20, 1 );
// At front-end render (after the theme is remapped to the Sales design).
add_filter( 'nx_filtered_post', [ $this, 'blank_default_verb_filtered_post' ], 20, 1 );
// Hide the free-text "verb" (second_param) field from the Content tab for
// Cart Peek ONLY. Cart Peek's message is count / product / time — it has no
// verb slot in its template, so anything typed there never renders and only
// confuses. Every other notification type still shows the field.
add_filter( 'nx_notification_template', [ $this, 'hide_verb_field' ], 20, 1 );
// Default param selection for each theme's text rows.
// row 1 (first_param) = tag_cart_peek_count -> "N shoppers have this in their cart"
// row 2 (third_param) = tag_product_title -> the product name
// row 3 (fourth_param) = tag_time
$common_fields = [
'first_param' => 'tag_cart_peek_count',
'custom_first_param' => __('A few shoppers have this in their cart', 'notificationx'),
'second_param' => '',
'third_param' => 'tag_product_title',
'custom_third_param' => __('this product', 'notificationx'),
'fourth_param' => 'tag_time',
'custom_fourth_param' => __('right now', 'notificationx'),
];
// Preview-image base URL + an mtime cache-buster, so regenerated preview
// artwork is picked up without a hard refresh (and never served stale
// from the browser cache at the same filename).
$cp = NOTIFICATIONX_ADMIN_URL . 'images/extensions/themes/cart-peek/';
$cpv = '?ver=' . ( @filemtime( NOTIFICATIONX_ASSETS_PATH . 'admin/images/extensions/themes/cart-peek/cp-14.png' ) ?: NOTIFICATIONX_VERSION );
// Desktop themes — reuse the Sales conversion + sales-count card designs.
// The count-card themes (fourteen/sixteen) read best for a "N people have
// this in their cart" message, so they lead the grid.
$this->themes = [
'conv-theme-fourteen' => [
'source' => $cp . 'cp-14.png' . $cpv,
'image_shape' => 'rounded',
'template' => $common_fields,
'defaults' => [
'link_button' => true,
'link_button_text' => __('View product', 'notificationx'),
],
],
'conv-theme-sixteen' => [
'source' => $cp . 'cp-16.png' . $cpv,
'image_shape' => 'rounded',
'template' => $common_fields,
'defaults' => [
'link_button' => true,
'link_button_text' => __('View product', 'notificationx'),
],
],
'theme-one' => [
'source' => $cp . 'cp-2.png' . $cpv,
'image_shape' => 'square',
'template' => $common_fields,
],
'theme-two' => [
'source' => $cp . 'cp-1.png' . $cpv,
'image_shape' => 'square',
'template' => $common_fields,
],
'theme-three' => [
'source' => $cp . 'cp-3.png' . $cpv,
'image_shape' => 'square',
'template' => $common_fields,
],
'conv-theme-twelve' => [
'source' => $cp . 'cp-12.png' . $cpv,
'image_shape' => 'circle',
'template' => $common_fields,
'defaults' => [
'link_button' => true,
'link_button_text' => __('View product', 'notificationx'),
],
],
];
// Mobile themes.
$this->res_themes = [
'res-theme-nine' => [
'source' => $cp . 'cp-res-9.png' . $cpv,
'_template' => 'cart_peek_count_template',
],
'res-theme-one' => [
'source' => $cp . 'cp-res-1.png' . $cpv,
'_template' => 'cart_peek_template',
],
'res-theme-two' => [
'source' => $cp . 'cp-res-2.png' . $cpv,
'_template' => 'cart_peek_template',
],
];
// Builder param dropdowns per template. `_themes` lists the full theme
// keys (type-id prefixed) that use each template.
$this->templates = [
'cart_peek_count_template' => [
'first_param' => ['tag_cart_peek_count' => __('Cart Count', 'notificationx')],
'third_param' => ['tag_product_title' => __('Product Title', 'notificationx')],
'fourth_param' => ['tag_time' => __('Definite Time', 'notificationx')],
'_themes' => [
'woocommerce_cart_peek_conv-theme-fourteen',
'woocommerce_cart_peek_conv-theme-sixteen',
],
],
'cart_peek_template' => [
'first_param' => ['tag_cart_peek_count' => __('Cart Count', 'notificationx')],
'third_param' => ['tag_product_title' => __('Product Title', 'notificationx')],
'fourth_param' => ['tag_time' => __('Definite Time', 'notificationx')],
'_themes' => [
'woocommerce_cart_peek_theme-one',
'woocommerce_cart_peek_theme-two',
'woocommerce_cart_peek_theme-three',
'woocommerce_cart_peek_conv-theme-twelve',
'woocommerce_cart_peek_res-theme-two',
],
],
];
$this->popup = [
"denyButtonText" => __("More Info", "notificationx"),
"confirmButtonText" => __("Upgrade to PRO", "notificationx"),
// phpcs:disable PluginCheck.CodeAnalysis.Offloading.OffloadedContent -- False positive for this context: remote documentation/pricing links in admin help text, not offloaded plugin assets.
"html" => __('
Show the live count of shoppers who currently have a product in their cart to spark urgency and boost conversions.
', 'notificationx')
// phpcs:enable PluginCheck.CodeAnalysis.Offloading.OffloadedContent
];
}
/**
* The inherited sales verbs that do not belong on a Cart Peek message.
* Anything else in the slot is treated as merchant-authored and kept.
*
* @param string $value
* @return bool
*/
protected function is_inherited_verb( $value ) {
return in_array(
trim( (string) $value ),
[ '', __( 'recently purchased', 'notificationx' ), __( 'just purchased', 'notificationx' ) ],
true
);
}
/**
* True when the first row still holds an inherited Sales default rather than
* Cart Peek's own headline. Every Cart Peek theme uses `tag_cart_peek_count`
* for row 1 ("N shoppers have this in their cart") — the builder offers no
* other option — so a `tag_name` (or empty) value can only be a default that
* leaked in from the Sales schema and must be normalised back to the count.
*
* @param string $value
* @return bool
*/
protected function is_inherited_first_param( $value ) {
return in_array( trim( (string) $value ), [ '', 'tag_name' ], true );
}
/**
* Blank the "verb" slot when it still holds the inherited Sales default
* (or is unset). Covers BOTH the flat `second_param` and the nested
* `notification-template.second_param` — the front end composes the message
* from the nested copy, so both must be cleared. Merchant-authored text is
* kept.
*
* @param array $arr
* @return array
*/
protected function blank_verb( $arr ) {
if ( ! is_array( $arr ) ) {
return $arr;
}
if ( ! isset( $arr['second_param'] ) || $this->is_inherited_verb( $arr['second_param'] ) ) {
$arr['second_param'] = '';
}
if ( isset( $arr['notification-template'] ) && is_array( $arr['notification-template'] )
&& ( ! isset( $arr['notification-template']['second_param'] ) || $this->is_inherited_verb( $arr['notification-template']['second_param'] ) ) ) {
$arr['notification-template']['second_param'] = '';
}
// Force row 1 back to the live-count headline when it still carries a
// leaked Sales default (`tag_name`/empty). The default Cart Peek theme
// (conv-theme-fourteen) can otherwise save `first_param => tag_name`,
// which renders "Someone " instead of the shopper count. Covers
// both the flat and the nested (front-end-composed) copies.
if ( ! isset( $arr['first_param'] ) || $this->is_inherited_first_param( $arr['first_param'] ) ) {
$arr['first_param'] = 'tag_cart_peek_count';
}
if ( isset( $arr['notification-template'] ) && is_array( $arr['notification-template'] )
&& ( ! isset( $arr['notification-template']['first_param'] ) || $this->is_inherited_first_param( $arr['notification-template']['first_param'] ) ) ) {
$arr['notification-template']['first_param'] = 'tag_cart_peek_count';
}
return $arr;
}
/**
* On save. Wired to nx_save_post_{source}; the params live in $post['data'].
*
* @param array $post
* @return array
*/
public function blank_default_verb_post( $post ) {
if ( isset( $post['data'] ) && is_array( $post['data'] ) ) {
$post['data'] = $this->blank_verb( $post['data'] );
}
return $post;
}
/**
* Builder preview + every read of a Cart Peek record
* (nx_preview_settings_{source} / nx_get_post_{source}).
*
* @param array $settings
* @return array
*/
public function blank_default_verb_settings( $settings ) {
return $this->blank_verb( $settings );
}
/**
* Front-end render filter (nx_filtered_post fires for every source, so gate
* on this one). This is the path the live popup reads.
*
* @param array $post
* @return array
*/
public function blank_default_verb_filtered_post( $post ) {
if ( isset( $post['source'] ) && $this->default_source === $post['source'] ) {
$post = $this->blank_verb( $post );
}
return $post;
}
/**
* Hooked to nx_notification_template. Hides the shared free-text `second_param`
* ("verb") field for Cart Peek only, via a source rule the builder evaluates
* client-side — every other source keeps the field. Cart Peek's template does
* not render second_param, so leaving it editable only invited confusion.
*
* @param array $fields notification-template group sub-fields.
* @return array
*/
public function hide_verb_field( $fields ) {
if ( isset( $fields['second_param'] ) ) {
// Show only when the source is NOT Cart Peek (i.e. hide for Cart Peek).
$fields['second_param']['rules'] = Rules::is( 'source', $this->default_source, true );
}
return $fields;
}
/**
* Hooked to nx_before_metabox_load. Adds the Cart Peek-only "Cart
* Confirmation Popup" controls to the Customize tab.
*
* @return void
*/
public function init_fields() {
parent::init_fields();
add_filter( 'nx_customize_fields', array( $this, 'cart_confirm_fields' ), 20 );
}
/**
* Builder fields for the shopper-facing cart confirmation popup — a small
* "Continue shopping / Go to checkout" prompt shown to the shopper right
* after they add a product to the cart (behaviour lives in the Pro
* extension). Scoped to the Cart Peek type only.
*
* @param array $fields
* @return array
*/
public function cart_confirm_fields( $fields ) {
$fields[] = [
'label' => __( 'Cart Confirmation Popup', 'notificationx' ),
'name' => 'cart_confirm_section',
'type' => 'section',
'priority' => 12,
'rules' => Rules::is( 'type', 'woocommerce_cart_peek' ),
'fields' => [
[
'label' => __( 'Cart Confirmation Popup', 'notificationx' ),
'name' => 'cart_confirm_enable',
'type' => 'toggle',
'default' => false,
'is_pro' => true,
'description' => __( 'Show the shopper a "Continue shopping / Go to checkout" popup right after they add a product to the cart.', 'notificationx' ),
],
[
'label' => __( 'Popup Heading', 'notificationx' ),
'name' => 'cart_confirm_heading',
'type' => 'text',
'default' => __( 'Leaving so soon?', 'notificationx' ),
'rules' => Rules::is( 'cart_confirm_enable', true ),
],
[
'label' => __( 'Continue Shopping Button', 'notificationx' ),
'name' => 'cart_confirm_continue_text',
'type' => 'text',
'default' => __( 'Continue shopping', 'notificationx' ),
'rules' => Rules::is( 'cart_confirm_enable', true ),
],
[
'label' => __( 'Checkout Button', 'notificationx' ),
'name' => 'cart_confirm_checkout_text',
'type' => 'text',
'default' => __( 'Go to checkout', 'notificationx' ),
'rules' => Rules::is( 'cart_confirm_enable', true ),
],
[
'label' => __( 'Multiple Products Display', 'notificationx' ),
'name' => 'cart_confirm_multi_display',
'type' => 'select',
'default' => 'count',
'description' => __( 'How to show the message when the cart has more than one product.', 'notificationx' ),
'options' => GlobalFields::get_instance()->normalize_fields( [
'count' => __( 'Show count — e.g. "Product A and 2 more products"', 'notificationx' ),
'list' => __( 'List product names — e.g. "Product A, Product B, Product C"', 'notificationx' ),
'total' => __( 'Show total items — e.g. "3 items"', 'notificationx' ),
] ),
'rules' => Rules::is( 'cart_confirm_enable', true ),
],
],
];
return $fields;
}
/**
* Bound to `nx_can_entry_{$id}` by the WooCommerce base extension (which
* assumes the type exposes this, like Sales/Conversions do). Cart Peek does
* not offer product-exclude controls, so every entry passes through.
*
* @param bool $result
* @param array $entry
* @param array $settings
* @return bool
*/
public function nx_can_entry( $result, $entry, $settings ) {
return $result;
}
/**
* Bound to `nx_filtered_data_{$id}` by the WooCommerce Pro base extension.
* No product-exclude filtering for Cart Peek — return data untouched.
*
* @param array $data
* @param array $settings
* @return array
*/
public function show_exclude_product( $data, $settings ) {
return $data;
}
}