|
|
|
@@ -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"; |
|
|
|
|