diff --git a/.changeset/fix-notification-preview-and-transport.md b/.changeset/fix-notification-preview-and-transport.md new file mode 100644 index 0000000000..368793b102 --- /dev/null +++ b/.changeset/fix-notification-preview-and-transport.md @@ -0,0 +1,5 @@ +--- +default: patch +--- + +Fix encrypted message content appearing in notifications when previews are disabled, and keep push working when switching notification transports fails. diff --git a/.changeset/fix-push-transport-switch.md b/.changeset/fix-push-transport-switch.md new file mode 100644 index 0000000000..9d2bd3668a --- /dev/null +++ b/.changeset/fix-push-transport-switch.md @@ -0,0 +1,5 @@ +--- +default: patch +--- + +Switching to native push notifications now correctly uses the platform's native delivery instead of keeping the old push app. diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index f561f39928..dfa82d03ab 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -6703,7 +6703,7 @@ dependencies = [ [[package]] name = "tauri-plugin-notifications" version = "0.5.0-rc.11" -source = "git+https://github.com/SableClient/tauri-plugin-notifications.git?rev=cc3a468263db75e40a8a4751f3dae9bce5bc4234#cc3a468263db75e40a8a4751f3dae9bce5bc4234" +source = "git+https://github.com/SableClient/tauri-plugin-notifications.git?rev=87e452fab17711a82422a97ab9541ebf647fa28a#87e452fab17711a82422a97ab9541ebf647fa28a" dependencies = [ "log", "notify-rust", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index cba85917ce..24c0876727 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -84,12 +84,12 @@ windows = { version = "0.61", features = [ tauri-plugin-single-instance = { version = "2.4.3", features = ["deep-link"] } [target.'cfg(any(windows, target_os = "linux"))'.dependencies] -tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "cc3a468263db75e40a8a4751f3dae9bce5bc4234" } +tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "87e452fab17711a82422a97ab9541ebf647fa28a" } # default-features = false drops notify-rust so macOS uses the native # UNUserNotificationCenter backend (needs a signed .app to deliver). [target.'cfg(target_os = "macos")'.dependencies] -tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "cc3a468263db75e40a8a4751f3dae9bce5bc4234", default-features = false } +tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "87e452fab17711a82422a97ab9541ebf647fa28a", default-features = false } [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-updater = { version = "2", optional = true } @@ -113,7 +113,7 @@ cef = { version = "=148.0.0", optional = true } libloading = "0.8" [target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies] -tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "cc3a468263db75e40a8a4751f3dae9bce5bc4234", features = [ +tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "87e452fab17711a82422a97ab9541ebf647fa28a", features = [ "push-notifications", ] } tauri-plugin-edge-to-edge = { git = "https://github.com/SableClient/tauri-plugin-edge-to-edge.git", rev = "33c6116c27be28c06df5a9d02231ecc5fdeb93c5" } diff --git a/src/app/features/settings/notifications/DeregisterPushNotifications.tsx b/src/app/features/settings/notifications/DeregisterPushNotifications.tsx index bc90d746ae..e27715ba2a 100644 --- a/src/app/features/settings/notifications/DeregisterPushNotifications.tsx +++ b/src/app/features/settings/notifications/DeregisterPushNotifications.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useCallback, useState } from 'react'; import FocusTrap from 'focus-trap-react'; import { Box, @@ -17,12 +17,16 @@ import { import { menuIcon, X } from '$components/icons/phosphor'; import { useAtom } from 'jotai'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; +import { useClientConfig } from '../../../hooks/useClientConfig'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { useSetting } from '../../../state/hooks/settings'; import { settingsAtom } from '../../../state/settings'; import { pushSubscriptionAtom } from '../../../state/pushSubscription'; import { deRegisterAllPushers } from './PushNotifications'; +import { disableNativePush } from './NotificationTransport'; +import { disableUnifiedPush } from './UnifiedPushNotifications'; import { SettingTile } from '../../../components/setting-tile'; +import { isTauri } from '@tauri-apps/api/core'; type ConfirmDeregisterDialogProps = { onClose: () => void; @@ -78,9 +82,21 @@ function ConfirmDeregisterDialog({ onClose, onConfirm, isLoading }: ConfirmDereg export function DeregisterAllPushersSetting() { const mx = useMatrixClient(); - const [deregisterState] = useAsyncCallback(deRegisterAllPushers); + const clientConfig = useClientConfig(); + const deregisterAll = useCallback(async () => { + await deRegisterAllPushers(mx); + if (isTauri()) { + await Promise.allSettled([disableNativePush(mx, clientConfig), disableUnifiedPush(mx)]); + } + }, [mx, clientConfig]); + const [deregisterState, runDeregisterAll] = useAsyncCallback(deregisterAll); const [isConfirming, setIsConfirming] = useState(false); const [, setPushNotifications] = useSetting(settingsAtom, 'usePushNotifications'); + const [backgroundPushEnabled, setBackgroundPushEnabled] = useSetting( + settingsAtom, + 'backgroundPushEnabled' + ); + const [, setBackgroundPushProvider] = useSetting(settingsAtom, 'backgroundPushProvider'); const [, setPushSubscription] = useAtom(pushSubscriptionAtom); @@ -94,9 +110,18 @@ export function DeregisterAllPushersSetting() { }; const handleConfirmDeregister = async () => { - await deRegisterAllPushers(mx); + try { + await runDeregisterAll(); + } catch { + return; + } setPushNotifications(false); setPushSubscription(null); + + if (backgroundPushEnabled) { + setBackgroundPushEnabled(false); + } + setBackgroundPushProvider(null); setIsConfirming(false); }; @@ -132,7 +157,6 @@ export function DeregisterAllPushersSetting() { } > - {/* FIXME: these two things below, even before my changes, don't really seem to ever appear? */} {deregisterState.status === AsyncStatus.Error && (
diff --git a/src/app/features/settings/notifications/NativePushNotificationsApiClient.test.ts b/src/app/features/settings/notifications/NativePushNotificationsApiClient.test.ts new file mode 100644 index 0000000000..de78c45747 --- /dev/null +++ b/src/app/features/settings/notifications/NativePushNotificationsApiClient.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it, vi } from 'vitest'; + +const invoke = vi.hoisted(() => + vi.fn<(cmd: string, args?: Record) => Promise>() +); + +const isAndroidTauri = vi.hoisted(() => vi.fn<() => boolean>()); + +vi.mock('@tauri-apps/api/core', () => ({ invoke })); +vi.mock('./TauriNotificationsApiClient', () => ({ isAndroidTauri })); + +import { getNativePushNotificationsApi } from './NativePushNotificationsApiClient'; + +describe('NativePushNotificationsApiClient', () => { + it('registers with provider: "fcm" on Android so the plugin bypasses a saved UnifiedPush distributor', async () => { + isAndroidTauri.mockReturnValue(true); + invoke.mockResolvedValueOnce({ + deviceToken: 'fcm-token', + p256dh: 'p256dh', + auth: 'auth', + }); + + const api = await getNativePushNotificationsApi(); + await api.registerForPushNotifications('vapid-pub'); + + expect(invoke).toHaveBeenCalledWith('plugin:notifications|register_for_push_notifications', { + vapid: 'vapid-pub', + provider: 'fcm', + }); + }); + + it('omits the provider arg on iOS (APNs ignores it)', async () => { + isAndroidTauri.mockReturnValue(false); + invoke.mockResolvedValueOnce({ + deviceToken: 'apns-token', + }); + + const api = await getNativePushNotificationsApi(); + await api.registerForPushNotifications('vapid-pub'); + + expect(invoke).toHaveBeenCalledWith('plugin:notifications|register_for_push_notifications', { + vapid: 'vapid-pub', + }); + }); +}); diff --git a/src/app/features/settings/notifications/NativePushNotificationsApiClient.ts b/src/app/features/settings/notifications/NativePushNotificationsApiClient.ts index e2ff5e7d75..269f727997 100644 --- a/src/app/features/settings/notifications/NativePushNotificationsApiClient.ts +++ b/src/app/features/settings/notifications/NativePushNotificationsApiClient.ts @@ -1,4 +1,5 @@ import { invoke } from '@tauri-apps/api/core'; +import { isAndroidTauri } from './TauriNotificationsApiClient'; export type NativePushRegistration = { deviceToken: string; @@ -24,6 +25,7 @@ export async function getNativePushNotificationsApi(): Promise invoke('plugin:notifications|register_for_push_notifications', { vapid, + ...(isAndroidTauri() ? { provider: 'fcm' } : {}), }), unregisterForPushNotifications: notificationsApi.unregisterForPushNotifications, }) diff --git a/src/app/features/settings/notifications/PushNotifications.test.ts b/src/app/features/settings/notifications/PushNotifications.test.ts new file mode 100644 index 0000000000..dfb86f7a30 --- /dev/null +++ b/src/app/features/settings/notifications/PushNotifications.test.ts @@ -0,0 +1,59 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +const isTauri = vi.hoisted(() => vi.fn<() => boolean>()); + +vi.mock('@tauri-apps/api/core', () => ({ isTauri })); + +import { + enablePushNotifications, + disablePushNotifications, + togglePusher, +} from './PushNotifications'; + +const matrixClient = vi.hoisted(() => ({ + getAccessToken: vi.fn<() => string>(() => 'token'), +})); +const clientConfig = {} as never; + +afterEach(() => { + vi.clearAllMocks(); +}); + +describe('enablePushNotifications', () => { + it('no-ops in the Tauri runtime instead of throwing', async () => { + isTauri.mockReturnValue(true); + + await expect( + enablePushNotifications(matrixClient as never, clientConfig, [ + null, + vi.fn<(sub: unknown) => void>(), + ]) + ).resolves.toBeUndefined(); + }); +}); + +describe('disablePushNotifications', () => { + it('no-ops in the Tauri runtime', async () => { + isTauri.mockReturnValue(true); + + await expect( + disablePushNotifications(matrixClient as never, clientConfig, [ + null, + vi.fn<(sub: unknown) => void>(), + ]) + ).resolves.toBeUndefined(); + }); +}); + +describe('togglePusher', () => { + it('does not throw in the Tauri runtime when push is enabled', async () => { + isTauri.mockReturnValue(true); + + await expect( + togglePusher(matrixClient as never, clientConfig, true, true, [ + null, + vi.fn<(sub: unknown) => void>(), + ]) + ).resolves.toBeUndefined(); + }); +}); diff --git a/src/app/features/settings/notifications/PushNotifications.tsx b/src/app/features/settings/notifications/PushNotifications.tsx index f936f8c806..1d3dae30da 100644 --- a/src/app/features/settings/notifications/PushNotifications.tsx +++ b/src/app/features/settings/notifications/PushNotifications.tsx @@ -33,9 +33,7 @@ export async function enablePushNotifications( clientConfig: ClientConfig, pushSubscriptionAtom: PushSubscriptionState ): Promise { - if (isTauri()) { - throw new Error('Push notifications are disabled in Tauri runtime.'); - } + if (isTauri()) return; if (!('serviceWorker' in navigator) || !('PushManager' in window)) { debugLog.error( 'notification', diff --git a/src/app/features/settings/notifications/SystemNotification.test.ts b/src/app/features/settings/notifications/SystemNotification.test.ts new file mode 100644 index 0000000000..a4f429ec8a --- /dev/null +++ b/src/app/features/settings/notifications/SystemNotification.test.ts @@ -0,0 +1,91 @@ +import { describe, expect, it, vi } from 'vitest'; +import { switchBackgroundPushTransport } from './SystemNotification'; +import type { NotificationTransportProvider } from './NotificationTransport'; + +describe('switchBackgroundPushTransport', () => { + it('deactivates the previous transport before activating the new one', async () => { + const order: string[] = []; + const deactivate = vi + .fn<(kind: NotificationTransportProvider | null) => Promise>() + .mockImplementation(async (kind) => { + order.push(`deactivate:${kind}`); + }); + const activate = vi + .fn<() => Promise>() + .mockImplementation(async () => { + order.push('activate'); + return 'native'; + }); + + const nextKind = await switchBackgroundPushTransport({ + previousKind: 'unifiedpush', + activate, + deactivate, + }); + + expect(nextKind).toBe('native'); + expect(order).toEqual(['deactivate:unifiedpush', 'activate']); + }); + + it('skips deactivate when there is no previous transport', async () => { + const deactivate = vi.fn<(kind: NotificationTransportProvider | null) => Promise>(); + const activate = vi + .fn<() => Promise>() + .mockResolvedValue('native'); + + const nextKind = await switchBackgroundPushTransport({ + previousKind: null, + activate, + deactivate, + }); + + expect(nextKind).toBe('native'); + expect(deactivate).not.toHaveBeenCalled(); + expect(activate).toHaveBeenCalledOnce(); + }); + + it('propagates a deactivate failure without activating', async () => { + const deactivate = vi + .fn<(kind: NotificationTransportProvider | null) => Promise>() + .mockRejectedValue(new Error('unregister failed')); + const activate = vi.fn<() => Promise>(); + + await expect( + switchBackgroundPushTransport({ + previousKind: 'unifiedpush', + activate, + deactivate, + }) + ).rejects.toThrow('unregister failed'); + + expect(activate).not.toHaveBeenCalled(); + }); + + it('restores the previous transport when activation fails, then rethrows', async () => { + const order: string[] = []; + const deactivate = vi + .fn<(kind: NotificationTransportProvider | null) => Promise>() + .mockImplementation(async (kind) => { + order.push(`deactivate:${kind}`); + }); + const activate = vi + .fn<() => Promise>() + .mockRejectedValue(new Error('register failed')); + const reactivate = vi + .fn<(kind: NotificationTransportProvider) => Promise>() + .mockImplementation(async (kind) => { + order.push(`reactivate:${kind}`); + }); + + await expect( + switchBackgroundPushTransport({ + previousKind: 'unifiedpush', + activate, + deactivate, + reactivate, + }) + ).rejects.toThrow('register failed'); + + expect(order).toEqual(['deactivate:unifiedpush', 'reactivate:unifiedpush']); + }); +}); diff --git a/src/app/features/settings/notifications/SystemNotification.tsx b/src/app/features/settings/notifications/SystemNotification.tsx index 8ee60c4c07..8b14ce9282 100644 --- a/src/app/features/settings/notifications/SystemNotification.tsx +++ b/src/app/features/settings/notifications/SystemNotification.tsx @@ -93,33 +93,28 @@ export function deriveLegacyPushSync(input: { return deriveLegacyPushFlags(input.enabled, input.provider); } -export function shouldRefreshBackgroundPushTransport( - previousKind: BackgroundPushKind | null, - nextKind: BackgroundPushKind | null -): boolean { - return previousKind !== nextKind; -} - export async function switchBackgroundPushTransport(params: { previousKind: BackgroundPushKind | null; activate: () => Promise; deactivate: (kind: BackgroundPushKind | null) => Promise; + reactivate?: (kind: BackgroundPushKind) => Promise; }): Promise { - const { previousKind, activate, deactivate } = params; - const nextKind = await activate(); + const { previousKind, activate, deactivate, reactivate } = params; - if (!shouldRefreshBackgroundPushTransport(previousKind, nextKind)) { - return nextKind; + if (previousKind) { + await deactivate(previousKind); } try { - await deactivate(previousKind); + return await activate(); } catch (error) { - await deactivate(nextKind).catch(() => undefined); + // The old transport is already torn down, so restore it rather than + // leaving the user with no push delivery. + if (previousKind && reactivate) { + await reactivate(previousKind); + } throw error; } - - return nextKind; } function getNativePushConfigError(clientConfig: ReturnType): string | null { @@ -699,19 +694,13 @@ function BackgroundPushNotificationSetting() { await disableNativePush(mx, clientConfig); }; - const activateAndroidAutoTransport = async ( - currentKind: BackgroundPushKind | null - ): Promise => { + const activateAndroidAutoTransport = async (): Promise => { const nativeFallback = async (failureReason: string): Promise => { const configError = getNativePushConfigError(clientConfig); if (configError) { throw new Error(`${failureReason} Native push fallback is unavailable: ${configError}`); } - if (currentKind === 'native') { - return 'native'; - } - await activateTransport('native'); return 'native'; }; @@ -752,7 +741,7 @@ function BackgroundPushNotificationSetting() { if (currentKind === 'unifiedpush') { return 'unifiedpush'; } - return activateAndroidAutoTransport(currentKind); + return activateAndroidAutoTransport(); } if (currentKind === nextPreferredKind) { @@ -793,12 +782,19 @@ function BackgroundPushNotificationSetting() { try { if (backgroundPushEnabled) { - const nextKind = await switchBackgroundPushTransport({ - previousKind, - activate: () => activateMode(nextMode, previousKind), - deactivate: deactivateTransport, - }); - setBackgroundPushProvider(nextKind); + const plannedKind = resolvePreferredNotificationTransportProvider( + normalizeNotificationTransportMode(nextMode, runtimePlatform), + runtimePlatform + ); + if (plannedKind !== previousKind) { + const nextKind = await switchBackgroundPushTransport({ + previousKind, + activate: () => activateMode(nextMode, previousKind), + deactivate: deactivateTransport, + reactivate: activateTransport, + }); + setBackgroundPushProvider(nextKind); + } } else { setBackgroundPushProvider(null); } diff --git a/src/app/features/settings/notifications/UnifiedPushNotifications.ts b/src/app/features/settings/notifications/UnifiedPushNotifications.ts index c1425eee3b..191eadecbb 100644 --- a/src/app/features/settings/notifications/UnifiedPushNotifications.ts +++ b/src/app/features/settings/notifications/UnifiedPushNotifications.ts @@ -312,6 +312,7 @@ type NotificationSettings = { const NOTIF_GROUP_KEY = 'matrix_messages'; const MAX_MESSAGES = 10; +const MAX_SEEN_EVENT_IDS = 200; type NotifPerson = { name: string; @@ -389,11 +390,11 @@ const roomNotifId = (userId: string, roomId: string) => hashCode(`${userId}\u000 const summaryNotifId = (userId: string) => hashCode(`sable-group-summary\u0000${userId}`); type RoomNotifCache = { + key: string; roomName: string; messages: NotifMessage[]; seenEventIds: Set; isGroupConversation: boolean; - latestEventId?: string; }; const roomNotifCaches = new Map(); @@ -410,7 +411,7 @@ function getOrCreateRoomCache(userId: string, roomId: string, roomName: string): const key = `${userId}\u0000${roomId}`; let cache = roomNotifCaches.get(key); if (!cache) { - cache = { roomName, messages: [], seenEventIds: new Set(), isGroupConversation: false }; + cache = { key, roomName, messages: [], seenEventIds: new Set(), isGroupConversation: false }; roomNotifCaches.set(key, cache); } cache.roomName = roomName; @@ -605,7 +606,6 @@ async function handleRichPushPayload( if (cache.messages.length > MAX_MESSAGES) { cache.messages = cache.messages.slice(-MAX_MESSAGES); } - cache.latestEventId = eventId; const room = settings.mx.getRoom(roomId); if (room) { @@ -701,29 +701,7 @@ async function handleMinimalPushPayload( } } - if ( - !previewText && - eventId && - settings.showMessageContent && - (!isEncryptedRoom || settings.showEncryptedMessageContent) - ) { - const fetched = await resolvePreviewEvent(settings.mx, roomId, eventId); - if (fetched) { - const sender = fetched.getSender(); - if (sender) { - senderName = room?.getMember(sender)?.name ?? getMxIdLocalPart(sender) ?? sender; - senderId = sender; - } - previewText = resolveNotificationPreviewText({ - content: fetched.getContent(), - eventType: fetched.getType(), - isEncryptedRoom, - showMessageContent: settings.showMessageContent, - showEncryptedMessageContent: settings.showEncryptedMessageContent, - }); - } - } - + const hasInMemoryPreview = Boolean(previewText); if (!previewText) { previewText = isEncryptedRoom ? 'Encrypted message' : 'New message'; } @@ -745,13 +723,18 @@ async function handleMinimalPushPayload( const cache = getOrCreateRoomCache(userId, roomId, roomName); if (eventId && cache.seenEventIds.has(eventId)) return; - if (eventId) cache.seenEventIds.add(eventId); + if (eventId) { + cache.seenEventIds.add(eventId); + if (cache.seenEventIds.size > MAX_SEEN_EVENT_IDS) { + const oldest = cache.seenEventIds.values().next().value; + if (oldest !== undefined) cache.seenEventIds.delete(oldest); + } + } cache.messages.push(message); if (cache.messages.length > MAX_MESSAGES) { cache.messages = cache.messages.slice(-MAX_MESSAGES); } - cache.latestEventId = eventId; if (room) { cache.isGroupConversation = (room.getJoinedMemberCount() ?? 0) > 2; @@ -762,6 +745,62 @@ async function handleMinimalPushPayload( event_id: eventId, user_id: pushData?.user_id, }); + + if ( + !hasInMemoryPreview && + eventId && + settings.showMessageContent && + (!isEncryptedRoom || settings.showEncryptedMessageContent) + ) { + resolvePreviewEvent(settings.mx, roomId, eventId) + .then(async (fetched) => { + // Skip if the notification was dismissed/replaced or the message evicted while fetching. + if (!fetched || roomNotifCaches.get(cache.key) !== cache) return; + if (!cache.messages.includes(message)) return; + + // Trust the fetched event's own encryption, not the (possibly-absent) room. + const eventEncrypted = isEncryptedRoom || fetched.isEncrypted(); + const enrichedPreview = resolveNotificationPreviewText({ + content: fetched.getContent(), + eventType: fetched.getType(), + isEncryptedRoom: eventEncrypted, + showMessageContent: settings.showMessageContent, + showEncryptedMessageContent: settings.showEncryptedMessageContent, + }); + if (!enrichedPreview) return; + + const fetchedSender = fetched.getSender(); + if (fetchedSender) { + senderName = + room?.getMember(fetchedSender)?.name ?? + getMxIdLocalPart(fetchedSender) ?? + fetchedSender; + senderId = fetchedSender; + } + message.text = enrichedPreview; + message.sender = senderName + ? { + name: senderName, + key: senderId, + iconUrl: + senderId && roomId ? resolveAvatarUrl(settings.mx, roomId, senderId) : undefined, + } + : sender; + + await postRoomNotification(userId, roomId, cache, true, { + room_id: roomId, + event_id: eventId, + user_id: pushData?.user_id, + }); + }) + .catch((error) => { + unifiedPushLog.warn( + 'notification', + 'Background preview fetch failed', + error instanceof Error ? error : new Error(String(error)) + ); + }); + } } async function handleUnifiedPushPayload( diff --git a/src/app/features/settings/notifications/UnifiedPushTransportApiClient.test.ts b/src/app/features/settings/notifications/UnifiedPushTransportApiClient.test.ts new file mode 100644 index 0000000000..1fa0d9294f --- /dev/null +++ b/src/app/features/settings/notifications/UnifiedPushTransportApiClient.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it, vi } from 'vitest'; + +const invoke = vi.hoisted(() => + vi.fn<(cmd: string, args?: Record) => Promise>() +); + +vi.mock('@tauri-apps/api/core', () => ({ invoke })); + +import { getUnifiedPushTransportApi } from './UnifiedPushTransportApiClient'; + +describe('UnifiedPushTransportApiClient', () => { + it('registers with provider: "unifiedpush" so the plugin routes through the distributor explicitly', async () => { + invoke.mockResolvedValueOnce({ + deviceToken: 'up-endpoint', + p256dh: 'p256dh', + auth: 'auth', + }); + + const api = await getUnifiedPushTransportApi(); + await api.registerForPushNotifications('vapid-pub'); + + expect(invoke).toHaveBeenCalledWith('plugin:notifications|register_for_push_notifications', { + vapid: 'vapid-pub', + provider: 'unifiedpush', + }); + }); +}); diff --git a/src/app/features/settings/notifications/UnifiedPushTransportApiClient.ts b/src/app/features/settings/notifications/UnifiedPushTransportApiClient.ts index 288da15de3..0b71d16beb 100644 --- a/src/app/features/settings/notifications/UnifiedPushTransportApiClient.ts +++ b/src/app/features/settings/notifications/UnifiedPushTransportApiClient.ts @@ -24,6 +24,7 @@ export async function getUnifiedPushTransportApi(): Promise invoke('plugin:notifications|register_for_push_notifications', { vapid, + provider: 'unifiedpush', }), unregisterForPushNotifications: notificationsApi.unregisterForPushNotifications, listDistributors: notificationsApi.listDistributors,