/** * Unified Mobile Detection Composable * Provides consistent mobile detection across the app */ export interface MobileDetectionState { isMobile: boolean; isSafari: boolean; isMobileSafari: boolean; isIOS: boolean; isAndroid: boolean; safariVersion?: number; viewportHeight: number; isInitialized: boolean; } // Global state to ensure single source of truth const globalState = reactive({ isMobile: false, isSafari: false, isMobileSafari: false, isIOS: false, isAndroid: false, safariVersion: undefined, viewportHeight: 0, isInitialized: false }); // Track if listeners are already set up let listenersSetup = false; export const useMobileDetection = () => { // Initialize detection on first use if (!globalState.isInitialized && typeof window !== 'undefined') { detectDevice(); globalState.isInitialized = true; // Set up listeners only once globally if (!listenersSetup) { setupListeners(); listenersSetup = true; } } // Return readonly state to prevent external modifications return readonly(globalState); }; function detectDevice() { if (typeof window === 'undefined') return; const userAgent = navigator.userAgent; // Detect mobile globalState.isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent); // Detect iOS globalState.isIOS = /iPad|iPhone|iPod/.test(userAgent) && !(window as any).MSStream; // Detect Android globalState.isAndroid = /Android/i.test(userAgent); // Detect Safari globalState.isSafari = /^((?!chrome|android).)*safari/i.test(userAgent); // Detect Mobile Safari specifically globalState.isMobileSafari = globalState.isIOS && globalState.isSafari; // Extract Safari version if (globalState.isSafari) { const match = userAgent.match(/Version\/(\d+)/); if (match) { globalState.safariVersion = parseInt(match[1]); } } // Set initial viewport height globalState.viewportHeight = window.innerHeight; } function setupListeners() { if (typeof window === 'undefined') return; let resizeTimeout: NodeJS.Timeout; let lastHeight = window.innerHeight; let initialHeight = window.innerHeight; let keyboardOpen = false; const handleResize = () => { // Don't handle resize if we're in the middle of a transition if (document.hidden) return; clearTimeout(resizeTimeout); // Debounce with longer delay for mobile const debounceDelay = globalState.isMobile ? 300 : 150; resizeTimeout = setTimeout(() => { const newHeight = window.innerHeight; const heightDiff = newHeight - lastHeight; const absoluteDiff = Math.abs(heightDiff); // Detect keyboard open/close on mobile if (globalState.isMobile) { // Keyboard likely opened (viewport got smaller) if (heightDiff < -100 && newHeight < initialHeight * 0.75) { keyboardOpen = true; // Don't trigger viewport updates for keyboard changes return; } // Keyboard likely closed (viewport got bigger) if (heightDiff > 100 && keyboardOpen) { keyboardOpen = false; // Don't trigger viewport updates for keyboard changes return; } } // Only update for significant non-keyboard changes (more than 50px) // or orientation changes (height differs significantly from initial) const isOrientationChange = absoluteDiff > initialHeight * 0.3; const isSignificantChange = absoluteDiff > 50; if (isOrientationChange || (isSignificantChange && !keyboardOpen)) { lastHeight = newHeight; globalState.viewportHeight = newHeight; // Update CSS custom property for mobile Safari if (globalState.isMobileSafari) { const vh = newHeight * 0.01; document.documentElement.style.setProperty('--vh', `${vh}px`); } // Update initial height after orientation change if (isOrientationChange) { initialHeight = newHeight; } } }, debounceDelay); }; // Add resize listener with passive option for better performance window.addEventListener('resize', handleResize, { passive: true }); // Also listen for orientation changes on mobile if (globalState.isMobile) { window.addEventListener('orientationchange', () => { // Reset keyboard state on orientation change keyboardOpen = false; // Wait for orientation change to complete setTimeout(handleResize, 200); }); } // Clean up on app unmount (if needed) if (typeof window !== 'undefined') { const cleanup = () => { window.removeEventListener('resize', handleResize); if (globalState.isMobile) { window.removeEventListener('orientationchange', handleResize); } }; // Store cleanup function for manual cleanup if needed (window as any).__mobileDetectionCleanup = cleanup; } } // Export helper functions export const isMobileSafari = () => globalState.isMobileSafari; export const isMobile = () => globalState.isMobile; export const isIOS = () => globalState.isIOS; export const getViewportHeight = () => globalState.viewportHeight;