- Added service worker (sw.js) for offline functionality, caching strategies, and performance improvements. - Registered service worker in App component to manage updates and offline readiness. - Introduced ServiceWorkerStatus component to display connection status and provide cache management controls. - Created manifest.json for PWA configuration, including app name, icons, and display settings. - Updated index.html with PWA meta tags and links to support mobile web app capabilities. - Refactored authentication guards to utilize useAuthStatus hook for improved user state management. - Removed deprecated unregister-sw.js file to streamline service worker management.
273 lines
8.1 KiB
TypeScript
273 lines
8.1 KiB
TypeScript
// Service Worker Registration Utility
|
|
// Handles registration, updates, and error handling
|
|
|
|
import React from 'react';
|
|
|
|
const isLocalhost = Boolean(
|
|
window.location.hostname === 'localhost' ||
|
|
window.location.hostname === '[::1]' ||
|
|
window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
|
|
);
|
|
|
|
type Config = {
|
|
onSuccess?: (registration: ServiceWorkerRegistration) => void;
|
|
onUpdate?: (registration: ServiceWorkerRegistration) => void;
|
|
onOfflineReady?: () => void;
|
|
onError?: (error: Error) => void;
|
|
};
|
|
|
|
export function registerSW(config?: Config) {
|
|
if ('serviceWorker' in navigator) {
|
|
// Only register in production or when explicitly testing
|
|
const swUrl = '/sw.js';
|
|
|
|
if (isLocalhost) {
|
|
// This is running on localhost. Let's check if a service worker still exists or not.
|
|
checkValidServiceWorker(swUrl, config);
|
|
|
|
// Add some additional logging to localhost, pointing developers to the
|
|
// service worker/PWA documentation.
|
|
navigator.serviceWorker.ready.then(() => {
|
|
console.log(
|
|
'This web app is being served cache-first by a service ' +
|
|
'worker. To learn more, visit https://bit.ly/CRA-PWA'
|
|
);
|
|
});
|
|
} else {
|
|
// Is not localhost. Just register service worker
|
|
registerValidSW(swUrl, config);
|
|
}
|
|
} else {
|
|
console.log('Service workers are not supported in this browser.');
|
|
}
|
|
}
|
|
|
|
function registerValidSW(swUrl: string, config?: Config) {
|
|
navigator.serviceWorker
|
|
.register(swUrl)
|
|
.then(registration => {
|
|
console.log('Service Worker registered successfully:', registration);
|
|
|
|
registration.onupdatefound = () => {
|
|
const installingWorker = registration.installing;
|
|
if (installingWorker == null) {
|
|
return;
|
|
}
|
|
|
|
installingWorker.onstatechange = () => {
|
|
if (installingWorker.state === 'installed') {
|
|
if (navigator.serviceWorker.controller) {
|
|
// At this point, the updated precached content has been fetched,
|
|
// but the previous service worker will still serve the older
|
|
// content until all client tabs are closed.
|
|
console.log(
|
|
'New content is available and will be used when all ' +
|
|
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
|
|
);
|
|
|
|
// Execute callback
|
|
if (config && config.onUpdate) {
|
|
config.onUpdate(registration);
|
|
}
|
|
} else {
|
|
// At this point, everything has been precached.
|
|
// It's the perfect time to display a
|
|
// "Content is cached for offline use." message.
|
|
console.log('Content is cached for offline use.');
|
|
|
|
// Execute callback
|
|
if (config && config.onSuccess) {
|
|
config.onSuccess(registration);
|
|
}
|
|
|
|
if (config && config.onOfflineReady) {
|
|
config.onOfflineReady();
|
|
}
|
|
}
|
|
}
|
|
};
|
|
};
|
|
})
|
|
.catch(error => {
|
|
console.error('Error during service worker registration:', error);
|
|
if (config && config.onError) {
|
|
config.onError(error);
|
|
}
|
|
});
|
|
}
|
|
|
|
function checkValidServiceWorker(swUrl: string, config?: Config) {
|
|
// Check if the service worker can be found. If it can't reload the page.
|
|
fetch(swUrl, {
|
|
headers: { 'Service-Worker': 'script' },
|
|
})
|
|
.then(response => {
|
|
// Ensure service worker exists, and that we really are getting a JS file.
|
|
const contentType = response.headers.get('content-type');
|
|
if (
|
|
response.status === 404 ||
|
|
(contentType != null && contentType.indexOf('javascript') === -1)
|
|
) {
|
|
// No service worker found. Probably a different app. Reload the page.
|
|
navigator.serviceWorker.ready.then(registration => {
|
|
registration.unregister().then(() => {
|
|
window.location.reload();
|
|
});
|
|
});
|
|
} else {
|
|
// Service worker found. Proceed as normal.
|
|
registerValidSW(swUrl, config);
|
|
}
|
|
})
|
|
.catch(() => {
|
|
console.log(
|
|
'No internet connection found. App is running in offline mode.'
|
|
);
|
|
});
|
|
}
|
|
|
|
export function unregisterSW() {
|
|
if ('serviceWorker' in navigator) {
|
|
navigator.serviceWorker.ready
|
|
.then(registration => {
|
|
registration.unregister();
|
|
console.log('Service Worker unregistered successfully');
|
|
})
|
|
.catch(error => {
|
|
console.error('Error during service worker unregistration:', error);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Utility to communicate with service worker
|
|
export class ServiceWorkerManager {
|
|
private registration: ServiceWorkerRegistration | null = null;
|
|
|
|
constructor(registration?: ServiceWorkerRegistration) {
|
|
this.registration = registration || null;
|
|
}
|
|
|
|
// Send message to service worker
|
|
async sendMessage(type: string, payload?: any): Promise<any> {
|
|
if (!this.registration || !this.registration.active) {
|
|
throw new Error('Service Worker not available');
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const messageChannel = new MessageChannel();
|
|
|
|
messageChannel.port1.onmessage = (event) => {
|
|
if (event.data.error) {
|
|
reject(event.data.error);
|
|
} else {
|
|
resolve(event.data);
|
|
}
|
|
};
|
|
|
|
this.registration!.active!.postMessage(
|
|
{ type, payload },
|
|
[messageChannel.port2]
|
|
);
|
|
|
|
// Timeout after 5 seconds
|
|
setTimeout(() => {
|
|
reject(new Error('Service Worker message timeout'));
|
|
}, 5000);
|
|
});
|
|
}
|
|
|
|
// Get service worker version
|
|
async getVersion(): Promise<string> {
|
|
try {
|
|
const response = await this.sendMessage('GET_VERSION');
|
|
return response.version;
|
|
} catch (error) {
|
|
console.error('Failed to get service worker version:', error);
|
|
return 'unknown';
|
|
}
|
|
}
|
|
|
|
// Clear all caches
|
|
async clearCache(): Promise<boolean> {
|
|
try {
|
|
await this.sendMessage('CLEAR_CACHE');
|
|
return true;
|
|
} catch (error) {
|
|
console.error('Failed to clear cache:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Force update service worker
|
|
async forceUpdate(): Promise<void> {
|
|
if (!this.registration) return;
|
|
|
|
try {
|
|
await this.registration.update();
|
|
await this.sendMessage('SKIP_WAITING');
|
|
window.location.reload();
|
|
} catch (error) {
|
|
console.error('Failed to force update service worker:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Check if app is running offline
|
|
isOffline(): boolean {
|
|
return !navigator.onLine;
|
|
}
|
|
|
|
// Get cache storage estimate
|
|
async getCacheSize(): Promise<StorageEstimate | null> {
|
|
if ('storage' in navigator && 'estimate' in navigator.storage) {
|
|
try {
|
|
return await navigator.storage.estimate();
|
|
} catch (error) {
|
|
console.error('Failed to get storage estimate:', error);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Hook to use service worker in React components
|
|
export function useServiceWorker() {
|
|
const [isOffline, setIsOffline] = React.useState(!navigator.onLine);
|
|
const [swManager, setSWManager] = React.useState<ServiceWorkerManager | null>(null);
|
|
|
|
React.useEffect(() => {
|
|
const handleOnline = () => setIsOffline(false);
|
|
const handleOffline = () => setIsOffline(true);
|
|
|
|
window.addEventListener('online', handleOnline);
|
|
window.addEventListener('offline', handleOffline);
|
|
|
|
// Register service worker
|
|
registerSW({
|
|
onSuccess: (registration) => {
|
|
setSWManager(new ServiceWorkerManager(registration));
|
|
},
|
|
onUpdate: (registration) => {
|
|
// You could show a toast here asking user to refresh
|
|
console.log('New version available');
|
|
setSWManager(new ServiceWorkerManager(registration));
|
|
},
|
|
onOfflineReady: () => {
|
|
console.log('App ready for offline use');
|
|
}
|
|
});
|
|
|
|
return () => {
|
|
window.removeEventListener('online', handleOnline);
|
|
window.removeEventListener('offline', handleOffline);
|
|
};
|
|
}, []);
|
|
|
|
return {
|
|
isOffline,
|
|
swManager,
|
|
clearCache: () => swManager?.clearCache(),
|
|
forceUpdate: () => swManager?.forceUpdate(),
|
|
getVersion: () => swManager?.getVersion(),
|
|
};
|
|
}
|