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:
@@ -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)
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user