feat(pwa): implement service worker and PWA enhancements

- 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.
This commit is contained in:
chamiakJ
2025-07-10 14:07:03 +05:30
parent bb8e6ee60f
commit bcfa18b1e8
16 changed files with 1238 additions and 187 deletions

View File

@@ -29,24 +29,12 @@ const withCodeSplitting = (Component: React.LazyExoticComponent<React.ComponentT
};
// Memoized guard components with defensive programming
import { useAuthStatus } from '@/hooks/useAuthStatus';
export const AuthGuard = memo(({ children }: GuardProps) => {
const authService = useAuthService();
const location = useLocation();
const { isAuthenticated, location } = useAuthStatus();
const shouldRedirect = useMemo(() => {
try {
// Defensive check to ensure authService and its methods exist
if (!authService || typeof authService.isAuthenticated !== 'function') {
return false; // Don't redirect if auth service is not ready
}
return !authService.isAuthenticated();
} catch (error) {
console.error('Error in AuthGuard:', error);
return false; // Don't redirect on error, let the app handle it
}
}, [authService]);
if (shouldRedirect) {
if (!isAuthenticated) {
return <Navigate to="/auth" state={{ from: location }} replace />;
}
@@ -56,41 +44,14 @@ export const AuthGuard = memo(({ children }: GuardProps) => {
AuthGuard.displayName = 'AuthGuard';
export const AdminGuard = memo(({ children }: GuardProps) => {
const authService = useAuthService();
const location = useLocation();
const { isAuthenticated, isAdmin, location } = useAuthStatus();
const guardResult = useMemo(() => {
try {
// Defensive checks to ensure authService and its methods exist
if (
!authService ||
typeof authService.isAuthenticated !== 'function' ||
typeof authService.isOwnerOrAdmin !== 'function' ||
typeof authService.getCurrentSession !== 'function'
) {
return null; // Don't redirect if auth service is not ready
}
if (!isAuthenticated) {
return <Navigate to="/auth" state={{ from: location }} replace />;
}
if (!authService.isAuthenticated()) {
return { redirect: '/auth', state: { from: location } };
}
const currentSession = authService.getCurrentSession();
const isFreePlan = currentSession?.subscription_type === ISUBSCRIPTION_TYPE.FREE;
if (!authService.isOwnerOrAdmin() || isFreePlan) {
return { redirect: '/worklenz/unauthorized' };
}
return null;
} catch (error) {
console.error('Error in AdminGuard:', error);
return null; // Don't redirect on error
}
}, [authService, location]);
if (guardResult) {
return <Navigate to={guardResult.redirect} state={guardResult.state} replace />;
if (!isAdmin) {
return <Navigate to="/worklenz/unauthorized" />;
}
return <>{children}</>;
@@ -99,77 +60,12 @@ export const AdminGuard = memo(({ children }: GuardProps) => {
AdminGuard.displayName = 'AdminGuard';
export const LicenseExpiryGuard = memo(({ children }: GuardProps) => {
const authService = useAuthService();
const location = useLocation();
const { isLicenseExpired, location } = useAuthStatus();
const shouldRedirect = useMemo(() => {
try {
// Defensive checks to ensure authService and its methods exist
if (
!authService ||
typeof authService.isAuthenticated !== 'function' ||
typeof authService.getCurrentSession !== 'function'
) {
return false; // Don't redirect if auth service is not ready
}
const isAdminCenterRoute = location.pathname.includes('/worklenz/admin-center');
const isLicenseExpiredRoute = location.pathname === '/worklenz/license-expired';
if (!authService.isAuthenticated()) return false;
const isAdminCenterRoute = location.pathname.includes('/worklenz/admin-center');
const isLicenseExpiredRoute = location.pathname === '/worklenz/license-expired';
// Don't check or redirect if we're already on the license-expired page
if (isLicenseExpiredRoute) return false;
const currentSession = authService.getCurrentSession();
// Check if trial is expired more than 7 days or if is_expired flag is set
const isLicenseExpiredMoreThan7Days = () => {
// Quick bail if no session data is available
if (!currentSession) return false;
// Check is_expired flag first
if (currentSession.is_expired) {
// If no trial_expire_date exists but is_expired is true, defer to backend check
if (!currentSession.trial_expire_date) return true;
// If there is a trial_expire_date, check if it's more than 7 days past
const today = new Date();
const expiryDate = new Date(currentSession.trial_expire_date);
const diffTime = today.getTime() - expiryDate.getTime();
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
// Redirect if more than 7 days past expiration
return diffDays > 7;
}
// If not marked as expired but has trial_expire_date, do a date check
if (
currentSession.subscription_type === ISUBSCRIPTION_TYPE.TRIAL &&
currentSession.trial_expire_date
) {
const today = new Date();
const expiryDate = new Date(currentSession.trial_expire_date);
const diffTime = today.getTime() - expiryDate.getTime();
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
// If expired more than 7 days, redirect
return diffDays > 7;
}
// No expiration data found
return false;
};
return isLicenseExpiredMoreThan7Days() && !isAdminCenterRoute;
} catch (error) {
console.error('Error in LicenseExpiryGuard:', error);
return false; // Don't redirect on error
}
}, [authService, location.pathname]);
if (shouldRedirect) {
if (isLicenseExpired && !isAdminCenterRoute && !isLicenseExpiredRoute) {
return <Navigate to="/worklenz/license-expired" replace />;
}
@@ -179,26 +75,16 @@ export const LicenseExpiryGuard = memo(({ children }: GuardProps) => {
LicenseExpiryGuard.displayName = 'LicenseExpiryGuard';
export const SetupGuard = memo(({ children }: GuardProps) => {
const authService = useAuthService();
const location = useLocation();
const { isAuthenticated, isSetupComplete, location } = useAuthStatus();
const shouldRedirect = useMemo(() => {
try {
// Defensive check to ensure authService and its methods exist
if (!authService || typeof authService.isAuthenticated !== 'function') {
return false; // Don't redirect if auth service is not ready
}
return !authService.isAuthenticated();
} catch (error) {
console.error('Error in SetupGuard:', error);
return false; // Don't redirect on error
}
}, [authService]);
if (shouldRedirect) {
if (!isAuthenticated) {
return <Navigate to="/auth" state={{ from: location }} replace />;
}
if (!isSetupComplete) {
return <Navigate to="/worklenz/setup" />;
}
return <>{children}</>;
});

View File

@@ -0,0 +1,17 @@
import { redirect } from 'react-router-dom';
import { store } from '../store';
import { verifyAuthentication } from '@/features/auth/authSlice';
export const authLoader = async () => {
const session = await store.dispatch(verifyAuthentication()).unwrap();
if (!session.user) {
return redirect('/auth/login');
}
if (session.user.is_expired) {
return redirect('/worklenz/license-expired');
}
return session;
};