File

projects/ngx-device-detector/src/lib/device-detector.service.ts

Index

Properties
Methods

Constructor

constructor(platformId: any)
Parameters :
Name Type Optional
platformId any No

Methods

Private detectDesktopMode
detectDesktopMode(ua: string)

Detects if a mobile device is running in desktop mode Desktop mode occurs when mobile browsers request desktop websites by sending desktop user agent strings while retaining mobile characteristics

Parameters :
Name Type Optional
ua string No
Returns : boolean
Private detectFromMapping
detectFromMapping(ua: string, mapping: literal type)

This value is later accessible for usage

Parameters :
Name Type Optional
ua string No
mapping literal type No
Returns : string
Public getDeviceInfo
getDeviceInfo()
Returns : DeviceInfo

the device information object.

Private hasMobileBrowserSignatures
hasMobileBrowserSignatures(ua: string)

Checks for mobile browser signatures that might leak through in desktop mode

Parameters :
Name Type Optional
ua string No
Returns : boolean
Private hasMobileScreenDimensions
hasMobileScreenDimensions()

Checks if screen dimensions suggest mobile device

Returns : boolean
Public isDesktop
isDesktop(userAgent)

if the current device is a desktop device.

Parameters :
Name Optional Default value
userAgent No this.userAgent
Returns : boolean

whether the current device is a desktop device

Public isDesktopModeEnabled
isDesktopModeEnabled()

Desktop mode occurs when mobile browsers request desktop websites by sending desktop user agent strings while retaining mobile hardware characteristics.

Returns : boolean

whether the current device is in desktop mode

Public isMobile
isMobile(userAgent)

if the current device is a mobile and also check current device is tablet so it will return false.

Parameters :
Name Optional Default value
userAgent No this.userAgent
Returns : boolean

whether the current device is a mobile

Public isTablet
isTablet(userAgent)

if the current device is a tablet.

Parameters :
Name Optional Default value
userAgent No this.userAgent
Returns : boolean

whether the current device is a tablet

setDeviceInfo
setDeviceInfo(ua)
Parameters :
Name Optional Default value
ua No this.userAgent
Returns : void

Properties

browser
Type : string
Default value : ''
browser_version
Type : string
Default value : ''
device
Type : string
Default value : ''
deviceType
Type : string
Default value : ''
isDesktopMode
Default value : false
orientation
Type : string
Default value : ''
os
Type : string
Default value : ''
os_version
Type : string
Default value : ''
reTree
Default value : new ReTree()
ua
Type : string
Default value : ''
userAgent
Type : string
Default value : ''
import { PLATFORM_ID, Inject, Injectable } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import * as Constants from './device-detector.constants';
import { ReTree } from './retree';

export interface DeviceInfo {
  userAgent: string;
  os: string;
  browser: string;
  device: string;
  os_version: string;
  browser_version: string;
  deviceType: string;
  orientation: string;
  isDesktopMode: boolean;
}
export enum DeviceType {
  Mobile = 'mobile',
  Tablet = 'tablet',
  Desktop = 'desktop',
  Unknown = 'unknown',
}
export enum OrientationType {
  Portrait = 'portrait',
  Landscape = 'landscape',
}

const iPad = 'iPad';

@Injectable({
  providedIn: 'root',
})
export class DeviceDetectorService {
  ua = '';
  userAgent = '';
  os = '';
  browser = '';
  device = '';
  os_version = '';
  browser_version = '';
  reTree = new ReTree();
  deviceType = '';
  orientation = '';
  isDesktopMode = false;
  constructor(@Inject(PLATFORM_ID) private platformId: any) {
    if (isPlatformBrowser(this.platformId) && typeof window !== 'undefined') {
      this.userAgent = window.navigator.userAgent;
    }
    this.setDeviceInfo(this.userAgent);
  }

  /**
   * @author Ahsan Ayaz
   * @desc Sets the initial value of the device when the service is initiated.
   * This value is later accessible for usage
   */
  private detectFromMapping(ua: string, mapping: { const: string; prop: string }): string {
    const constants = Constants[mapping.const] as any;
    const regexMap = Constants[`${mapping.const}_RE`] as any;

    // Special case for iOS 13 Tablet hack
    if (
      constants.device &&
      constants.device === 'device' &&
      isPlatformBrowser(this.platformId) &&
      (!!this.reTree.test(this.userAgent, Constants.TABLETS_RE[iPad]) ||
        (typeof navigator !== 'undefined' &&
          navigator.platform &&
          // eslint-disable-next-line deprecation/deprecation
          navigator.platform.includes('Mac') &&
          navigator.maxTouchPoints > 1))
    ) {
      return iPad;
    }

    // Find first match - early exit optimization
    for (const key of Object.keys(constants)) {
      const regexPattern = regexMap[key];
      if (regexPattern && this.reTree.test(ua, regexPattern)) {
        return constants[key];
      }
    }

    return constants.UNKNOWN || Constants.GENERAL.UKNOWN;
  }

