| interface NavigatorUADataLike { |
| mobile?: boolean |
| } |
|
|
| interface NavigatorLike { |
| userAgent?: string |
| platform?: string |
| maxTouchPoints?: number |
| userAgentData?: NavigatorUADataLike |
| } |
|
|
| interface MediaQueryResultLike { |
| matches: boolean |
| } |
|
|
| interface DeviceDetectionEnvironment { |
| navigator?: NavigatorLike |
| matchMedia?: (query: string) => MediaQueryResultLike | null | undefined |
| } |
|
|
| const MOBILE_UA_RE = /\b(Mobi|Android|iPhone|iPod|Windows Phone|webOS|BlackBerry|IEMobile)\b/i |
| const TABLET_UA_RE = /\b(iPad|Tablet)\b/i |
|
|
| function matchesQuery( |
| matchMedia: DeviceDetectionEnvironment['matchMedia'], |
| query: string, |
| ): boolean { |
| try { |
| return matchMedia?.(query)?.matches === true |
| } catch { |
| return false |
| } |
| } |
|
|
| export function detectMobileDevice(env: DeviceDetectionEnvironment = {}): boolean { |
| const nav = env.navigator |
| if (!nav) return false |
|
|
| if (nav.userAgentData?.mobile === true) { |
| return true |
| } |
|
|
| const userAgent = nav.userAgent || '' |
| const maxTouchPoints = nav.maxTouchPoints ?? 0 |
| const isIPadOSDesktopMode = nav.platform === 'MacIntel' && maxTouchPoints > 1 |
| const isMobileUA = MOBILE_UA_RE.test(userAgent) |
| const isTabletUA = TABLET_UA_RE.test(userAgent) || isIPadOSDesktopMode |
| const coarsePointer = matchesQuery(env.matchMedia, '(pointer: coarse)') |
| const noHover = matchesQuery(env.matchMedia, '(hover: none)') |
| const hasTouch = maxTouchPoints > 0 |
|
|
| return isMobileUA || isTabletUA || (coarsePointer && noHover && hasTouch) |
| } |
|
|
| export function isMobileDevice(): boolean { |
| if (typeof navigator === 'undefined') return false |
|
|
| return detectMobileDevice({ |
| navigator, |
| matchMedia: typeof window !== 'undefined' ? window.matchMedia.bind(window) : undefined, |
| }) |
| } |
|
|