Skip to content

Commit e151c81

Browse files
committed
Add user option to globally disable web push prompts
1 parent 4d98463 commit e151c81

File tree

11 files changed

+297
-1
lines changed

11 files changed

+297
-1
lines changed

config/wpn_ucp.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,7 @@ phpbb_webpushnotifications_ucp_push_subscribe_controller:
1313
phpbb_webpushnotifications_ucp_push_unsubscribe_controller:
1414
path: /push/unsubscribe
1515
defaults: { _controller: phpbb.wpn.ucp.controller.webpush:unsubscribe }
16+
17+
phpbb_webpushnotifications_ucp_push_toggle_popup_controller:
18+
path: /push/toggle-popup
19+
defaults: { _controller: phpbb.wpn.ucp.controller.webpush:toggle_popup }

language/en/webpushnotifications_module_ucp.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,6 @@
5353
'NOTIFY_WEBPUSH_POPUP_MESSAGE' => 'We would like to send you browser notifications for replies, private messages, and relevant forum activity. Optional — you can manage these settings at any time.',
5454
'NOTIFY_WEBPUSH_POPUP_ALLOW' => 'Allow',
5555
'NOTIFY_WEBPUSH_POPUP_DENY' => 'Deny',
56+
'NOTIFY_WEBPUSH_POPUP_DISABLE' => 'Disable web push notification prompts',
57+
'NOTIFY_WEBPUSH_POPUP_DISABLE_EXPLAIN' => 'Turn this on to stop us from asking you to enable web push notifications on any of your devices. If you disable web push notification prompts, we won’t be able to alert you if you ever become unsubscribed.',
5658
]);

language/ru/webpushnotifications_module_ucp.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,6 @@
5353
'NOTIFY_WEBPUSH_POPUP_MESSAGE' => 'Браузерные уведомления позволяют быстро получать информацию о новых ответах, личных сообщениях и других активностях на данной конференции. Функцию можно отключить или включить в любое время в настройках уведомлений в Личном разделе.',
5454
'NOTIFY_WEBPUSH_POPUP_ALLOW' => 'Включить',
5555
'NOTIFY_WEBPUSH_POPUP_DENY' => 'Отклонить',
56+
'NOTIFY_WEBPUSH_POPUP_DISABLE' => 'Disable web push notification prompts',
57+
'NOTIFY_WEBPUSH_POPUP_DISABLE_EXPLAIN' => 'Turn this on to stop us from asking you to enable web push notifications on any of your devices. If you disable web push notification prompts, we won’t be able to alert you if you ever become unsubscribed.',
5658
]);
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
/**
3+
*
4+
* phpBB Browser Push Notifications. An extension for the phpBB Forum Software package.
5+
*
6+
* @copyright (c) 2025, phpBB Limited <https://www.phpbb.com>
7+
* @license GNU General Public License, version 2 (GPL-2.0)
8+
*
9+
*/
10+
11+
namespace phpbb\webpushnotifications\migrations;
12+
13+
use phpbb\db\migration\migration;
14+
15+
class add_user_popup_preference extends migration
16+
{
17+
public function effectively_installed()
18+
{
19+
return $this->db_tools->sql_column_exists($this->table_prefix . 'users', 'user_wpn_popup_disabled');
20+
}
21+
22+
public static function depends_on()
23+
{
24+
return ['\phpbb\webpushnotifications\migrations\add_popup_prompt'];
25+
}
26+
27+
public function update_schema()
28+
{
29+
return [
30+
'add_columns' => [
31+
$this->table_prefix . 'users' => [
32+
'user_wpn_popup_disabled' => ['BOOL', 0],
33+
],
34+
],
35+
];
36+
}
37+
38+
public function revert_schema()
39+
{
40+
return [
41+
'drop_columns' => [
42+
$this->table_prefix . 'users' => [
43+
'user_wpn_popup_disabled',
44+
],
45+
],
46+
];
47+
}
48+
}

