| /** | |
| * Mobile Detection Utility | |
| * | |
| * Provides a cached, non-reactive mobile detection for use outside React components. | |
| * Used by service worker registration, query client configuration, and other | |
| * non-component code that needs to know if the device is mobile. | |
| * | |
| * For React components, use the `useIsMobile()` hook from `hooks/use-media-query.ts` | |
| * instead, which responds to viewport changes reactively. | |
| */ | |
| /** | |
| * Cached mobile detection result. | |
| * Evaluated once on module load for consistent behavior across the app lifetime. | |
| * Uses both media query and user agent for reliability: | |
| * - Media query catches small desktop windows | |
| * - User agent catches mobile browsers at any viewport size | |
| * - Touch detection as supplementary signal | |
| */ | |
| export const isMobileDevice: boolean = (() => { | |
| if (typeof window === 'undefined') return false; | |
| // Check viewport width (consistent with useIsMobile hook's 768px breakpoint) | |
| const isSmallViewport = window.matchMedia('(max-width: 768px)').matches; | |
| // Check user agent for mobile devices | |
| const isMobileUA = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( | |
| navigator.userAgent | |
| ); | |
| // Check for touch-primary device (most mobile devices) | |
| const isTouchPrimary = 'ontouchstart' in window || navigator.maxTouchPoints > 0; | |
| // Consider it mobile if viewport is small OR if it's a mobile UA with touch | |
| return isSmallViewport || (isMobileUA && isTouchPrimary); | |
| })(); | |
| /** | |
| * Check if the device has a slow connection. | |
| * Uses the Network Information API when available. | |
| * Falls back to mobile detection as a heuristic. | |
| */ | |
| export function isSlowConnection(): boolean { | |
| if (typeof navigator === 'undefined') return false; | |
| const connection = ( | |
| navigator as Navigator & { | |
| connection?: { | |
| effectiveType?: string; | |
| saveData?: boolean; | |
| }; | |
| } | |
| ).connection; | |
| if (connection) { | |
| // Respect data saver mode | |
| if (connection.saveData) return true; | |
| // 2g and slow-2g are definitely slow | |
| if (connection.effectiveType === '2g' || connection.effectiveType === 'slow-2g') return true; | |
| } | |
| // On mobile without connection info, assume potentially slow | |
| return false; | |
| } | |
| /** | |
| * Detect if the app is running as an installed PWA (standalone mode). | |
| * Checks both the standard display-mode media query and the iOS-specific | |
| * navigator.standalone property for comprehensive detection. | |
| * | |
| * When running as a PWA, the browser chrome is hidden so safe area insets | |
| * can be reduced further to maximize usable screen space. | |
| */ | |
| export const isPwaStandalone: boolean = (() => { | |
| if (typeof window === 'undefined') return false; | |
| // Standard: works on Chrome, Edge, Firefox, and modern Safari | |
| const isStandalone = window.matchMedia('(display-mode: standalone)').matches; | |
| // iOS Safari: navigator.standalone is true when launched from home screen | |
| const isIOSStandalone = (navigator as Navigator & { standalone?: boolean }).standalone === true; | |
| return isStandalone || isIOSStandalone; | |
| })(); | |
| /** | |
| * Multiplier for polling intervals on mobile. | |
| * Mobile devices benefit from less frequent polling to save battery and bandwidth. | |
| * Slow connections get an even larger multiplier. | |
| */ | |
| export function getMobilePollingMultiplier(): number { | |
| if (!isMobileDevice) return 1; | |
| if (isSlowConnection()) return 4; | |
| return 2; | |
| } | |