Compare commits

...

8 Commits

5 changed files with 96 additions and 21 deletions
+1 -1
View File
@@ -1 +1 @@
0.1.0.96
0.1.0.100
+15
View File
@@ -14,6 +14,7 @@ import {
reconcileVersionOnStartup
} from './services/pwaStartup.ts'
import { redirectToPasskeyCompatibleHostIfNeeded } from './utils/passkeyHost.ts'
import { logToBackend } from './services/pushNotifications.ts'
declare global {
interface Window {
@@ -73,6 +74,20 @@ async function bootstrap(): Promise<void> {
return
}
if ('serviceWorker' in navigator && !import.meta.env.DEV) {
logToBackend('Attempting manual Service Worker registration...')
navigator.serviceWorker
.register('/sw.js', { scope: '/' })
.then((reg) => {
console.log('Service Worker registered successfully with scope:', reg.scope)
logToBackend('Service Worker registered successfully with scope: ' + reg.scope)
})
.catch((err) => {
console.error('Service Worker registration failed:', err)
logToBackend('Service Worker registration failed', err)
})
}
const rootEl = document.getElementById('root')
if (!rootEl) {
throw new Error('Missing #root element')
+71 -16
View File
@@ -2,6 +2,29 @@ import { apiFetch, apiJson } from './api.js'
const API_BASE = '/api/push'
export async function logToBackend(message: string, error?: any): Promise<void> {
try {
await fetch(`${API_BASE}/debug-log`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message,
error: error ? {
name: error.name,
message: error.message,
stack: error.stack,
...error
} : undefined,
userAgent: navigator.userAgent,
href: window.location.href,
timestamp: new Date().toISOString()
})
})
} catch (err) {
console.warn('Failed to send debug log:', err)
}
}
function urlBase64ToUint8Array(base64String: string): Uint8Array {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4)
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/')
@@ -30,6 +53,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 +81,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)
@@ -168,27 +212,35 @@ async function saveSubscriptionToServer(subscription: PushSubscription): Promise
}
export async function subscribeToPush(): Promise<void> {
logToBackend('subscribeToPush called')
if (!isPushSupported()) {
logToBackend('subscribeToPush: push not supported')
throw new Error('Push notifications are not supported on this device')
}
// Pre-resolve registration with timeout to prevent silent hangs
// Pre-resolve registration using getRegistrationCompat to prevent ready state hangs
let registration = cachedRegistration
if (!registration) {
const readyPromise = navigator.serviceWorker.ready
const readyTimeout = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('Timeout waiting for Service Worker ready state')), 8000)
)
registration = await Promise.race([readyPromise, readyTimeout])
cachedRegistration = registration
try {
logToBackend('subscribeToPush: getting registration...')
registration = await getRegistrationCompat()
cachedRegistration = registration
logToBackend('subscribeToPush: got registration successfully')
} catch (err) {
logToBackend('subscribeToPush: failed to get registration', err)
throw err
}
}
const publicKey = cachedVapidKey || await fetchVapidPublicKey()
if (!publicKey) {
logToBackend('subscribeToPush: no public key available')
throw new Error('Push notifications are not configured on this server')
}
logToBackend('subscribeToPush: requesting permission...')
const permission = await requestNotificationPermission()
logToBackend(`subscribeToPush: permission result: ${permission}`)
if (permission !== 'granted') {
throw new Error('Notification permission denied')
}
@@ -197,6 +249,7 @@ export async function subscribeToPush(): Promise<void> {
const applicationServerKey = new Uint8Array(keyBytes)
// Always call subscribe with timeout to prevent silent hangs on push network errors
logToBackend('subscribeToPush: subscribing via pushManager...')
const subscribePromise = registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey
@@ -204,9 +257,15 @@ export async function subscribeToPush(): Promise<void> {
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)
try {
const subscription = await Promise.race([subscribePromise, subscribeTimeout])
logToBackend('subscribeToPush: subscribed successfully, saving to server...')
await saveSubscriptionToServer(subscription)
logToBackend('subscribeToPush: saved to server successfully')
} catch (err) {
logToBackend('subscribeToPush: subscription or save failed', err)
throw err
}
}
export async function unsubscribeFromPush(): Promise<void> {
@@ -214,11 +273,7 @@ export async function unsubscribeFromPush(): Promise<void> {
let registration = cachedRegistration
if (!registration) {
const readyPromise = navigator.serviceWorker.ready
const readyTimeout = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('Timeout waiting for Service Worker ready state')), 8000)
)
registration = await Promise.race([readyPromise, readyTimeout])
registration = await getRegistrationCompat()
cachedRegistration = registration
}
+4 -4
View File
@@ -6,6 +6,10 @@ import { NetworkFirst, NetworkOnly } from 'workbox-strategies'
declare let self: ServiceWorkerGlobalScope
precacheAndRoute(self.__WB_MANIFEST)
cleanupOutdatedCaches()
clientsClaim()
const appShellFallback = createHandlerBoundToURL('/index.html')
const navigationStrategy = new NetworkFirst({
cacheName: 'app-shell',
@@ -20,10 +24,6 @@ registerRoute(({ request }) => request.mode === 'navigate', async (context) => {
}
})
precacheAndRoute(self.__WB_MANIFEST)
cleanupOutdatedCaches()
clientsClaim()
// Always fetch the live deploy version, even under an older precache.
registerRoute(({ url }) => url.pathname === '/version.json', new NetworkOnly())
+5
View File
@@ -22,6 +22,11 @@ router.get('/vapid-public-key', (_req, res) => {
return res.json({ publicKey })
})
router.post('/debug-log', (req, res) => {
console.log('[CLIENT_DEBUG]', req.body)
return res.json({ success: true })
})
router.use(requireUser)
router.get('/prefs', async (req: any, res) => {