veza/apps/web/src/hooks/usePWA.ts

205 lines
4.8 KiB
TypeScript

/**
* PWA Hook - React hook for Progressive Web App functionality
* FE-TYPE-012: Fully typed hook return
*/
import { useState, useEffect } from 'react';
import { pwaService, type PWAStatus } from '@/services/pwa';
import { logger } from '@/utils/logger';
import type {
UsePWAReturn,
UsePWAInstallBannerReturn,
UseOfflineDetectionReturn,
} from './types';
export function usePWA(): UsePWAReturn {
const [status, setStatus] = useState<PWAStatus>(pwaService.getStatus());
const [isInstalling, setIsInstalling] = useState(false);
const [isUpdating, setIsUpdating] = useState(false);
useEffect(() => {
// Subscribe to PWA status changes
const unsubscribe = pwaService.onStatusChange(setStatus);
// Initial status check
setStatus(pwaService.getStatus());
return unsubscribe;
}, []);
/**
* Install the PWA
*/
const install = async (): Promise<boolean> => {
if (!status.isInstallable || isInstalling) {
return false;
}
setIsInstalling(true);
try {
const result = await pwaService.promptInstall();
return result;
} catch (error) {
logger.error('[PWA Hook] Install failed', {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
});
return false;
} finally {
setIsInstalling(false);
}
};
/**
* Update the service worker
*/
const update = async (): Promise<void> => {
if (!status.updateAvailable || isUpdating) {
return;
}
setIsUpdating(true);
try {
await pwaService.updateServiceWorker();
} catch (error) {
logger.error('[PWA Hook] Update failed', {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
});
} finally {
setIsUpdating(false);
}
};
/**
* Request notification permission
*/
const requestNotifications = async (): Promise<NotificationPermission> => {
return await pwaService.requestNotificationPermission();
};
/**
* Show a notification
*/
const showNotification = async (
title: string,
options?: NotificationOptions,
): Promise<void> => {
return await pwaService.showNotification(title, options);
};
/**
* Clear all caches
*/
const clearCaches = async (): Promise<void> => {
return await pwaService.clearCaches();
};
/**
* Get service worker version
*/
const getVersion = async (): Promise<string> => {
return await pwaService.getVersion();
};
return {
// Status
...status,
hasServiceWorker: status.serviceWorkerReady,
// Loading states
isInstalling,
isUpdating,
// Actions
install,
update,
requestNotifications,
showNotification,
clearCaches,
getVersion,
// Computed properties
canInstall: status.isInstallable && !isInstalling,
canUpdate: status.updateAvailable && !isUpdating,
isOffline: !status.isOnline,
};
}
/**
* Hook for PWA install banner
* FE-TYPE-012: Fully typed hook return
*/
export function usePWAInstallBanner(): UsePWAInstallBannerReturn {
const { isInstallable, isInstalled, install, isInstalling } = usePWA();
const [showBanner, setShowBanner] = useState(false);
const [dismissed, setDismissed] = useState(false);
useEffect(() => {
// Show banner if app is installable and not dismissed
const shouldShow = isInstallable && !isInstalled && !dismissed;
setShowBanner(shouldShow);
}, [isInstallable, isInstalled, dismissed]);
const handleInstall = async () => {
const success = await install();
if (success) {
setShowBanner(false);
}
};
const handleDismiss = () => {
setDismissed(true);
setShowBanner(false);
// Remember dismissal for this session
sessionStorage.setItem('pwa-install-dismissed', 'true');
};
// Check if banner was dismissed this session
useEffect(() => {
const wasDismissed =
sessionStorage.getItem('pwa-install-dismissed') === 'true';
setDismissed(wasDismissed);
}, []);
return {
showBanner,
isInstalling,
handleInstall,
handleDismiss,
};
}
/**
* Hook for offline detection and handling
* FE-TYPE-012: Fully typed hook return
*/
export function useOfflineDetection(): UseOfflineDetectionReturn {
const { isOnline } = usePWA();
const [showOfflineBanner, setShowOfflineBanner] = useState(false);
useEffect(() => {
if (!isOnline) {
setShowOfflineBanner(true);
// Auto-hide after 5 seconds
const timer = setTimeout(() => {
setShowOfflineBanner(false);
}, 5000);
return () => clearTimeout(timer);
}
setShowOfflineBanner(false);
return undefined;
}, [isOnline]);
return {
isOnline,
isOffline: !isOnline,
showOfflineBanner,
dismissOfflineBanner: () => setShowOfflineBanner(false),
};
}