maintenance

This commit is contained in:
2025-09-04 20:25:15 -04:00
parent 1469c7f52f
commit e8f2df9e69
214 changed files with 8507 additions and 1836 deletions

View File

@@ -1,165 +1,191 @@
/**
* 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<Record<string, string>>;
toJSON?: () => object;
};
export default class PlatformDetectionService {
private static _userAgent: string = navigator.userAgent;
private static _areClientHintsSupported: boolean = 'userAgentData' in navigator;
private static _platform: string = '?';
private static _platformVersion: string = '?';
private static _browser: string = 'Unknown';
private static _version: string | number = '?';
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;
private static _initialized: 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 {
this.initializeIfNeeded();
return this._platform;
return PlatformDetectionService._platform;
}
static get platformVersion(): string {
this.initializeIfNeeded();
return this._platformVersion;
return PlatformDetectionService._platformVersion;
}
static get userAgent(): string {
return this._userAgent;
return PlatformDetectionService._userAgent;
}
static get browser(): string {
this.initializeIfNeeded();
return this._browser;
static get browserName(): string {
return PlatformDetectionService._browserName;
}
static get version(): string | number {
this.initializeIfNeeded();
return this._version;
static get browserVersion(): string {
return PlatformDetectionService._browserVersion;
}
static get isWebview(): boolean {
this.initializeIfNeeded();
return this._isWebview;
return PlatformDetectionService._isWebview;
}
static get areClientHintsSupported(): boolean {
return this._areClientHintsSupported;
return PlatformDetectionService._areClientHintsSupported;
}
private static initializeIfNeeded(): void {
if (this._initialized) return;
this.initialize();
}
/**
* Optional async initialization for high-entropy values like platformVersion
*/
static async initAsync(): Promise<void> {
if (PlatformDetectionService._areClientHintsSupported && PlatformDetectionService._userAgentData?.getHighEntropyValues) {
const values = await PlatformDetectionService._userAgentData.getHighEntropyValues(['platformVersion']);
private static initialize(): void {
try {
const browserVersionMatch = this._userAgent.match(/(Chrome|Chromium|Firefox|Opera|Safari|Edge|OPR)\/([0-9]+)/);
if (browserVersionMatch) {
const [, browser, version] = browserVersionMatch;
PlatformDetectionService._browser = browser === 'OPR' ? 'Opera' : browser;
PlatformDetectionService._version = parseInt(version, 10)?.toString() || '?';
} else if (this._userAgent.match(/^\(?Mozilla/)) {
PlatformDetectionService._browser = 'Mozilla';
// Check for IE/Edge
if (this._userAgent.match(/MSIE/) || this._userAgent.match(/; Trident\/.*rv:[0-9]+/)) {
PlatformDetectionService._browser = 'IE';
const ieVersionMatch = this._userAgent.match(/rv:([0-9]+)/);
if (ieVersionMatch) {
PlatformDetectionService._version = parseInt(ieVersionMatch[1], 10)?.toString() || '?';
}
} else if (this._userAgent.match(/Edge\//)) {
PlatformDetectionService._browser = 'Edge';
const edgeVersionMatch = this._userAgent.match(/Edge\/([0-9]+)/);
if (edgeVersionMatch) {
PlatformDetectionService._version = parseInt(edgeVersionMatch[1], 10)?.toString() || '?';
}
}
if (values.platformVersion) {
PlatformDetectionService._platformVersion = values.platformVersion;
}
// Handle Opera masquerading as other browsers
if (this._userAgent.match(/OPR\//)) {
PlatformDetectionService._browser = 'Opera';
const operaVersionMatch = this._userAgent.match(/OPR\/([0-9]+)/);
if (operaVersionMatch) {
PlatformDetectionService._version = parseInt(operaVersionMatch[1], 10)?.toString() || '?';
}
}
// Safari and iOS webviews
if (this._userAgent.match(/AppleWebKit/i)) {
if (this._userAgent.match(/iphone|ipod|ipad/i)) {
PlatformDetectionService._browser = 'Safari';
PlatformDetectionService._isWebview = true;
const iosVersionMatch = this._userAgent.match(/OS\s([0-9]+)/);
if (iosVersionMatch) {
PlatformDetectionService._version = parseInt(iosVersionMatch[1], 10)?.toString() || '?';
}
} else if (this._userAgent.match(/Safari\//) && !this._userAgent.match(/Chrome/)) {
PlatformDetectionService._browser = 'Safari';
const safariVersionMatch = this._userAgent.match(/Version\/([0-9]+)/);
if (safariVersionMatch) {
PlatformDetectionService._version = parseInt(safariVersionMatch[1], 10)?.toString() || '?';
}
}
}
// Android webviews
if (this._userAgent.match(/; wv/) || (this._userAgent.match(/Android/) && this._userAgent.match(/Version\/[0-9].[0-9]/))) {
PlatformDetectionService._isWebview = true;
}
// React Native
if (globalThis.navigator.product === 'ReactNative') {
PlatformDetectionService._browser = 'ReactNative';
PlatformDetectionService._version = navigator.productSub || '?';
}
// platform information
if (this._userAgent.match(/Windows/)) {
PlatformDetectionService._platform = 'Windows';
const windowsVersionMatch = this._userAgent.match(/Windows NT ([0-9.]+)/);
if (windowsVersionMatch) {
PlatformDetectionService._platformVersion = windowsVersionMatch[1];
}
} else if (this._userAgent.match(/Mac OS X/)) {
PlatformDetectionService._platform = 'macOS';
const macVersionMatch = this._userAgent.match(/Mac OS X ([0-9._]+)/);
if (macVersionMatch) {
PlatformDetectionService._platformVersion = macVersionMatch[1].replace(/_/g, '.');
}
} else if (this._userAgent.match(/Linux/)) {
PlatformDetectionService._platform = 'Linux';
} else if (this._userAgent.match(/Android/)) {
PlatformDetectionService._platform = 'Android';
const androidVersionMatch = this._userAgent.match(/Android ([0-9.]+)/);
if (androidVersionMatch) {
PlatformDetectionService._platformVersion = androidVersionMatch[1];
}
} else if (this._userAgent.match(/iPhone|iPad|iPod/)) {
PlatformDetectionService._platform = 'iOS';
const iosVersionMatch = this._userAgent.match(/OS ([0-9_]+)/);
if (iosVersionMatch) {
PlatformDetectionService._platformVersion = iosVersionMatch[1].replace(/_/g, '.');
}
}
this._initialized = true;
} catch (error) {
console.warn('Failed to initialize PlatformDetectionService:', error);
// fallback values
this._browser = 'Unknown';
this._version = '?';
this._platform = '?';
this._platformVersion = '?';
this._isWebview = false;
this._initialized = true;
}
}
}
// ---- 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;
}
}