/** * @module util/device-helper * * Pure utility functions for generating clean, friendly, auto-generated device names/tags. */ export interface DeviceTagOptions { userAgent?: string; userAgentData?: { mobile?: boolean; platform?: string; brands?: Array<{ brand: string; version: string }>; }; maxTouchPoints?: number; screenWidth?: number; randomSuffix?: string; } /** * Generates a friendly, URL-safe, kebab-cased default device tag. * (e.g., `chrome-win-a1b2`, `mobile-safari-ios-3c4d`, `tablet-android-e5f6`). * * Pure function: Uses global browser objects (`navigator`, `window`) only as fallbacks * when explicit options are omitted, enabling deterministic unit testing. * * @param opts Optional mock options for deterministic unit testing. */ export function generateDefaultDeviceTag(opts?: DeviceTagOptions): string { const ua = (opts?.userAgent ?? (typeof navigator !== 'undefined' ? navigator.userAgent : '') ?? '').toLowerCase(); const uaData = opts?.userAgentData ?? (typeof navigator !== 'undefined' ? (navigator as any).userAgentData : undefined); const touchPoints = opts?.maxTouchPoints ?? (typeof navigator !== 'undefined' ? navigator.maxTouchPoints : 0) ?? 0; const width = opts?.screenWidth ?? (typeof window !== 'undefined' ? window.innerWidth : 1024) ?? 1024; // 1. Detect Form Factor (Mobile, Tablet, Desktop) let formFactor = ''; const isMobileUaData = uaData?.mobile; const isMobileUa = /mobile|iphone|ipod|android.*mobile|windows phone/i.test(ua); const isTabletUa = /ipad|tablet|(android(?!.*mobile))/i.test(ua) || (touchPoints > 1 && /macintosh/i.test(ua) && width <= 1366); if (isTabletUa) { formFactor = 'tablet'; } else if (isMobileUaData || isMobileUa) { formFactor = 'mobile'; } // 2. Detect OS / Platform let platform = ''; const uaPlatform = (uaData?.platform || '').toLowerCase(); if (uaPlatform.includes('win') || ua.includes('windows')) { platform = 'win'; } else if (uaPlatform.includes('ios') || ua.includes('iphone') || ua.includes('ipad') || ua.includes('ipod')) { platform = 'ios'; } else if (uaPlatform.includes('mac') || ua.includes('macintosh') || ua.includes('mac os')) { platform = 'mac'; } else if (uaPlatform.includes('android') || ua.includes('android')) { platform = 'android'; } else if (uaPlatform.includes('linux') || ua.includes('linux')) { platform = 'linux'; } // 3. Detect Browser let browser = ''; if (ua.includes('edg/') || ua.includes('edge/')) { browser = 'edge'; } else if (ua.includes('chrome/') || ua.includes('crios/')) { browser = 'chrome'; } else if (ua.includes('firefox/') || ua.includes('fxios/')) { browser = 'firefox'; } else if (ua.includes('safari/') && !ua.includes('chrome/') && !ua.includes('crios/')) { browser = 'safari'; } // 4. Generate or use provided 4-character random suffix let suffix = opts?.randomSuffix; if (!suffix) { const chars = '0123456789abcdef'; suffix = ''; for (let i = 0; i < 4; i++) { suffix += chars.charAt(Math.floor(Math.random() * chars.length)); } } suffix = suffix.toLowerCase().replace(/[^a-z0-9]/g, '').slice(0, 8); // 5. Assemble Tokens const tokens: string[] = []; if (formFactor) { tokens.push(formFactor); } if (browser) { tokens.push(browser); } if (platform) { tokens.push(platform); } if (tokens.length === 0) { tokens.push('device'); } tokens.push(suffix); return tokens.join('-'); }