  /**
   * Detects if a mobile device is running in desktop mode
   * Desktop mode occurs when mobile browsers request desktop websites
   * by sending desktop user agent strings while retaining mobile characteristics
   */
  private detectDesktopMode(ua: string): boolean {
    if (!isPlatformBrowser(this.platformId) || typeof window === 'undefined') {
      return false;
    }

    // If user agent already indicates mobile/tablet, it's not desktop mode
    if (this.deviceType === DeviceType.Mobile || this.deviceType === DeviceType.Tablet) {
      return false;
    }

    // If it's genuinely desktop, it's not desktop mode
    if (this.deviceType !== DeviceType.Desktop) {
      return false;
    }

    // Desktop mode detection indicators
    const indicators = {
      // Touch support despite desktop UA
      hasTouch: 'ontouchstart' in window || (navigator.maxTouchPoints && navigator.maxTouchPoints > 0),

      // Mobile-like screen dimensions (rough heuristics)
      hasMobileScreenSize: this.hasMobileScreenDimensions(),

      // Mobile browser signatures in desktop mode
      hasMobileBrowserSignatures: this.hasMobileBrowserSignatures(ua),

      // Orientation support (more common on mobile)
      hasOrientationSupport: 'orientation' in window || 'onorientationchange' in window,

      // Device motion/orientation APIs (mobile-specific)
      hasDeviceMotion: 'DeviceMotionEvent' in window || 'DeviceOrientationEvent' in window,
    };

    // If device has touch + desktop UA, it's likely desktop mode
    if (indicators.hasTouch) {
      return true;
    }

    // If multiple mobile indicators are present with desktop UA, likely desktop mode
    const mobileIndicatorCount = Object.values(indicators).filter(Boolean).length;
    return mobileIndicatorCount >= 2;
  }

  /**
   * Checks if screen dimensions suggest mobile device
   */
  private hasMobileScreenDimensions(): boolean {
    if (typeof window === 'undefined' || !window.screen) {
      return false;
    }

    const { width, height } = window.screen;
    const maxDimension = Math.max(width, height);
    const minDimension = Math.min(width, height);

    // Typical mobile screen characteristics
    // Most phones: width <= 428px, most tablets: width <= 1024px
    return maxDimension <= 1024 && minDimension <= 768;
  }

  /**
   * Checks for mobile browser signatures that might leak through in desktop mode
   */
  private hasMobileBrowserSignatures(ua: string): boolean {
    // Some mobile browsers leave traces even in desktop mode
    const mobileSignatures = [
      /Mobile.*Safari/, // Mobile Safari signatures
      /Chrome.*Mobile/, // Chrome mobile signatures
      /Android.*Chrome/, // Android Chrome hints
      /iPhone.*CriOS/, // Chrome on iOS hints
      /iPad.*CriOS/, // Chrome on iPad hints
      /Mobile.*Firefox/, // Firefox mobile hints
      /FxiOS/, // Firefox on iOS
    ];

    return mobileSignatures.some(pattern => pattern.test(ua));
  }

