Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
16
20
5 changes: 4 additions & 1 deletion README.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Tags: sales notification, fomo, social proof, woocommerce sales, notification ba
Requires at least: 5.0
Tested up to: 7.0
Requires PHP: 5.6
Stable tag: 3.2.13
Stable tag: 3.2.14
License: GPL-3.0-or-later
License URI: https://www.gnu.org/licenses/gpl-3.0.html

Expand Down Expand Up @@ -219,6 +219,9 @@ Yes. Your data is legally safe and we guarantee to not make use of your data und

== Changelog ==

= 3.2.14 - 13/08/2026 =
Few minor bug fixes and improvements.

= 3.2.13 - 06/08/2026 =
Added: New Coupon Popup theme under Announcement type notifications.
Improved: Country/User-based targeting options for all notification types.
Expand Down
4 changes: 2 additions & 2 deletions assets/admin/css/admin.css

Large diffs are not rendered by default.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion assets/admin/js/admin.asset.php
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<?php return array('dependencies' => array('lodash', 'moment', 'react', 'react-dom', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-date', 'wp-element', 'wp-escape-html', 'wp-hooks', 'wp-i18n', 'wp-media-utils', 'wp-polyfill'), 'version' => 'f5b59023c8730ebfff2c');
<?php return array('dependencies' => array('lodash', 'moment', 'react', 'react-dom', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-date', 'wp-element', 'wp-escape-html', 'wp-hooks', 'wp-i18n', 'wp-media-utils', 'wp-polyfill'), 'version' => 'a32866be83d570d28a5e');
2 changes: 1 addition & 1 deletion assets/admin/js/admin.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion assets/public/js/crossSite.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion assets/public/js/frontend.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion blocks/notificationx/index.asset.php
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<?php return array('dependencies' => array('react', 'react-dom', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-server-side-render'), 'version' => '5d5d06f6a083656f9459');
<?php return array('dependencies' => array('react', 'react-dom', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-server-side-render'), 'version' => '086b6a8940b6618134b9');
2 changes: 1 addition & 1 deletion blocks/notificationx/index.js

Large diffs are not rendered by default.

15 changes: 14 additions & 1 deletion docs/extensions/google.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Google Extension (`modules_google_reviews` / `modules_google_analytics` / `modules_google_youtube`)

> Bundles three unrelated Google integrations under one directory: **Google Reviews**
> Bundles three Google integrations under one directory: **Google Reviews**
> (place ratings/reviews → `reviews` Type), **Google Analytics** (site traffic →
> `page_analytics` Type), and **YouTube** (channel/video stats → `video` Type). All three
> are `$is_pro = true` — in this free plugin they register as sources/modules and provide
Expand Down Expand Up @@ -99,6 +99,19 @@ are not in this directory or in `GlobalFields.php` — they are defined in
`notificationx-pro` (its `includes/Admin/Settings.php` and the Pro Google extension
classes, confirmed by grep) and/or the React admin app (`nxdev/`).

## Realtime viewers helper (Pro)

The `modules_google_analytics` module also powers the WooCommerce Growth Alert
**`live-viewers`** design, which is a theme on the `woo_inline` and
`woocommerce_sales_inline` extensions rather than a source of its own. The Google directory
owns only the data side: Pro's
`NotificationXPro\Extensions\Google_Analytics\RealtimeViewers` resolves "how many people
are viewing this product right now" and both WooCommerce inline classes call
`RealtimeViewers::get_viewers()`.
Free registers the theme (gated on the module being enabled) and its
`source_error_message()` entry; see [woocommerce.md](woocommerce.md) and the Pro doc for
the GA4 realtime constraint that shapes it.

## Dependency & detection

- **Required service:** Google Places API key (Reviews), a connected Google Analytics
Expand Down
72 changes: 72 additions & 0 deletions docs/extensions/woocommerce.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,78 @@ Real events that drive data:
arrays (inline sales-count and stock-count widgets) rather than pulling from
`GlobalFields`.

## The `live-viewers` design (Google Analytics)

`WooInline` **and** `WooCommerceSalesInline` each carry a fourth Growth Alert design,
**`live-viewers`**, rendering "N people are viewing this product right now" from
**realtime Google Analytics** rather than from orders. The two are separate registrations
of the same design — one per Type (`inline` and `woocommerce_sales` respectively) — because
`WooCommerceSalesInline` overrides `init_extension()` wholesale rather than extending its
parent's arrays, and Pro's `WooCommerceSalesInline` extends the *Free* `WooInline` chain,
not Pro's `WooInline`. Everything below is implemented identically in both, with
`{$this->id}` resolving to `woo_inline` or `woocommerce_sales_inline`:

- **Registered conditionally.** `init_extension()` only adds the theme (and its
`woo_live_viewers_template` tag group) when `Modules::is_enabled('modules_google_analytics')`
— the number comes from a connected GA4 property, so without that module the design could
never render anything.
- **Writes no entries.** `nx_can_entry_woo_inline` returns `false` for it, exactly as for
the low-stock designs, and `nx_filtered_notice` injects a placeholder so the campaign
still reaches the render loop. The count is resolved in `before_add_to_cart_form()`.
- **Its own error message.** `source_error_message()` adds a second entry keyed
`{$this->id}_ga`, scoped with `Rules::includes('themes', ["{$this->id}_live-viewers"])`, so
the "connect Google Analytics" / "needs a GA4 property" notices appear only when this
design is selected — never for merchants using the sales-count or stock designs.
`WooCommerceSalesInline` inherits this method unchanged; `$this->id` does the rest.
- **Threshold.** Pro adds a `ga_min_viewers` field (default `2`, same theme scoping). The
default is deliberately 2: the copy has no singular form, so a value of 1 would render
"1 people are viewing". Both extensions register **the same field key** into the one
schema `nx_content_fields` builds, so each `ga_content_fields()` *extends* whichever
definition landed first rather than replacing it — passing the existing field back through
`Rules::includes('themes', ...)` merges the second theme id into the existing rule
(`Rule::can_add()` only merges `includes`, never `!includes`). Registration order is
therefore irrelevant.

- **It colours itself.** `Core\Inline::get_template()` only wraps params in `<span>` while
previewing, so on the frontend a Growth Alert is one flat text run with nothing to style —
which is why the other designs render uncoloured on a live page even though their
thumbnails and builder previews show green/red. `before_add_to_cart_form()` therefore
applies its own highlighting (`.nx-live-viewers-count`, `.nx-live-viewers-tail`) and emits
the matching CSS inline. Colours (`#5D9733`, `#E5554D`) are sampled from the artwork, and
the inline CSS is the single source of truth — there is deliberately no duplicate rule in
`Preview.php`.

The artwork highlights the count **and** the noun after it ("3 people"), but those sit in
two different params (`first_param` / `second_param`). So the leading word of
`second_param` is marked with `GA_HL_*` placeholders **before** `get_template()` composes
the string, and the placeholders are swapped for real spans afterwards. Doing it
pre-composition is what makes the frontend and the preview identical: in preview the
markers simply end up nested inside the preview's own param spans. The trailing phrase is
marked the other way round — `{{right_now}}` is wrapped in `GA_TAIL_*` *after* composition,
since it is a placeholder rather than merchant copy; the pre-composition marking of
`custom_fourth_param` remains for merchants who switched the select to "Custom". All of it
is gated on `is_ga_theme()`, so no other design's markup changes.

- **The trailing phrase is a tag, not custom text.** `fourth_param` is `tag_right_now`
(declared in the `woo_live_viewers_template` group and therefore scoped to these two
themes), and the word itself comes from `fallback_data()['right_now']` — the same
arrangement the low-stock designs use for "left in stock" / "order soon".

This is deliberate and worth not undoing: `custom_fourth_param` is **one text field shared
by every design in the builder**. An earlier version set `fourth_param => 'tag_custom'` and
seeded `custom_fourth_param => 'right now'`, hiding the (then one-choice) select so the
merchant typed directly into the box. But Quickbuilder's `useDefaults` writes
`savedValue || triggerValue`, and a field flipping hidden→visible also writes its existing
form value back — so switching in from `conv-theme-seven` could leave that design's
"in last {{day:7}}" text sitting in the box. A tag cannot go stale. The select now carries
two real options ("right now" + "Custom"), so merchants who want their own wording pick
Custom and get the text box; the theme still seeds `custom_fourth_param` with "right now"
for that path.

The Google API side lives in the Google directory, not here — Pro's
`Google_Analytics\RealtimeViewers::get_viewers()` is the only entry point `WooInline`
touches. See [google.md](google.md).

## Dependency & detection

- Required plugin: **WooCommerce**. Every concrete class in this directory sets
Expand Down
11 changes: 11 additions & 0 deletions docs/types/inline.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,17 @@ Found via `grep -rl "'inline'" includes/Extensions` (excluding `GlobalFields.php

All six are registered in [`ExtensionFactory.php`](../../includes/Extensions/ExtensionFactory.php) (lines ~32–36 and ~81).

**Two entry-sourcing shapes within a single extension.** Most designs read local
order/enrolment data and persist `nx_entries` rows, but `woo_inline` also carries designs
whose number is resolved at render time and never stored: the low-stock themes
(`stock-theme-one/two`) and the Google Analytics **`live-viewers`** theme. Both work the
same way — `nx_can_entry_woo_inline` returns `false` so no rows are written, and
`nx_filtered_notice` injects a placeholder purely so the campaign reaches the render loop,
where the live value is computed and substituted. `live-viewers` is registered only when
the `modules_google_analytics` module is enabled, and the sibling `woocommerce_sales_inline`
source (a different Type) registers its own copy of the same design; see
[extensions/woocommerce.md](../extensions/woocommerce.md).

**Important naming trap:** [`WooCommerceSalesInline`](../../includes/Extensions/WooCommerce/WooCommerceSalesInline.php) (`$id = 'woocommerce_sales_inline'`) *extends* `WooInline` and reuses the same `inline_location` mechanics, but overrides `$types = 'woocommerce_sales'` — it belongs to the **`woocommerce_sales` Type**, not this one. See [woocommerce_sales.md](woocommerce_sales.md) for that extension. Don't confuse the two "inline" families.

## Data flow
Expand Down
2 changes: 1 addition & 1 deletion docs/types/page_analytics.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
| **Default source** | `google` (`$default_source`) — matches `Google_Analytics::$id` |
| **Default theme** | Not seeded — `$default_theme` / `$default_res_theme` are inherited from `Types` (`''`) and never set in `PageAnalytics::init()`, and the `Google_Analytics` extension does not set them either. Since `Extension::__source_trigger()` ([includes/Extensions/Extension.php:217-224](../../includes/Extensions/Extension.php#L217-L224)) only emits a `@themes:<id>` pre-fill when `default_theme` is non-empty, this source seeds no initial theme server-side. |
| **Module gate (`$module`)** | Declares `['modules_google_analytics']` on the Type object, but — same as documented for `conversions` in [conversions.md](conversions.md) — `TypeFactory::register_types()` ([includes/Types/TypesFactory.php](../../includes/Types/TypesFactory.php)) never reads `Types::$module`. What actually gates loading is the `Google_Analytics` extension's own `$module = 'modules_google_analytics'`, checked via `Modules::get_instance()->is_enabled($obj->module)` in `ExtensionFactory::register_extensions()` ([includes/Extensions/ExtensionFactory.php](../../includes/Extensions/ExtensionFactory.php)). |
| **Compatible extensions** | Only one: [`Google_Analytics`](../../includes/Extensions/Google/Google_Analytics.php) (`$id = 'google'`, `$types = 'page_analytics'`) |
| **Compatible extensions** | Only one: [`Google_Analytics`](../../includes/Extensions/Google/Google_Analytics.php) (`$id = 'google'`, `$types = 'page_analytics'`). For realtime viewers of a single WooCommerce product see the `live-viewers` Growth Alert design on the `woo_inline` extension instead ([inline](inline.md), [extensions/woocommerce.md](../extensions/woocommerce.md)). |

## What it does

Expand Down
2 changes: 1 addition & 1 deletion docs/types/woocommerce_sales.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Registered in [`TypesFactory.php`](../../includes/Types/TypesFactory.php#L25):

- **`WooCommerceSales`** (`includes/Extensions/WooCommerce/WooCommerceSales.php`, id `woocommerce_sales`, module `modules_woocommerce`) — the main "someone just purchased X" feed, sourced from WooCommerce orders.
- **`WooCommerceSalesReviews`** (`includes/Extensions/WooCommerce/WooCommerceSalesReviews.php`, id `woocommerce_sales_reviews`, module `modules_woocommerce`) — product reviews.
- **`WooCommerceSalesInline`** (`includes/Extensions/WooCommerce/WooCommerceSalesInline.php`, id `woocommerce_sales_inline`, module `modules_woocommerce`, Pro) — an inline "growth alert" variant rendered via shortcode/embed rather than the floating popup (see the `'inline'`/`show_on === 'only_shortcode'` branch in `FrontEnd.php` below).
- **`WooCommerceSalesInline`** (`includes/Extensions/WooCommerce/WooCommerceSalesInline.php`, id `woocommerce_sales_inline`, module `modules_woocommerce`, Pro) — an inline "growth alert" variant rendered via shortcode/embed rather than the floating popup (see the `'inline'`/`show_on === 'only_shortcode'` branch in `FrontEnd.php` below). It declares its own `$themes`/`$templates` (it does not extend its parent's), so designs added to `WooInline` must be added here too — including the Google Analytics **`live-viewers`** design, gated on `modules_google_analytics`, whose count is resolved at render time from realtime GA4 data rather than from orders (`nx_can_entry` returns `false`, `nx_filtered_notice` injects a placeholder). See [extensions/woocommerce.md](../extensions/woocommerce.md#the-live-viewers-design-google-analytics).

All three extensions call `parent::__construct()` from `WooCommerce` / `WooInline` / `WooReviews` (`includes/Extensions/WooCommerce/WooCommerce.php`, `WooInline.php`, `WOOReviews.php`), which is where the actual WooCommerce order/review fetching (`get_orders()`, `ordered_product()`, `status_transition()`, etc.) lives — that's Extension-layer logic, out of scope for this Type doc.

Expand Down
5 changes: 5 additions & 0 deletions includes/Admin/Admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,9 @@ public function notices() {
}

public function plugin_usage_insights(){
if ( null !== $this->insights ) {
return $this->insights;
}
$this->insights = PluginInsights::get_instance( NOTIFICATIONX_FILE, [
'opt_in' => true,
'goodbye_form' => true,
Expand All @@ -433,6 +436,8 @@ public function plugin_usage_insights(){
popular plugins and themes. No spam, I promise.', 'notificationx' ),
));
$this->insights->init();

return $this->insights;
}

/**
Expand Down
58 changes: 46 additions & 12 deletions includes/Admin/PluginInsights.php
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,14 @@ public function force_tracking() {
$this->do_tracking( true );
}
/**
* Programmatically opt the site into tracking and (optionally) send the
* data to the insights API immediately. Used by the onboarding Setup Wizard
* when the user proceeds past the Welcome step (which is the consent point).
* Record the user's explicit opt-in and (optionally) send the data to the
* insights API immediately. Used by the onboarding Setup Wizard when the
* user proceeds past the Welcome step (which is the consent point).
*
* Collection itself no longer depends on this call — see
* {@see self::is_tracking_allowed()}; this only stores the consent state
* (which suppresses the opt-in notice and enables the feedback form) and
* triggers an immediate send.
*
* @param bool $send Send the collected data right away.
* @return bool|\WP_Error
Expand Down Expand Up @@ -241,23 +246,49 @@ public function do_tracking( $force = false ) {
return $this->send_data( $body );
}
/**
* Is tracking allowed?
* Is data collection allowed?
*
* Since 3.3.0 collection is no longer gated behind the opt-in notice or
* the Setup Wizard: it is enabled from the backend for every install, so
* the payload is collected whether the user accepts, rejects, ignores or
* never opens the consent/onboarding flow. The only remaining hard stop is
* the programmatic opt-out exposed through the `options` constructor
* argument (see {@see self::has_user_opted_out()}).
*
* The deactivation feedback form follows this same gate, so it is
* available as soon as the plugin is activated. The user's explicit
* consent state is still recorded separately and is readable through
* {@see self::has_user_consented()}, but nothing is gated on it.
*
* @since 1.0.0
*/
private function is_tracking_allowed() {
// First, check if the user has changed their mind and opted out of tracking
// A programmatic opt-out (an option flagged via the `options` arg) is
// still honoured and clears any recorded consent.
if ( $this->has_user_opted_out() ) {
$this->set_is_tracking_allowed( false, $this->plugin_name );
return false;
}
// The wpins_allow_tracking option is an array of plugins that are being tracked
$allow_tracking = get_option( 'wpins_allow_tracking' );
// If this plugin is in the array, then tracking is allowed
if ( isset( $allow_tracking[ $this->plugin_name ] ) ) {
return true;
return true;
}
/**
* Has the user explicitly consented to tracking?
*
* This is the legacy `wpins_allow_tracking` state, set when the user
* accepts the opt-in notice or proceeds past the Setup Wizard welcome
* step. It is recorded for reporting only: neither data collection nor the
* deactivation feedback form is gated on it any more.
*
* @since 3.3.0
* @return bool
*/
public function has_user_consented() {
if ( $this->has_user_opted_out() ) {
return false;
}
return false;
// The wpins_allow_tracking option is an array of plugins the user has opted in for.
$allow_tracking = get_option( 'wpins_allow_tracking' );
return is_array( $allow_tracking ) && isset( $allow_tracking[ $this->plugin_name ] );
}
/**
* Set a flag in DB If tracking is allowed.
Expand Down Expand Up @@ -763,7 +794,10 @@ public function deactivate_reasons_form_submit() {
*/
public function deactivate_action_links( $links ) {
/**
* Check is tracking allowed or not.
* The feedback form follows data collection, not the opt-in choice:
* collection is enabled from the backend on activation, so the form is
* available from that moment too. The programmatic opt-out is still a
* hard stop, because it turns collection off entirely.
*/
if ( ! $this->is_tracking_allowed() ) {
return $links;
Expand Down
Loading