@dfsol/platform-detector
Version:
Universal platform detector for web, PWA, Telegram Mini Apps, and native mobile applications with Client Hints API, feature detection, and enhanced accuracy
299 lines (298 loc) • 9.86 kB
JavaScript
/**
* Feature-based detection module
* Uses browser APIs and feature detection to validate and enhance user agent parsing
* More reliable than user agent strings as features cannot be spoofed easily
*/
/**
* Feature detector for device capability detection
*/
export class FeatureDetector {
/**
* Detect all available device features
*/
static detect() {
if (typeof window === 'undefined') {
return this.createServerSideFeatures();
}
return {
touch: this.detectTouchSupport(),
maxTouchPoints: navigator.maxTouchPoints || 0,
pointer: this.getPointerCapability(),
hover: this.getHoverCapability(),
anyPointer: this.getAnyPointerCapability(),
anyHover: this.getAnyHoverCapability(),
orientation: this.detectOrientationSupport(),
deviceMemory: this.getDeviceMemory(),
hardwareConcurrency: navigator.hardwareConcurrency,
connection: this.getConnectionInfo(),
screen: this.getScreenInfo()
};
}
/**
* Detect complete features and infer device type
*/
static detectWithInference() {
const features = this.detect();
const inferredDevice = this.inferDeviceType(features);
const inferredOS = this.inferOS(features);
const confidence = this.calculateConfidence(features);
return {
features,
inferredDevice,
inferredOS,
confidence
};
}
/**
* Detect touch support
*/
static detectTouchSupport() {
return ('ontouchstart' in window ||
navigator.maxTouchPoints > 0 ||
window.DocumentTouch !== undefined);
}
/**
* Get pointer capability (CSS media query)
*/
static getPointerCapability() {
if (window.matchMedia('(pointer: fine)').matches) {
return 'fine';
}
if (window.matchMedia('(pointer: coarse)').matches) {
return 'coarse';
}
return 'none';
}
/**
* Get hover capability (CSS media query)
*/
static getHoverCapability() {
if (window.matchMedia('(hover: hover)').matches) {
return 'hover';
}
return 'none';
}
/**
* Get any-pointer capability (for hybrid devices)
*/
static getAnyPointerCapability() {
if (window.matchMedia('(any-pointer: fine)').matches) {
return 'fine';
}
if (window.matchMedia('(any-pointer: coarse)').matches) {
return 'coarse';
}
return 'none';
}
/**
* Get any-hover capability
*/
static getAnyHoverCapability() {
if (window.matchMedia('(any-hover: hover)').matches) {
return 'hover';
}
return 'none';
}
/**
* Detect orientation support
*/
static detectOrientationSupport() {
return 'orientation' in window || 'onorientationchange' in window;
}
/**
* Get device memory (if available)
*/
static getDeviceMemory() {
return navigator.deviceMemory;
}
/**
* Get network connection information
*/
static getConnectionInfo() {
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
if (!connection) {
return undefined;
}
return {
effectiveType: connection.effectiveType,
downlink: connection.downlink,
rtt: connection.rtt,
saveData: connection.saveData
};
}
/**
* Get screen information
*/
static getScreenInfo() {
return {
width: screen.width,
height: screen.height,
availWidth: screen.availWidth,
availHeight: screen.availHeight,
pixelRatio: window.devicePixelRatio || 1,
orientation: screen.orientation?.type
};
}
/**
* Infer device type from features
*/
static inferDeviceType(features) {
// Desktop: fine pointer + hover + no/minimal touch
if (features.pointer === 'fine' &&
features.hover === 'hover' &&
features.maxTouchPoints <= 1) {
return 'desktop';
}
// Mobile phone: coarse pointer + no hover + touch
if (features.pointer === 'coarse' &&
features.hover === 'none' &&
features.touch) {
// Distinguish phone from tablet by screen size
const screenSize = Math.min(features.screen.width, features.screen.height);
if (screenSize < 768) {
return 'mobile';
}
// Tablet size but phone-like features
return 'tablet';
}
// Tablet: touch with larger screen
if (features.touch && features.maxTouchPoints > 1) {
const screenSize = Math.min(features.screen.width, features.screen.height);
// Tablets typically 7+ inches (≥768px)
if (screenSize >= 768) {
return 'tablet';
}
return 'mobile';
}
// Hybrid devices (e.g., Surface): fine pointer + touch
if (features.anyPointer === 'fine' && features.touch) {
// Check screen size for classification
const screenSize = Math.min(features.screen.width, features.screen.height);
if (screenSize >= 1024) {
return 'desktop'; // Likely a touch-enabled laptop
}
return 'tablet';
}
// Default to desktop for unknown configurations
return 'desktop';
}
/**
* Infer OS from features
*/
static inferOS(features) {
// iOS: specific touch characteristics
if (features.maxTouchPoints === 5 &&
features.touch &&
features.pointer === 'coarse') {
return 'ios';
}
// iPad: MacIntel platform + touch (handled elsewhere)
if (navigator.platform === 'MacIntel' && features.maxTouchPoints > 1) {
return 'ios';
}
// Android: touch + orientation on mobile
if (features.touch &&
features.orientation &&
features.pointer === 'coarse') {
return 'android';
}
// Cannot reliably infer OS from features alone
return undefined;
}
/**
* Calculate detection confidence based on available features
*/
static calculateConfidence(features) {
let confidence = 100;
// Reduce confidence if key features are missing
if (features.pointer === 'none')
confidence -= 20;
if (features.hover === 'none' && !features.touch)
confidence -= 15;
if (!features.maxTouchPoints && features.touch)
confidence -= 10;
if (!features.hardwareConcurrency)
confidence -= 5;
if (!features.deviceMemory)
confidence -= 5;
return Math.max(0, Math.min(100, confidence));
}
/**
* Validate device type against features
*/
static validateDeviceType(detectedDevice, features) {
const inferredDevice = this.inferDeviceType(features);
// Perfect match
if (detectedDevice === inferredDevice) {
return { valid: true, confidence: 100 };
}
// Compatible matches
if (detectedDevice === 'mobile' && inferredDevice === 'tablet') {
// Small tablets might be detected as mobile
return { valid: true, confidence: 75, reason: 'Small tablet detected as mobile' };
}
if (detectedDevice === 'tablet' && inferredDevice === 'mobile') {
// Large phones might be detected as tablets
return { valid: true, confidence: 75, reason: 'Large phone detected as tablet' };
}
if (detectedDevice === 'desktop' && inferredDevice === 'tablet' && features.touch) {
// Touch-enabled desktops
return { valid: true, confidence: 70, reason: 'Touch-enabled desktop' };
}
// Mismatch
return {
valid: false,
confidence: 30,
reason: `Detected as ${detectedDevice} but features suggest ${inferredDevice}`
};
}
/**
* Check if device is likely a tablet based on features
*/
static isLikelyTablet(features) {
const screenSize = Math.min(features.screen.width, features.screen.height);
return (features.touch &&
features.maxTouchPoints > 1 &&
screenSize >= 768 &&
screenSize <= 1366);
}
/**
* Check if device is likely a phone based on features
*/
static isLikelyPhone(features) {
const screenSize = Math.min(features.screen.width, features.screen.height);
return (features.touch &&
features.pointer === 'coarse' &&
features.hover === 'none' &&
screenSize < 768);
}
/**
* Check if device is likely a desktop based on features
*/
static isLikelyDesktop(features) {
return (features.pointer === 'fine' &&
features.hover === 'hover' &&
features.maxTouchPoints <= 1);
}
/**
* Create default features for server-side rendering
*/
static createServerSideFeatures() {
return {
touch: false,
maxTouchPoints: 0,
pointer: 'none',
hover: 'none',
anyPointer: 'none',
anyHover: 'none',
orientation: false,
screen: {
width: 0,
height: 0,
availWidth: 0,
availHeight: 0,
pixelRatio: 1
}
};
}
}