notification/method/webpush.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,11 +390,13 @@ public function get_ucp_template_data(helper $controller_helper, form_helper $fo
390390
'NOTIFICATIONS_WEBPUSH_ENABLE' => ($this->config['load_notifications'] && $this->config['allow_board_notifications'] && $this->config['wpn_webpush_dropdown_subscribe']) || stripos($this->user->page['page'], 'notification_options'),
391391
'U_WEBPUSH_SUBSCRIBE' => $controller_helper->route('phpbb_webpushnotifications_ucp_push_subscribe_controller'),
392392
'U_WEBPUSH_UNSUBSCRIBE' => $controller_helper->route('phpbb_webpushnotifications_ucp_push_unsubscribe_controller'),
393+
'U_WEBPUSH_TOGGLE_POPUP' => $controller_helper->route('phpbb_webpushnotifications_ucp_push_toggle_popup_controller'),
393394
'VAPID_PUBLIC_KEY' => $this->config['wpn_webpush_vapid_public'],
394395
'U_WEBPUSH_WORKER_URL' => $controller_helper->route('phpbb_webpushnotifications_ucp_push_worker_controller'),
395396
'SUBSCRIPTIONS' => $subscriptions,
396397
'WEBPUSH_FORM_TOKENS' => $form_helper->get_form_tokens(\phpbb\webpushnotifications\ucp\controller\webpush::FORM_TOKEN_UCP),
397-
'S_WEBPUSH_POPUP_PROMPT' => $this->config['wpn_webpush_popup_prompt'] && $this->user->id() != ANONYMOUS && $this->user->data['user_type'] != USER_IGNORE,
398+
'S_WEBPUSH_POPUP_PROMPT' => $this->config['wpn_webpush_popup_prompt'] && $this->user->id() != ANONYMOUS && $this->user->data['user_type'] != USER_IGNORE && !($this->user->data['user_wpn_popup_disabled'] ?? 0),
399+
'S_WEBPUSH_POPUP_DISABLED' => $this->user->data['user_wpn_popup_disabled'] ?? 0,
398400
];
399401
}
400402