  setDeviceInfo(ua = this.userAgent): void {
    if (ua !== this.userAgent) {
      this.userAgent = ua;
    }

    // Optimized detection with early exit
    this.os = this.detectFromMapping(ua, { const: 'OS', prop: 'os' });
    this.browser = this.detectFromMapping(ua, { const: 'BROWSERS', prop: 'browser' });
    this.device = this.detectFromMapping(ua, { const: 'DEVICES', prop: 'device' });

    // Handle Android device refinement
    if (this.device === Constants.DEVICES.ANDROID) {
      const deviceConstants = Constants.DEVICES as any;
      const deviceRegexMap = Constants.DEVICES_RE as any;

      for (const key of Object.keys(deviceConstants)) {
        const regexPattern = deviceRegexMap[key];
        if (regexPattern && this.reTree.test(ua, regexPattern) && deviceConstants[key] !== Constants.DEVICES.ANDROID) {
          this.device = deviceConstants[key];
          break; // Early exit when found
        }
      }
    }

    this.os_version = this.detectFromMapping(ua, { const: 'OS_VERSIONS', prop: 'os_version' });

    // Browser version detection - only if browser is known
    this.browser_version = '0';
    if (this.browser !== Constants.BROWSERS.UNKNOWN) {
      const re = Constants.BROWSER_VERSIONS_RE[this.browser];
      if (re) {
        const res = this.reTree.exec(ua, re);
        if (res) {
          this.browser_version = res[1];
        }
      }
    }

    // Orientation detection
    if (typeof window !== 'undefined' && window.matchMedia) {
      this.orientation = window.matchMedia('(orientation: landscape)').matches
        ? OrientationType.Landscape
        : OrientationType.Portrait;
    } else {
      this.orientation = Constants.GENERAL.UKNOWN;
    }

    // Device type detection - lazy evaluation
    this.deviceType = this.isTablet()
      ? DeviceType.Tablet
      : this.isMobile(this.userAgent)
        ? DeviceType.Mobile
        : this.isDesktop(this.userAgent)
          ? DeviceType.Desktop
          : DeviceType.Unknown;

    // Desktop mode detection - check if mobile device is masquerading as desktop
    this.isDesktopMode = this.detectDesktopMode(ua);
  }

  /**
   * @author Ahsan Ayaz
   * @desc Returns the device information
   * @returns the device information object.
   */
  public getDeviceInfo(): DeviceInfo {
    const deviceInfo: DeviceInfo = {
      userAgent: this.userAgent,
      os: this.os,
      browser: this.browser,
      device: this.device,
      os_version: this.os_version,
      browser_version: this.browser_version,
      deviceType: this.deviceType,
      orientation: this.orientation,
      isDesktopMode: this.isDesktopMode,
    };
    return deviceInfo;
  }

  /**
   * @author Ahsan Ayaz
   * @desc Compares the current device info with the mobile devices to check
   * if the current device is a mobile and also check current device is tablet so it will return false.
   * @returns whether the current device is a mobile
   */
  public isMobile(userAgent = this.userAgent): boolean {
    if (this.isTablet(userAgent)) {
      return false;
    }

    // Early exit optimization - stop at first match
    for (const key of Object.keys(Constants.MOBILES_RE)) {
      if (this.reTree.test(userAgent, Constants.MOBILES_RE[key])) {
        return true;
      }
    }
    return false;
  }

  /**
   * @author Ahsan Ayaz
   * @desc Compares the current device info with the tablet devices to check
   * if the current device is a tablet.
   * @returns whether the current device is a tablet
   */
  public isTablet(userAgent = this.userAgent): boolean {
    // iOS 13 Tablet special case
    if (
      isPlatformBrowser(this.platformId) &&
      (!!this.reTree.test(this.userAgent, Constants.TABLETS_RE[iPad]) ||
        (typeof navigator !== 'undefined' &&
          navigator.platform &&
          // eslint-disable-next-line deprecation/deprecation
          navigator.platform.includes('Mac') &&
          navigator.maxTouchPoints > 1))
    ) {
      return true;
    }

    // Early exit optimization - stop at first match
    for (const key of Object.keys(Constants.TABLETS_RE)) {
      if (this.reTree.test(userAgent, Constants.TABLETS_RE[key])) {
        return true;
      }
    }
    return false;
  }

  /**
   * @author Ahsan Ayaz
   * @desc Compares the current device info with the desktop devices to check
   * if the current device is a desktop device.
   * @returns whether the current device is a desktop device
   */
  public isDesktop(userAgent = this.userAgent): boolean {
    if (this.device === Constants.DEVICES.UNKNOWN && (this.isMobile(userAgent) || this.isTablet(userAgent))) {
      return false;
    }
    return Constants.DESKTOP_DEVICES.includes(this.device);
  }

  /**
   * @desc Checks if the current device is a mobile device running in desktop mode.
   * Desktop mode occurs when mobile browsers request desktop websites by sending
   * desktop user agent strings while retaining mobile hardware characteristics.
   * @returns whether the current device is in desktop mode
   */
  public isDesktopModeEnabled(): boolean {
    return this.isDesktopMode;
  }
}

results matching ""

    No results matching ""