diff --git a/src/constants/index.ts b/src/constants/index.ts index 9751b23..7782efc 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -5,6 +5,7 @@ import * as LocalStorageConstants from "./localstorage.constants"; import * as URLConstants from "./url.constants"; import * as ValidationConstants from "./validation.constants"; import * as BrandConstants from "./brand.constants"; +import * as MobileDevicesConstants from "./mobile-devices.constants"; export { AppIDConstants, @@ -14,4 +15,5 @@ export { URLConstants, ValidationConstants, BrandConstants, + MobileDevicesConstants, }; diff --git a/src/constants/mobile-devices.constants.ts b/src/constants/mobile-devices.constants.ts new file mode 100644 index 0000000..7ab3a8a --- /dev/null +++ b/src/constants/mobile-devices.constants.ts @@ -0,0 +1,12 @@ +/** + * @description + * This pattern matches any string that contains a sequence of three uppercase letters followed by a hyphen. + * The sequence must be a word on its own (i.e., it must be surrounded by word boundaries). + * The 'g' flag is used for global search (to find all matches rather than stopping after the first match), and the 'i' flag is used for case-insensitive search. + * @example huaweiDevicesRegex.test("AMN-") // returns true + * @example huaweiDevicesRegex.test("ANA-") // returns true + * @example huaweiDevicesRegex.test("ANE-") // returns true + * Source of list is from: https://gist.github.com/megaacheyounes/e1c7eec5c790e577db602381b8c50bfa + */ +export const huaweiDevicesRegex = + /(ALP-|AMN-|ANA-|ANE-|ANG-|AQM-|ARS-|ART-|ATU-|BAC-|BLA-|BRQ-|CAG-|CAM-|CAN-|CAZ-|CDL-|CDY-|CLT-|CRO-|CUN-|DIG-|DRA-|DUA-|DUB-|DVC-|ELE-|ELS-|EML-|EVA-|EVR-|FIG-|FLA-|FRL-|GLK-|HMA-|HW-|HWI-|INE-|JAT-|JEF-|JER-|JKM-|JNY-|JSC-|LDN-|LIO-|LON-|LUA-|LYA-|LYO-|MAR-|MED-|MHA-|MLA-|MRD-|MYA-|NCE-|NEO-|NOH-|NOP-|OCE-|PAR-|PIC-|POT-|PPA-|PRA-|RNE-|SEA-|SLA-|SNE-|SPN-|STK-|TAH-|TAS-|TET-|TRT-|VCE-|VIE-|VKY-|VNS-|VOG-|VTR-|WAS-|WKG-|WLZ-|JAD-|MLD-|RTE-|NAM-|NEN-|BAL-|JLN-|YAL-|MGA-|FGD-|XYAO-|BON-|ALN-|ALT-|BRA-|DBY2-|STG-|MAO-|LEM-|GOA-|FOA-|MNA-|LNA-)/; diff --git a/src/utils/__test__/mobileOSDetectAsync.utils.spec.ts b/src/utils/__test__/mobileOSDetectAsync.utils.spec.ts new file mode 100644 index 0000000..dc5562c --- /dev/null +++ b/src/utils/__test__/mobileOSDetectAsync.utils.spec.ts @@ -0,0 +1,36 @@ +import { describe, expect, test } from "vitest"; +import { mobileOSDetectAsync } from "../os-detect.utils"; + +describe("mobileOSDetectAsync", () => { + test('should return "Windows Phone" for Windows Phone user agent', async () => { + Object.defineProperty(window.navigator, "userAgent", { + value: "windows phone", + configurable: true, + }); + expect(await mobileOSDetectAsync()).toBe("Windows Phone"); + }); + + test('should return "Android" for Android user agent', async () => { + Object.defineProperty(window.navigator, "userAgent", { + value: "android", + configurable: true, + }); + expect(await mobileOSDetectAsync()).toBe("Android"); + }); + + test('should return "iOS" for iOS user agent', async () => { + Object.defineProperty(window.navigator, "userAgent", { + value: "iPhone", + configurable: true, + }); + expect(await mobileOSDetectAsync()).toBe("iOS"); + }); + + test('should return "unknown" for unknown user agent', async () => { + Object.defineProperty(window.navigator, "userAgent", { + value: "unknown", + configurable: true, + }); + expect(await mobileOSDetectAsync()).toBe("unknown"); + }); +}); diff --git a/src/utils/index.ts b/src/utils/index.ts index 19c337e..6c6e9db 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -6,5 +6,16 @@ import * as PromiseUtils from "./promise.utils"; import * as URLUtils from "./url.utils"; import * as WebSocketUtils from "./websocket.utils"; import * as BrandUtils from "./brand.utils"; +import * as OSDetectionUtils from "./os-detect.utils"; -export { ImageUtils, FormatUtils, LocalStorageUtils, ObjectUtils, PromiseUtils, URLUtils, WebSocketUtils, BrandUtils }; +export { + ImageUtils, + FormatUtils, + LocalStorageUtils, + ObjectUtils, + PromiseUtils, + URLUtils, + WebSocketUtils, + BrandUtils, + OSDetectionUtils, +}; diff --git a/src/utils/os-detect.utils.ts b/src/utils/os-detect.utils.ts new file mode 100644 index 0000000..446632c --- /dev/null +++ b/src/utils/os-detect.utils.ts @@ -0,0 +1,88 @@ +import { huaweiDevicesRegex } from "../constants/mobile-devices.constants"; + +/** + * This file contains utility functions and types for detecting the mobile operating system. + * It uses the User-Agent string and the User-Agent Client Hints API to determine the OS. + */ + +type ExtendedWindow = Window & { + // MSStream is specific to IE and Edge browsers + MSStream?: { + msClose: () => void; + msDetachStream: () => void; + readonly type: string; + }; + // opera is specific to Opera browser + opera?: string; +}; + +type ExtendedNavigator = Navigator & { + // userAgentData is part of the User-Agent Client Hints API + userAgentData?: NavigatorUAData; +}; + +/** + * Type representing the User-Agent Client Hints API. + */ +type NavigatorUAData = { + brands: { brand: string; version: string }[]; + getHighEntropyValues(hints: string[]): Promise; + mobile: boolean; +}; + +/** + * Type representing the high entropy values that can be obtained from the User-Agent Client Hints API. + */ +type HighEntropyValues = { + model?: string; + platform?: string; + platformVersion?: string; + uaFullVersion?: string; +}; + +/** + * It checks if the input string contains any of the valid Huawei device codes. + * + * @param {string} inputString - The string to check for Huawei device codes. + * @returns {boolean} Returns true if the input string contains a valid Huawei device code, false otherwise. + */ +const validateHuaweiCodes = (inputString: string) => { + return huaweiDevicesRegex.test(inputString); +}; + +/** + * It uses the User-Agent string and the User-Agent Client Hints API to detects the mobile operating system asynchronously. + * + * @returns {Promise} Returns a promise that resolves to the name of the detected mobile OS. + */ +export const mobileOSDetectAsync = async () => { + const extendedWindow = window as ExtendedWindow; + const extendedNavigator = navigator as ExtendedNavigator; + + const userAgent = extendedNavigator.userAgent ?? extendedWindow.opera ?? ""; + + // Windows Phone must come first because its UA also contains "Android" + if (/windows phone/i.test(userAgent)) { + return "Windows Phone"; + } + + if (/android/i.test(userAgent)) { + // Check if navigator.userAgentData is available for modern browsers + // userAgent only returns a string, while userAgentData returns an object with more detailed information + if (extendedNavigator.userAgentData) { + const ua = await extendedNavigator.userAgentData.getHighEntropyValues(["model"]); + if (validateHuaweiCodes(ua?.model || "")) { + return "huawei"; + } + } else if (validateHuaweiCodes(userAgent) || /huawei/i.test(userAgent)) { + return "huawei"; + } + return "Android"; + } + + if (/iPad|iPhone|iPod/.test(userAgent) && !extendedWindow.MSStream) { + return "iOS"; + } + + return "unknown"; +}; diff --git a/utils-docs/docs/Constants/mobile-devices.md b/utils-docs/docs/Constants/mobile-devices.md new file mode 100644 index 0000000..7234779 --- /dev/null +++ b/utils-docs/docs/Constants/mobile-devices.md @@ -0,0 +1,31 @@ +--- +sidebar_position: 2 +--- + +# mobile-devices + +This utility module provides a regular expression and a set of valid codes to detect and validate Huawei device codes in a string. + +### `huaweiDevicesRegex` + +This regex matches standalone sequences of three uppercase letters followed by a hyphen, using global and case-insensitive search. + +```typescript +import { huaweiDevicesRegex } from "@deriv-com/utils"; + +const isValid = huaweiDevicesRegex.test("ALP-"); // returns true +``` + +### `validCodes` + +This is a set of valid Huawei device codes. It can be used to check if a detected code is a valid Huawei device code. + +```typescript +import { validCodes } from "@deriv-com/utils"; + +const isValidCode = validCodes.has("ALP-"); // returns true +``` + +#### Note + +These utilities can be used in conjunction with the `mobileOSDetectAsync` function to detect if a user is on a Huawei device running Android. If `mobileOSDetectAsync` returns "huawei", you can use `huaweiDevicesRegex` and `validCodes` to further validate the device code. diff --git a/utils-docs/docs/Utils/os-detect.md b/utils-docs/docs/Utils/os-detect.md new file mode 100644 index 0000000..c9a5a3c --- /dev/null +++ b/utils-docs/docs/Utils/os-detect.md @@ -0,0 +1,34 @@ +--- +sidebar_position: 2 +--- + +# os-detect + +This utility module provides functions to detect mobile operating systems and extract information from user agent strings. + +### `mobileOSDetectAsync` + +This function asynchronously detects the mobile operating system based on the user agent string. + +#### Returns + +- `"Windows Phone"` if the user agent string indicates a Windows Phone device. +- `"huawei"` if the user agent string indicates a Huawei device running Android. +- `"Android"` if the user agent string indicates an Android device. +- `"iOS"` if the user agent string indicates an iOS device (iPad, iPhone, or iPod). +- `"unknown"` if the mobile operating system cannot be determined. + +#### Usage + +```typescript +import { mobileOSDetectAsync } from "@deriv-com/utils"; + +const os = await mobileOSDetectAsync(); + +if (os === "iOS") { + console.log("client on iOS"); +} else if (os === "huawei") { + console.log("client on huawei"); +} +console.log("client on android"); +```