Compare commits
16 Commits
v0.1.0.92
...
v0.1.0.100
| Author | SHA1 | Date | |
|---|---|---|---|
| e3ea45f717 | |||
| 8f57b6ff22 | |||
| 60e1b714b7 | |||
| 1e203bfec1 | |||
| 11420685cf | |||
| c674aac344 | |||
| 9c91a0f1fc | |||
| 2bcbbba626 | |||
| b1500f8361 | |||
| bc7512003e | |||
| eaf126b584 | |||
| a9c712be45 | |||
| b0195601de | |||
| c2b58baa6e | |||
| a85d6e42fc | |||
| 53da4a14a0 |
@@ -58,7 +58,8 @@ export default function PushNotificationSettings() {
|
||||
trackPlausibleEvent(PlausibleEvents.PUSH_DISABLED)
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : t('profile.push_error')
|
||||
console.error('Failed to toggle push notifications:', err)
|
||||
const message = err instanceof Error ? `${err.name}: ${err.message}` : String(err)
|
||||
showAlert(message)
|
||||
void loadPrefs()
|
||||
} finally {
|
||||
|
||||
@@ -193,7 +193,8 @@ export default function SettingsForm({ logbookId, onLogbookRestored }: SettingsF
|
||||
trackPlausibleEvent(PlausibleEvents.PUSH_ENABLED)
|
||||
} catch (err: unknown) {
|
||||
console.error('Failed to enable push after invite:', err)
|
||||
await showAlert(err instanceof Error ? err.message : t('profile.push_error'))
|
||||
const message = err instanceof Error ? `${err.name}: ${err.message}` : String(err)
|
||||
await showAlert(message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,12 +42,14 @@ function scheduleUpdateChecks(
|
||||
|
||||
const onVisibilityChange = () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
checkForUpdate()
|
||||
// Delay check on wake-up to allow the mobile network stack to stabilize
|
||||
setTimeout(checkForUpdate, 2000)
|
||||
}
|
||||
}
|
||||
|
||||
const onOnline = () => {
|
||||
checkForUpdate()
|
||||
// Small delay to ensure connection is fully established
|
||||
setTimeout(checkForUpdate, 500)
|
||||
}
|
||||
|
||||
document.addEventListener('visibilitychange', onVisibilityChange)
|
||||
|
||||
@@ -73,6 +73,17 @@ async function bootstrap(): Promise<void> {
|
||||
return
|
||||
}
|
||||
|
||||
if ('serviceWorker' in navigator && !import.meta.env.DEV) {
|
||||
navigator.serviceWorker
|
||||
.register('/sw.js', { scope: '/' })
|
||||
.then((reg) => {
|
||||
console.log('Service Worker registered successfully with scope:', reg.scope)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Service Worker registration failed:', err)
|
||||
})
|
||||
}
|
||||
|
||||
const rootEl = document.getElementById('root')
|
||||
if (!rootEl) {
|
||||
throw new Error('Missing #root element')
|
||||
|
||||
@@ -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)
|
||||
@@ -96,11 +117,61 @@ export async function savePushPrefs(collaboratorChangesEnabled: boolean): Promis
|
||||
})
|
||||
}
|
||||
|
||||
async function requestNotificationPermission(): Promise<NotificationPermission> {
|
||||
if (typeof Notification === 'undefined') return 'denied'
|
||||
|
||||
// Try promise-based signature first
|
||||
try {
|
||||
const result = Notification.requestPermission()
|
||||
if (result !== undefined) {
|
||||
return await result
|
||||
}
|
||||
} catch {
|
||||
// Ignore and fall back to callback
|
||||
}
|
||||
|
||||
// Callback-based fallback
|
||||
return new Promise<NotificationPermission>((resolve) => {
|
||||
Notification.requestPermission((permission) => {
|
||||
resolve(permission)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function saveSubscriptionToServer(subscription: PushSubscription): Promise<void> {
|
||||
if (!localStorage.getItem('active_userid')) throw new Error('Not authenticated')
|
||||
|
||||
const endpoint = subscription.endpoint
|
||||
const json = subscription.toJSON()
|
||||
if (!json.endpoint || !json.keys?.p256dh || !json.keys?.auth) {
|
||||
let p256dh = json.keys?.p256dh
|
||||
let auth = json.keys?.auth
|
||||
|
||||
// Fallback for browsers (like Safari) that might not serialize keys in toJSON()
|
||||
if (!p256dh && typeof subscription.getKey === 'function') {
|
||||
try {
|
||||
const rawKey = subscription.getKey('p256dh')
|
||||
if (rawKey) {
|
||||
p256dh = btoa(String.fromCharCode(...new Uint8Array(rawKey)))
|
||||
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to extract p256dh key manually:', e)
|
||||
}
|
||||
}
|
||||
|
||||
if (!auth && typeof subscription.getKey === 'function') {
|
||||
try {
|
||||
const rawAuth = subscription.getKey('auth')
|
||||
if (rawAuth) {
|
||||
auth = btoa(String.fromCharCode(...new Uint8Array(rawAuth)))
|
||||
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to extract auth key manually:', e)
|
||||
}
|
||||
}
|
||||
|
||||
if (!endpoint || !p256dh || !auth) {
|
||||
throw new Error('Invalid push subscription')
|
||||
}
|
||||
|
||||
@@ -109,8 +180,8 @@ async function saveSubscriptionToServer(subscription: PushSubscription): Promise
|
||||
await apiJson(`${API_BASE}/subscription`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
endpoint: json.endpoint,
|
||||
keys: json.keys,
|
||||
endpoint,
|
||||
keys: { p256dh, auth },
|
||||
locale,
|
||||
userAgent: navigator.userAgent
|
||||
})
|
||||
@@ -122,16 +193,19 @@ 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')
|
||||
}
|
||||
|
||||
const permission = await Notification.requestPermission()
|
||||
const permission = await requestNotificationPermission()
|
||||
if (permission !== 'granted') {
|
||||
throw new Error('Notification permission denied')
|
||||
}
|
||||
@@ -139,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)
|
||||
}
|
||||
@@ -151,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
|
||||
|
||||
|
||||
+4
-4
@@ -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())
|
||||
|
||||
|
||||
Reference in New Issue
Block a user