styles/all/template/ucp_notifications_webpush.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
serviceWorkerUrl: '{{ U_WEBPUSH_WORKER_URL }}',
44
subscribeUrl: '{{ U_WEBPUSH_SUBSCRIBE }}',
55
unsubscribeUrl: '{{ U_WEBPUSH_UNSUBSCRIBE }}',
6+
togglePopupUrl: '{{ U_WEBPUSH_TOGGLE_POPUP }}',
67
ajaxErrorTitle: '{{ lang_js('AJAX_ERROR_TITLE') }}',
78
vapidPublicKey: '{{ VAPID_PUBLIC_KEY }}',
89
formTokens: {

styles/all/template/webpush.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ function PhpbbWebpush() {
3333
/** @type {HTMLElement} Unsubscribe button */
3434
let unsubscribeButton;
3535

36+
/** @type {HTMLElement} Toggle popup button */
37+
let togglePopupButton;
38+
39+
/** @type {string} URL to toggle popup prompt preference */
40+
let togglePopupUrl = '';
41+
3642
/** @type {function} Escape key handler for popup */
3743
let popupEscapeHandler;
3844

@@ -44,13 +50,20 @@ function PhpbbWebpush() {
4450
serviceWorkerUrl = options.serviceWorkerUrl;
4551
subscribeUrl = options.subscribeUrl;
4652
unsubscribeUrl = options.unsubscribeUrl;
53+
togglePopupUrl = options.togglePopupUrl;
4754
this.formTokens = options.formTokens;
4855
subscriptions = options.subscriptions;
4956
ajaxErrorTitle = options.ajaxErrorTitle;
5057
vapidPublicKey = options.vapidPublicKey;
5158

5259
subscribeButton = document.querySelector('#subscribe_webpush');
5360
unsubscribeButton = document.querySelector('#unsubscribe_webpush');
61+
togglePopupButton = document.querySelector('#toggle_popup_prompt');
62+
63+
// Set up toggle popup button handler if it exists (on UCP settings page)
64+
if (togglePopupButton) {
65+
togglePopupButton.addEventListener('click', togglePopupHandler);
66+
}
5467

5568
// Service workers are only supported in secure context
5669
if (window.isSecureContext !== true) {
@@ -352,6 +365,55 @@ function PhpbbWebpush() {
352365
});
353366
}
354367

368+
/**
369+
* Handler for toggle popup prompt button
370+
*
371+
* @param {Object} event Toggle button push event
372+
*/
373+
function togglePopupHandler(event) {
374+
event.preventDefault();
375+
376+
const loadingIndicator = phpbb.loadingIndicator();
377+
const formData = new FormData();
378+
formData.append('form_token', phpbb.webpush.formTokens.formToken);
379+
formData.append('creation_time', phpbb.webpush.formTokens.creationTime.toString());
380+
381+
fetch(togglePopupUrl, {
382+
method: 'POST',
383+
headers: {
384+
'X-Requested-With': 'XMLHttpRequest',
385+
},
386+
body: formData,
387+
})
388+
.then(response => response.json())
389+
.then(data => {
390+
loadingIndicator.fadeOut(phpbb.alertTime);
391+
if (data.success) {
392+
// Update toggle icon based on new state
393+
const button = document.getElementById('toggle_popup_prompt');
394+
if (button) {
395+
const icon = button.querySelector('i');
396+
if (icon) {
397+
if (data.disabled) {
398+
icon.classList.remove('fa-toggle-off');
399+
icon.classList.add('fa-toggle-on');
400+
} else {
401+
icon.classList.remove('fa-toggle-on');
402+
icon.classList.add('fa-toggle-off');
403+
}
404+
}
405+
}
406+
if ('form_tokens' in data) {
407+
updateFormTokens(data.form_tokens);
408+
}
409+
}
410+
})
411+
.catch(error => {
412+
loadingIndicator.fadeOut(phpbb.alertTime);
413+
phpbb.alert(ajaxErrorTitle, error);
414+
});
415+
}
416+
355417
/**
356418
* Handle subscribe response
357419
*

styles/prosilver/template/event/ucp_notifications_content_before.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@
1010
<br><span>{{ lang('NOTIFY_WEBPUSH_ENABLE_EXPLAIN') }}</span>
1111
</dd>
1212
</dl>
13+
<dl>
14+
<dt><label for="toggle_popup_prompt">{{ lang('NOTIFY_WEBPUSH_POPUP_DISABLE') ~ lang('COLON') }}</label></dt>
15+
<dd>
16+
<button id="toggle_popup_prompt" type="button" name="toggle_popup_prompt" class="wpn-toggle-button" aria-label="{{ lang('NOTIFY_WEBPUSH_POPUP_DISABLE') }}">
17+
<i class="icon icon-lg fa-fw fa-toggle-{% if S_WEBPUSH_POPUP_DISABLED %}on{% else %}off{% endif %}" aria-hidden="true"></i>
18+
</button>
19+
<br><span>{{ lang('NOTIFY_WEBPUSH_POPUP_DISABLE_EXPLAIN') }}</span>
20+
</dd>
21+
</dl>
1322
</fieldset>
1423
</div>
1524
</div>

tests/controller/controller_webpush_test.php

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,85 @@ public function test_sub_unsubscribe_success()
420420
$this->assertEmpty($this->get_subscription_data());
421421
}
422422

423+
public function test_toggle_popup_enable_to_disable()
424+
{
425+
$this->form_helper->method('check_form_tokens')->willReturn(true);
426+
$this->request->method('is_ajax')->willReturn(true);
427+
$this->user->data['user_id'] = 2;
428+
$this->user->data['is_bot'] = false;
429+
$this->user->data['user_type'] = USER_NORMAL;
430+
$this->user->data['user_wpn_popup_disabled'] = 0;
431+
432+
$response = $this->controller->toggle_popup();
433+
434+
$this->assertInstanceOf(JsonResponse::class, $response);
435+
$response_data = json_decode($response->getContent(), true);
436+
437+
$this->assertTrue($response_data['success']);
438+
$this->assertTrue($response_data['disabled']);
439+
$this->assertEquals(1, $this->get_user_popup_preference(2));
440+
}
441+
442+
public function test_toggle_popup_disable_to_enable()
443+
{
444+
$this->form_helper->method('check_form_tokens')->willReturn(true);
445+
$this->request->method('is_ajax')->willReturn(true);
446+
$this->user->data['user_id'] = 2;
447+
$this->user->data['is_bot'] = false;
448+
$this->user->data['user_type'] = USER_NORMAL;
449+
$this->user->data['user_wpn_popup_disabled'] = 1;
450+
451+
// Set initial state
452+
$sql = 'UPDATE phpbb_users
453+
SET user_wpn_popup_disabled = 1
454+
WHERE user_id = 2';
455+
$this->db->sql_query($sql);
456+
457+
$response = $this->controller->toggle_popup();
458+
459+
$this->assertInstanceOf(JsonResponse::class, $response);
460+
$response_data = json_decode($response->getContent(), true);
461+
462+
$this->assertTrue($response_data['success']);
463+
$this->assertFalse($response_data['disabled']);
464+
$this->assertEquals(0, $this->get_user_popup_preference(2));
465+
}
466+
467+
public function test_toggle_popup_invalid_form_token()
468+
{
469+
$this->form_helper->method('check_form_tokens')->willReturn(false);
470+
471+
$this->expectException(http_exception::class);
472+
$this->expectExceptionMessage('FORM_INVALID');
473+
474+
$this->controller->toggle_popup();
475+
}
476+
477+
public function test_toggle_popup_not_ajax()
478+
{
479+
$this->form_helper->method('check_form_tokens')->willReturn(true);
480+
$this->request->method('is_ajax')->willReturn(false);
481+
482+
$this->expectException(http_exception::class);
483+
$this->expectExceptionMessage('NO_AUTH_OPERATION');
484+
485+
$this->controller->toggle_popup();
486+
}
487+
488+
public function test_toggle_popup_anonymous_user()
489+
{
490+
$this->form_helper->method('check_form_tokens')->willReturn(true);
491+
$this->request->method('is_ajax')->willReturn(true);
492+
$this->user->data['user_id'] = ANONYMOUS;
493+
$this->user->data['is_bot'] = false;
494+
$this->user->data['user_type'] = USER_NORMAL;
495+
496+
$this->expectException(http_exception::class);
497+
$this->expectExceptionMessage('NO_AUTH_OPERATION');
498+
499+
$this->controller->toggle_popup();
500+
}
501+
423502
private function get_subscription_data()
424503
{
425504
$sql = 'SELECT *
@@ -430,4 +509,15 @@ private function get_subscription_data()
430509
$this->db->sql_freeresult($result);
431510
return $row;
432511
}
512+
513+
private function get_user_popup_preference($user_id)
514+
{
515+
$sql = 'SELECT user_wpn_popup_disabled
516+
FROM phpbb_users
517+
WHERE user_id = ' . (int) $user_id;
518+
$result = $this->db->sql_query($sql);
519+
$value = (int) $this->db->sql_fetchfield('user_wpn_popup_disabled');
520+
$this->db->sql_freeresult($result);
521+
return $value;
522+
}
433523
}

tests/functional/functional_test.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,58 @@ public function test_popup_prompt()
162162
$this->assertContainsLang('NOTIFY_WEBPUSH_POPUP_DENY', $crawler->filter('#wpn_popup_deny')->text());
163163
}
164164

165+
public function test_popup_preference_toggle()
166+
{
167+
$this->login();
168+
$this->admin_login();
169+
170+
$this->add_lang_ext('phpbb/webpushnotifications', 'webpushnotifications_module_ucp');
171+
172+
$this->set_acp_option('wpn_webpush_popup_prompt', 1);
173+
174+
// Go to UCP notification settings
175+
$crawler = self::request('GET', 'ucp.php?i=ucp_notifications&mode=notification_options');
176+
177+
// Assert toggle button is present
178+
$this->assertCount(1, $crawler->filter('#toggle_popup_prompt'));
179+
$this->assertContainsLang('NOTIFY_WEBPUSH_POPUP_DISABLE', $crawler->filter('label[for="toggle_popup_prompt"]')->text());
180+
181+
// Assert toggle is initially off (prompts enabled)
182+
$toggle_icon = $crawler->filter('#toggle_popup_prompt i');
183+
$this->assertCount(1, $toggle_icon);
184+
$this->assertTrue($toggle_icon->attr('class') !== null && strpos($toggle_icon->attr('class'), 'fa-toggle-off') !== false);
185+
186+
// After user disables popup (in reality this would be via AJAX, but we test the state)
187+
// Set user preference to disabled via DB
188+
$db = $this->get_db();
189+
$sql = 'UPDATE phpbb_users
190+
SET user_wpn_popup_disabled = 1
191+
WHERE user_id = 2';
192+
$db->sql_query($sql);
193+
194+
// Reload page
195+
$crawler = self::request('GET', 'ucp.php?i=ucp_notifications&mode=notification_options');
196+
197+
// Assert toggle is now on (prompts disabled)
198+
$toggle_icon = $crawler->filter('#toggle_popup_prompt i');
199+
$this->assertCount(1, $toggle_icon);
200+
$this->assertTrue($toggle_icon->attr('class') !== null && strpos($toggle_icon->attr('class'), 'fa-toggle-on') !== false);
201+
202+
// Assert popup is not shown on index when user has disabled it
203+
$crawler = self::request('GET', 'index.php');
204+
$this->assertCount(0, $crawler->filter('#wpn_popup_prompt'));
205+
206+
// Re-enable popup preference
207+
$sql = 'UPDATE phpbb_users
208+
SET user_wpn_popup_disabled = 0
209+
WHERE user_id = 2';
210+
$db->sql_query($sql);
211+
212+
// Assert popup is shown again
213+
$crawler = self::request('GET', 'index.php');
214+
$this->assertCount(1, $crawler->filter('#wpn_popup_prompt'));
215+
}
216+
165217
protected function set_acp_option($option, $value)
166218
{
167219
$crawler = self::request('GET', 'adm/index.php?i=-phpbb-webpushnotifications-acp-wpn_acp_module&mode=webpush&sid=' . $this->sid);

0 commit comments

Comments
 (0)