import React, { createContext, useContext, useState, useEffect, useCallback, } from "react"; // Device types enum for better type safety export enum DeviceType { DESKTOP = "desktop", TABLET = "tablet", MOBILE = "mobile", } // Screen size breakpoints configuration export const BREAKPOINTS = { mobile: 768, tablet: 1024, desktop: 1200, } as const; // Device detection result interface export interface DeviceInfo { type: DeviceType; isMobile: boolean; isTablet: boolean; isDesktop: boolean; screenWidth: number; screenHeight: number; orientation: "portrait" | "landscape"; isTouchDevice: boolean; pixelRatio: number; userAgent: string; } // Context for device detection interface DeviceDetectorContextType { deviceInfo: DeviceInfo; isLoading: boolean; forceRefresh: () => void; } const DeviceDetectorContext = createContext< DeviceDetectorContextType | undefined >(undefined); // Utility functions for device detection const getUserAgent = (): string => { if (typeof window === "undefined") return ""; return window.navigator.userAgent; }; const isTouchSupported = (): boolean => { if (typeof window === "undefined") return false; return "ontouchstart" in window || navigator.maxTouchPoints > 0; }; const getMobileUserAgentPattern = (): RegExp => { return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|mobile|CriOS/i; }; const getTabletUserAgentPattern = (): RegExp => { return /iPad|Android(?=.*\bMobile\b)(?=.*\bTablet\b)|KFAPWI|KFAPWA|KFARWI|KFASWI|KFTHWI|KFTHWA|KFAPWI|KFAPWA|KFOT|KFTT|KFJWI|KFJWA|KFSOWI|KFGIWI|KFMEWI/i; }; const detectDeviceFromUserAgent = (userAgent: string): DeviceType => { if (getTabletUserAgentPattern().test(userAgent)) { return DeviceType.TABLET; } if (getMobileUserAgentPattern().test(userAgent)) { return DeviceType.MOBILE; } return DeviceType.DESKTOP; }; const detectDeviceFromScreenSize = ( width: number, height: number ): DeviceType => { const screenSize = Math.max(width, height); if (screenSize < BREAKPOINTS.mobile) { return DeviceType.MOBILE; } if (screenSize < BREAKPOINTS.tablet) { return DeviceType.TABLET; } return DeviceType.DESKTOP; }; const getOrientation = ( width: number, height: number ): "portrait" | "landscape" => { return width > height ? "landscape" : "portrait"; }; const createDeviceInfo = (): DeviceInfo => { if (typeof window === "undefined") { // Server-side rendering fallback return { type: DeviceType.DESKTOP, isMobile: false, isTablet: false, isDesktop: true, screenWidth: 1920, screenHeight: 1080, orientation: "landscape", isTouchDevice: false, pixelRatio: 1, userAgent: "", }; } const screenWidth = window.innerWidth; const screenHeight = window.innerHeight; const userAgent = getUserAgent(); const isTouchDevice = isTouchSupported(); // Combine user agent and screen size detection for accuracy const deviceFromUA = detectDeviceFromUserAgent(userAgent); const deviceFromScreen = detectDeviceFromScreenSize( screenWidth, screenHeight ); // Priority logic: user agent detection takes precedence, but screen size can override for edge cases let finalDeviceType = deviceFromUA; // Special case: if user agent says mobile but screen is very large, it might be desktop if ( deviceFromUA === DeviceType.MOBILE && screenWidth > BREAKPOINTS.tablet && !isTouchDevice ) { finalDeviceType = DeviceType.DESKTOP; } // Special case: if user agent says desktop but screen is small and touch-enabled, likely mobile if ( deviceFromUA === DeviceType.DESKTOP && screenWidth < BREAKPOINTS.mobile && isTouchDevice ) { finalDeviceType = DeviceType.MOBILE; } return { type: finalDeviceType, isMobile: finalDeviceType === DeviceType.MOBILE, isTablet: finalDeviceType === DeviceType.TABLET, isDesktop: finalDeviceType === DeviceType.DESKTOP, screenWidth, screenHeight, orientation: getOrientation(screenWidth, screenHeight), isTouchDevice, pixelRatio: window.devicePixelRatio || 1, userAgent, }; }; // Custom hook for device detection export const useDeviceDetection = (): DeviceDetectorContextType => { const context = useContext(DeviceDetectorContext); if (context === undefined) { throw new Error( "useDeviceDetection must be used within a DeviceDetectorProvider" ); } return context; }; // Legacy hook for backward compatibility export const useIsMobile = (): boolean => { const { deviceInfo } = useDeviceDetection(); return deviceInfo.isMobile; }; // Device detector provider component interface DeviceDetectorProviderProps { children: React.ReactNode; debounceMs?: number; enableLogging?: boolean; } export const DeviceDetectorProvider: React.FC = ({ children, debounceMs = 250, enableLogging = false, }) => { const [deviceInfo, setDeviceInfo] = useState(createDeviceInfo); const [isLoading, setIsLoading] = useState(true); const updateDeviceInfo = useCallback(() => { const newDeviceInfo = createDeviceInfo(); if (enableLogging) { console.log("Device detection updated:", newDeviceInfo); } setDeviceInfo(newDeviceInfo); setIsLoading(false); }, [enableLogging]); const forceRefresh = useCallback(() => { setIsLoading(true); updateDeviceInfo(); }, [updateDeviceInfo]); useEffect(() => { // Initial detection updateDeviceInfo(); // Debounced resize handler let timeoutId: NodeJS.Timeout; const handleResize = () => { clearTimeout(timeoutId); setIsLoading(true); timeoutId = setTimeout(() => { updateDeviceInfo(); }, debounceMs); }; // Listen for screen size changes window.addEventListener("resize", handleResize); window.addEventListener("orientationchange", handleResize); // Visual viewport API for mobile keyboard detection if ("visualViewport" in window) { window.visualViewport?.addEventListener("resize", handleResize); } return () => { clearTimeout(timeoutId); window.removeEventListener("resize", handleResize); window.removeEventListener("orientationchange", handleResize); if ("visualViewport" in window) { window.visualViewport?.removeEventListener("resize", handleResize); } }; }, [updateDeviceInfo, debounceMs]); const contextValue: DeviceDetectorContextType = { deviceInfo, isLoading, forceRefresh, }; return ( {children} ); }; // Utility component for conditional rendering interface DeviceConditionalProps { mobile?: React.ReactNode; tablet?: React.ReactNode; desktop?: React.ReactNode; fallback?: React.ReactNode; } export const DeviceConditional: React.FC = ({ mobile, tablet, desktop, fallback, }) => { const { deviceInfo } = useDeviceDetection(); switch (deviceInfo.type) { case DeviceType.MOBILE: return mobile ? <>{mobile} : fallback ? <>{fallback} : null; case DeviceType.TABLET: return tablet ? <>{tablet} : fallback ? <>{fallback} : null; case DeviceType.DESKTOP: return desktop ? <>{desktop} : fallback ? <>{fallback} : null; default: return fallback ? <>{fallback} : null; } }; // Debug component for development export const DeviceDetectionDebug: React.FC = () => { const { deviceInfo, isLoading, forceRefresh } = useDeviceDetection(); if (process.env.NODE_ENV === "production") { return null; } return (
Device Detection {isLoading && "(Loading...)"}
Type: {deviceInfo.type}
Screen: {deviceInfo.screenWidth}x{deviceInfo.screenHeight}
Orientation: {deviceInfo.orientation}
Touch: {deviceInfo.isTouchDevice ? "Yes" : "No"}
Pixel Ratio: {deviceInfo.pixelRatio}
UA: {deviceInfo.userAgent.substring(0, 50)}...
); }; export default DeviceDetectorProvider;