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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## 2.1.0 - 2026-09-15

- Changed (V2): an expired license no longer shows a sitewide admin notice. `V2\Admin\Notices`
now nags only when no license key is saved. An expired plugin keeps working and only stops
receiving updates, so a notice on every admin screen that returned weekly after dismissal
was more nag than help. Expiry still shows where the product lives: the license page (status
and `renewal_url` link) and the plugins-list update row (`V2\Admin\UpdateMessage`).
`get_license_status()` and `get_license_renewal_url()` are unchanged
- Sites that snoozed the old expired notice keep a harmless `{storage}_notice_snooze_expired`
option; nothing reads it any more and no migration removes it

## 2.0.1 - 2026-08-19

- The license admin notice now steps aside on the plugins list screen while an update for the
Expand Down
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,9 @@ Subsystems are public properties:
- **`$admin`** (`Admin/LicensePage.php`) — License page; only when `license` is on and `menu`
is not `false`.
- **`$notices`** (`Admin/Notices.php`) — Whenever `license` is on: dismissible "enter your
license key" / "license expired — renew" admin notices for users with `update_plugins`,
one-week nonce-protected snooze per product per notice type. Steps aside on the plugins
license key" admin notice for users with `update_plugins`, one-week nonce-protected snooze
per product per notice type. No sitewide notice for an expired license (removed in 2.1.0,
decided 2026-09-15): expiry shows only on the license page and in the update row. Steps aside on the plugins
list while an update row for the product already carries the message.
- **`$update_message`** (`Admin/UpdateMessage.php`) — Whenever `license` is on: an
`in_plugin_update_message-{file}` line explaining why the update package is missing on
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ if ( class_exists( \Shazzad\PluginUpdater\Integration::class ) ) {
├── License/Store.php # Option/transient keys, uid-keyed storage, legacy-key migration
└── Admin/
├── LicensePage.php # License admin page
├── Notices.php # Dismissible "enter license" / "license expired" notices
├── Notices.php # Dismissible "enter your license key" notice
└── UpdateMessage.php # Explanation line in the plugins-list update row
```

Expand Down Expand Up @@ -357,7 +357,7 @@ When licensing is enabled, the updater adds an admin page with:
- Direct upgrade buttons
- Changelog and upgrade notices

V2 additionally shows, to users with the `update_plugins` capability, a dismissible admin notice when no license key is saved or the license has expired (linking `renewal_url`), snoozable for one week per product and notice type, plus an explanation line inside the plugin's update row on the Plugins screen when the update package is withheld.
V2 additionally shows, to users with the `update_plugins` capability, a dismissible admin notice when no license key is saved, snoozable for one week per product. An expired license gets no sitewide notice (since 2.1.0); its status and `renewal_url` link show on the license page and in the update row. V2 also shows an explanation line inside the plugin's update row on the Plugins screen when the update package is withheld.

### Menu Placement

Expand Down
67 changes: 26 additions & 41 deletions src/V2/Admin/Notices.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@
/**
* Class Notices
*
* Renders sitewide admin notices when the site cannot receive updates:
* no license key saved, or the saved license has expired. Each notice is
* dismissible for one week, per product and per notice type, stored
* site-wide.
* Renders a sitewide admin notice when no license key is saved, the one
* state a customer can miss entirely. The notice is dismissible for one
* week, per product and per notice type, stored site-wide.
*
* An expired license deliberately gets no sitewide notice: the plugin
* keeps working, and its state already shows on the license page and in
* the plugins-list update row (UpdateMessage).
*
* @since 2.0.0
* @since 2.1.0 The expired-license notice was removed.
*/
class Notices {

Expand All @@ -41,10 +45,11 @@ class Notices {
* Notice types this class knows how to render.
*
* @since 2.0.0
* @since 2.1.0 `expired` removed.
*
* @var string[]
*/
const TYPES = [ 'unlicensed', 'expired' ];
const TYPES = [ 'unlicensed' ];

/**
* Integration instance holding shared state.
Expand Down Expand Up @@ -72,12 +77,13 @@ public function __construct( Integration $integration ) {
/**
* Resolves which notice, if any, applies right now.
*
* The two states are mutually exclusive: a site without a license key
* has no stored status worth reporting, so `unlicensed` wins.
* Only a missing license key produces a notice. An expired license
* returns '' like an active one.
*
* @since 2.0.0
* @since 2.1.0 No longer returns `expired`.
*
* @return string 'unlicensed', 'expired', or '' when no notice applies.
* @return string 'unlicensed', or '' when no notice applies.
*/
public function get_notice_type() {
if ( ! $this->integration->license_enabled ) {
Expand All @@ -88,10 +94,6 @@ public function get_notice_type() {
return 'unlicensed';
}

if ( 'expired' === $this->integration->get_license_status() ) {
return 'expired';
}

return '';
}

Expand Down Expand Up @@ -157,36 +159,19 @@ public function render() {
? $this->integration->product_name
: $this->integration->product_slug;

if ( 'unlicensed' === $type ) {
$license_url = $this->get_license_page_url();

if ( $license_url ) {
$message = \sprintf(
'<strong>%s</strong>: <a href="%s">enter your license key</a> to enable plugin updates.',
esc_html( $name ),
esc_url( $license_url )
);
} else {
$message = \sprintf(
'<strong>%s</strong>: enter your license key to enable plugin updates.',
esc_html( $name )
);
}
$license_url = $this->get_license_page_url();

if ( $license_url ) {
$message = \sprintf(
'<strong>%s</strong>: <a href="%s">enter your license key</a> to enable plugin updates.',
esc_html( $name ),
esc_url( $license_url )
);
} else {
$renewal_url = $this->integration->get_license_renewal_url();

if ( $renewal_url ) {
$message = \sprintf(
'<strong>%s</strong>: your license has expired. <a href="%s">Renew your license</a> to keep receiving updates.',
esc_html( $name ),
esc_url( $renewal_url )
);
} else {
$message = \sprintf(
'<strong>%s</strong>: your license has expired. Renew your license to keep receiving updates.',
esc_html( $name )
);
}
$message = \sprintf(
'<strong>%s</strong>: enter your license key to enable plugin updates.',
esc_html( $name )
);
}

\printf(
Expand Down
35 changes: 28 additions & 7 deletions tests/V2/NoticesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
use Shazzad\PluginUpdater\V2\Admin\Notices;

/**
* V2-only coverage: sitewide license notices with the one-week snooze.
* V2-only coverage: the sitewide "enter your license key" notice with its
* one-week snooze. An expired license never gets a sitewide notice.
*/
class NoticesTest extends TestCase {

Expand Down Expand Up @@ -61,8 +62,8 @@ public function renders_set_license_notice_when_unlicensed() {
}

/** @test */
public function renders_renew_notice_with_renewal_link_when_expired() {
$integration = $this->create_integration( [ 'license' => true ] );
public function renders_nothing_when_license_is_expired() {
$integration = $this->create_integration( [ 'license' => true, 'menu' => [] ] );
$this->stub_render_environment( [
'my-plugin42_code' => 'ABC-123',
'my-plugin42_data' => [
Expand All @@ -71,11 +72,18 @@ public function renders_renew_notice_with_renewal_link_when_expired() {
],
] );

$output = $this->render_output( $integration->notices );
$this->assertSame( '', $this->render_output( $integration->notices ) );
}

/** @test */
public function expired_license_has_no_notice_type() {
$integration = $this->create_integration( [ 'license' => true ] );
$this->stub_render_environment( [
'my-plugin42_code' => 'ABC-123',
'my-plugin42_data' => [ 'status' => 'expired' ],
] );

$this->assertStringContainsString( 'your license has expired', $output );
$this->assertStringContainsString( 'https://portal.example.test/renew', $output );
$this->assertStringContainsString( 'Renew your license', $output );
$this->assertSame( '', $integration->notices->get_notice_type() );
}

/** @test */
Expand Down Expand Up @@ -242,6 +250,19 @@ public function snooze_handler_ignores_other_products_dismissals() {
$this->assertNull( $notices->redirected );
}

/** @test */
public function snooze_handler_ignores_the_removed_expired_type() {
$integration = $this->create_integration( [ 'license' => true ] );
$notices = $this->create_testable_notices( $integration );

$this->stub_snooze_request( 'my-plugin42', 'expired' );
Functions\expect( 'update_option' )->never();

$notices->handle_snooze();

$this->assertNull( $notices->redirected );
}

/** @test */
public function snooze_handler_ignores_unknown_notice_types() {
$integration = $this->create_integration( [ 'license' => true ] );
Expand Down
Loading