Skip to content

Commit b4717e5

Browse files
committed
feat: dismissible pre-release admin notice (#812)
1 parent bb6671d commit b4717e5

6 files changed

Lines changed: 578 additions & 0 deletions

File tree

assets/js/admin-notice.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* Dismiss the pre-release notice.
3+
*/
4+
( () => {
5+
'use strict';
6+
7+
document.addEventListener( 'DOMContentLoaded', () => {
8+
const notices = document.querySelectorAll( '[data-antispam-bee-pre-release-notice]' );
9+
10+
for ( const notice of notices ) {
11+
notice.addEventListener( 'click', ( event ) => {
12+
const dismiss = event.target.closest(
13+
'[data-antispam-bee-dismiss], [data-antispam-bee-dismiss-link]'
14+
);
15+
16+
if ( ! dismiss ) {
17+
return;
18+
}
19+
20+
event.preventDefault();
21+
22+
const href = dismiss.dataset.antispamBeeDismissLink;
23+
24+
wp.ajax.post( antispamBeePreReleaseNotice.action, {
25+
_ajax_nonce: antispamBeePreReleaseNotice.nonce,
26+
} )
27+
.done( () => {
28+
notice.remove();
29+
} )
30+
.fail( () => {
31+
if ( href ) {
32+
window.location.href = href;
33+
}
34+
} );
35+
} );
36+
}
37+
} );
38+
} )();

src/Admin/PreReleaseNotice.php

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
<?php
2+
/**
3+
* Pre-release notice.
4+
*
5+
* @package AntispamBee\Admin
6+
*/
7+
8+
namespace AntispamBee\Admin;
9+
10+
use AntispamBee\Helpers\StringHelper;
11+
use const AntispamBee\MAIN_PLUGIN_FILE;
12+
use const AntispamBee\PLUGIN_VERSION;
13+
14+
/**
15+
* Show a dismissible notice while the installed version is a pre-release.
16+
*/
17+
class PreReleaseNotice {
18+
19+
/**
20+
* The user meta key that stores whether the notice was dismissed.
21+
*
22+
* @var string
23+
*/
24+
public const DISMISSED_META_KEY = 'antispam_bee_pre_release_notice_dismissed';
25+
26+
/**
27+
* The nonce action used to dismiss the notice.
28+
*
29+
* @var string
30+
*/
31+
public const DISMISS_ACTION = 'antispam_bee_dismiss_pre_release_notice';
32+
33+
/**
34+
* The admin page hook suffix of the plugins list.
35+
*
36+
* @var string
37+
*/
38+
public const PLUGINS_PAGE = 'plugins.php';
39+
40+
/**
41+
* The URL of the issue tracker used as the feedback channel.
42+
*
43+
* @var string
44+
*/
45+
public const FEEDBACK_URL = 'https://github.com/pluginkollektiv/antispam-bee/issues';
46+
47+
/**
48+
* Register the notice hooks.
49+
*/
50+
public static function init(): void {
51+
add_action( 'admin_notices', [ __CLASS__, 'admin_notices' ] );
52+
add_action( 'admin_enqueue_scripts', [ __CLASS__, 'maybe_enqueue_assets' ] );
53+
}
54+
55+
/**
56+
* Register the dismissal handlers.
57+
*
58+
* The handlers must be available whenever a request hits them, so they are
59+
* registered here rather than in {@see init()}, which the bootloader skips
60+
* during AJAX requests.
61+
*/
62+
public static function always_init(): void {
63+
add_action( 'wp_ajax_' . self::DISMISS_ACTION, [ __CLASS__, 'handle_dismiss' ] );
64+
add_action( 'admin_post_' . self::DISMISS_ACTION, [ __CLASS__, 'handle_dismiss' ] );
65+
}
66+
67+
/**
68+
* Render the notice, reading the page WordPress does not pass along.
69+
*
70+
* `admin_notices` invokes callbacks without the hook suffix, so it is read
71+
* from the global that WordPress sets for admin pages.
72+
*/
73+
public static function admin_notices(): void {
74+
global $hook_suffix;
75+
76+
self::maybe_render( (string) $hook_suffix );
77+
}
78+
79+
/**
80+
* Render the notice on the settings page and the plugins list, unless the
81+
* installed version is stable or the user already dismissed it.
82+
*
83+
* @param string $hook_suffix The current admin page hook suffix.
84+
*/
85+
public static function maybe_render( string $hook_suffix = '' ): void {
86+
if ( ! self::should_show( $hook_suffix ) ) {
87+
return;
88+
}
89+
90+
$dismiss_url = wp_nonce_url(
91+
admin_url( 'admin-post.php?action=' . self::DISMISS_ACTION ),
92+
self::DISMISS_ACTION
93+
);
94+
95+
printf(
96+
'<div class="notice notice-warning" data-antispam-bee-pre-release-notice>' .
97+
'<p><strong>%1$s</strong></p>' .
98+
'<p>%2$s <code>%3$s</code></p>' .
99+
'<p>%4$s</p>' .
100+
'<p><a class="button" href="%5$s" target="_blank" rel="noopener noreferrer">%6$s</a> ' .
101+
'<a class="button-link" href="%7$s" data-antispam-bee-dismiss-link="%7$s">%8$s</a></p>' .
102+
'<button type="button" class="notice-dismiss" data-antispam-bee-dismiss aria-label="%9$s"><span class="screen-reader-text">%9$s</span></button>' .
103+
'</div>',
104+
esc_html__( 'Antispam Bee is a pre-release version', 'antispam-bee' ),
105+
esc_html__( 'You are running version', 'antispam-bee' ),
106+
esc_html( PLUGIN_VERSION ),
107+
esc_html__(
108+
'This is a pre-release and not intended for production. Please test it and report any issues you find.',
109+
'antispam-bee'
110+
),
111+
esc_url( self::FEEDBACK_URL ),
112+
esc_html__( 'Report a bug', 'antispam-bee' ),
113+
esc_url( $dismiss_url ),
114+
esc_html__( 'Dismiss', 'antispam-bee' ),
115+
esc_html__( 'Dismiss this notice', 'antispam-bee' )
116+
);
117+
}
118+
119+
/**
120+
* Enqueue the dismiss handler on the pages that show the notice.
121+
*
122+
* @param string $hook_suffix The current admin page hook suffix.
123+
*/
124+
public static function maybe_enqueue_assets( string $hook_suffix = '' ): void {
125+
if ( ! self::should_show( $hook_suffix ) ) {
126+
return;
127+
}
128+
129+
wp_enqueue_script(
130+
'antispam-bee-pre-release-notice',
131+
plugin_dir_url( MAIN_PLUGIN_FILE ) . 'assets/js/admin-notice.js',
132+
[ 'wp-util' ],
133+
PLUGIN_VERSION,
134+
true
135+
);
136+
137+
wp_localize_script(
138+
'antispam-bee-pre-release-notice',
139+
'antispamBeePreReleaseNotice',
140+
[
141+
'action' => self::DISMISS_ACTION,
142+
'nonce' => wp_create_nonce( self::DISMISS_ACTION ),
143+
]
144+
);
145+
}
146+
147+
/**
148+
* Whether the notice should render for the current user and page.
149+
*
150+
* @param string $hook_suffix The current admin page hook suffix.
151+
*
152+
* @return bool Whether to show the notice.
153+
*/
154+
private static function should_show( string $hook_suffix ): bool {
155+
if ( ! StringHelper::is_pre_release( PLUGIN_VERSION ) ) {
156+
return false;
157+
}
158+
159+
if ( ! current_user_can( 'manage_options' ) ) {
160+
return false;
161+
}
162+
163+
if ( 'settings_page_' . SettingsPage::SETTINGS_PAGE_SLUG !== $hook_suffix && self::PLUGINS_PAGE !== $hook_suffix ) {
164+
return false;
165+
}
166+
167+
return false === (bool) get_user_meta( get_current_user_id(), self::DISMISSED_META_KEY, true );
168+
}
169+
170+
/**
171+
* Persist the dismissal, then acknowledge an AJAX request or redirect.
172+
*
173+
* Both `wp_send_json_success()` and the redirect end the request, so the
174+
* non-AJAX branch is the only one that reaches the redirect and `exit`.
175+
*/
176+
public static function handle_dismiss(): void {
177+
if ( ! current_user_can( 'manage_options' ) ) {
178+
wp_die( esc_html__( 'You do not have permission to do this.', 'antispam-bee' ), 403 );
179+
}
180+
181+
$is_ajax = wp_doing_ajax();
182+
183+
if ( $is_ajax ) {
184+
check_ajax_referer( self::DISMISS_ACTION );
185+
update_user_meta( get_current_user_id(), self::DISMISSED_META_KEY, 1 );
186+
wp_send_json_success();
187+
} else {
188+
check_admin_referer( self::DISMISS_ACTION );
189+
update_user_meta( get_current_user_id(), self::DISMISSED_META_KEY, 1 );
190+
191+
wp_safe_redirect( wp_get_referer() ? wp_get_referer() : admin_url() );
192+
193+
exit;
194+
}
195+
}
196+
}

src/Helpers/StringHelper.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
/**
3+
* String helper.
4+
*
5+
* @package AntispamBee\Helpers
6+
*/
7+
8+
namespace AntispamBee\Helpers;
9+
10+
/**
11+
* Helper for string operations.
12+
*/
13+
class StringHelper {
14+
15+
/**
16+
* Whether a plugin version string marks a pre-release.
17+
*
18+
* A version is a pre-release if the measured number is followed by a
19+
* semantic versioning pre-release suffix, e.g. `3.0.0-RC.1` or
20+
* `3.0.0-beta.2`. The stable `3.0.0` has no such suffix.
21+
*
22+
* @param string $version The version string.
23+
*
24+
* @return bool Whether the version is a pre-release.
25+
*/
26+
public static function is_pre_release( string $version ): bool {
27+
return 1 === preg_match( '/^\d+(?:\.\d+){0,2}-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*$/', $version );
28+
}
29+
}

src/load.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
use AntispamBee\Admin\CommentsColumns;
1111
use AntispamBee\Admin\DashboardWidgets;
12+
use AntispamBee\Admin\PreReleaseNotice;
1213
use AntispamBee\Admin\SettingsPage;
1314
use AntispamBee\Admin\UpgradeNotice;
1415
use AntispamBee\Crons\DeleteSpamCron;
@@ -55,6 +56,7 @@ function init(): void {
5556
new SettingsPage(),
5657
CommentsColumns::class,
5758
UpgradeNotice::class,
59+
PreReleaseNotice::class,
5860
DeleteSpamCron::class,
5961
Settings::class,
6062
// Handlers.

0 commit comments

Comments
 (0)