diff --git a/packages/cfd/src/Containers/__tests__/mt5-mobile-redirect-option.spec.js b/packages/cfd/src/Containers/__tests__/mt5-mobile-redirect-option.spec.js
index cc30b5ac3441..7f9374c4e586 100644
--- a/packages/cfd/src/Containers/__tests__/mt5-mobile-redirect-option.spec.js
+++ b/packages/cfd/src/Containers/__tests__/mt5-mobile-redirect-option.spec.js
@@ -1,6 +1,6 @@
import React from 'react';
import { render } from '@testing-library/react';
-import { isSafariBrowser, mobileOSDetect } from '@deriv/shared';
+import { isSafariBrowser, mobileOSDetect, mobileOSDetectAsync } from '@deriv/shared';
import MT5MobileRedirectOption from '../mt5-mobile-redirect-option';
import { DEEP_LINK, getMobileAppInstallerURL, getPlatformMt5DownloadLink } from '../../../src/Helpers/constants';
@@ -63,10 +63,11 @@ describe('', () => {
configurable: true,
});
- expect(mobileOSDetect()).toBe('iOS');
+ const os = await mobileOSDetectAsync();
+ expect(os).toBe('iOS');
expect(isSafariBrowser()).toBe(true);
- const expectedUrl = getMobileAppInstallerURL({ mt5_trade_account: mock_props.mt5_trade_account });
+ const expectedUrl = await getMobileAppInstallerURL({ mt5_trade_account: mock_props.mt5_trade_account });
expect(expectedUrl).toBe(mock_props.mt5_trade_account.white_label_links.ios);
});
@@ -75,10 +76,11 @@ describe('', () => {
value: 'Mozilla/5.0 (Linux; Android 10; SM-A205U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.105 Mobile Safari/537.36',
configurable: true,
});
- expect(mobileOSDetect()).toBe('Android');
+ const os = await mobileOSDetectAsync();
+ expect(os).toBe('Android');
expect(isSafariBrowser()).toBe(false);
- const expectedUrl = getMobileAppInstallerURL({ mt5_trade_account: mock_props.mt5_trade_account });
+ const expectedUrl = await getMobileAppInstallerURL({ mt5_trade_account: mock_props.mt5_trade_account });
expect(expectedUrl).toBe(mock_props.mt5_trade_account.white_label_links.android);
});
@@ -87,10 +89,11 @@ describe('', () => {
value: 'Mozilla/5.0 (Linux; Android 10; ELE-L29) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.105 Mobile Safari/537.36',
configurable: true,
});
- expect(mobileOSDetect()).toBe('huawei');
+ const os = await mobileOSDetectAsync();
+ expect(os).toBe('huawei');
expect(isSafariBrowser()).toBe(false);
- const expectedUrl = getMobileAppInstallerURL({ mt5_trade_account: mock_props.mt5_trade_account });
+ const expectedUrl = await getMobileAppInstallerURL({ mt5_trade_account: mock_props.mt5_trade_account });
expect(expectedUrl).toBe(getPlatformMt5DownloadLink('huawei'));
});
diff --git a/packages/cfd/src/Containers/mt5-mobile-redirect-option.tsx b/packages/cfd/src/Containers/mt5-mobile-redirect-option.tsx
index c98d142a10d0..bb4d966863d3 100644
--- a/packages/cfd/src/Containers/mt5-mobile-redirect-option.tsx
+++ b/packages/cfd/src/Containers/mt5-mobile-redirect-option.tsx
@@ -12,11 +12,12 @@ type TMT5MobileRedirectOptionProps = {
const MT5MobileRedirectOption = ({ mt5_trade_account }: TMT5MobileRedirectOptionProps) => {
let mobile_url;
- const mobileURLSet = () => {
+ const mobileURLSet = async () => {
mobile_url = window.location.replace(DEEP_LINK({ mt5_trade_account }));
+ const mobileAppURL = await getMobileAppInstallerURL({ mt5_trade_account });
const timeout = setTimeout(() => {
- mobile_url = window.location.replace(getMobileAppInstallerURL({ mt5_trade_account }) as string);
+ mobile_url = mobileAppURL && window.location.replace(mobileAppURL);
}, 1500);
if (!isSafariBrowser()) {
diff --git a/packages/cfd/src/Helpers/constants.ts b/packages/cfd/src/Helpers/constants.ts
index 1b3c71ab9e9e..70b028b651b9 100644
--- a/packages/cfd/src/Helpers/constants.ts
+++ b/packages/cfd/src/Helpers/constants.ts
@@ -5,7 +5,7 @@ import {
validLength,
validPassword,
validMT5Password,
- mobileOSDetect,
+ mobileOSDetectAsync,
} from '@deriv/shared';
import { localize } from '@deriv/translations';
import { TCFDsPlatformType, TDetailsOfEachMT5Loginid, TMobilePlatforms } from 'Components/props.types';
@@ -151,10 +151,12 @@ const validatePassword = (password: string): string | undefined => {
}
};
-const getMobileAppInstallerURL = ({ mt5_trade_account }: { mt5_trade_account: TDetailsOfEachMT5Loginid }) => {
- if (mobileOSDetect() === 'iOS') {
+const getMobileAppInstallerURL = async ({ mt5_trade_account }: { mt5_trade_account: TDetailsOfEachMT5Loginid }) => {
+ const os = await mobileOSDetectAsync();
+
+ if (os === 'iOS') {
return mt5_trade_account?.white_label_links?.ios;
- } else if (mobileOSDetect() === 'huawei') {
+ } else if (os === 'huawei') {
return getPlatformMt5DownloadLink('huawei');
}
return mt5_trade_account?.white_label_links?.android;
diff --git a/packages/shared/src/utils/os/os_detect.ts b/packages/shared/src/utils/os/os_detect.ts
index 33e9ad860d4d..273a4a6578da 100644
--- a/packages/shared/src/utils/os/os_detect.ts
+++ b/packages/shared/src/utils/os/os_detect.ts
@@ -9,8 +9,24 @@ declare global {
msDetachStream: () => void;
};
}
+ interface Navigator {
+ userAgentData?: NavigatorUAData;
+ }
}
+type NavigatorUAData = {
+ brands: Array<{ brand: string; version: string }>;
+ mobile: boolean;
+ getHighEntropyValues(hints: string[]): Promise;
+};
+
+type HighEntropyValues = {
+ platform?: string;
+ platformVersion?: string;
+ model?: string;
+ uaFullVersion?: string;
+};
+
export const systems = {
mac: ['Mac68K', 'MacIntel', 'MacPPC'],
linux: [
@@ -92,4 +108,156 @@ export const mobileOSDetect = () => {
return 'unknown';
};
+// Simple regular expression to match potential Huawei device codes
+const huaweiDevicesRegex = /\b([A-Z]{3}-)\b/gi;
+
+// Set of valid Huawei device codes
+const validCodes = new Set([
+ '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-',
+]);
+
+// Function to validate Huawei device codes from a string
+function validateHuaweiCodes(inputString: string) {
+ const matches = inputString.match(huaweiDevicesRegex);
+ if (matches) {
+ return matches.filter(code => validCodes.has(code.toUpperCase())).length > 0;
+ }
+ return false;
+}
+
+export const mobileOSDetectAsync = async () => {
+ const userAgent = navigator.userAgent ?? window.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
+ if (navigator?.userAgentData) {
+ const ua = await navigator.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) && !window.MSStream) {
+ return 'iOS';
+ }
+
+ return 'unknown';
+};
+
export const getOSNameWithUAParser = () => UAParser().os.name;