From 753e87c96db220db6c6f584a6e518c50812c83b1 Mon Sep 17 00:00:00 2001 From: Erwan Leboucher Date: Mon, 20 Jul 2026 14:36:24 +0200 Subject: [PATCH] fix(notifications): use native permission tri-state to sync background push toggle The native plugin returns Option (true=granted, false=denied, null=never-asked) but the JS bindings typed it as Promise, flattening the tri-state. Fix the types to Promise, add a non-prompting isUnifiedPushPermissionGranted, and pre-check permission at startup: if denied, sync backgroundPushEnabled to false and skip registration; if never-asked or granted, proceed (prompting if needed). On registration failure, re-check and sync if now denied. --- .../notifications/NativePushNotifications.ts | 2 +- .../NativePushNotificationsApiClient.ts | 2 +- .../NotificationTransportRuntimeFeature.tsx | 61 +++++++++++++------ .../notifications/UnifiedPushTransport.ts | 5 ++ .../UnifiedPushTransportApiClient.ts | 2 +- src/types/tauri-plugin-notifications-api.d.ts | 2 +- 6 files changed, 51 insertions(+), 23 deletions(-) diff --git a/src/app/features/settings/notifications/NativePushNotifications.ts b/src/app/features/settings/notifications/NativePushNotifications.ts index 96382e9e8..408a8eccf 100644 --- a/src/app/features/settings/notifications/NativePushNotifications.ts +++ b/src/app/features/settings/notifications/NativePushNotifications.ts @@ -69,7 +69,7 @@ function getPushGatewayUrl(clientConfig: ClientConfig): string { return pushGatewayUrl; } -export async function isNativePushPermissionGranted(): Promise { +export async function isNativePushPermissionGranted(): Promise { const api = await getNativePushNotificationsApi(); return api.isPermissionGranted(); } diff --git a/src/app/features/settings/notifications/NativePushNotificationsApiClient.ts b/src/app/features/settings/notifications/NativePushNotificationsApiClient.ts index a30a028a7..e2ff5e7d7 100644 --- a/src/app/features/settings/notifications/NativePushNotificationsApiClient.ts +++ b/src/app/features/settings/notifications/NativePushNotificationsApiClient.ts @@ -7,7 +7,7 @@ export type NativePushRegistration = { }; export type NativePushNotificationsApi = { - isPermissionGranted: () => Promise; + isPermissionGranted: () => Promise; requestPermission: () => Promise; registerForPushNotifications: (vapid?: string) => Promise; unregisterForPushNotifications: () => Promise; diff --git a/src/app/features/settings/notifications/NotificationTransportRuntimeFeature.tsx b/src/app/features/settings/notifications/NotificationTransportRuntimeFeature.tsx index 1c2c02e59..17181352b 100644 --- a/src/app/features/settings/notifications/NotificationTransportRuntimeFeature.tsx +++ b/src/app/features/settings/notifications/NotificationTransportRuntimeFeature.tsx @@ -7,19 +7,29 @@ import { useClientConfig } from '$hooks/useClientConfig'; import { useSetting } from '$state/hooks/settings'; import { settingsAtom } from '$state/settings'; import { enableUnifiedPush } from './UnifiedPushNotifications'; -import { enableNativePush } from './NativePushNotifications'; +import { enableNativePush, isNativePushPermissionGranted } from './NativePushNotifications'; +import { isUnifiedPushPermissionGranted } from './UnifiedPushTransport'; import { NotificationTransportRuntime, type NotificationTransportRuntimeContext, } from './NotificationTransportRuntime'; import { type NotificationTransportPlatform, + type NotificationTransportProvider, normalizeNotificationTransportMode, resolvePreferredNotificationTransportProvider, } from './NotificationTransport'; const log = createLogger('NotificationTransportRuntimeFeature'); +async function checkPushPermission( + provider: NotificationTransportProvider | null +): Promise { + if (provider === 'unifiedpush') return isUnifiedPushPermissionGranted(); + if (provider === 'native') return isNativePushPermissionGranted(); + return null; +} + function currentPlatform(): NotificationTransportPlatform { if (!isTauri()) return 'web'; const platform = osType(); @@ -31,8 +41,14 @@ function currentPlatform(): NotificationTransportPlatform { export function NotificationTransportRuntimeFeature() { const mx = useMatrixClient(); const clientConfig = useClientConfig(); - const [backgroundPushEnabled] = useSetting(settingsAtom, 'backgroundPushEnabled'); - const [backgroundPushProvider] = useSetting(settingsAtom, 'backgroundPushProvider'); + const [backgroundPushEnabled, setBackgroundPushEnabled] = useSetting( + settingsAtom, + 'backgroundPushEnabled' + ); + const [backgroundPushProvider, setBackgroundPushProvider] = useSetting( + settingsAtom, + 'backgroundPushProvider' + ); const [pushTransportMode] = useSetting(settingsAtom, 'pushTransportMode'); const [pushTransportOverride] = useSetting(settingsAtom, 'pushTransportOverride'); const [useInAppNotifications] = useSetting(settingsAtom, 'useInAppNotifications'); @@ -107,26 +123,33 @@ export function NotificationTransportRuntimeFeature() { useEffect(() => { if (!mx) return undefined; - if (provider === 'unifiedpush') { - enableUnifiedPush(mx, upConfigRef.current) - .then((result) => { + void (async () => { + const granted = await checkPushPermission(provider); + if (granted === false) { + setBackgroundPushEnabled(false); + setBackgroundPushProvider(null); + return; + } + + try { + if (provider === 'unifiedpush') { + const result = await enableUnifiedPush(mx, upConfigRef.current); lastEndpointRef.current = result.endpoint; - }) - .catch((error) => { - log.warn('UnifiedPush pusher registration failed at startup', error); - }); - } else if (provider === 'native') { - enableNativePush(mx, clientConfigRef.current) - .then((token) => { + } else if (provider === 'native') { + const token = await enableNativePush(mx, clientConfigRef.current); lastEndpointRef.current = token; - }) - .catch((error) => { - log.warn('Native push pusher registration failed at startup', error); - }); - } + } + } catch (error) { + log.warn('Pusher registration failed at startup', error); + if ((await checkPushPermission(provider)) === false) { + setBackgroundPushEnabled(false); + setBackgroundPushProvider(null); + } + } + })(); return () => {}; - }, [provider, mx]); + }, [provider, mx, setBackgroundPushEnabled, setBackgroundPushProvider]); useEffect( () => () => { diff --git a/src/app/features/settings/notifications/UnifiedPushTransport.ts b/src/app/features/settings/notifications/UnifiedPushTransport.ts index cf43752db..416d2e4b4 100644 --- a/src/app/features/settings/notifications/UnifiedPushTransport.ts +++ b/src/app/features/settings/notifications/UnifiedPushTransport.ts @@ -110,6 +110,11 @@ async function getUnifiedPushPermissionState(): Promise { + const api = await getUnifiedPushTransportApi(); + return api.isPermissionGranted(); +} + export async function getUnifiedPushDistributors(): Promise { const api = await getUnifiedPushTransportApi(); return api.listDistributors(); diff --git a/src/app/features/settings/notifications/UnifiedPushTransportApiClient.ts b/src/app/features/settings/notifications/UnifiedPushTransportApiClient.ts index dea0fbb80..288da15de 100644 --- a/src/app/features/settings/notifications/UnifiedPushTransportApiClient.ts +++ b/src/app/features/settings/notifications/UnifiedPushTransportApiClient.ts @@ -7,7 +7,7 @@ export type UnifiedPushRegistration = { }; export type UnifiedPushTransportApi = { - isPermissionGranted: () => Promise; + isPermissionGranted: () => Promise; requestPermission: () => Promise; registerForPushNotifications: (vapid?: string) => Promise; unregisterForPushNotifications: () => Promise; diff --git a/src/types/tauri-plugin-notifications-api.d.ts b/src/types/tauri-plugin-notifications-api.d.ts index bec70ff8e..dbcb7c3a5 100644 --- a/src/types/tauri-plugin-notifications-api.d.ts +++ b/src/types/tauri-plugin-notifications-api.d.ts @@ -62,7 +62,7 @@ declare module '@choochmeque/tauri-plugin-notifications-api' { unregister: () => Promise | void; }; - export function isPermissionGranted(): Promise; + export function isPermissionGranted(): Promise; export function requestPermission(): Promise; export function sendNotification(options: NotificationOptions | string): Promise; export function registerForPushNotifications(): Promise;