fix: resolve push notification issues on iPad and Android by preloading VAPID keys and ready service worker to preserve user gesture context and by forcing clean re-subscription

This commit is contained in:
2026-06-02 19:28:03 +02:00
parent 1d511e0f8c
commit 671cb2dd9a
3 changed files with 54 additions and 21 deletions
@@ -6,7 +6,8 @@ import {
enableCollaboratorChangePush, enableCollaboratorChangePush,
fetchPushPrefs, fetchPushPrefs,
getNotificationPermission, getNotificationPermission,
isPushSupported isPushSupported,
preloadPushService
} from '../services/pushNotifications.js' } from '../services/pushNotifications.js'
import { isIosDevice, isRunningStandalone } from '../hooks/usePwaInstall.js' import { isIosDevice, isRunningStandalone } from '../hooks/usePwaInstall.js'
import { PlausibleEvents, trackPlausibleEvent } from '../services/analytics.js' import { PlausibleEvents, trackPlausibleEvent } from '../services/analytics.js'
@@ -28,6 +29,7 @@ export default function PushNotificationSettings() {
setLoading(false) setLoading(false)
return return
} }
void preloadPushService()
try { try {
const prefs = await fetchPushPrefs() const prefs = await fetchPushPrefs()
setEnabled(prefs.collaboratorChangesEnabled) setEnabled(prefs.collaboratorChangesEnabled)
+3 -1
View File
@@ -10,7 +10,8 @@ import { apiFetch } from '../services/api.js'
import { import {
enableCollaboratorChangePush, enableCollaboratorChangePush,
isCollaboratorPushActive, isCollaboratorPushActive,
isPushSupported isPushSupported,
preloadPushService
} from '../services/pushNotifications.js' } from '../services/pushNotifications.js'
import { isIosDevice, isRunningStandalone } from '../hooks/usePwaInstall.js' import { isIosDevice, isRunningStandalone } from '../hooks/usePwaInstall.js'
@@ -55,6 +56,7 @@ export default function SettingsForm({ logbookId, onLogbookRestored }: SettingsF
loadCollaborators() loadCollaborators()
loadShareLink() loadShareLink()
} }
void preloadPushService()
}, [logbookId]) }, [logbookId])
const loadShareLink = async () => { const loadShareLink = async () => {
+47 -18
View File
@@ -27,17 +27,41 @@ export function getNotificationPermission(): NotificationPermission | 'unsupport
return Notification.permission return Notification.permission
} }
let cachedVapidKey: string | null = null
let cachedRegistration: ServiceWorkerRegistration | null = null
export async function preloadPushService(): Promise<void> {
if (!isPushSupported()) return
try {
if (!cachedVapidKey) {
await fetchVapidPublicKey()
}
if (!cachedRegistration) {
cachedRegistration = await navigator.serviceWorker.ready
}
} catch (err) {
console.warn('Failed to preload push service:', err)
}
}
async function fetchVapidPublicKey(): Promise<string | null> { async function fetchVapidPublicKey(): Promise<string | null> {
if (cachedVapidKey) return cachedVapidKey
const envKey = import.meta.env.VITE_VAPID_PUBLIC_KEY const envKey = import.meta.env.VITE_VAPID_PUBLIC_KEY
if (typeof envKey === 'string' && envKey.trim()) { if (typeof envKey === 'string' && envKey.trim()) {
return envKey.trim() cachedVapidKey = envKey.trim()
return cachedVapidKey
} }
try { try {
const res = await fetch(`${API_BASE}/vapid-public-key`) const res = await fetch(`${API_BASE}/vapid-public-key`)
if (!res.ok) return null if (!res.ok) return null
const data = await res.json() const data = await res.json()
return typeof data.publicKey === 'string' ? data.publicKey : null if (typeof data.publicKey === 'string') {
cachedVapidKey = data.publicKey.trim()
return cachedVapidKey
}
return null
} catch { } catch {
return null return null
} }
@@ -98,27 +122,28 @@ export async function subscribeToPush(): Promise<void> {
throw new Error('Push notifications are not supported on this device') throw new Error('Push notifications are not supported on this device')
} }
// Pre-resolve registration and VAPID key synchronously if preloaded.
// This keeps the user gesture active for iOS Safari.
const registration = cachedRegistration || await navigator.serviceWorker.ready
const publicKey = cachedVapidKey || await fetchVapidPublicKey()
if (!publicKey) {
throw new Error('Push notifications are not configured on this server')
}
const permission = await Notification.requestPermission() const permission = await Notification.requestPermission()
if (permission !== 'granted') { if (permission !== 'granted') {
throw new Error('Notification permission denied') throw new Error('Notification permission denied')
} }
const publicKey = await fetchVapidPublicKey() const keyBytes = urlBase64ToUint8Array(publicKey)
if (!publicKey) { const applicationServerKey = new Uint8Array(keyBytes)
throw new Error('Push notifications are not configured on this server')
}
const registration = await navigator.serviceWorker.ready // Always call subscribe to renew/ensure subscription without reusing stale state
let subscription = await registration.pushManager.getSubscription() const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
if (!subscription) { applicationServerKey
const keyBytes = urlBase64ToUint8Array(publicKey) })
const applicationServerKey = new Uint8Array(keyBytes)
subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey
})
}
await saveSubscriptionToServer(subscription) await saveSubscriptionToServer(subscription)
} }
@@ -126,7 +151,7 @@ export async function subscribeToPush(): Promise<void> {
export async function unsubscribeFromPush(): Promise<void> { export async function unsubscribeFromPush(): Promise<void> {
if (!isPushSupported()) return if (!isPushSupported()) return
const registration = await navigator.serviceWorker.ready const registration = cachedRegistration || await navigator.serviceWorker.ready
const subscription = await registration.pushManager.getSubscription() const subscription = await registration.pushManager.getSubscription()
if (!subscription) return if (!subscription) return
@@ -164,3 +189,7 @@ export async function disableCollaboratorChangePush(): Promise<void> {
await savePushPrefs(false) await savePushPrefs(false)
await unsubscribeFromPush() await unsubscribeFromPush()
} }
if (isPushSupported()) {
void preloadPushService()
}