word-waltz / commit
added a way to disable push notifications
src/main.ts | 53 +++++++++++++++++++++++++++++----------------- src/styles/main.css | 4 ++++ src/utils/notifications.ts | 26 ++++++++++++++++++++++- 3 files changed, 62 insertions(+), 21 deletions(-) diff --git a/src/main.ts b/src/main.ts index 4087c2b..7620672 100644 --- a/src/main.ts +++ b/src/main.ts @@ -18,7 +18,7 @@ import { mountWordleBoard } from './components/WordleBoard.js'; import { mountScoreboard } from './components/Scoreboard.js'; import { mountSoloScoreboard } from './components/SoloScoreboard.js'; import { mountShareDialog } from './components/ShareDialog.js'; -import { notifyMyTurn, clearNotification, registerServiceWorker, subscribeToPush } from './utils/notifications.js'; +import { notifyMyTurn, clearNotification, registerServiceWorker, subscribeToPush, isNotifOptedOut, setNotifOptOut, unsubscribeFromPush } from './utils/notifications.js'; import { getRandomWord } from './utils/wordList.js'; import type { GameState, PlayerId, Round, SoloGameState, SoloRound, PendingNotification } from './types.js'; @@ -809,31 +809,26 @@ function initNotifButton(): void { function updateBtn(): void { const perm = Notification.permission; - if (perm === 'granted') { + const optedOut = isNotifOptedOut(); + if (perm === 'granted' && !optedOut) { btn!.textContent = '๐'; - btn!.title = 'Notifications enabled โ click to test'; + btn!.title = 'Notifications enabled โ click to disable'; btn!.classList.add('header__notif-btn--on'); - btn!.classList.remove('header__notif-btn--blocked'); + btn!.classList.remove('header__notif-btn--off', 'header__notif-btn--blocked'); } else if (perm === 'denied') { btn!.textContent = '๐'; - btn!.title = 'Notifications blocked'; - btn!.classList.remove('header__notif-btn--on'); + btn!.title = 'Notifications blocked by browser'; + btn!.classList.remove('header__notif-btn--on', 'header__notif-btn--off'); btn!.classList.add('header__notif-btn--blocked'); + } else if (perm === 'granted' && optedOut) { + btn!.textContent = '๐'; + btn!.title = 'Notifications disabled โ click to enable'; + btn!.classList.remove('header__notif-btn--on', 'header__notif-btn--blocked'); + btn!.classList.add('header__notif-btn--off'); } else { btn!.textContent = '๐'; btn!.title = 'Enable notifications'; - btn!.classList.remove('header__notif-btn--on', 'header__notif-btn--blocked'); - } - } - - async function showTestNotification(): Promise<void> { - const reg = await registerServiceWorker(); - if (reg) { - await reg.showNotification('Word Waltz', { - body: "Notifications are working! You'll be alerted when it's your turn.", - icon: `${import.meta.env.BASE_URL}apple-touch-icon.png`, - tag: 'word-waltz-test', - }); + btn!.classList.remove('header__notif-btn--on', 'header__notif-btn--off', 'header__notif-btn--blocked'); } } @@ -848,16 +843,34 @@ function initNotifButton(): void { } if (perm === 'granted') { - await showTestNotification(); + if (isNotifOptedOut()) { + // Re-enable + setNotifOptOut(false); + updateBtn(); + await subscribeToPush(); + } else { + // Disable + setNotifOptOut(true); + updateBtn(); + await unsubscribeFromPush(); + } return; } // 'default' โ request permission await registerServiceWorker(); const result = await Notification.requestPermission(); + setNotifOptOut(false); updateBtn(); if (result === 'granted') { - await showTestNotification(); + const reg = await registerServiceWorker(); + if (reg) { + await reg.showNotification('Word Waltz', { + body: "Notifications are working! You'll be alerted when it's your turn.", + icon: `${import.meta.env.BASE_URL}apple-touch-icon.png`, + tag: 'word-waltz-test', + }); + } } }); } diff --git a/src/styles/main.css b/src/styles/main.css index 775f007..a02ecf3 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -129,6 +129,10 @@ header { opacity: 1; } +.header__notif-btn--off { + opacity: 0.6; +} + .header__notif-btn--blocked { opacity: 0.3; cursor: default; diff --git a/src/utils/notifications.ts b/src/utils/notifications.ts index 31f92e2..5e3a144 100644 --- a/src/utils/notifications.ts +++ b/src/utils/notifications.ts @@ -54,9 +54,32 @@ export async function registerServiceWorker(): Promise<ServiceWorkerRegistration // Push subscription // ------------------------------------------------------------------------- +const OPT_OUT_KEY = 'word-waltz:notif-disabled'; + +export function isNotifOptedOut(): boolean { + return localStorage.getItem(OPT_OUT_KEY) === '1'; +} + +export function setNotifOptOut(disabled: boolean): void { + if (disabled) localStorage.setItem(OPT_OUT_KEY, '1'); + else localStorage.removeItem(OPT_OUT_KEY); +} + +/** + * Unsubscribe this device from Web Push. The Cloud Function will receive a + * 410 Gone on the next delivery attempt and clear the subscription from + * Firestore automatically. + */ +export async function unsubscribeFromPush(): Promise<void> { + const reg = swRegistration ?? (await registerServiceWorker()); + if (!reg) return; + const sub = await reg.pushManager.getSubscription(); + if (sub) await sub.unsubscribe(); +} + /** * Subscribe this device to Web Push and return the serialised subscription - * JSON, or null if push is unavailable / not configured. + * JSON, or null if push is unavailable / not configured / opted out. * * Requires VITE_VAPID_PUBLIC_KEY to be defined at build time. */ @@ -74,6 +97,7 @@ export async function subscribeToPush(): Promise<string | null> { await Notification.requestPermission(); } if (Notification.permission !== 'granted') return null; + if (isNotifOptedOut()) return null; const existing = await reg.pushManager.getSubscription(); if (existing) return JSON.stringify(existing);