Skip to content

Commit 7f9598a

Browse files
Add PWA update notification prompt
Implements service worker update detection with user prompt to refresh
1 parent e343a50 commit 7f9598a

4 files changed

Lines changed: 89 additions & 3 deletions

File tree

frontend/src/App.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { Register } from './pages/Register'
1414
import { Setup } from './pages/Setup'
1515
import { SettingsDialog } from './components/settings/SettingsDialog'
1616
import { VersionNotifier } from './components/VersionNotifier'
17+
import { PwaUpdatePrompt } from '@/components/PwaUpdatePrompt'
1718
import { useTheme } from './hooks/useTheme'
1819
import { TTSProvider } from './contexts/TTSContext'
1920
import { AuthProvider } from './contexts/AuthContext'
@@ -72,8 +73,9 @@ function AppShell() {
7273
useEffect(() => {
7374
const channel = new BroadcastChannel('notification-click')
7475
channel.onmessage = (event: MessageEvent) => {
75-
if (event.data?.url) {
76-
navigate(event.data.url)
76+
const data = event.data as { url?: string } | null | undefined
77+
if (typeof data?.url === 'string') {
78+
navigate(data.url)
7779
}
7880
}
7981
return () => channel.close()
@@ -87,6 +89,7 @@ function AppShell() {
8789
<SSHHostKeyDialogWrapper />
8890
<SettingsDialog />
8991
<VersionNotifier />
92+
<PwaUpdatePrompt />
9093
<Toaster
9194
position="bottom-right"
9295
expand={false}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { useEffect } from 'react'
2+
import { showToast } from '@/lib/toast'
3+
import { onServiceWorkerUpdate, offServiceWorkerUpdate } from '@/lib/serviceWorker'
4+
5+
export function PwaUpdatePrompt() {
6+
useEffect(() => {
7+
onServiceWorkerUpdate(() => {
8+
showToast.info('New build deployed', {
9+
description: 'Refresh to load the latest changes.',
10+
action: {
11+
label: 'Refresh',
12+
onClick: () => window.location.reload(),
13+
},
14+
duration: Infinity,
15+
})
16+
})
17+
return () => offServiceWorkerUpdate()
18+
}, [])
19+
20+
return null
21+
}

frontend/src/lib/serviceWorker.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,64 @@
11
import "../sw?worker";
22

3+
type UpdateCallback = () => void;
4+
5+
let updateCallback: UpdateCallback | null = null;
6+
let updatePending = false;
7+
8+
function notifyUpdate() {
9+
if (!updateCallback) {
10+
updatePending = true;
11+
return;
12+
}
13+
updatePending = false;
14+
updateCallback();
15+
}
16+
17+
export function onServiceWorkerUpdate(callback: UpdateCallback): void {
18+
updateCallback = callback;
19+
if (updatePending) {
20+
updatePending = false;
21+
callback();
22+
}
23+
}
24+
25+
export function offServiceWorkerUpdate(): void {
26+
updateCallback = null;
27+
}
28+
329
export function registerServiceWorker(): void {
430
if (!("serviceWorker" in navigator)) return;
5-
navigator.serviceWorker.register("/sw.js", { scope: "/" }).catch(() => {});
31+
32+
navigator.serviceWorker.addEventListener("message", (event) => {
33+
if (event.data?.type === "SW_UPDATED") {
34+
notifyUpdate();
35+
}
36+
});
37+
38+
navigator.serviceWorker.ready.then((registration) => {
39+
if (registration.waiting) {
40+
notifyUpdate();
41+
}
42+
});
43+
44+
navigator.serviceWorker
45+
.register("/sw.js", { scope: "/" })
46+
.then((registration) => {
47+
registration.addEventListener("updatefound", () => {
48+
const installing = registration.installing;
49+
if (!installing) return;
50+
installing.addEventListener("statechange", () => {
51+
if (installing.state === "installed" && navigator.serviceWorker.controller) {
52+
notifyUpdate();
53+
}
54+
});
55+
});
56+
57+
setInterval(() => {
58+
registration.update().catch(() => {});
59+
}, 60 * 60 * 1000);
60+
})
61+
.catch(() => {});
662
}
763

864
export async function getServiceWorkerRegistration(): Promise<ServiceWorkerRegistration | null> {

frontend/src/sw.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ self.addEventListener("activate", (event) => {
1111
caches.keys().then((names) =>
1212
Promise.all(names.map((name) => caches.delete(name)))
1313
).then(() => self.clients.claim())
14+
.then(() => self.clients.matchAll({ type: "window" }))
15+
.then((clients) => {
16+
for (const client of clients) {
17+
client.postMessage({ type: "SW_UPDATED" });
18+
}
19+
})
1420
);
1521
});
1622

0 commit comments

Comments
 (0)