// Define an interface for the browser result interface BrowserResult { name: string; version: string; } // Browser detection strategy for privacy browsers that mask their user agent interface BrowserDetectionStrategy { name: string; detect: () => boolean; } // Module-scoped cache keyed on brave-marker + UA string. // Brave masks its UA so we must distinguish brave vs non-brave for the same UA. const browserCache = new Map(); /** Test-only: reset the memoization cache between test runs. */ export function __resetBrowserCache(): void { browserCache.clear(); } // Define a function to parse the user agent string and return the browser name and version export function getBrowser(): BrowserResult { if (typeof navigator === 'undefined') { return { name: "unknown", version: "unknown" } } const isBrave = !!(navigator as any).brave; const cacheKey = (isBrave ? 'B|' : 'N|') + navigator.userAgent; const cached = browserCache.get(cacheKey); if (cached) return cached; // Check for privacy browsers first (these mask their user agent) const privacyBrowserStrategies: BrowserDetectionStrategy[] = [ { name: 'Brave', detect: () => isBrave } ]; let result: BrowserResult; for (const strategy of privacyBrowserStrategies) { if (strategy.detect()) { const parsed = parseBrowserFromUA(navigator.userAgent); result = { name: strategy.name, version: parsed.version }; browserCache.set(cacheKey, result); return result; } } // Fall back to user agent parsing for standard browsers result = parseBrowserFromUA(navigator.userAgent); browserCache.set(cacheKey, result); return result; } // Parse browser from user agent string function parseBrowserFromUA(ua: string): BrowserResult { // DeviceAtlas authoritative regex order and patterns const regexes = [ // Samsung Internet (Android) /(?SamsungBrowser)\/(?\d+(?:\.\d+)+)/, // Edge (Chromium, Android, iOS) /(?EdgA|EdgiOS|Edg)\/(?\d+(?:\.\d+)+)/, // Opera (OPR, OPX, Opera Mini, Opera Mobi) /(?OPR|OPX)\/(?\d+(?:\.\d+)+)/, /Opera[\s\/](?\d+(?:\.\d+)+)/, /Opera Mini\/(?\d+(?:\.\d+)+)/, /Opera Mobi\/(?\d+(?:\.\d+)+)/, // Vivaldi /(?Vivaldi)\/(?\d+(?:\.\d+)+)/, // Chrome iOS (CriOS) /(?CriOS)\/(?\d+(?:\.\d+)+)/, // Firefox iOS (FxiOS) /(?FxiOS)\/(?\d+(?:\.\d+)+)/, // Chrome, Chromium (desktop & Android) /(?Chrome|Chromium)\/(?\d+(?:\.\d+)+)/, // Firefox (desktop & Android) /(?Firefox|Waterfox|Iceweasel|IceCat)\/(?\d+(?:\.\d+)+)/, // Safari (desktop & iOS): prefer Version/x.y if present, else Safari/x.y /Version\/(?[\d.]+).*Safari\/[\d.]+|(?Safari)\/(?[\d.]+)/, // Internet Explorer, IE Mobile /(?MSIE|Trident|IEMobile).+?(?\d+(?:\.\d+)+)/, // Other browsers that use the format "BrowserName/version" /(?[A-Za-z]+)\/(?\d+(?:\.\d+)+)/, ]; // Map UA tokens to canonical browser names const browserNameMap: { [key: string]: string } = { 'edg': 'Edge', 'edga': 'Edge', 'edgios': 'Edge', 'opr': 'Opera', 'opx': 'Opera', 'crios': 'Chrome', 'fxios': 'Firefox', 'samsung': 'SamsungBrowser', 'vivaldi': 'Vivaldi', }; for (const regex of regexes) { const match = ua.match(regex); if (match) { let name = match.groups?.name; let version = match.groups?.version || match.groups?.version1 || match.groups?.version2; // For Safari, if Version/x.y matched, set name to Safari if (!name && (match.groups?.version1 || match.groups?.version2)) name = 'Safari'; // Fallbacks for legacy Opera/Opera Mini/Opera Mobi if (!name && regex.source.includes('Opera Mini')) name = 'Opera Mini'; if (!name && regex.source.includes('Opera Mobi')) name = 'Opera Mobi'; if (!name && regex.source.includes('Opera')) name = 'Opera'; // Fallback for generic [A-Za-z]+/version if (!name && match[1]) name = match[1]; if (!version && match[2]) version = match[2]; if (name) { const canonical = browserNameMap[name.toLowerCase()] || name; return { name: canonical, version: version || 'unknown' }; } } } return { name: "unknown", version: "unknown" }; } // Utility function to detect if a user agent is a mobile device (DeviceAtlas-style) export function isMobileUserAgent(): boolean { if (typeof navigator === 'undefined' || !navigator.userAgent) return false; const ua = navigator.userAgent; // Exclude iPad from 'mobile' (treat as tablet) return /Mobi|Android|iPhone|iPod|IEMobile|Opera Mini|Opera Mobi|webOS|BlackBerry|Windows Phone/i.test(ua) && !/iPad/i.test(ua); }