diff --git a/plugins/notification-resources/package.json b/plugins/notification-resources/package.json index 6e1555e0e42..dc6d91b78cb 100644 --- a/plugins/notification-resources/package.json +++ b/plugins/notification-resources/package.json @@ -13,7 +13,9 @@ "build:watch": "compile ui", "_phase:build": "compile ui", "_phase:format": "format src", - "_phase:validate": "compile validate" + "_phase:validate": "compile validate", + "test": "jest --passWithNoTests --silent", + "_phase:test": "jest --passWithNoTests --silent" }, "devDependencies": { "@hcengineering/platform-rig": "workspace:^0.7.21", diff --git a/plugins/notification-resources/src/desktop.test.ts b/plugins/notification-resources/src/desktop.test.ts new file mode 100644 index 00000000000..4845444607d --- /dev/null +++ b/plugins/notification-resources/src/desktop.test.ts @@ -0,0 +1,70 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { isDesktopClient } from './desktop' + +describe('isDesktopClient', () => { + const globalAny = globalThis as any + const originalWindow = globalAny.window + + afterEach(() => { + if (originalWindow === undefined) { + delete globalAny.window + } else { + globalAny.window = originalWindow + } + }) + + it('returns false in a regular https browser context', () => { + globalAny.window = { + location: { protocol: 'https:' } + } + expect(isDesktopClient()).toBe(false) + }) + + it('returns false when window is undefined (SSR / node)', () => { + delete globalAny.window + expect(isDesktopClient()).toBe(false) + }) + + it('returns true when the Electron IPC bridge is exposed on window', () => { + // The desktop preload script exposes the IPC bridge as `window.electron` + // (see desktop/src/ui/typesUtils.ts). + globalAny.window = { + electron: { sendNotification: jest.fn() }, + // protocol is still https here — bridge alone must be enough + location: { protocol: 'https:' } + } + expect(isDesktopClient()).toBe(true) + }) + + it('returns true when the page is loaded from file:// (matches the failing scope in the bug report)', () => { + // The reported error showed scope `file:///workbench/n3-…` and script + // `file:///serviceWorker.js`. The file: protocol alone should disable + // push registration even if the bridge isn't visible yet. + globalAny.window = { + location: { protocol: 'file:' } + } + expect(isDesktopClient()).toBe(true) + }) + + it('treats a null electron property as not-a-bridge', () => { + globalAny.window = { + electron: null, + location: { protocol: 'https:' } + } + expect(isDesktopClient()).toBe(false) + }) +}) diff --git a/plugins/notification-resources/src/desktop.ts b/plugins/notification-resources/src/desktop.ts new file mode 100644 index 00000000000..86d312240ff --- /dev/null +++ b/plugins/notification-resources/src/desktop.ts @@ -0,0 +1,23 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +/** + * Detects whether the renderer is running inside the desktop (Electron) client. + */ +export function isDesktopClient (): boolean { + if (typeof window === 'undefined') return false + if ((window as any).electron != null) return true + return window.location?.protocol === 'file:' +} diff --git a/plugins/notification-resources/src/utils.ts b/plugins/notification-resources/src/utils.ts index e0a1dd69910..fd777a88b2c 100644 --- a/plugins/notification-resources/src/utils.ts +++ b/plugins/notification-resources/src/utils.ts @@ -66,6 +66,7 @@ import { getObjectLinkId, parseLinkId } from '@hcengineering/view-resources' import type { LocationData } from '@hcengineering/workbench' import { get, writable } from 'svelte/store' +import { isDesktopClient } from './desktop' import { InboxNotificationsClientImpl } from './inboxNotificationsClient' import { type InboxData, type InboxNotificationsFilter } from './types' @@ -689,6 +690,10 @@ export const pushAllowed = writable(false) export async function checkPermission (value: boolean): Promise { if (!value) return true + if (isDesktopClient()) { + pushAllowed.set(false) + return false + } if ('serviceWorker' in navigator && 'PushManager' in window) { try { const loc = getCurrentLocation() @@ -725,6 +730,7 @@ function addWorkerListener (): void { } export function pushAvailable (): boolean { + if (isDesktopClient()) return false const publicKey = getMetadata(notification.metadata.PushPublicKey) return ( 'serviceWorker' in navigator && @@ -736,6 +742,10 @@ export function pushAvailable (): boolean { } export async function subscribePush (): Promise { + if (isDesktopClient()) { + pushAllowed.set(false) + return false + } const client = getClient() const publicKey = getMetadata(notification.metadata.PushPublicKey) if ('serviceWorker' in navigator && 'PushManager' in window && publicKey !== undefined) {