/** * Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved. */ type NavigatorUAData = { brands?: {brand: string; version: string}[]; mobile?: boolean; platform?: string; getHighEntropyValues?: (hints: string[]) => Promise>; toJSON?: () => object; }; export default class PlatformDetectionService { private static readonly _userAgent: string = globalThis.navigator?.userAgent ?? ''; // @ts-expect-error NavigatorUAData is experimental and not defined in the lib dom yet https://developer.mozilla.org/en-US/docs/Web/API/Navigator/userAgentData private static readonly _userAgentData: NavigatorUAData | undefined = globalThis.navigator?.userAgentData; private static readonly _areClientHintsSupported: boolean = !!PlatformDetectionService._userAgentData; private static _platform: string = 'Unknown'; private static _platformVersion: string = ''; private static _browserName: string = 'Unknown'; private static _browserVersion: string = '?'; private static _isWebview: boolean = false; static { if (PlatformDetectionService._areClientHintsSupported) { PlatformDetectionService.initFromClientHints(); } else { PlatformDetectionService.initFromUserAgent(); } } private constructor() { throw new Error('PlatformDetectionService is a static class that may not be instantiated'); } // ---- Public API ---- static get platform(): string { return PlatformDetectionService._platform; } static get platformVersion(): string { return PlatformDetectionService._platformVersion; } static get userAgent(): string { return PlatformDetectionService._userAgent; } static get browserName(): string { return PlatformDetectionService._browserName; } static get browserVersion(): string { return PlatformDetectionService._browserVersion; } static get isWebview(): boolean { return PlatformDetectionService._isWebview; } static get areClientHintsSupported(): boolean { return PlatformDetectionService._areClientHintsSupported; } /** * Optional async initialization for high-entropy values like platformVersion */ static async initAsync(): Promise { if (PlatformDetectionService._areClientHintsSupported && PlatformDetectionService._userAgentData?.getHighEntropyValues) { const values = await PlatformDetectionService._userAgentData.getHighEntropyValues(['platformVersion']); if (values.platformVersion) { PlatformDetectionService._platformVersion = values.platformVersion; } } } // ---- Init strategies ---- private static initFromClientHints() { const data = PlatformDetectionService._userAgentData as NavigatorUAData; const nonChromiumBrand = data.brands?.find(b => b.brand !== 'Chromium'); PlatformDetectionService._browserName = nonChromiumBrand?.brand ?? 'Unknown'; PlatformDetectionService._browserVersion = nonChromiumBrand?.version ?? '?'; PlatformDetectionService._platform = data.platform ?? 'Unknown'; PlatformDetectionService._isWebview = PlatformDetectionService.extractIsWebviewFromUserAgent(); // Fallback check } private static initFromUserAgent() { PlatformDetectionService._platform = PlatformDetectionService.extractPlatformFromUserAgent(); PlatformDetectionService._platformVersion = PlatformDetectionService.extractPlatformVersionFromUserAgent(); PlatformDetectionService._browserName = PlatformDetectionService.extractBrowserNameFromUserAgent(); PlatformDetectionService._browserVersion = PlatformDetectionService.extractBrowserVersionFromUserAgent(); PlatformDetectionService._isWebview = PlatformDetectionService.extractIsWebviewFromUserAgent(); } // ---- Helpers ---- private static extractBrowserNameFromUserAgent(): string { if (/Edg\//.test(PlatformDetectionService._userAgent)) { return 'Edge'; } if (/OPR\//.test(PlatformDetectionService._userAgent)) { return 'Opera'; } if (/Firefox\//.test(PlatformDetectionService._userAgent)) { return 'Firefox'; } if (/Trident\/.*rv:/.test(PlatformDetectionService._userAgent)) { return 'IE'; } if (/Chrome\//.test(PlatformDetectionService._userAgent)) { return 'Chrome'; } if (/Safari\//.test(PlatformDetectionService._userAgent)) { return 'Safari'; } if (/ReactNative\//.test(PlatformDetectionService._userAgent)) { return 'ReactNative'; } return 'Unknown'; } private static extractBrowserVersionFromUserAgent(): string { return ( PlatformDetectionService.matchVersion(/Edg\/([\d.]+)/) ?? PlatformDetectionService.matchVersion(/OPR\/([\d.]+)/) ?? PlatformDetectionService.matchVersion(/Firefox\/([\d.]+)/) ?? PlatformDetectionService.matchVersion(/rv:([\d.]+)/) ?? // IE PlatformDetectionService.matchVersion(/Chrome\/([\d.]+)/) ?? PlatformDetectionService.matchVersion(/Version\/([\d.]+)/) ?? // Safari often uses "Version/" PlatformDetectionService.matchVersion(/Safari\/([\d.]+)/) ?? PlatformDetectionService.matchVersion(/ReactNative\/([\d.]+)/) ?? '?' ); } private static extractPlatformFromUserAgent(): string { if (/Windows/.test(PlatformDetectionService._userAgent)) { return 'Windows'; } if (/iPhone|iPad|iPod/.test(PlatformDetectionService._userAgent)) { return 'iOS'; } if (/Mac OS X/.test(PlatformDetectionService._userAgent)) { return 'macOS'; } if (/Android/.test(PlatformDetectionService._userAgent)) { return 'Android'; } if (/Linux/.test(PlatformDetectionService._userAgent)) { return 'Linux'; } return 'Unknown'; } private static extractPlatformVersionFromUserAgent(): string { switch (PlatformDetectionService._platform) { case 'Windows': return PlatformDetectionService.matchVersion(/Windows NT ([\d.]+)/) ?? ''; case 'iOS': return PlatformDetectionService.matchVersion(/OS ([\d_]+)/)?.replace(/_/g, '.') ?? ''; case 'macOS': return PlatformDetectionService.matchVersion(/Mac OS X ([\d_]+)/)?.replace(/_/g, '.') ?? ''; case 'Android': return PlatformDetectionService.matchVersion(/Android ([\d.]+)/) ?? ''; default: return ''; } } private static extractIsWebviewFromUserAgent(): boolean { return ( /; wv/.test(PlatformDetectionService._userAgent) || // Android webview (/Android/.test(PlatformDetectionService._userAgent) && /Version\/[\d.]+/.test(PlatformDetectionService._userAgent)) || // Some Android webviews /(iPhone|iPod|iPad).*AppleWebKit(?!.*Safari)/.test(PlatformDetectionService._userAgent) // IOS webview ); } private static matchVersion(pattern: RegExp): string | null { const match = PlatformDetectionService._userAgent.match(pattern); return match ? match[1] : null; } }