|
|
|
@@ -30,6 +30,27 @@ export function getNotificationPermission(): NotificationPermission | 'unsupport
|
|
|
|
|
let cachedVapidKey: string | null = null
|
|
|
|
|
let cachedRegistration: ServiceWorkerRegistration | null = null
|
|
|
|
|
|
|
|
|
|
async function getRegistrationCompat(timeoutMs = 8000): Promise<ServiceWorkerRegistration> {
|
|
|
|
|
if (!('serviceWorker' in navigator)) {
|
|
|
|
|
throw new Error('Service Worker is not supported by your browser')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const reg = await navigator.serviceWorker.getRegistration()
|
|
|
|
|
if (reg) return reg
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn('Failed to get service worker registration directly:', e)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fallback to waiting for ready state with a timeout
|
|
|
|
|
const readyPromise = navigator.serviceWorker.ready
|
|
|
|
|
const timeoutPromise = new Promise<never>((_, reject) =>
|
|
|
|
|
setTimeout(() => reject(new Error('Timeout waiting for Service Worker ready state')), timeoutMs)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return Promise.race([readyPromise, timeoutPromise])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function preloadPushService(): Promise<void> {
|
|
|
|
|
if (!isPushSupported()) return
|
|
|
|
|
try {
|
|
|
|
@@ -37,7 +58,7 @@ export async function preloadPushService(): Promise<void> {
|
|
|
|
|
await fetchVapidPublicKey()
|
|
|
|
|
}
|
|
|
|
|
if (!cachedRegistration) {
|
|
|
|
|
cachedRegistration = await navigator.serviceWorker.ready
|
|
|
|
|
cachedRegistration = await getRegistrationCompat()
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.warn('Failed to preload push service:', err)
|
|
|
|
@@ -172,11 +193,14 @@ export async function subscribeToPush(): Promise<void> {
|
|
|
|
|
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()
|
|
|
|
|
// Pre-resolve registration using getRegistrationCompat to prevent ready state hangs
|
|
|
|
|
let registration = cachedRegistration
|
|
|
|
|
if (!registration) {
|
|
|
|
|
registration = await getRegistrationCompat()
|
|
|
|
|
cachedRegistration = registration
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const publicKey = cachedVapidKey || await fetchVapidPublicKey()
|
|
|
|
|
if (!publicKey) {
|
|
|
|
|
throw new Error('Push notifications are not configured on this server')
|
|
|
|
|
}
|
|
|
|
@@ -189,11 +213,15 @@ export async function subscribeToPush(): Promise<void> {
|
|
|
|
|
const keyBytes = urlBase64ToUint8Array(publicKey)
|
|
|
|
|
const applicationServerKey = new Uint8Array(keyBytes)
|
|
|
|
|
|
|
|
|
|
// Always call subscribe to renew/ensure subscription without reusing stale state
|
|
|
|
|
const subscription = await registration.pushManager.subscribe({
|
|
|
|
|
// Always call subscribe with timeout to prevent silent hangs on push network errors
|
|
|
|
|
const subscribePromise = registration.pushManager.subscribe({
|
|
|
|
|
userVisibleOnly: true,
|
|
|
|
|
applicationServerKey
|
|
|
|
|
})
|
|
|
|
|
const subscribeTimeout = new Promise<never>((_, reject) =>
|
|
|
|
|
setTimeout(() => reject(new Error('Timeout establishing subscription with push service (FCM/APNs)')), 12000)
|
|
|
|
|
)
|
|
|
|
|
const subscription = await Promise.race([subscribePromise, subscribeTimeout])
|
|
|
|
|
|
|
|
|
|
await saveSubscriptionToServer(subscription)
|
|
|
|
|
}
|
|
|
|
@@ -201,7 +229,12 @@ export async function subscribeToPush(): Promise<void> {
|
|
|
|
|
export async function unsubscribeFromPush(): Promise<void> {
|
|
|
|
|
if (!isPushSupported()) return
|
|
|
|
|
|
|
|
|
|
const registration = cachedRegistration || await navigator.serviceWorker.ready
|
|
|
|
|
let registration = cachedRegistration
|
|
|
|
|
if (!registration) {
|
|
|
|
|
registration = await getRegistrationCompat()
|
|
|
|
|
cachedRegistration = registration
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const subscription = await registration.pushManager.getSubscription()
|
|
|
|
|
if (!subscription) return
|
|
|
|
|
|
|
|
|
|