UNPKG

@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

240 lines (239 loc) 7.62 kB
/** * Client Hints API integration for enhanced platform detection * Provides better accuracy for Chromium-based browsers (Chrome, Edge, Opera) * * Browser Support: * - Chrome/Edge 101+ * - Opera * - Not supported: Safari, Firefox (as of 2025) */ /** * Client Hints detector for modern browsers */ export class ClientHintsDetector { /** * Check if Client Hints API is available */ static isSupported() { return (typeof navigator !== 'undefined' && 'userAgentData' in navigator && typeof navigator.userAgentData === 'object'); } /** * Get basic client hints (low entropy) */ static getBasicHints() { if (!this.isSupported()) { return null; } const uaData = navigator.userAgentData; return { brands: uaData.brands || [], mobile: uaData.mobile || false, platform: uaData.platform || undefined }; } /** * Get high entropy client hints (requires user permission in some browsers) */ static async getHighEntropyHints() { if (!this.isSupported()) { return null; } try { const uaData = navigator.userAgentData; const hints = await uaData.getHighEntropyValues([ 'architecture', 'bitness', 'brands', 'formFactor', 'fullVersionList', 'model', 'platform', 'platformVersion', 'uaFullVersion', 'wow64' ]); // Include mobile from basic hints return { ...hints, mobile: uaData.mobile || false }; } catch (error) { if (navigator.userAgentData?.platform) { // Fallback to basic hints if high entropy fails return this.getBasicHints(); } return null; } } /** * Detect OS from Client Hints */ static detectOS(hints) { if (!hints.platform) { return {}; } const platform = hints.platform.toLowerCase(); const version = hints.platformVersion || ''; // Windows detection if (platform === 'windows') { const majorVersion = parseFloat(version); let osVersion; // Windows 11 detection (version 13.0.0+) if (majorVersion >= 13) { osVersion = '11'; } else if (majorVersion >= 10) { osVersion = '10'; } else if (majorVersion >= 6.3) { osVersion = '8.1'; } else if (majorVersion >= 6.2) { osVersion = '8'; } else if (majorVersion >= 6.1) { osVersion = '7'; } return { os: 'windows', version: osVersion }; } // macOS detection if (platform === 'macos') { const majorVersion = parseInt(version.split('.')[0] || '0', 10); return { os: 'macos', version: majorVersion >= 10 ? majorVersion.toString() : undefined }; } // Android detection if (platform === 'android') { const majorVersion = parseInt(version.split('.')[0] || '0', 10); return { os: 'android', version: majorVersion > 0 ? majorVersion.toString() : undefined }; } // iOS detection (though iOS doesn't support Client Hints as of 2025) if (platform === 'ios') { return { os: 'ios', version: version || undefined }; } // Linux detection if (platform === 'linux') { return { os: 'linux' }; } // ChromeOS detection if (platform === 'chromeos' || platform === 'chrome os') { return { os: 'chromeos' }; } return {}; } /** * Detect device type from Client Hints */ static detectDevice(hints) { // Use formFactor if available (new in recent Client Hints spec) if (hints.formFactor) { const formFactor = hints.formFactor.toLowerCase(); switch (formFactor) { case 'mobile': return 'mobile'; case 'tablet': return 'tablet'; case 'desktop': return 'desktop'; case 'xr': // XR devices (AR/VR) - treat as specialized device return 'desktop'; // or create new type default: break; } } // Fallback to mobile property if (hints.mobile === true) { // Check if it's a tablet based on model name if (hints.model && this.isTabletModel(hints.model)) { return 'tablet'; } return 'mobile'; } else if (hints.mobile === false) { return 'desktop'; } return undefined; } /** * Check if model name indicates a tablet */ static isTabletModel(model) { const tabletPatterns = [ /ipad/i, /tablet/i, /tab\s?\d+/i, /galaxy tab/i, /nexus (7|9|10)/i, /pixel (c|slate)/i, /surface/i ]; return tabletPatterns.some(pattern => pattern.test(model)); } /** * Get browser version from Client Hints */ static getBrowserVersion(hints) { if (hints.fullVersionList && hints.fullVersionList.length > 0) { // Find the main browser brand (not Chromium) const mainBrowser = hints.fullVersionList.find(brand => !brand.brand.toLowerCase().includes('chromium') && !brand.brand.toLowerCase().includes('not')); return mainBrowser?.version; } return hints.uaFullVersion; } /** * Detect complete platform information from Client Hints */ static async detect() { if (!this.isSupported()) { return null; } try { const hints = await this.getHighEntropyHints(); if (!hints) { return null; } const osInfo = this.detectOS(hints); const device = this.detectDevice(hints); const browserVersion = this.getBrowserVersion(hints); return { os: osInfo.os, osVersion: osInfo.version, device, isMobile: hints.mobile, architecture: hints.architecture, model: hints.model, browserVersion }; } catch (error) { console.error('Client Hints detection failed:', error); return null; } } /** * Check if user agent is frozen (reduced) * Frozen UAs indicate Client Hints should be preferred */ static isFrozenUA(userAgent) { // Chrome's frozen user agent patterns const frozenPatterns = [ // Desktop: Version frozen at Chrome 110 /Chrome\/110\.0\.0\.0/, // Android: Version frozen at Chrome 110 /Android.*Chrome\/110\.0\.0\.0/, // Generic frozen pattern /\bChrome\/1[0-9]{2}\.0\.0\.0\b/ ]; return frozenPatterns.some(pattern => pattern.test(userAgent)); } }