From 20b1f7ef2112bcc0a14a75d1c793769f6cb83b5e Mon Sep 17 00:00:00 2001 From: Grace Kind Date: Sun, 16 Aug 2026 01:02:06 -0500 Subject: [PATCH] Disable on desktop --- src/js/push/courierPushService.js | 2 ++ src/js/views/settings/notifications.view.js | 2 +- .../views/settingsNotifications.view.test.js | 29 +++++++++++++++++++ tests/unit/specs/courierPushService.test.js | 27 +++++++++++++++++ 4 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/js/push/courierPushService.js b/src/js/push/courierPushService.js index c41bc80e..0becd937 100644 --- a/src/js/push/courierPushService.js +++ b/src/js/push/courierPushService.js @@ -1,5 +1,6 @@ import { resolveDid, getServiceEndpointFromDidDoc } from "/js/atproto.js"; import { Signal } from "/js/signals.js"; +import { isTouchOnlyDevice } from "/js/utils.js"; const STORAGE_KEY = "courier-push-enabled"; const CONFIG_CACHE_KEY = "courier-push-service-config"; @@ -60,6 +61,7 @@ export class CourierPushService { get isSupported() { return ( + isTouchOnlyDevice() && typeof Notification !== "undefined" && "serviceWorker" in navigator && "PushManager" in window diff --git a/src/js/views/settings/notifications.view.js b/src/js/views/settings/notifications.view.js index 4b733e00..408a7782 100644 --- a/src/js/views/settings/notifications.view.js +++ b/src/js/views/settings/notifications.view.js @@ -229,7 +229,7 @@ export default async function settingsNotificationsView({

Push notifications (beta)

${!pushSupported - ? "Your browser doesn't support push notifications." + ? "Only available on mobile devices." : hasService ? "Get notified even when the page is closed." : html`Get notified even when the page is closed. You must diff --git a/tests/e2e/specs/views/settingsNotifications.view.test.js b/tests/e2e/specs/views/settingsNotifications.view.test.js index 4ee932ca..8f9b6809 100644 --- a/tests/e2e/specs/views/settingsNotifications.view.test.js +++ b/tests/e2e/specs/views/settingsNotifications.view.test.js @@ -154,6 +154,35 @@ test.describe("Settings > Notifications view", () => { }); test.describe("push notifications", () => { + // Push is for devices where a tab isn't reliably open, so every case below + // needs a touch-only device — on desktop the setting is disabled outright. + test.use({ + viewport: { width: 375, height: 667 }, + hasTouch: true, + isMobile: true, + }); + + test("is disabled on a desktop device", async ({ browser }) => { + const context = await browser.newContext({ + viewport: { width: 1280, height: 800 }, + hasTouch: false, + isMobile: false, + }); + const page = await context.newPage(); + const mockServer = new MockServer(); + mockServer.setNotificationServiceDid(notificationService.did); + await mockServer.setup(page); + await stubNotificationPermission(page, { initial: "default" }); + await login(page); + await page.goto("/settings/notifications"); + + // Even with a service chosen, desktop gets in-tab notifications instead. + await expect( + page.locator('[data-testid="push-notifications-toggle"]'), + ).toHaveAttribute("disabled", "", { timeout: 10000 }); + await context.close(); + }); + test("with no service named, push cannot be turned on", async ({ page, }) => { diff --git a/tests/unit/specs/courierPushService.test.js b/tests/unit/specs/courierPushService.test.js index 7fa9a924..b3147ab2 100644 --- a/tests/unit/specs/courierPushService.test.js +++ b/tests/unit/specs/courierPushService.test.js @@ -25,6 +25,14 @@ function setupDom({ enabled = true, granted = true } = {}) { globalThis.Notification = { permission: granted ? "granted" : "denied" }; // isSupported checks for these on window/navigator; jsdom has neither. globalThis.window.PushManager = function PushManager() {}; + // Push is touch-only-device territory; the env's default matchMedia stub + // reports every query as non-matching, which would read as a desktop. + globalThis.window.matchMedia = (query) => ({ + matches: query === "(hover: none) and (pointer: coarse)", + media: query, + addEventListener: () => {}, + removeEventListener: () => {}, + }); globalThis.document = { _listeners: {}, addEventListener(type, fn) { @@ -96,6 +104,7 @@ describe("CourierPushService heartbeat", () => { document: globalThis.document, navigator: Object.getOwnPropertyDescriptor(globalThis, "navigator"), pushManager: globalThis.window.PushManager, + matchMedia: globalThis.window.matchMedia, }; }); @@ -103,6 +112,7 @@ describe("CourierPushService heartbeat", () => { globalThis.localStorage = originals.localStorage; globalThis.Notification = originals.notification; globalThis.document = originals.document; + globalThis.window.matchMedia = originals.matchMedia; Object.defineProperty(globalThis, "navigator", originals.navigator); if (originals.pushManager === undefined) { delete globalThis.window.PushManager; @@ -179,6 +189,21 @@ describe("CourierPushService heartbeat", () => { await assert.doesNotReject(() => service._heartbeat()); }); + it("is unsupported on a device that isn't touch-only", () => { + setupDom(); + // Desktop gets SystemNotificationService's in-tab notifications instead, + // which is gated on the same check the other way round. + globalThis.window.matchMedia = (query) => ({ + matches: false, + media: query, + addEventListener: () => {}, + removeEventListener: () => {}, + }); + const { service } = createService(); + assert.equal(service.isSupported, false); + assert.equal(service.isEnabled, false); + }); + it("registers against the selected service, not the default", async () => { setupDom(); const { service, registerPush } = createService( @@ -205,6 +230,7 @@ describe("CourierPushService service selection", () => { document: globalThis.document, navigator: Object.getOwnPropertyDescriptor(globalThis, "navigator"), pushManager: globalThis.window.PushManager, + matchMedia: globalThis.window.matchMedia, fetch: globalThis.fetch, }; setupDom({ enabled: false }); @@ -230,6 +256,7 @@ describe("CourierPushService service selection", () => { globalThis.localStorage = originals.localStorage; globalThis.Notification = originals.notification; globalThis.document = originals.document; + globalThis.window.matchMedia = originals.matchMedia; Object.defineProperty(globalThis, "navigator", originals.navigator); globalThis.fetch = originals.fetch; if (originals.pushManager === undefined) { -- 2.51.2