| @@ -61,19 +61,36 @@ export function formatClientIpDisplay(ip?: string | null): string { | |||
| return ip.trim(); | |||
| } | |||
| /** CSS px; tablets in portrait are often 600–800; phones are usually smaller. */ | |||
| const TABLET_MIN_SHORT_SIDE_PX = 480; | |||
| function hasTouchCapability(): boolean { | |||
| if (typeof navigator === "undefined") return false; | |||
| const touchPoints = navigator.maxTouchPoints ?? 0; | |||
| return touchPoints > 0 || "ontouchstart" in window; | |||
| } | |||
| export function detectClientTypeFromUa(): string { | |||
| if (typeof navigator === "undefined") return "unknown"; | |||
| const ua = navigator.userAgent.toLowerCase(); | |||
| const touchPoints = navigator.maxTouchPoints ?? 0; | |||
| const hasTouch = hasTouchCapability(); | |||
| const shortSide = screenShortSidePx(); | |||
| // iPadOS 13+ "desktop" Safari: MacIntel + multi-touch, no "iPad" in UA string | |||
| const isIpad = | |||
| ua.includes("ipad") || | |||
| (navigator.platform === "MacIntel" && touchPoints > 1); | |||
| if (ua.includes("iphone") || ua.includes("ipod")) { | |||
| return "mobile"; | |||
| } | |||
| if (ua.includes("ipad") || ua.includes("tablet")) { | |||
| return "tablet"; | |||
| } | |||
| if (isIpad || ua.includes("tablet")) { | |||
| // iPadOS 13+ "desktop" Safari: MacIntel + multi-touch, no "iPad" in UA string | |||
| if ( | |||
| (navigator.platform === "MacIntel" || ua.includes("macintosh")) && | |||
| touchPoints > 1 | |||
| ) { | |||
| return "tablet"; | |||
| } | |||
| @@ -83,24 +100,30 @@ export function detectClientTypeFromUa(): string { | |||
| const platform = uad.platform?.toLowerCase() ?? ""; | |||
| if (platform.includes("android")) { | |||
| if (!uad.mobile) return "tablet"; | |||
| if (touchPoints > 0 && shortSide >= 600) return "tablet"; | |||
| if (hasTouch && shortSide >= TABLET_MIN_SHORT_SIDE_PX) return "tablet"; | |||
| return "mobile"; | |||
| } | |||
| if (platform === "ios" && hasTouch) { | |||
| return "tablet"; | |||
| } | |||
| } | |||
| if (ua.includes("android")) { | |||
| // Many Android tablets still include "Mobile" in the UA string | |||
| // Warehouse slates almost always include "Mobile" in UA even when not phones | |||
| if (!ua.includes("mobile")) return "tablet"; | |||
| if (touchPoints > 0 && shortSide >= 600) return "tablet"; | |||
| return "mobile"; | |||
| if (hasTouch && shortSide >= TABLET_MIN_SHORT_SIDE_PX) return "tablet"; | |||
| // Small Android phone | |||
| if (shortSide > 0 && shortSide < 400) return "mobile"; | |||
| return "tablet"; | |||
| } | |||
| if (ua.includes("iphone") || ua.includes("ipod")) { | |||
| if (ua.includes("mobile")) { | |||
| return "mobile"; | |||
| } | |||
| if (ua.includes("mobile")) { | |||
| return "mobile"; | |||
| // Touch slate / Surface / kiosk with desktop-like UA | |||
| if (hasTouch && shortSide >= 400) { | |||
| return "tablet"; | |||
| } | |||
| return "desktop"; | |||