'use client';
/**
* ServiceWorkerRegistration Component
*
* Registers the service worker for PWA functionality.
* Handles updates and provides user notification for new versions.
*/
import { useEffect, useState, useCallback } from 'react';
import { RefreshCw, X } from 'lucide-react';
export function ServiceWorkerRegistration() {
const [waitingWorker, setWaitingWorker] = useState<ServiceWorker | null>(null);
const [showUpdateBanner, setShowUpdateBanner] = useState(false);
const reloadPage = useCallback(() => {
if (waitingWorker) {
waitingWorker.postMessage({ type: 'SKIP_WAITING' });
}
window.location.reload();
}, [waitingWorker]);
useEffect(() => {
// Only register in production and if service workers are supported
if (
typeof window === 'undefined' ||
!('serviceWorker' in navigator) ||
process.env.NODE_ENV === 'development'
) {
return;
}
const registerServiceWorker = async () => {
try {
const registration = await navigator.serviceWorker.register('/sw.js', {
scope: '/',
});
console.log('[PWA] Service worker registered:', registration.scope);
// Check for updates periodically
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
if (!newWorker) return;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// New content is available
setWaitingWorker(newWorker);
setShowUpdateBanner(true);
console.log('[PWA] New content available, update ready');
}
});
});
// Check for waiting service worker on load
if (registration.waiting) {
setWaitingWorker(registration.waiting);
setShowUpdateBanner(true);
}
// Detect controller change (update applied)
let refreshing = false;
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (refreshing) return;
refreshing = true;
window.location.reload();
});
} catch (error) {
console.error('[PWA] Service worker registration failed:', error);
}
};
// Register after page load to not block initial render
if (document.readyState === 'complete') {
registerServiceWorker();
} else {
window.addEventListener('load', registerServiceWorker);
return () => window.removeEventListener('load', registerServiceWorker);
}
}, []);
// Handle messages from service worker
useEffect(() => {
if (typeof window === 'undefined' || !('serviceWorker' in navigator)) {
return;
}
const handleMessage = (event: MessageEvent) => {
if (event.data?.type === 'CACHE_UPDATED') {
console.log('[PWA] Cache updated:', event.data.url);
}
};
navigator.serviceWorker.addEventListener('message', handleMessage);
return () => navigator.serviceWorker.removeEventListener('message', handleMessage);
}, []);
// Don't render anything if no update is available
if (!showUpdateBanner) {
return null;
}
return (
<div className="fixed bottom-4 left-4 right-4 md:left-auto md:right-4 md:w-96 z-50 animate-in slide-in-from-bottom-4">
<div className="bg-bg-elevated border border-border-subtle rounded-lg shadow-lg p-4">
<div className="flex items-start gap-3">
<div className="flex-shrink-0 p-2 bg-accent-red/10 rounded-lg">
<RefreshCw className="w-5 h-5 text-accent-red" />
</div>
<div className="flex-1 min-w-0">
<h3 className="text-sm font-semibold text-text-primary">
Update Available
</h3>
<p className="text-sm text-text-secondary mt-1">
A new version of CanadaGPT is available. Refresh to get the latest features.
</p>
<div className="flex gap-2 mt-3">
<button
onClick={reloadPage}
className="px-3 py-1.5 text-sm font-medium bg-accent-red text-white rounded-lg hover:bg-accent-red/90 transition-colors"
>
Refresh Now
</button>
<button
onClick={() => setShowUpdateBanner(false)}
className="px-3 py-1.5 text-sm font-medium text-text-secondary hover:text-text-primary transition-colors"
>
Later
</button>
</div>
</div>
<button
onClick={() => setShowUpdateBanner(false)}
className="flex-shrink-0 p-1 text-text-tertiary hover:text-text-primary transition-colors"
aria-label="Dismiss"
>
<X className="w-4 h-4" />
</button>
</div>
</div>
</div>
);
}