From 92c487eb00032865457864263d538e7bb1950c6e Mon Sep 17 00:00:00 2001 From: Hasan Mobarak <126637868+hasan-deriv@users.noreply.github.com> Date: Fri, 17 May 2024 10:56:16 +0800 Subject: [PATCH] [CFDS] Hasan/CFDS-3424/mt5 deeplink redirect issue (#14547) * chore: console mobile os detect func * chore: console mobile os detect func * chore: console mobile os detect func * chore: updated mobile os detect function * chore: updated mobile os detect function * chore: created async based os detect function * chore: console * chore: console * chore: test react-device-detect package * chore: test react-device-detect package * chore: test react-device-detect package * Revert "chore: test react-device-detect package" This reverts commit 1ec874f140d40f42627f5079da334f793a951d00. * Revert "chore: test react-device-detect package" This reverts commit a71712abe2969943057fd6af5f3e1db26713c25b. * Revert "chore: test react-device-detect package" This reverts commit 397a006d9e206e487e598b85dcf1439eef8df91c. * chore: test new regex * chore: test new regex * Revert "chore: test new regex" This reverts commit 74835137bdf93181acfcab5ff0f1844e127a83b9. * Revert "chore: test new regex" This reverts commit fba5af888b0168a11792b046d68d7ea72772c1e3. * chore: removed test useEffect * chore: updated test cases * fix: sonarcloud issues * fix: updated huawei regex * fix: console * fix: removed updated regex * fix: updated regex * fix: updated regex to Set * chore: console ua parser * chore: console ua parser * Revert "chore: console ua parser" This reverts commit 4f6e5813c57a421e9f84356b88d5798656a9e975. * Revert "chore: console ua parser" This reverts commit 6d3e714b5752e02ef7a8a9e4f58467c675a55b12. * chore: merge master --- .../mt5-mobile-redirect-option.spec.js | 17 +- .../Containers/mt5-mobile-redirect-option.tsx | 5 +- packages/cfd/src/Helpers/constants.ts | 10 +- packages/shared/src/utils/os/os_detect.ts | 168 ++++++++++++++++++ 4 files changed, 187 insertions(+), 13 deletions(-) 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;