diff --git a/packages/api-v2/src/hooks/__tests__/useSortedMT5Accounts.spec.ts b/packages/api-v2/src/hooks/__tests__/useSortedMT5Accounts.spec.ts new file mode 100644 index 000000000000..cce22069325d --- /dev/null +++ b/packages/api-v2/src/hooks/__tests__/useSortedMT5Accounts.spec.ts @@ -0,0 +1,312 @@ +import { renderHook } from '@testing-library/react-hooks'; +import useActiveAccount from '../useActiveAccount'; +import useAvailableMT5Accounts from '../useAvailableMT5Accounts'; +import useIsEuRegion from '../useIsEuRegion'; +import useMT5AccountsList from '../useMT5AccountsList'; +import useSortedMT5Accounts from '../useSortedMT5Accounts'; +import { cleanup } from '@testing-library/react'; + +jest.mock('../useActiveAccount', () => jest.fn()); +jest.mock('../useAvailableMT5Accounts', () => jest.fn()); +jest.mock('../useIsEuRegion', () => jest.fn()); +jest.mock('../useMT5AccountsList', () => jest.fn()); + +const mockMT5NonEUAvailableAccounts = [ + { + is_default_jurisdiction: 'false', + product: 'standard', + shortcode: 'svg', + }, + { + is_default_jurisdiction: 'false', + product: 'financial', + shortcode: 'svg', + }, + { + is_default_jurisdiction: 'true', + product: 'financial', + shortcode: 'vanuatu', + }, + { + is_default_jurisdiction: 'true', + product: 'stp', + shortcode: 'vanuatu', + }, + { + is_default_jurisdiction: 'true', + product: 'standard', + shortcode: 'vanuatu', + }, + { + is_default_jurisdiction: 'true', + product: 'zero_spread', + shortcode: 'bvi', + }, + { + is_default_jurisdiction: 'true', + product: 'swap_free', + shortcode: 'svg', + }, +]; + +const mockMT5NonEUAddedAccounts = [ + { + is_virtual: false, + landing_company_short: 'vanuatu', + product: 'standard', + }, + { + is_virtual: false, + landing_company_short: 'vanuatu', + product: 'financial', + }, + { + is_virtual: false, + landing_company_short: 'bvi', + product: 'zero_spread', + }, +]; + +const mockMT5EUAvailableAccounts = [ + { + is_default_jurisdiction: 'true', + product: 'financial', + shortcode: 'maltainvest', + }, +]; + +const mockMT5EUAddedAccounts = [ + { + is_virtual: false, + landing_company_short: 'maltainvest', + product: 'financial', + }, +]; + +describe('useSortedMT5Accounts', () => { + beforeEach(() => { + (useActiveAccount as jest.Mock).mockReturnValue({ + data: { is_virtual: false }, + }); + (useIsEuRegion as jest.Mock).mockReturnValue({ + isEUCountry: false, + }); + }); + afterEach(cleanup); + + it('returns non-eu available accounts with default jurisdiction', () => { + (useAvailableMT5Accounts as jest.Mock).mockReturnValue({ + data: mockMT5NonEUAvailableAccounts, + }); + (useMT5AccountsList as jest.Mock).mockReturnValue({ + data: [], + }); + + const { result } = renderHook(() => useSortedMT5Accounts()); + + expect(result.current.data).toEqual([ + { + is_added: false, + is_default_jurisdiction: 'true', + product: 'standard', + shortcode: 'vanuatu', + }, + { + is_added: false, + is_default_jurisdiction: 'true', + product: 'financial', + shortcode: 'vanuatu', + }, + { + is_added: false, + is_default_jurisdiction: 'true', + product: 'swap_free', + shortcode: 'svg', + }, + { + is_added: false, + is_default_jurisdiction: 'true', + product: 'zero_spread', + shortcode: 'bvi', + }, + ]); + }); + + it('returns eu available accounts with default jurisdiction', () => { + (useAvailableMT5Accounts as jest.Mock).mockReturnValue({ + data: mockMT5EUAvailableAccounts, + }); + (useMT5AccountsList as jest.Mock).mockReturnValue({ + data: [], + }); + + const { result } = renderHook(() => useSortedMT5Accounts()); + + expect(result.current.data).toEqual([ + { + is_added: false, + is_default_jurisdiction: 'true', + product: 'financial', + shortcode: 'maltainvest', + }, + ]); + }); + + it('returns list of non-eu added and available accounts after some accounts are created', () => { + (useAvailableMT5Accounts as jest.Mock).mockReturnValue({ + data: mockMT5NonEUAvailableAccounts, + }); + (useMT5AccountsList as jest.Mock).mockReturnValue({ + data: mockMT5NonEUAddedAccounts, + }); + + const { result } = renderHook(() => useSortedMT5Accounts()); + + expect(result.current.data).toEqual([ + { + is_added: true, + is_default_jurisdiction: 'true', + is_virtual: false, + landing_company_short: 'vanuatu', + product: 'standard', + shortcode: 'vanuatu', + }, + { + is_added: true, + is_default_jurisdiction: 'true', + is_virtual: false, + landing_company_short: 'vanuatu', + product: 'financial', + shortcode: 'vanuatu', + }, + { + is_added: false, + is_default_jurisdiction: 'true', + product: 'swap_free', + shortcode: 'svg', + }, + { + is_added: true, + is_default_jurisdiction: 'true', + is_virtual: false, + landing_company_short: 'bvi', + product: 'zero_spread', + shortcode: 'bvi', + }, + ]); + }); + + it('returns list of eu added and available accounts after some accounts are created', () => { + (useAvailableMT5Accounts as jest.Mock).mockReturnValue({ + data: mockMT5EUAvailableAccounts, + }); + (useMT5AccountsList as jest.Mock).mockReturnValue({ + data: mockMT5EUAddedAccounts, + }); + (useIsEuRegion as jest.Mock).mockReturnValue({ + isEUCountry: true, + }); + + const { result } = renderHook(() => useSortedMT5Accounts()); + + expect(result.current.data).toEqual([ + { + is_added: true, + is_default_jurisdiction: 'true', + is_virtual: false, + landing_company_short: 'maltainvest', + product: 'financial', + shortcode: 'maltainvest', + }, + ]); + }); + + it('returns sorted non-eu accounts list in the correct order', () => { + (useAvailableMT5Accounts as jest.Mock).mockReturnValue({ + data: mockMT5NonEUAvailableAccounts, + }); + (useMT5AccountsList as jest.Mock).mockReturnValue({ + data: [], + }); + + const { result } = renderHook(() => useSortedMT5Accounts()); + + expect(result.current.data?.map(account => account.product)).toStrictEqual([ + 'standard', + 'financial', + 'swap_free', + 'zero_spread', + ]); + }); + + it('filters-out available MT5 financial stp account disabling clients to create it', () => { + (useAvailableMT5Accounts as jest.Mock).mockReturnValue({ + data: mockMT5NonEUAvailableAccounts, + }); + (useMT5AccountsList as jest.Mock).mockReturnValue({ + data: [], + }); + + const { result } = renderHook(() => useSortedMT5Accounts()); + + expect(result.current.data).not.toContain({ + is_added: false, + is_default_jurisdiction: 'true', + product: 'stp', + shortcode: 'vanuatu', + }); + }); + + it('all available MT5 accounts are created', () => { + (useAvailableMT5Accounts as jest.Mock).mockReturnValue({ + data: mockMT5NonEUAvailableAccounts, + }); + (useMT5AccountsList as jest.Mock).mockReturnValue({ + data: [ + ...mockMT5NonEUAddedAccounts, + { + is_virtual: false, + landing_company_short: 'svg', + product: 'swap_free', + }, + ], + }); + + const { result } = renderHook(() => useSortedMT5Accounts()); + + expect(result.current.data).toEqual([ + { + is_added: true, + is_default_jurisdiction: 'true', + is_virtual: false, + landing_company_short: 'vanuatu', + product: 'standard', + shortcode: 'vanuatu', + }, + { + is_added: true, + is_default_jurisdiction: 'true', + is_virtual: false, + landing_company_short: 'vanuatu', + product: 'financial', + shortcode: 'vanuatu', + }, + { + is_added: true, + is_default_jurisdiction: 'true', + is_virtual: false, + landing_company_short: 'svg', + product: 'swap_free', + shortcode: 'svg', + }, + { + is_added: true, + is_default_jurisdiction: 'true', + is_virtual: false, + landing_company_short: 'bvi', + product: 'zero_spread', + shortcode: 'bvi', + }, + ]); + }); +}); diff --git a/packages/api-v2/src/hooks/useSortedMT5Accounts.ts b/packages/api-v2/src/hooks/useSortedMT5Accounts.ts index 7db93f7d010f..5cc315d4a15f 100644 --- a/packages/api-v2/src/hooks/useSortedMT5Accounts.ts +++ b/packages/api-v2/src/hooks/useSortedMT5Accounts.ts @@ -28,79 +28,65 @@ const useSortedMT5Accounts = (regulation?: string) => { : account.landing_company_short !== 'maltainvest') ); - return filtered_available_accounts?.map(available_account => { + const combined_accounts = filtered_available_accounts?.map(available_account => { const created_account = filtered_mt5_accounts?.find(account => { return ( - available_account.market_type === account.market_type && + available_account.product === account.product && available_account.shortcode === account.landing_company_short ); }); - if (created_account) - return { - ...created_account, - /** Determine if the account is added or not */ - is_added: true, - } as const; - return { ...available_account, + ...created_account, /** Determine if the account is added or not */ - is_added: false, - } as const; + is_added: Boolean(created_account), + }; }); - }, [activeAccount?.is_virtual, all_available_mt5_accounts, isEU, mt5_accounts]); - // // Reduce out the added and non added accounts to make sure only one of each market_type is shown for not added - const filtered_data = useMemo(() => { - if (!modified_data) return; - - const added_accounts = modified_data.filter(account => account.is_added); - const non_added_accounts = modified_data.filter(account => !account.is_added); - - const filtered_non_added_accounts = non_added_accounts.reduce((acc, account) => { - const { market_type, product } = account; - const key = product === 'zero_spread' ? `${market_type}_${product}` : market_type; - - const existing_account = acc.find(acc_account => - acc_account.product === 'zero_spread' - ? `${acc_account.market_type}_${acc_account.product}` === key - : acc_account.market_type === key - ); - const added_account = added_accounts.find(acc_account => - acc_account.product === 'zero_spread' - ? `${acc_account.market_type}_${acc_account.product}` === key - : acc_account.market_type === key - ); - if (existing_account || added_account) return acc; - - return [...acc, account]; - }, [] as typeof non_added_accounts); - - return [...added_accounts, ...filtered_non_added_accounts]; - }, [modified_data]); + return Array.from( + // filter out only one account per product type + combined_accounts + .reduce( + ( + acc: Map, + cur: typeof combined_accounts[number] + ) => { + const existingItem = acc.get(cur.product); + + // Note: 'stp' product type account is not available for creation but we still support existing 'stp' accounts + // @ts-expect-error type for is_default_jurisdiction is unavailable in mt5_login_list and trading_platform_available_accounts + if (!existingItem && cur.is_default_jurisdiction === 'true' && cur.product !== 'stp') { + // No existing item for this product, add it directly + acc.set(cur.product, cur); + } else if (cur.is_added) { + // If `is_added` is true, replace the older entry (prioritization) + acc.set(cur.product, cur); + } + + return acc; + }, + new Map() + ) + .values() + ); + }, [activeAccount?.is_virtual, all_available_mt5_accounts, isEU, mt5_accounts]); - // Sort the data by market_type and product to make sure the order is 'synthetic', 'financial', 'swap_free' and 'zero_spread' const sorted_data = useMemo(() => { - const sorting_order = ['synthetic', 'financial', 'swap_free', 'zero_spread']; + const sorting_order = ['standard', 'financial', 'stp', 'swap_free', 'zero_spread']; - if (!filtered_data) return; + if (!modified_data) return; const sorted_data = sorting_order.reduce((acc, sort_order) => { - const accounts = filtered_data.filter(account => { - if (account.market_type === 'all') { - return account.product === sort_order; - } - return account.market_type === sort_order; - }); + const accounts = modified_data.filter(account => account.product === sort_order); if (!accounts.length) return acc; return [...acc, ...accounts]; - }, [] as typeof filtered_data); + }, [] as typeof modified_data); return sorted_data; - }, [filtered_data]); + }, [modified_data]); - const areAllAccountsCreated = sorted_data?.length === all_available_mt5_accounts?.length; + const areAllAccountsCreated = modified_data?.length === all_available_mt5_accounts?.length; return { data: sorted_data, diff --git a/packages/core/package.json b/packages/core/package.json index dd3fcd73d8e5..c4fb55a2368a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -158,4 +158,4 @@ "react-window": "^1.8.5", "usehooks-ts": "^2.7.0" } -} \ No newline at end of file +} diff --git a/packages/wallets/src/features/cfd/components/CFDPasswordModalTnc/CFDPasswordModalTnc.scss b/packages/wallets/src/features/cfd/components/CFDPasswordModalTnc/CFDPasswordModalTnc.scss deleted file mode 100644 index 5f1047baca7e..000000000000 --- a/packages/wallets/src/features/cfd/components/CFDPasswordModalTnc/CFDPasswordModalTnc.scss +++ /dev/null @@ -1,9 +0,0 @@ -.wallets-cfd-modal-tnc { - display: flex; - flex-direction: column; - gap: 1.6rem; - - @include mobile-or-tablet-screen { - margin-top: auto; - } -} diff --git a/packages/wallets/src/features/cfd/components/CFDPasswordModalTnc/CFDPasswordModalTnc.tsx b/packages/wallets/src/features/cfd/components/CFDPasswordModalTnc/CFDPasswordModalTnc.tsx deleted file mode 100644 index ed0ccb3c891c..000000000000 --- a/packages/wallets/src/features/cfd/components/CFDPasswordModalTnc/CFDPasswordModalTnc.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import React from 'react'; -import { Localize, useTranslations } from '@deriv-com/translations'; -import { Checkbox, InlineMessage, Text, useDevice } from '@deriv-com/ui'; -import { WalletLink } from '../../../../components/Base'; -import { useModal } from '../../../../components/ModalProvider'; -import { THooks, TPlatforms } from '../../../../types'; -import { companyNamesAndUrls, getMarketTypeDetails, PlatformDetails } from '../../constants'; -import './CFDPasswordModalTnc.scss'; - -export type TCFDPasswordModalTncProps = { - checked: boolean; - onChange: () => void; - platform: TPlatforms.All; - product?: THooks.AvailableMT5Accounts['product']; -}; - -const CFDPasswordModalTnc = ({ checked, onChange, platform, product }: TCFDPasswordModalTncProps) => { - const { isDesktop } = useDevice(); - const { getModalState } = useModal(); - const { localize } = useTranslations(); - const selectedJurisdiction = getModalState('selectedJurisdiction'); - const selectedCompany = companyNamesAndUrls[selectedJurisdiction as keyof typeof companyNamesAndUrls]; - const platformTitle = PlatformDetails[platform].title; - const productTitle = getMarketTypeDetails(localize, product).all.title; - - return ( -
- - - - - - - ]} - i18n_default_text='I confirm and accept {{company}}’s <0>terms and conditions' - values={{ - company: selectedCompany.name, - }} - /> - - } - name='zerospread-checkbox' - onChange={onChange} - /> -
- ); -}; - -export default CFDPasswordModalTnc; diff --git a/packages/wallets/src/features/cfd/components/CFDPasswordModalTnc/index.ts b/packages/wallets/src/features/cfd/components/CFDPasswordModalTnc/index.ts deleted file mode 100644 index bea48cf4ec04..000000000000 --- a/packages/wallets/src/features/cfd/components/CFDPasswordModalTnc/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as CFDPasswordModalTnc } from './CFDPasswordModalTnc'; diff --git a/packages/wallets/src/features/cfd/components/ClientVerificationBadge/ClientVerificationStatusBadge.scss b/packages/wallets/src/features/cfd/components/ClientVerificationBadge/ClientVerificationStatusBadge.scss new file mode 100644 index 000000000000..101e4280e4a1 --- /dev/null +++ b/packages/wallets/src/features/cfd/components/ClientVerificationBadge/ClientVerificationStatusBadge.scss @@ -0,0 +1,9 @@ +.wallets-client-verification-badge { + margin-inline-end: auto; + + &__content { + &--underlined { + text-decoration: underline; + } + } +} diff --git a/packages/wallets/src/features/cfd/components/ClientVerificationBadge/ClientVerificationStatusBadge.tsx b/packages/wallets/src/features/cfd/components/ClientVerificationBadge/ClientVerificationStatusBadge.tsx new file mode 100644 index 000000000000..6eea858836d2 --- /dev/null +++ b/packages/wallets/src/features/cfd/components/ClientVerificationBadge/ClientVerificationStatusBadge.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import classNames from 'classnames'; +import { + LabelPairedCircleCheckCaptionBoldIcon, + LabelPairedCircleExclamationCaptionBoldIcon, + LabelPairedClockThreeCaptionBoldIcon, + LabelPairedTriangleExclamationCaptionBoldIcon, +} from '@deriv/quill-icons'; +import { useTranslations } from '@deriv-com/translations'; +import { Badge, Text, useDevice } from '@deriv-com/ui'; +import { TTranslations } from '../../../../types'; +import './ClientVerificationStatusBadge.scss'; + +type TBadgeColor = React.ComponentProps['color']; + +const getBadgeVariations = (localize: TTranslations['localize']) => { + return { + failed: { + color: 'danger-secondary', + content: localize('Failed'), + icon: , + }, + in_review: { + color: 'warning-secondary', + content: localize('In review'), + icon: , + }, + needs_verification: { + color: 'blue-secondary', + content: localize('Needs verification'), + icon: , + }, + verified: { + color: 'success-secondary', + content: localize('Verified'), + icon: , + }, + }; +}; + +type TClientVerificationBadgeProps = { + onClick?: VoidFunction; + variant: keyof ReturnType; +}; + +const ClientVerificationStatusBadge: React.FC = ({ onClick, variant }) => { + const { localize } = useTranslations(); + const { isDesktop } = useDevice(); + const { color, content, icon } = getBadgeVariations(localize)[variant]; + return ( + { + if (onClick) { + e.stopPropagation(); + onClick(); + } + }} + > + + {content} + + + ); +}; + +export default ClientVerificationStatusBadge; diff --git a/packages/wallets/src/features/cfd/components/ClientVerificationBadge/index.ts b/packages/wallets/src/features/cfd/components/ClientVerificationBadge/index.ts new file mode 100644 index 000000000000..7c8dfffb6e93 --- /dev/null +++ b/packages/wallets/src/features/cfd/components/ClientVerificationBadge/index.ts @@ -0,0 +1 @@ +export { default as ClientVerificationStatusBadge } from './ClientVerificationStatusBadge'; diff --git a/packages/wallets/src/features/cfd/components/PlatformStatusBadge/PlatformStatusBadge.tsx b/packages/wallets/src/features/cfd/components/PlatformStatusBadge/PlatformStatusBadge.tsx index 61faecd6b7b0..3403b31707f4 100644 --- a/packages/wallets/src/features/cfd/components/PlatformStatusBadge/PlatformStatusBadge.tsx +++ b/packages/wallets/src/features/cfd/components/PlatformStatusBadge/PlatformStatusBadge.tsx @@ -11,7 +11,7 @@ type TProps = { badgeSize: ComponentProps['badgeSize']; cashierAccount?: TAccount; className?: ComponentProps['className']; - mt5Account?: THooks.MT5AccountsList; + mt5Account?: THooks.SortedMT5Accounts; }; const PlatformStatusBadge: React.FC = ({ badgeSize, cashierAccount, className, mt5Account }) => { diff --git a/packages/wallets/src/features/cfd/components/index.ts b/packages/wallets/src/features/cfd/components/index.ts index ac62e79079c5..04b24a2fe7fc 100644 --- a/packages/wallets/src/features/cfd/components/index.ts +++ b/packages/wallets/src/features/cfd/components/index.ts @@ -1,4 +1,5 @@ export * from './CFDPlatformsListAccounts'; +export * from './ClientVerificationBadge'; export * from './CompareAccountsCarousel'; export * from './ModalTradeWrapper'; export * from './PlatformStatusBadge'; diff --git a/packages/wallets/src/features/cfd/constants.tsx b/packages/wallets/src/features/cfd/constants.tsx index bd51f9972fba..4e9645d70194 100644 --- a/packages/wallets/src/features/cfd/constants.tsx +++ b/packages/wallets/src/features/cfd/constants.tsx @@ -185,12 +185,22 @@ export const MT5_ACCOUNT_STATUS = { FAILED: 'failed', MIGRATED_WITH_POSITION: 'migrated_with_position', MIGRATED_WITHOUT_POSITION: 'migrated_without_position', - NEEDS_VERIFICATION: 'needs_verification', PENDING: 'pending', - POA_PENDING: 'poa_pending', - POA_VERIFIED: 'poa_verified', UNAVAILABLE: 'unavailable', UNDER_MAINTENANCE: 'under_maintenance', + // TODO: remove all the statuses below once the KYC statuses are consolidated by BE + // eslint-disable-next-line sort-keys + POA_FAILED: 'poa_failed', + POA_OUTDATED: 'poa_outdated', + PROOF_FAILED: 'proof_failed', + + // eslint-disable-next-line sort-keys + NEEDS_VERIFICATION: 'needs_verification', + POA_PENDING: 'poa_pending', + // eslint-disable-next-line sort-keys + VERIFICATION_PENDING: 'verification_pending', + // eslint-disable-next-line sort-keys + POA_VERIFIED: 'poa_verified', } as const; /** diff --git a/packages/wallets/src/features/cfd/flows/MT5/AddedMT5AccountsList/AddedMT5AccountsList.tsx b/packages/wallets/src/features/cfd/flows/MT5/AddedMT5AccountsList/AddedMT5AccountsList.tsx index 740ca6c56dd7..a302a7cad5a2 100644 --- a/packages/wallets/src/features/cfd/flows/MT5/AddedMT5AccountsList/AddedMT5AccountsList.tsx +++ b/packages/wallets/src/features/cfd/flows/MT5/AddedMT5AccountsList/AddedMT5AccountsList.tsx @@ -1,149 +1,80 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import classNames from 'classnames'; -import { useJurisdictionStatus, useTradingPlatformStatus } from '@deriv/api-v2'; import { LabelPairedChevronLeftCaptionRegularIcon, LabelPairedChevronRightCaptionRegularIcon, - LabelPairedCircleExclamationLgBoldIcon, - LabelPairedTriangleExclamationMdBoldIcon, } from '@deriv/quill-icons'; -import { Localize, useTranslations } from '@deriv-com/translations'; -import { InlineMessage, Text } from '@deriv-com/ui'; +import { Text } from '@deriv-com/ui'; import { useModal } from '../../../../../components/ModalProvider'; import { TradingAccountCard } from '../../../../../components/TradingAccountCard'; import useIsRtl from '../../../../../hooks/useIsRtl'; import { THooks } from '../../../../../types'; -import { PlatformStatusBadge } from '../../../components/PlatformStatusBadge'; -import { - getMarketTypeDetails, - JURISDICTION, - MARKET_TYPE, - MT5_ACCOUNT_STATUS, - PlatformDetails, - TRADING_PLATFORM_STATUS, -} from '../../../constants'; -import { MT5TradeModal, TradingPlatformStatusModal, VerificationFailedModal } from '../../../modals'; +import { ClientVerificationStatusBadge, PlatformStatusBadge } from '../../../components'; +import { MARKET_TYPE, PlatformDetails } from '../../../constants'; +import { ClientVerificationModal, MT5TradeModal, TradingPlatformStatusModal } from '../../../modals'; +import { TModifiedMT5Accounts } from '../../../types'; +import { useAddedMT5Account } from './hooks'; import './AddedMT5AccountsList.scss'; type TProps = { - account: THooks.MT5AccountsList; + account: THooks.SortedMT5Accounts; }; const AddedMT5AccountsList: React.FC = ({ account }) => { - const { getVerificationStatus } = useJurisdictionStatus(); - const { localize } = useTranslations(); const isRtl = useIsRtl(); - const jurisdictionStatus = useMemo( - () => getVerificationStatus(account.landing_company_short || JURISDICTION.SVG, account.status), - [account.landing_company_short, account.status, getVerificationStatus] - ); - const { title } = getMarketTypeDetails(localize, account.product)[account.market_type ?? MARKET_TYPE.ALL]; - const { show } = useModal(); + const { accountDetails, isServerMaintenance, kycStatus, showMT5TradeModal, showPlatformStatus } = + useAddedMT5Account(account as TModifiedMT5Accounts); - const { getPlatformStatus } = useTradingPlatformStatus(); - const platformStatus = getPlatformStatus(account.platform); - - const hasPlatformStatus = - account.status === TRADING_PLATFORM_STATUS.UNAVAILABLE || - account.status === MT5_ACCOUNT_STATUS.UNDER_MAINTENANCE || - platformStatus === TRADING_PLATFORM_STATUS.MAINTENANCE; + const { show } = useModal(); - const isServerMaintenance = - platformStatus === TRADING_PLATFORM_STATUS.MAINTENANCE || - account.status === MT5_ACCOUNT_STATUS.UNDER_MAINTENANCE; - const showPlatformStatus = hasPlatformStatus && !(jurisdictionStatus.is_pending || jurisdictionStatus.is_failed); return ( { - if (hasPlatformStatus) + if (showPlatformStatus) { return show(, { defaultRootId: 'wallets_modal_root', }); - if (platformStatus === TRADING_PLATFORM_STATUS.ACTIVE) { - return jurisdictionStatus.is_failed - ? show(, { - defaultRootId: 'wallets_modal_root', - }) - : show( - - ); + } + + if (showMT5TradeModal) { + return show( + , + { defaultRootId: 'wallets_modal_root' } + ); } }} > - - {getMarketTypeDetails(localize, account.product)[account.market_type || MARKET_TYPE.ALL].icon} - + {accountDetails.icon}
- {title} + {accountDetails.title}
- {!(jurisdictionStatus.is_failed || jurisdictionStatus.is_pending) && ( + {!kycStatus && ( {account.display_balance} )} - {account.display_login} - {jurisdictionStatus.is_pending && ( - - } - > - - - - - )} - - {jurisdictionStatus.is_failed && ( - + {kycStatus && ( + + show(, { + defaultRootId: 'wallets_modal_root', + }) } - > - - - show( - , - { - defaultRootId: 'wallets_modal_root', - } - ) - } - />, - ]} - i18n_default_text='Verification failed <0>Why?' - /> - - + variant={kycStatus} + /> )}
{showPlatformStatus ? ( diff --git a/packages/wallets/src/features/cfd/flows/MT5/AddedMT5AccountsList/__test__/AddedMT5AccountsList.spec.tsx b/packages/wallets/src/features/cfd/flows/MT5/AddedMT5AccountsList/__test__/AddedMT5AccountsList.spec.tsx index 19c39d23ff48..2862ea76e7e3 100644 --- a/packages/wallets/src/features/cfd/flows/MT5/AddedMT5AccountsList/__test__/AddedMT5AccountsList.spec.tsx +++ b/packages/wallets/src/features/cfd/flows/MT5/AddedMT5AccountsList/__test__/AddedMT5AccountsList.spec.tsx @@ -1,135 +1,168 @@ import React from 'react'; -import { useJurisdictionStatus, useTradingPlatformStatus } from '@deriv/api-v2'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { useModal } from '../../../../../../components/ModalProvider'; -import { MT5TradeModal, TradingPlatformStatusModal, VerificationFailedModal } from '../../../../modals'; +import { ModalProvider } from '../../../../../../components/ModalProvider'; +import { PlatformDetails } from '../../../../constants'; import AddedMT5AccountsList from '../AddedMT5AccountsList'; +import { useAddedMT5Account } from '../hooks'; -jest.mock('@deriv/api-v2', () => ({ - useJurisdictionStatus: jest.fn(), - useTradingPlatformStatus: jest.fn(), +// mock function to check if correct props are passed to the modal components +const mockPropsFn = jest.fn(); + +jest.mock('../hooks', () => ({ + useAddedMT5Account: jest.fn(), +})); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useHistory: jest.fn(() => ({ + push: jest.fn(), + })), +})); + +jest.mock('../../../../components', () => ({ + ...jest.requireActual('../../../../components'), + ClientVerificationStatusBadge: jest.fn(props => { + mockPropsFn(props.variant); + return ( +
{ + e.stopPropagation(); + props.onClick(); + }} + > + ClientVerificationStatusBadge +
+ ); + }), + PlatformStatusBadge: jest.fn(props => { + mockPropsFn(props); + return
PlatformStatusBadge
; + }), })); -jest.mock('../../../../../../components/ModalProvider', () => ({ - useModal: jest.fn(), +jest.mock('../../../../modals', () => ({ + ...jest.requireActual('../../../../modals'), + ClientVerificationModal: jest.fn(props => { + mockPropsFn(props); + return
ClientVerificationModal
; + }), + MT5TradeModal: jest.fn(props => { + mockPropsFn(props); + return
MT5TradeModal
; + }), + TradingPlatformStatusModal: jest.fn(props => { + mockPropsFn(props); + return
TradingPlatformStatusModal
; + }), })); +const mockAccount = { + display_balance: 'USD 1000.00', + display_login: '12345678', + landing_company_short: 'svg', + market_type: 'financial', + platform: 'mt5', + product: 'financial', + status: 'active', +}; + +const mockUseAddedMT5AccountData = { + accountDetails: { + icon: ( + <> + icon-{mockAccount.platform}-{mockAccount.product} + + ), + title: 'Financial', + }, + isServerMaintenance: false, + showClientVerificationModal: false, + showMT5TradeModal: true, + showPlatformStatus: false, +}; + +const wrapper: React.FC = ({ children }) => ( + <> + {children} + +); + describe('AddedMT5AccountsList', () => { - const mockAccount = { - display_balance: 'USD 1000.00', - display_login: '12345678', - landing_company_short: 'svg', - market_type: 'financial', - platform: 'mt5', - product: 'standard', - status: 'active', - }; - - const mockShow = jest.fn(); + // const mockShow = jest.fn(); + beforeAll(() => { + const modalRoot = document.createElement('div'); + modalRoot.setAttribute('id', 'wallets_modal_root'); + document.body.appendChild(modalRoot); + }); beforeEach(() => { - (useJurisdictionStatus as jest.Mock).mockReturnValue({ - getVerificationStatus: jest.fn().mockReturnValue({ is_failed: false, is_pending: false }), - }); - (useTradingPlatformStatus as jest.Mock).mockReturnValue({ - getPlatformStatus: jest.fn().mockReturnValue('active'), - }); - (useModal as jest.Mock).mockReturnValue({ show: mockShow }); + (useAddedMT5Account as jest.Mock).mockReturnValue(mockUseAddedMT5AccountData); }); - it('renders added mt5 accounts list with correct account details', () => { + it('displays added mt5 account with correct account details', () => { // @ts-expect-error - since this is a mock, we only need partial properties of the account - render(); + render(, { wrapper }); + expect(screen.getByText('icon-mt5-financial')).toBeInTheDocument(); expect(screen.getByText('Financial')).toBeInTheDocument(); expect(screen.getByText('USD 1000.00')).toBeInTheDocument(); expect(screen.getByText('12345678')).toBeInTheDocument(); }); - it('shows MT5TradeModal when list is clicked and status is active', async () => { - // @ts-expect-error - since this is a mock, we only need partial properties of the account - render(); - - userEvent.click(screen.getByTestId('dt_wallets_trading_account_card')); - - await waitFor(() => { - expect(mockShow).toHaveBeenCalledWith( - // @ts-expect-error - since this is a mock, we only need partial properties of the account - - ); - }); - }); - - it('shows TradingPlatformStatusModal when platform is under maintenance', async () => { - (useTradingPlatformStatus as jest.Mock).mockReturnValue({ - getPlatformStatus: jest.fn().mockReturnValue('maintenance'), + it('displays correct variant of ClientVerificationStatusBadge and renders modal with ClientVerificationModal when clicked on it', async () => { + (useAddedMT5Account as jest.Mock).mockReturnValue({ + ...mockUseAddedMT5AccountData, + kycStatus: 'mockKycStatus', }); // @ts-expect-error - since this is a mock, we only need partial properties of the account - render(); + render(, { wrapper }); - userEvent.click(screen.getByTestId('dt_wallets_trading_account_card')); + const badge = screen.getByText('ClientVerificationStatusBadge'); + + expect(badge).toBeInTheDocument(); + expect(mockPropsFn).toBeCalledWith('mockKycStatus'); + + userEvent.click(badge); await waitFor(() => { - expect(mockShow).toHaveBeenCalledWith(, { - defaultRootId: 'wallets_modal_root', - }); + expect(screen.getByText('ClientVerificationModal')).toBeInTheDocument(); }); }); - it('shows VerificationFailedModal when verification has failed', async () => { - (useJurisdictionStatus as jest.Mock).mockReturnValue({ - getVerificationStatus: jest.fn().mockReturnValue({ is_failed: true, is_pending: false }), - }); + it('shows MT5TradeModal when list is clicked and status is active', async () => { // @ts-expect-error - since this is a mock, we only need partial properties of the account - render(); + render(, { wrapper }); userEvent.click(screen.getByTestId('dt_wallets_trading_account_card')); await waitFor(() => { - expect(mockShow).toHaveBeenCalledWith(, { - defaultRootId: 'wallets_modal_root', + expect(screen.getByText('MT5TradeModal')).toBeInTheDocument(); + expect(mockPropsFn).toBeCalledWith({ + marketType: mockAccount.market_type, + mt5Account: mockAccount, + platform: PlatformDetails.mt5.platform, }); }); }); - it('displays pending verification message when status is pending', () => { - (useJurisdictionStatus as jest.Mock).mockReturnValue({ - getVerificationStatus: jest.fn().mockReturnValue({ is_failed: false, is_pending: true }), - }); - - // @ts-expect-error - since this is a mock, we only need partial properties of the account - render(); - - expect(screen.getByText('Pending verification')).toBeInTheDocument(); - }); - - it('displays verification failed message when verification has failed', () => { - (useJurisdictionStatus as jest.Mock).mockReturnValue({ - getVerificationStatus: jest.fn().mockReturnValue({ is_failed: true, is_pending: false }), - }); - // @ts-expect-error - since this is a mock, we only need partial properties of the account - render(); - - expect(screen.getByText('Verification failed')).toBeInTheDocument(); - expect(screen.getByText('Why?')).toBeInTheDocument(); - }); - - it('displays VerificationFailedModal when "Why?" link is clicked', async () => { - (useJurisdictionStatus as jest.Mock).mockReturnValue({ - getVerificationStatus: jest.fn().mockReturnValue({ is_failed: true, is_pending: false }), + it('shows TradingPlatformStatusModal when platform is under maintenance', async () => { + (useAddedMT5Account as jest.Mock).mockReturnValue({ + ...mockUseAddedMT5AccountData, + isServerMaintenance: true, + showPlatformStatus: true, }); // @ts-expect-error - since this is a mock, we only need partial properties of the account - render(); + render(, { wrapper }); - const link = screen.getByText('Why?'); - userEvent.click(link); + userEvent.click(screen.getByTestId('dt_wallets_trading_account_card')); await waitFor(() => { - expect(mockShow).toHaveBeenCalledWith(, { - defaultRootId: 'wallets_modal_root', + expect(screen.getByText('TradingPlatformStatusModal')).toBeInTheDocument(); + expect(mockPropsFn).toBeCalledWith({ + isServerMaintenance: true, }); }); }); diff --git a/packages/wallets/src/features/cfd/flows/MT5/AddedMT5AccountsList/hooks/__tests__/useAddedMT5Account.spec.ts b/packages/wallets/src/features/cfd/flows/MT5/AddedMT5AccountsList/hooks/__tests__/useAddedMT5Account.spec.ts new file mode 100644 index 000000000000..c974bd85c4f7 --- /dev/null +++ b/packages/wallets/src/features/cfd/flows/MT5/AddedMT5AccountsList/hooks/__tests__/useAddedMT5Account.spec.ts @@ -0,0 +1,125 @@ +import { useTradingPlatformStatus } from '@deriv/api-v2'; +import { cleanup } from '@testing-library/react'; +import { renderHook } from '@testing-library/react-hooks'; +import { getMarketTypeDetails } from '../../../../../constants'; +import { TModifiedMT5Accounts } from '../../../../../types'; +import useAddedMT5Account from '../useAddedMT5Account'; + +jest.mock('@deriv/api-v2', () => ({ + ...jest.requireActual('@deriv/api-v2'), + useTradingPlatformStatus: jest.fn(), +})); + +jest.mock('../../../../../constants', () => ({ + ...jest.requireActual('../../../../../constants'), + getMarketTypeDetails: jest.fn(), +})); + +const mockAccount = { + market_type: 'financial', + product: 'financial', + status: '', +}; + +describe('useAddedMT5Account', () => { + beforeEach(() => { + (useTradingPlatformStatus as jest.Mock).mockReturnValue({ + getPlatformStatus: jest.fn(), + }); + }); + afterEach(cleanup); + + it('provides correct account details based on the market type', () => { + (getMarketTypeDetails as jest.Mock).mockReturnValue({ financial: 'mock-account-details' }); + + const { result } = renderHook(() => useAddedMT5Account(mockAccount as TModifiedMT5Accounts)); + + expect(result.current.accountDetails).toEqual('mock-account-details'); + }); + + it('isServerMaintenance is `true` when trading platform status is `maintenance`', () => { + (useTradingPlatformStatus as jest.Mock).mockReturnValue({ + getPlatformStatus: jest.fn(() => 'maintenance'), + }); + + const { result } = renderHook(() => useAddedMT5Account(mockAccount as TModifiedMT5Accounts)); + + expect(result.current.isServerMaintenance).toEqual(true); + }); + + it('isServerMaintenance is `true` when account status is `under_maintenance`', () => { + const { result } = renderHook(() => + useAddedMT5Account({ ...mockAccount, status: 'under_maintenance' } as TModifiedMT5Accounts) + ); + + expect(result.current.isServerMaintenance).toEqual(true); + }); + + it('kycStatus is `failed` when status received for account is `proof_failed`', () => { + const { result } = renderHook(() => + useAddedMT5Account({ ...mockAccount, status: 'proof_failed' } as TModifiedMT5Accounts) + ); + + expect(result.current.kycStatus).toEqual('failed'); + }); + + it('kycStatus is `failed` when status received for account is `poa_failed`', () => { + const { result } = renderHook(() => + useAddedMT5Account({ ...mockAccount, status: 'poa_failed' } as TModifiedMT5Accounts) + ); + + expect(result.current.kycStatus).toEqual('failed'); + }); + + it('kycStatus is `in_review` when status received for account is `verification_pending`', () => { + const { result } = renderHook(() => + useAddedMT5Account({ ...mockAccount, status: 'verification_pending' } as TModifiedMT5Accounts) + ); + + expect(result.current.kycStatus).toEqual('in_review'); + }); + + it('kycStatus is `needs_verification` when status received for account is `needs_verification`', () => { + const { result } = renderHook(() => + useAddedMT5Account({ ...mockAccount, status: 'needs_verification' } as TModifiedMT5Accounts) + ); + + expect(result.current.kycStatus).toEqual('needs_verification'); + }); + + it('showMT5TradeModal is `true` when platform status is `active`', () => { + (useTradingPlatformStatus as jest.Mock).mockReturnValue({ + getPlatformStatus: jest.fn(() => 'active'), + }); + + const { result } = renderHook(() => useAddedMT5Account(mockAccount as TModifiedMT5Accounts)); + + expect(result.current.showMT5TradeModal).toEqual(true); + }); + + it('showPlatformStatus is `true` when account status is `unavailable`', () => { + const { result } = renderHook(() => + useAddedMT5Account({ ...mockAccount, status: 'unavailable' } as TModifiedMT5Accounts) + ); + + expect(result.current.showPlatformStatus).toEqual(true); + }); + + it('showPlatformStatus is `true` when account status is `under_maintenance`', () => { + const { result } = renderHook(() => + useAddedMT5Account({ ...mockAccount, status: 'under_maintenance' } as TModifiedMT5Accounts) + ); + + expect(result.current.showPlatformStatus).toEqual(true); + }); + + it('showPlatformStatus is `true` when trading platform status is `maintenance`', () => { + (useTradingPlatformStatus as jest.Mock).mockReturnValue({ + getPlatformStatus: jest.fn(() => 'maintenance'), + }); + + const { result } = renderHook(() => useAddedMT5Account(mockAccount as TModifiedMT5Accounts)); + + expect(result.current.showPlatformStatus).toEqual(true); + }); +}); diff --git a/packages/wallets/src/features/cfd/flows/MT5/AddedMT5AccountsList/hooks/index.ts b/packages/wallets/src/features/cfd/flows/MT5/AddedMT5AccountsList/hooks/index.ts new file mode 100644 index 000000000000..1ac54db629bf --- /dev/null +++ b/packages/wallets/src/features/cfd/flows/MT5/AddedMT5AccountsList/hooks/index.ts @@ -0,0 +1 @@ +export { default as useAddedMT5Account } from './useAddedMT5Account'; diff --git a/packages/wallets/src/features/cfd/flows/MT5/AddedMT5AccountsList/hooks/useAddedMT5Account.ts b/packages/wallets/src/features/cfd/flows/MT5/AddedMT5AccountsList/hooks/useAddedMT5Account.ts new file mode 100644 index 000000000000..49e20e5c9aba --- /dev/null +++ b/packages/wallets/src/features/cfd/flows/MT5/AddedMT5AccountsList/hooks/useAddedMT5Account.ts @@ -0,0 +1,54 @@ +import React, { useMemo } from 'react'; +import { useTradingPlatformStatus } from '@deriv/api-v2'; +import { useTranslations } from '@deriv-com/translations'; +import { ClientVerificationStatusBadge } from '../../../../components'; +import { getMarketTypeDetails, MARKET_TYPE, MT5_ACCOUNT_STATUS, TRADING_PLATFORM_STATUS } from '../../../../constants'; +import { TModifiedMT5Accounts } from '../../../../types'; + +type TBadgeVariations = Partial['variant']> | undefined; + +const getClientKycStatus = (status: TModifiedMT5Accounts['status']): TBadgeVariations => { + switch (status) { + case MT5_ACCOUNT_STATUS.POA_FAILED: + case MT5_ACCOUNT_STATUS.PROOF_FAILED: + return 'failed'; + case MT5_ACCOUNT_STATUS.VERIFICATION_PENDING: + return 'in_review'; + case MT5_ACCOUNT_STATUS.NEEDS_VERIFICATION: + return 'needs_verification'; + default: + } +}; + +const useAddedMT5Account = (account: TModifiedMT5Accounts) => { + const { localize } = useTranslations(); + const accountDetails = useMemo( + () => getMarketTypeDetails(localize, account.product)[account.market_type ?? MARKET_TYPE.ALL], + [account.market_type, account.product, localize] + ); + + const { getPlatformStatus } = useTradingPlatformStatus(); + const platformStatus = getPlatformStatus(account.platform); + const kycStatus = getClientKycStatus(account.status); + + const isServerMaintenance = + platformStatus === TRADING_PLATFORM_STATUS.MAINTENANCE || + account.status === MT5_ACCOUNT_STATUS.UNDER_MAINTENANCE; + + const showPlatformStatus = + account.status === MT5_ACCOUNT_STATUS.UNAVAILABLE || + account.status === MT5_ACCOUNT_STATUS.UNDER_MAINTENANCE || + platformStatus === TRADING_PLATFORM_STATUS.MAINTENANCE; + + const showMT5TradeModal = platformStatus === TRADING_PLATFORM_STATUS.ACTIVE; + + return { + accountDetails, + isServerMaintenance, + kycStatus, + showMT5TradeModal, + showPlatformStatus, + }; +}; + +export default useAddedMT5Account; diff --git a/packages/wallets/src/features/cfd/flows/MT5/AvailableMT5AccountsList/AvailableMT5AccountsList.tsx b/packages/wallets/src/features/cfd/flows/MT5/AvailableMT5AccountsList/AvailableMT5AccountsList.tsx index 084b91272fd3..20e4c3fd2871 100644 --- a/packages/wallets/src/features/cfd/flows/MT5/AvailableMT5AccountsList/AvailableMT5AccountsList.tsx +++ b/packages/wallets/src/features/cfd/flows/MT5/AvailableMT5AccountsList/AvailableMT5AccountsList.tsx @@ -1,25 +1,23 @@ -import React, { lazy, Suspense, useCallback, useEffect, useState } from 'react'; +import React, { useCallback } from 'react'; import { useActiveWalletAccount, useMT5AccountsList, useTradingPlatformStatus } from '@deriv/api-v2'; import { LabelPairedChevronLeftCaptionRegularIcon, LabelPairedChevronRightCaptionRegularIcon, } from '@deriv/quill-icons'; import { Localize, useTranslations } from '@deriv-com/translations'; -import { Loader, Text } from '@deriv-com/ui'; +import { Text } from '@deriv-com/ui'; import { TradingAccountCard } from '../../../../../components'; import { useModal } from '../../../../../components/ModalProvider'; import useIsRtl from '../../../../../hooks/useIsRtl'; import { THooks } from '../../../../../types'; import { getMarketTypeDetails, MARKET_TYPE, PRODUCT, TRADING_PLATFORM_STATUS } from '../../../constants'; -import { JurisdictionModal, MT5PasswordModal, TradingPlatformStatusModal } from '../../../modals'; +import { ClientVerificationModal, MT5PasswordModal, TradingPlatformStatusModal } from '../../../modals'; +import { TModifiedMT5Accounts } from '../../../types'; +import { getClientVerification } from '../../../utils'; import './AvailableMT5AccountsList.scss'; -const LazyVerification = lazy( - () => import(/* webpackChunkName: "wallets-client-verification" */ '../../ClientVerification/ClientVerification') -); - type TProps = { - account: THooks.AvailableMT5Accounts; + account: THooks.SortedMT5Accounts; }; const AvailableMT5AccountsList: React.FC = ({ account }) => { @@ -31,10 +29,11 @@ const AvailableMT5AccountsList: React.FC = ({ account }) => { const { description, title } = getMarketTypeDetails(localize, account.product)[ account.market_type || MARKET_TYPE.ALL ]; - const [showMt5PasswordModal, setShowMt5PasswordModal] = useState(false); const { data: mt5Accounts } = useMT5AccountsList(); const platformStatus = getPlatformStatus(account.platform); const hasUnavailableAccount = mt5Accounts?.some(account => account.status === 'unavailable'); + const isVirtual = activeWallet?.is_virtual; + const { hasClientKycStatus } = getClientVerification(account as TModifiedMT5Accounts); const onButtonClick = useCallback(() => { if (hasUnavailableAccount) return show(); @@ -46,58 +45,16 @@ const AvailableMT5AccountsList: React.FC = ({ account }) => { return show(); case TRADING_PLATFORM_STATUS.ACTIVE: default: - if (activeWallet?.is_virtual || account.product === PRODUCT.SWAPFREE) { - show( - - ); - } else if (account.product === PRODUCT.ZEROSPREAD) { - show( - }> - { - setShowMt5PasswordModal(true); - }} - selectedJurisdiction={account.shortcode} - /> - - ); + if (!isVirtual && hasClientKycStatus) { + show(); } else { - show(); + show(); } setModalState('marketType', account.market_type); setModalState('selectedJurisdiction', account.shortcode); break; } - }, [ - hasUnavailableAccount, - show, - platformStatus, - account.platform, - account.market_type, - account.product, - account.shortcode, - activeWallet?.is_virtual, - setModalState, - ]); - - useEffect(() => { - if (showMt5PasswordModal) { - show( - - ); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [showMt5PasswordModal]); + }, [hasUnavailableAccount, show, platformStatus, isVirtual, hasClientKycStatus, setModalState, account]); return ( diff --git a/packages/wallets/src/features/cfd/flows/MT5/AvailableMT5AccountsList/__test__/AvailableMT5AcountsList.spec.tsx b/packages/wallets/src/features/cfd/flows/MT5/AvailableMT5AccountsList/__test__/AvailableMT5AcountsList.spec.tsx index c9756151deeb..8b6ef7708732 100644 --- a/packages/wallets/src/features/cfd/flows/MT5/AvailableMT5AccountsList/__test__/AvailableMT5AcountsList.spec.tsx +++ b/packages/wallets/src/features/cfd/flows/MT5/AvailableMT5AccountsList/__test__/AvailableMT5AcountsList.spec.tsx @@ -1,9 +1,9 @@ import React from 'react'; import { useActiveWalletAccount, useMT5AccountsList, useTradingPlatformStatus } from '@deriv/api-v2'; -import { act, render, screen, waitFor } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { useModal } from '../../../../../../components/ModalProvider'; -import { JurisdictionModal, MT5PasswordModal, TradingPlatformStatusModal } from '../../../../modals'; +import { ClientVerificationModal, MT5PasswordModal, TradingPlatformStatusModal } from '../../../../modals'; import AvailableMT5AccountsList from '../AvailableMT5AccountsList'; jest.mock('@deriv/api-v2', () => ({ @@ -42,16 +42,38 @@ describe('AvailableMT5AccountsList', () => { }); }); - const defaultAccount = { + const nonRegulatedAccount = { market_type: 'synthetic', platform: 'mt5', product: 'swap_free', shortcode: 'svg', }; + const regulatedVerifiedAccount = { + client_kyc_status: { + poi_status: 'verified', + valid_tin: 1, + }, + market_type: 'synthetic', + platform: 'mt5', + product: 'swap_free', + shortcode: 'svg', + }; + + const regulatedUnverifiedAccount = { + client_kyc_status: { + poi_status: 'none', + valid_tin: 0, + }, + market_type: 'synthetic', + platform: 'mt5', + product: 'financial', + shortcode: 'bvi', + }; + it('renders default content for available mt5 account', () => { // @ts-expect-error - since this is a mock, we only need partial properties of the account - render(); + render(); expect(screen.getByTestId('dt_wallets_trading_account_card')).toBeInTheDocument(); expect(screen.getByText('Standard')).toBeInTheDocument(); @@ -59,14 +81,13 @@ describe('AvailableMT5AccountsList', () => { it('handles button click when platform status is active for real wallet account', () => { // @ts-expect-error - since this is a mock, we only need partial properties of the account - render(); + render(); const button = screen.getByTestId('dt_wallets_trading_account_card'); userEvent.click(button); - expect(mockShow).toHaveBeenCalledWith( - - ); + // @ts-expect-error - since this is a mock, we only need partial properties of the account + expect(mockShow).toHaveBeenCalledWith(); expect(mockSetModalState).toHaveBeenCalledWith('marketType', 'synthetic'); expect(mockSetModalState).toHaveBeenCalledWith('selectedJurisdiction', 'svg'); }); @@ -77,7 +98,7 @@ describe('AvailableMT5AccountsList', () => { }); // @ts-expect-error - since this is a mock, we only need partial properties of the account - render(); + render(); const button = screen.getByTestId('dt_wallets_trading_account_card'); userEvent.click(button); @@ -90,7 +111,7 @@ describe('AvailableMT5AccountsList', () => { getPlatformStatus: jest.fn(() => 'unavailable'), }); // @ts-expect-error - since this is a mock, we only need partial properties of the account - render(); + render(); const button = screen.getByTestId('dt_wallets_trading_account_card'); userEvent.click(button); @@ -98,25 +119,12 @@ describe('AvailableMT5AccountsList', () => { expect(mockShow).toHaveBeenCalledWith(); }); - it('shows JurisdictionModal by default when account is undefined', () => { - (useActiveWalletAccount as jest.Mock).mockReturnValue({ - data: undefined, - }); - // @ts-expect-error - since this is a mock, we only need partial properties of the account - render(); - - const button = screen.getByTestId('dt_wallets_trading_account_card'); - userEvent.click(button); - - expect(mockShow).toHaveBeenCalledWith(); - }); - it('shows TradingPlatformStatusModal with isServerMaintenance when platform status is maintenance', () => { (useTradingPlatformStatus as jest.Mock).mockReturnValue({ getPlatformStatus: jest.fn(() => 'maintenance'), }); // @ts-expect-error - since this is a mock, we only need partial properties of the account - render(); + render(); const button = screen.getByTestId('dt_wallets_trading_account_card'); userEvent.click(button); @@ -124,70 +132,49 @@ describe('AvailableMT5AccountsList', () => { expect(mockShow).toHaveBeenCalledWith(); }); - it('shows JurisdictionModal when product is neither swap-free nor zero-spread', () => { - const nonSwapAccount = { ...defaultAccount, product: 'ctrader' }; + it('shows MT5PasswordModal for non-regulated real accounts if client is verified', () => { + (useActiveWalletAccount as jest.Mock).mockReturnValue({ + data: undefined, + }); // @ts-expect-error - since this is a mock, we only need partial properties of the account - render(); + render(); const button = screen.getByTestId('dt_wallets_trading_account_card'); userEvent.click(button); - expect(mockShow).toHaveBeenCalledWith(); - }); - - it('shows ClientVerification when product is zero-spread', async () => { - const zeroSpreadAccount = { ...defaultAccount, product: 'zero_spread' }; // @ts-expect-error - since this is a mock, we only need partial properties of the account - render(); - - expect(screen.getByText('NEW')).toBeInTheDocument(); - const button = screen.getByTestId('dt_wallets_trading_account_card'); - userEvent.click(button); - - await waitFor(() => { - expect(mockShow).toHaveBeenCalled(); - }); + expect(mockShow).toHaveBeenCalledWith(); }); - it('handles virtual wallet accounts correctly', () => { + it('shows ClientVerificationModal for regulated real accounts if client is unverified', () => { (useActiveWalletAccount as jest.Mock).mockReturnValue({ - data: { is_virtual: true }, + data: { + is_virtual: false, + }, }); // @ts-expect-error - since this is a mock, we only need partial properties of the account - render(); + render(); const button = screen.getByTestId('dt_wallets_trading_account_card'); userEvent.click(button); - expect(mockShow).toHaveBeenCalledWith( - - ); + // @ts-expect-error - since this is a mock, we only need partial properties of the account + expect(mockShow).toHaveBeenCalledWith(); }); - it('shows MT5PasswordModal after ClientVerification completion', async () => { - const zeroSpreadAccount = { ...defaultAccount, product: 'zero_spread' }; + it('shows MT5PasswordModal for demo accounts for verified clients', () => { + (useActiveWalletAccount as jest.Mock).mockReturnValue({ + data: { + is_virtual: true, + }, + }); // @ts-expect-error - since this is a mock, we only need partial properties of the account - render(); + render(); const button = screen.getByTestId('dt_wallets_trading_account_card'); userEvent.click(button); - await waitFor(() => { - expect(mockShow).toHaveBeenCalled(); - }); - - const lastCall = mockShow.mock.calls[mockShow.mock.calls.length - 1][0]; - // eslint-disable-next-line testing-library/no-node-access - const { onCompletion } = lastCall.props.children.props; //required to access the function of lazy-loaded ClientVerification - - act(() => { - onCompletion(); - }); - - await waitFor(() => { - expect(mockShow).toHaveBeenCalledWith( - - ); - }); + // @ts-expect-error - since this is a mock, we only need partial properties of the account + expect(mockShow).toHaveBeenCalledWith(); }); }); diff --git a/packages/wallets/src/features/cfd/modals/ClientVerificationModal/ClientVerificationModal.scss b/packages/wallets/src/features/cfd/modals/ClientVerificationModal/ClientVerificationModal.scss new file mode 100644 index 000000000000..305e021bca3e --- /dev/null +++ b/packages/wallets/src/features/cfd/modals/ClientVerificationModal/ClientVerificationModal.scss @@ -0,0 +1,22 @@ +.wallets-client-verification-modal { + width: 100%; + min-width: 44rem; + height: 100%; + padding: 2.4rem; + display: flex; + flex-direction: column; + align-items: center; + gap: 2.4rem; + + &__description { + max-width: 36rem; + + @include mobile-or-tablet-screen { + max-width: 100%; + } + } + + @include mobile-or-tablet-screen { + min-width: 100%; + } +} diff --git a/packages/wallets/src/features/cfd/modals/ClientVerificationModal/ClientVerificationModal.tsx b/packages/wallets/src/features/cfd/modals/ClientVerificationModal/ClientVerificationModal.tsx new file mode 100644 index 000000000000..3c40ef845e39 --- /dev/null +++ b/packages/wallets/src/features/cfd/modals/ClientVerificationModal/ClientVerificationModal.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { DerivLightUploadPoiIcon } from '@deriv/quill-icons'; +import { Localize, useTranslations } from '@deriv-com/translations'; +import { Text, useDevice } from '@deriv-com/ui'; +import { ModalStepWrapper } from '../../../../components'; +import { getMarketTypeDetails, MARKET_TYPE } from '../../constants'; +import { TModifiedMT5Accounts } from '../../types'; +import { DocumentsList } from './components'; +import './ClientVerificationModal.scss'; + +type TClientVerificationModal = { + account: TModifiedMT5Accounts; +}; + +const ClientVerificationModal: React.FC = ({ account }) => { + const { localize } = useTranslations(); + const { isMobile } = useDevice(); + const { title } = getMarketTypeDetails(localize, account.product)[account.market_type || MARKET_TYPE.ALL]; + + return ( + +
+ + + {account.is_added ? ( + + ) : ( + + )} + + +
+
+ ); +}; + +export default ClientVerificationModal; diff --git a/packages/wallets/src/features/cfd/modals/ClientVerificationModal/components/DocumentsList/DocumentsList.scss b/packages/wallets/src/features/cfd/modals/ClientVerificationModal/components/DocumentsList/DocumentsList.scss new file mode 100644 index 000000000000..264c1f00b394 --- /dev/null +++ b/packages/wallets/src/features/cfd/modals/ClientVerificationModal/components/DocumentsList/DocumentsList.scss @@ -0,0 +1,7 @@ +.wallets-documents-list { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + gap: 1.6rem; +} diff --git a/packages/wallets/src/features/cfd/modals/ClientVerificationModal/components/DocumentsList/DocumentsList.tsx b/packages/wallets/src/features/cfd/modals/ClientVerificationModal/components/DocumentsList/DocumentsList.tsx new file mode 100644 index 000000000000..49d93fd70c4c --- /dev/null +++ b/packages/wallets/src/features/cfd/modals/ClientVerificationModal/components/DocumentsList/DocumentsList.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import { useHistory } from 'react-router-dom'; +import { TModifiedMT5Accounts } from 'src/features/cfd/types'; +import { useTranslations } from '@deriv-com/translations'; +import { ClientVerificationStatusBadge } from '../../../../components'; +import { getClientVerification } from '../../../../utils'; +import { DocumentTile } from './components'; +import './DocumentsList.scss'; + +type TDocumentsListProps = { + account: TModifiedMT5Accounts; +}; + +type TStatusBadgeProps = Record; + +const statusBadge: TStatusBadgeProps = { + expired: , + none: <>, + pending: , + rejected: , + suspected: , + verified: , +}; + +const DocumentsList: React.FC = ({ account }) => { + const history = useHistory(); + const { localize } = useTranslations(); + const { hasPoaStatus, hasPoiStatus, hasTinStatus, isPoaRequired, isPoiRequired, isTinRequired, statuses } = + getClientVerification(account); + + return ( +
+ {hasPoiStatus && ( + history.push('/account/proof-of-identity')} + title={localize('Proof of identity')} + /> + )} + {hasPoaStatus && ( + history.push('/account/proof-of-address')} + title={localize('Proof of address')} + /> + )} + {hasTinStatus && isTinRequired && ( + history.push('/account/personal-details')} + title={localize('Personal details')} + /> + )} +
+ ); +}; + +export default DocumentsList; diff --git a/packages/wallets/src/features/cfd/modals/ClientVerificationModal/components/DocumentsList/__tests__/DocumentsList.spec.tsx b/packages/wallets/src/features/cfd/modals/ClientVerificationModal/components/DocumentsList/__tests__/DocumentsList.spec.tsx new file mode 100644 index 000000000000..cec52fd1c3f7 --- /dev/null +++ b/packages/wallets/src/features/cfd/modals/ClientVerificationModal/components/DocumentsList/__tests__/DocumentsList.spec.tsx @@ -0,0 +1,172 @@ +import React from 'react'; +import { render, screen, waitFor, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import DocumentsList from '../DocumentsList'; + +const mockHistoryPush = jest.fn(); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useHistory: jest.fn(() => ({ + push: mockHistoryPush, + })), +})); + +jest.mock('../../../../../components', () => ({ + ...jest.requireActual('../../../../../components'), + ClientVerificationStatusBadge: jest.fn(({ variant }) => variant), +})); + +jest.mock('../components', () => ({ + ...jest.requireActual('../components'), + DocumentTile: jest.fn(({ badge, isDisabled, onClick, title }) => ( + + )), +})); + +describe('', () => { + it('poi tile is not rendered', () => { + render( + + ); + + expect(screen.queryByText('Proof of identity')).not.toBeInTheDocument(); + }); + + it('poi tile is not rendered', () => { + render( + + ); + + expect(screen.queryByText('Proof of address')).not.toBeInTheDocument(); + }); + + it('personal details tile is not rendered', () => { + render( + + ); + + expect(screen.queryByText('Personal details')).not.toBeInTheDocument(); + }); + + it('on click poi tile redirects to correct page', async () => { + render( + + ); + + const poiTile = screen.getByText('Proof of identity'); + userEvent.click(poiTile); + + await waitFor(() => { + expect(mockHistoryPush).toBeCalledWith('/account/proof-of-identity'); + }); + }); + + it('on click poa tile redirects to correct page', async () => { + render( + + ); + + const poaTile = screen.getByText('Proof of address'); + userEvent.click(poaTile); + + await waitFor(() => { + expect(mockHistoryPush).toBeCalledWith('/account/proof-of-address'); + }); + }); + + it('on click personal details tile redirects to correct page', async () => { + render( + + ); + + const personalDetailsTile = screen.getByText('Personal details'); + userEvent.click(personalDetailsTile); + + await waitFor(() => { + expect(mockHistoryPush).toBeCalledWith('/account/personal-details'); + }); + }); + + it('renders poi tile with correct badge', () => { + render( + + ); + + const poiTile = screen.getByText('Proof of identity'); + + expect(within(poiTile).getByText('verified')).toBeInTheDocument; + }); + + it('renders poa tile with correct badge', () => { + render( + + ); + + const poaTile = screen.getByText('Proof of address'); + + expect(within(poaTile).getByText('verified')).toBeInTheDocument; + }); +}); diff --git a/packages/wallets/src/features/cfd/modals/ClientVerificationModal/components/DocumentsList/components/DocumentTile/DocumentTile.scss b/packages/wallets/src/features/cfd/modals/ClientVerificationModal/components/DocumentsList/components/DocumentTile/DocumentTile.scss new file mode 100644 index 000000000000..e1b8aab8fd48 --- /dev/null +++ b/packages/wallets/src/features/cfd/modals/ClientVerificationModal/components/DocumentsList/components/DocumentTile/DocumentTile.scss @@ -0,0 +1,28 @@ +.wallets-document-tile { + width: 100%; + height: 5.6rem; + padding-inline: 1.6rem; + border: none; + border-radius: 8px; + background: #f6f7f8; + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; + + &:disabled { + cursor: not-allowed; + } + + &__status { + display: flex; + align-items: center; + gap: 0.8rem; + } + + &__chevron { + &--disabled { + fill: #d6d6d6; + } + } +} diff --git a/packages/wallets/src/features/cfd/modals/ClientVerificationModal/components/DocumentsList/components/DocumentTile/DocumentTile.tsx b/packages/wallets/src/features/cfd/modals/ClientVerificationModal/components/DocumentsList/components/DocumentTile/DocumentTile.tsx new file mode 100644 index 000000000000..1a3395392faf --- /dev/null +++ b/packages/wallets/src/features/cfd/modals/ClientVerificationModal/components/DocumentsList/components/DocumentTile/DocumentTile.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import classNames from 'classnames'; +import { LabelPairedChevronRightMdRegularIcon } from '@deriv/quill-icons'; +import { Text } from '@deriv-com/ui'; +import './DocumentTile.scss'; + +type TDocumentTileProps = { + badge?: JSX.Element; + disabled?: boolean; + onClick: VoidFunction; + title: string; +}; + +const DocumentTile: React.FC = ({ badge, disabled, onClick, title }) => { + return ( + + ); +}; + +export default DocumentTile; diff --git a/packages/wallets/src/features/cfd/modals/ClientVerificationModal/components/DocumentsList/components/DocumentTile/index.ts b/packages/wallets/src/features/cfd/modals/ClientVerificationModal/components/DocumentsList/components/DocumentTile/index.ts new file mode 100644 index 000000000000..e6b33e8515a5 --- /dev/null +++ b/packages/wallets/src/features/cfd/modals/ClientVerificationModal/components/DocumentsList/components/DocumentTile/index.ts @@ -0,0 +1 @@ +export { default as DocumentTile } from './DocumentTile'; diff --git a/packages/wallets/src/features/cfd/modals/ClientVerificationModal/components/DocumentsList/components/index.ts b/packages/wallets/src/features/cfd/modals/ClientVerificationModal/components/DocumentsList/components/index.ts new file mode 100644 index 000000000000..76c67643d998 --- /dev/null +++ b/packages/wallets/src/features/cfd/modals/ClientVerificationModal/components/DocumentsList/components/index.ts @@ -0,0 +1 @@ +export * from './DocumentTile'; diff --git a/packages/wallets/src/features/cfd/modals/ClientVerificationModal/components/DocumentsList/index.ts b/packages/wallets/src/features/cfd/modals/ClientVerificationModal/components/DocumentsList/index.ts new file mode 100644 index 000000000000..d23ca16a13b6 --- /dev/null +++ b/packages/wallets/src/features/cfd/modals/ClientVerificationModal/components/DocumentsList/index.ts @@ -0,0 +1 @@ +export { default as DocumentsList } from './DocumentsList'; diff --git a/packages/wallets/src/features/cfd/modals/ClientVerificationModal/components/index.ts b/packages/wallets/src/features/cfd/modals/ClientVerificationModal/components/index.ts new file mode 100644 index 000000000000..a711e93c8860 --- /dev/null +++ b/packages/wallets/src/features/cfd/modals/ClientVerificationModal/components/index.ts @@ -0,0 +1 @@ +export * from './DocumentsList'; diff --git a/packages/wallets/src/features/cfd/modals/ClientVerificationModal/index.ts b/packages/wallets/src/features/cfd/modals/ClientVerificationModal/index.ts new file mode 100644 index 000000000000..7625bdeff6b4 --- /dev/null +++ b/packages/wallets/src/features/cfd/modals/ClientVerificationModal/index.ts @@ -0,0 +1 @@ +export { default as ClientVerificationModal } from './ClientVerificationModal'; diff --git a/packages/wallets/src/features/cfd/modals/JurisdictionModal/JurisdictionModal.tsx b/packages/wallets/src/features/cfd/modals/JurisdictionModal/JurisdictionModal.tsx index e32bbbd5f544..1698de810892 100644 --- a/packages/wallets/src/features/cfd/modals/JurisdictionModal/JurisdictionModal.tsx +++ b/packages/wallets/src/features/cfd/modals/JurisdictionModal/JurisdictionModal.tsx @@ -5,10 +5,8 @@ import { Button, Loader, useDevice } from '@deriv-com/ui'; import { ModalStepWrapper } from '../../../../components/Base'; import { useModal } from '../../../../components/ModalProvider'; import { DynamicLeverageContext } from '../../components/DynamicLeverageContext'; -import { PlatformDetails } from '../../constants'; import { DynamicLeverageScreen, DynamicLeverageTitle } from '../../screens/DynamicLeverage'; import { JurisdictionScreen } from '../../screens/Jurisdiction'; -import { MT5PasswordModal } from '..'; import './JurisdictionModal.scss'; const LazyVerification = lazy( @@ -23,32 +21,19 @@ const JurisdictionModal = () => { const [isDynamicLeverageVisible, setIsDynamicLeverageVisible] = useState(false); const [isCheckBoxChecked, setIsCheckBoxChecked] = useState(false); - const { getModalState, setModalState, show } = useModal(); + const { setModalState, show } = useModal(); const { isLoading } = useAvailableMT5Accounts(); const { isDesktop } = useDevice(); const { localize } = useTranslations(); - const marketType = getModalState('marketType') ?? 'all'; - const platform = getModalState('platform') ?? PlatformDetails.mt5.platform; - const toggleDynamicLeverage = useCallback(() => { setIsDynamicLeverageVisible(!isDynamicLeverageVisible); }, [isDynamicLeverageVisible, setIsDynamicLeverageVisible]); const JurisdictionFlow = () => { - const [showMt5PasswordModal, setShowMt5PasswordModal] = useState(false); - if (selectedJurisdiction === 'svg' || showMt5PasswordModal) { - return ; - } - return ( }> - { - setShowMt5PasswordModal(true); - }} - selectedJurisdiction={selectedJurisdiction} - /> + ); }; diff --git a/packages/wallets/src/features/cfd/modals/MT5AccountAdded/MT5AccountAdded.tsx b/packages/wallets/src/features/cfd/modals/MT5AccountAdded/MT5AccountAdded.tsx index 076b42a78b02..2552375f2083 100644 --- a/packages/wallets/src/features/cfd/modals/MT5AccountAdded/MT5AccountAdded.tsx +++ b/packages/wallets/src/features/cfd/modals/MT5AccountAdded/MT5AccountAdded.tsx @@ -31,8 +31,8 @@ const MT5AccountAdded: FC = ({ account, marketType, platform, product }) const history = useHistory(); const { isDesktop } = useDevice(); - const { getModalState, hide } = useModal(); const { localize } = useTranslations(); + const { getModalState, hide } = useModal(); const addedAccount = mt5Accounts?.find(acc => acc.login === account?.login); @@ -97,7 +97,7 @@ const MT5AccountAdded: FC = ({ account, marketType, platform, product }) ); }, - [hide, isDesktop, history, addedAccount?.loginid] + [hide, buttonSize, history, addedAccount?.loginid] ); const renderSuccessDescription = useMemo(() => { diff --git a/packages/wallets/src/features/cfd/modals/MT5PasswordModal/MT5PasswordModal.tsx b/packages/wallets/src/features/cfd/modals/MT5PasswordModal/MT5PasswordModal.tsx index 1624b1d04d7d..e95f170a3fde 100644 --- a/packages/wallets/src/features/cfd/modals/MT5PasswordModal/MT5PasswordModal.tsx +++ b/packages/wallets/src/features/cfd/modals/MT5PasswordModal/MT5PasswordModal.tsx @@ -1,7 +1,6 @@ import React, { useCallback, useMemo, useState } from 'react'; import { useAccountStatus, - useActiveWalletAccount, useAvailableMT5Accounts, useCreateMT5Account, useMT5AccountsList, @@ -10,25 +9,23 @@ import { useVerifyEmail, } from '@deriv/api-v2'; import { Localize, useTranslations } from '@deriv-com/translations'; -import { Button, useDevice, Loader } from '@deriv-com/ui'; +import { Button, Loader, useDevice } from '@deriv-com/ui'; import { SentEmailContent, WalletError } from '../../../../components'; import { ModalStepWrapper, ModalWrapper } from '../../../../components/Base'; import { useModal } from '../../../../components/ModalProvider'; -import { THooks, TMarketTypes, TPlatforms } from '../../../../types'; import { platformPasswordResetRedirectLink } from '../../../../utils/cfd'; import { validPassword, validPasswordMT5 } from '../../../../utils/password-validation'; -import { CFD_PLATFORMS, JURISDICTION, MARKET_TYPE, PlatformDetails, PRODUCT } from '../../constants'; +import { CFD_PLATFORMS, JURISDICTION, MARKET_TYPE, PlatformDetails } from '../../constants'; import { CreatePassword, CreatePasswordMT5, EnterPassword, MT5ResetPasswordModal } from '../../screens'; +import { TModifiedMT5Accounts } from '../../types'; import MT5AccountAdded from '../MT5AccountAdded/MT5AccountAdded'; import { PasswordLimitExceededModal } from '../PasswordLimitExceededModal'; import { MT5PasswordModalFooter, SuccessModalFooter } from './MT5PasswordModalFooters'; import './MT5PasswordModal.scss'; type TProps = { + account: TModifiedMT5Accounts; isVirtual?: boolean; - marketType: TMarketTypes.SortedMT5Accounts; - platform: TPlatforms.All; - product?: THooks.AvailableMT5Accounts['product']; }; export type TPlatformPasswordChange = { @@ -36,8 +33,11 @@ export type TPlatformPasswordChange = { newPassword: string; }; -const MT5PasswordModal: React.FC = ({ isVirtual, marketType, platform, product }) => { - const [isTncChecked, setIsTncChecked] = useState(!(product === PRODUCT.ZEROSPREAD && !isVirtual)); +const MT5PasswordModal: React.FC = ({ account, isVirtual = false }) => { + const [isTncChecked, setIsTncChecked] = useState( + // tnc is automatically checked for real SVG accounts and all demo accounts + account.shortcode === JURISDICTION.SVG || isVirtual + ); const { data: createMT5AccountData, error: createMT5AccountError, @@ -52,7 +52,6 @@ const MT5PasswordModal: React.FC = ({ isVirtual, marketType, platform, p mutateAsync: tradingPasswordChangeMutateAsync, } = useTradingPlatformPasswordChange(); const { data: accountStatusData, isLoading: accountStatusLoading } = useAccountStatus(); - const { data: activeWalletData } = useActiveWalletAccount(); const { data: availableMT5AccountsData } = useAvailableMT5Accounts(); const { error: emailVerificationError, @@ -69,11 +68,14 @@ const MT5PasswordModal: React.FC = ({ isVirtual, marketType, platform, p const [password, setPassword] = useState(''); + const marketType = account.market_type ?? 'synthetic'; + const platform = account.platform; + const product = account.product; + const isMT5PasswordNotSet = accountStatusData?.is_mt5_password_not_set; const hasMT5Account = mt5AccountsData?.find(account => account.login); - const isDemo = activeWalletData?.is_virtual; const { platform: mt5Platform, title: mt5Title } = PlatformDetails.mt5; - const selectedJurisdiction = isDemo ? JURISDICTION.SVG : getModalState('selectedJurisdiction'); + const selectedJurisdiction = isVirtual ? JURISDICTION.SVG : getModalState('selectedJurisdiction'); const isLoading = accountStatusLoading || createMT5AccountLoading || tradingPlatformPasswordChangeLoading; @@ -89,7 +91,7 @@ const MT5PasswordModal: React.FC = ({ isVirtual, marketType, platform, p // ================================= const accountType = marketType === MARKET_TYPE.SYNTHETIC ? 'gaming' : marketType; - const categoryAccountType = isDemo ? 'demo' : accountType; + const categoryAccountType = isVirtual ? 'demo' : accountType; if (isMT5PasswordNotSet) { await tradingPasswordChangeMutateAsync({ @@ -107,7 +109,7 @@ const MT5PasswordModal: React.FC = ({ isVirtual, marketType, platform, p email: settingsData?.email ?? '', leverage: availableMT5AccountsData?.find(acc => acc.market_type === marketType)?.leverage ?? 500, mainPassword: password, - ...(selectedJurisdiction && !isDemo ? { company: selectedJurisdiction } : {}), + ...(selectedJurisdiction && !isVirtual ? { company: selectedJurisdiction } : {}), ...(marketType === MARKET_TYPE.FINANCIAL && { mt5_account_type: MARKET_TYPE.FINANCIAL }), ...(selectedJurisdiction && (selectedJurisdiction !== JURISDICTION.LABUAN @@ -131,7 +133,7 @@ const MT5PasswordModal: React.FC = ({ isVirtual, marketType, platform, p }, [ availableMT5AccountsData, createMT5AccountMutate, - isDemo, + isVirtual, isMT5PasswordNotSet, marketType, mt5Platform, @@ -154,12 +156,12 @@ const MT5PasswordModal: React.FC = ({ isVirtual, marketType, platform, p emailVerificationMutate({ type: 'trading_platform_mt5_password_reset', url_parameters: { - redirect_to: platformPasswordResetRedirectLink(CFD_PLATFORMS.MT5, activeWalletData?.is_virtual), + redirect_to: platformPasswordResetRedirectLink(CFD_PLATFORMS.MT5, isVirtual), }, verify_email: email, }); } - }, [activeWalletData?.is_virtual, email, emailVerificationMutate]); + }, [email, emailVerificationMutate, isVirtual]); const onSubmitPasswordChange = useCallback( ({ currentPassword, newPassword }: TPlatformPasswordChange) => { @@ -175,7 +177,7 @@ const MT5PasswordModal: React.FC = ({ isVirtual, marketType, platform, p const renderTitle = useCallback(() => { const accountAction = hasMT5Account ? localize('Add') : localize('Create'); - const accountType = isDemo ? localize('demo') : localize('real'); + const accountType = isVirtual ? localize('demo') : localize('real'); return updateMT5Password ? localize('{{mt5Title}} latest password requirements', { mt5Title }) @@ -184,13 +186,13 @@ const MT5PasswordModal: React.FC = ({ isVirtual, marketType, platform, p accountType, mt5Title, }); - }, [hasMT5Account, isDemo, localize, mt5Title, updateMT5Password]); + }, [hasMT5Account, isVirtual, localize, mt5Title, updateMT5Password]); const renderFooter = useCallback(() => { if (createMT5AccountSuccess) return (
- +
); @@ -235,7 +237,7 @@ const MT5PasswordModal: React.FC = ({ isVirtual, marketType, platform, p }, [ createMT5AccountLoading, createMT5AccountSuccess, - isDemo, + isVirtual, isDesktop, isMT5PasswordNotSet, mt5Title, @@ -263,6 +265,7 @@ const MT5PasswordModal: React.FC = ({ isVirtual, marketType, platform, p if (isMT5PasswordNotSet && platform === CFD_PLATFORMS.MT5) return ( = ({ isVirtual, marketType, platform, p }} password={password} platform={mt5Platform} - product={product} /> ); @@ -289,9 +291,10 @@ const MT5PasswordModal: React.FC = ({ isVirtual, marketType, platform, p return ( setPassword(e.target.value)} @@ -307,18 +310,18 @@ const MT5PasswordModal: React.FC = ({ isVirtual, marketType, platform, p ); }, [ isMT5PasswordNotSet, + platform, tradingPlatformPasswordChangeLoading, createMT5AccountLoading, - isTncChecked, onSubmit, password, mt5Platform, - updateMT5Password, - tradingPasswordChangeError, - platform, + account, + isTncChecked, isVirtual, product, - activeWalletData?.is_virtual, + updateMT5Password, + tradingPasswordChangeError, onSubmitPasswordChange, marketType, localize, @@ -328,6 +331,10 @@ const MT5PasswordModal: React.FC = ({ isVirtual, marketType, platform, p isLoading, ]); + if (accountStatusLoading) { + return ; + } + if (emailVerificationStatus === 'error') { return ( = ({ isVirtual, marketType, platform, p ); } - if (createMT5AccountSuccess && !isMT5PasswordNotSet) { + if (createMT5AccountSuccess) { return ( void; password: string; platform: TPlatforms.All; - product?: THooks.AvailableMT5Accounts['product']; }; const CreatePasswordMT5: React.FC = ({ + account, isLoading, isTncChecked, isVirtual, @@ -30,7 +32,6 @@ const CreatePasswordMT5: React.FC = ({ onTncChange, password, platform, - product, }) => { const { isDesktop } = useDevice(); const { localize } = useTranslations(); @@ -61,13 +62,9 @@ const CreatePasswordMT5: React.FC = ({ onChange={onPasswordChange} password={password} /> - {product === PRODUCT.ZEROSPREAD && !isVirtual && ( - + {!isVirtual && } + {!isVirtual && account.shortcode !== 'svg' && ( + )} diff --git a/packages/wallets/src/features/cfd/screens/EnterPassword/EnterPassword.tsx b/packages/wallets/src/features/cfd/screens/EnterPassword/EnterPassword.tsx index d48df9ae70e7..d856a7b87424 100644 --- a/packages/wallets/src/features/cfd/screens/EnterPassword/EnterPassword.tsx +++ b/packages/wallets/src/features/cfd/screens/EnterPassword/EnterPassword.tsx @@ -4,12 +4,15 @@ import { Localize, useTranslations } from '@deriv-com/translations'; import { Button, Text, useDevice } from '@deriv-com/ui'; import { WalletPasswordFieldLazy } from '../../../../components/Base'; import { THooks, TMarketTypes, TPlatforms } from '../../../../types'; +import { CFD_PLATFORMS, getMarketTypeDetails, JURISDICTION, PlatformDetails } from '../../constants'; +import { TModifiedMT5Accounts } from '../../types'; +import { MT5LicenceMessage, MT5PasswordModalTnc } from '../components'; import { validPassword, validPasswordMT5 } from '../../../../utils/password-validation'; -import { CFDPasswordModalTnc } from '../../components/CFDPasswordModalTnc'; -import { CFD_PLATFORMS, getMarketTypeDetails, PlatformDetails, PRODUCT } from '../../constants'; import './EnterPassword.scss'; +// Note: this component requires a proper refactor to remove props for keys available under the `account` prop type TProps = { + account?: TModifiedMT5Accounts; isForgotPasswordLoading?: boolean; isLoading?: boolean; isTncChecked?: boolean; @@ -28,6 +31,7 @@ type TProps = { }; const EnterPassword: React.FC = ({ + account, isForgotPasswordLoading, isLoading, isTncChecked = true, @@ -100,13 +104,9 @@ const EnterPassword: React.FC = ({ {passwordErrorHints} )} - {product === PRODUCT.ZEROSPREAD && !isVirtual && ( - onTncChange?.()} - platform={platform} - product={product} - /> + {account && !isVirtual && } + {account && account.shortcode !== JURISDICTION.SVG && platform === CFD_PLATFORMS.MT5 && !isVirtual && ( + onTncChange?.()} /> )} {isDesktop && ( diff --git a/packages/wallets/src/features/cfd/screens/EnterPassword/__test__/EnterPassword.spec.tsx b/packages/wallets/src/features/cfd/screens/EnterPassword/__test__/EnterPassword.spec.tsx index fbdb92674752..c5fe094d0dda 100644 --- a/packages/wallets/src/features/cfd/screens/EnterPassword/__test__/EnterPassword.spec.tsx +++ b/packages/wallets/src/features/cfd/screens/EnterPassword/__test__/EnterPassword.spec.tsx @@ -1,12 +1,18 @@ import React from 'react'; import { useActiveWalletAccount } from '@deriv/api-v2'; -import { render, screen } from '@testing-library/react'; +import { cleanup, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { MARKET_TYPE, PlatformDetails } from '../../../constants'; import EnterPassword from '../EnterPassword'; jest.mock('@deriv/api-v2'); +jest.mock('../../components', () => ({ + ...jest.requireActual('../../components'), + MT5LicenceMessage: jest.fn(() =>
MT5LicenceMessage
), + MT5PasswordModalTnc: jest.fn(() =>
MT5PasswordModalTnc
), +})); + describe('EnterPassword', () => { const mockUseActiveWalletAccount = useActiveWalletAccount as jest.Mock; @@ -14,6 +20,8 @@ describe('EnterPassword', () => { mockUseActiveWalletAccount.mockReturnValue({ data: { is_virtual: false } }); }); + afterEach(cleanup); + const title = `Enter your ${PlatformDetails.mt5.title} password`; const shortPassword = 'abcd'; const validPassword = 'Abcd1234!'; @@ -82,6 +90,12 @@ describe('EnterPassword', () => { expect(addAccountButton).toBeDisabled(); }); + it('disables the "Add account" button when tnc is not checked', () => { + renderComponent({ isTncChecked: false }); + const addAccountButton = screen.getByRole('button', { name: 'Add account' }); + expect(addAccountButton).toBeDisabled(); + }); + it('shows password error hints when passwordError is true', () => { renderComponent({ passwordError: true }); expect( @@ -90,4 +104,29 @@ describe('EnterPassword', () => { ) ).toBeInTheDocument(); }); + + it('shows the mt5 licence message component for real MT5 accounts', () => { + renderComponent({ account: { shortcode: 'svg' } }); + + expect(screen.getByText('MT5LicenceMessage')).toBeInTheDocument(); + }); + + it('hides the mt5 licence message for virtual accounts', () => { + mockUseActiveWalletAccount.mockReturnValue({ data: { is_virtual: true } }); + renderComponent(); + + expect(screen.queryByText('MT5LicenceMessage')).not.toBeInTheDocument(); + }); + + it('shows the mt5 tnc checkbox for regulated real accounts', () => { + renderComponent({ account: { shortcode: 'bvi' } }); + + expect(screen.getByText('MT5PasswordModalTnc')).toBeInTheDocument(); + }); + + it('hides the mt5 tnc checkbox for non-regulated real accounts', () => { + renderComponent({ account: { shortcode: 'svg' } }); + + expect(screen.queryByText('MT5PasswordModalTnc')).not.toBeInTheDocument(); + }); }); diff --git a/packages/wallets/src/features/cfd/screens/MT5TradeScreen/MT5TradeScreen.tsx b/packages/wallets/src/features/cfd/screens/MT5TradeScreen/MT5TradeScreen.tsx index 9c03bccd69c2..08bfb530480a 100644 --- a/packages/wallets/src/features/cfd/screens/MT5TradeScreen/MT5TradeScreen.tsx +++ b/packages/wallets/src/features/cfd/screens/MT5TradeScreen/MT5TradeScreen.tsx @@ -15,7 +15,7 @@ import { MT5TradeLink } from './MT5TradeLink'; import './MT5TradeScreen.scss'; type MT5TradeScreenProps = { - mt5Account?: THooks.MT5AccountsList; + mt5Account?: THooks.SortedMT5Accounts; }; const MT5TradeScreen: FC = ({ mt5Account }) => { diff --git a/packages/wallets/src/features/cfd/screens/components/MT5LicenceMessage/MT5LicenceMessage.scss b/packages/wallets/src/features/cfd/screens/components/MT5LicenceMessage/MT5LicenceMessage.scss new file mode 100644 index 000000000000..10eefbe6dcd5 --- /dev/null +++ b/packages/wallets/src/features/cfd/screens/components/MT5LicenceMessage/MT5LicenceMessage.scss @@ -0,0 +1,5 @@ +.wallets-mt5-licence-message { + @include mobile-or-tablet-screen { + margin-top: auto; + } +} diff --git a/packages/wallets/src/features/cfd/screens/components/MT5LicenceMessage/MT5LicenceMessage.tsx b/packages/wallets/src/features/cfd/screens/components/MT5LicenceMessage/MT5LicenceMessage.tsx new file mode 100644 index 000000000000..51010c95a127 --- /dev/null +++ b/packages/wallets/src/features/cfd/screens/components/MT5LicenceMessage/MT5LicenceMessage.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { Localize, useTranslations } from '@deriv-com/translations'; +import { InlineMessage, Text, useDevice } from '@deriv-com/ui'; +import { getMarketTypeDetails, JURISDICTION, MARKET_TYPE, PlatformDetails } from '../../../constants'; +import { TModifiedMT5Accounts } from '../../../types'; +import './MT5LicenceMessage.scss'; + +type TMT5LicenseMessageProps = { + account: TModifiedMT5Accounts; +}; + +const MT5LicenseMessage: React.FC = ({ account }) => { + const { isDesktop } = useDevice(); + const { localize } = useTranslations(); + const isSvg = account.shortcode === JURISDICTION.SVG; + + return ( + + + {isSvg ? ( + // TODO: remove this hardcoded logic for the company number for SVG once BE provides company_number key for non-regulated accounts + + ) : ( + + )} + + + ); +}; + +export default MT5LicenseMessage; diff --git a/packages/wallets/src/features/cfd/screens/components/MT5LicenceMessage/__tests__/MT5LicenceMessage.spec.tsx b/packages/wallets/src/features/cfd/screens/components/MT5LicenceMessage/__tests__/MT5LicenceMessage.spec.tsx new file mode 100644 index 000000000000..d0d40201f9ca --- /dev/null +++ b/packages/wallets/src/features/cfd/screens/components/MT5LicenceMessage/__tests__/MT5LicenceMessage.spec.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import MT5LicenseMessage from '../MT5LicenceMessage'; + +jest.mock('@deriv-com/ui', () => ({ + ...jest.requireActual('@deriv-com/ui'), + useDevice: jest.fn(() => ({ isDesktop: false })), +})); + +const mockRegulatedAccount = { + licence_number: 'mock_licence_number', + market_type: 'financial', + name: 'mock_company_name', + product: 'financial', + regulatory_authority: 'mock_regulatory_authority', + shortcode: 'bvi', +}; + +const mockNonRegulatedAccount = { + market_type: 'all', + name: 'mock_company_name', + product: 'swap_free', + shortcode: 'svg', +}; + +describe('', () => { + it('displays correct message for regulated account', () => { + // @ts-expect-error - since this is a mock, we only need partial properties of the account + render(); + + expect( + screen.getByText( + 'You are adding your Deriv MT5 Financial account under mock_company_name, regulated by the mock_regulatory_authority (licence no. mock_licence_number).' + ) + ); + }); + + it('displays correct message for non-regulated account', () => { + // @ts-expect-error - since this is a mock, we only need partial properties of the account + render(); + + expect( + screen.getByText( + 'You are adding your Deriv MT5 Swap-Free account under mock_company_name (company no. 273 LLC 2020).' + ) + ); + }); +}); diff --git a/packages/wallets/src/features/cfd/screens/components/MT5LicenceMessage/index.ts b/packages/wallets/src/features/cfd/screens/components/MT5LicenceMessage/index.ts new file mode 100644 index 000000000000..c939c664e13b --- /dev/null +++ b/packages/wallets/src/features/cfd/screens/components/MT5LicenceMessage/index.ts @@ -0,0 +1 @@ +export { default as MT5LicenceMessage } from './MT5LicenceMessage'; diff --git a/packages/wallets/src/features/cfd/screens/components/MT5PasswordModalTnc/MT5PasswordModalTnc.scss b/packages/wallets/src/features/cfd/screens/components/MT5PasswordModalTnc/MT5PasswordModalTnc.scss new file mode 100644 index 000000000000..b6fb09376e7e --- /dev/null +++ b/packages/wallets/src/features/cfd/screens/components/MT5PasswordModalTnc/MT5PasswordModalTnc.scss @@ -0,0 +1,5 @@ +.wallets-mt5-modal-tnc { + display: flex; + flex-direction: column; + gap: 1.6rem; +} diff --git a/packages/wallets/src/features/cfd/screens/components/MT5PasswordModalTnc/MT5PasswordModalTnc.tsx b/packages/wallets/src/features/cfd/screens/components/MT5PasswordModalTnc/MT5PasswordModalTnc.tsx new file mode 100644 index 000000000000..507b9bb26e54 --- /dev/null +++ b/packages/wallets/src/features/cfd/screens/components/MT5PasswordModalTnc/MT5PasswordModalTnc.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { Localize } from '@deriv-com/translations'; +import { Checkbox, Text, useDevice } from '@deriv-com/ui'; +import { WalletLink } from '../../../../../components/Base'; +import { useModal } from '../../../../../components/ModalProvider'; +import { companyNamesAndUrls } from '../../../constants'; +import './MT5PasswordModalTnc.scss'; + +export type TMT5PasswordModalTncProps = { + checked: boolean; + onChange: () => void; +}; + +const MT5PasswordModalTnc = ({ checked, onChange }: TMT5PasswordModalTncProps) => { + const { isDesktop } = useDevice(); + const { getModalState } = useModal(); + const selectedJurisdiction = getModalState('selectedJurisdiction'); + // TODO: replace the company name with the information provided by the trading_platform_account_available API's BE response + const selectedCompany = companyNamesAndUrls[selectedJurisdiction as keyof typeof companyNamesAndUrls]; + + return ( +
+ + ]} + i18n_default_text="I confirm and accept {{company}}'s <0>terms and conditions" + values={{ + company: selectedCompany.name, + }} + /> + + } + name='mt5-tnc-checkbox' + onChange={onChange} + /> +
+ ); +}; + +export default MT5PasswordModalTnc; diff --git a/packages/wallets/src/features/cfd/components/CFDPasswordModalTnc/__tests__/CFDPasswordModalTnc.spec.tsx b/packages/wallets/src/features/cfd/screens/components/MT5PasswordModalTnc/__tests__/MT5PasswordModalTnc.spec.tsx similarity index 54% rename from packages/wallets/src/features/cfd/components/CFDPasswordModalTnc/__tests__/CFDPasswordModalTnc.spec.tsx rename to packages/wallets/src/features/cfd/screens/components/MT5PasswordModalTnc/__tests__/MT5PasswordModalTnc.spec.tsx index 820b654032c3..35e05b9cb1ac 100644 --- a/packages/wallets/src/features/cfd/components/CFDPasswordModalTnc/__tests__/CFDPasswordModalTnc.spec.tsx +++ b/packages/wallets/src/features/cfd/screens/components/MT5PasswordModalTnc/__tests__/MT5PasswordModalTnc.spec.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { fireEvent, render, screen } from '@testing-library/react'; -import CFDPasswordModalTnc, { type TCFDPasswordModalTncProps } from '../CFDPasswordModalTnc'; +import MT5PasswordModalTnc, { type TMT5PasswordModalTncProps } from '../MT5PasswordModalTnc'; jest.mock('@deriv-com/ui', () => ({ Checkbox: jest.fn(({ checked, label, onChange }) => ( @@ -18,53 +18,44 @@ jest.mock('@deriv-com/ui', () => ({ useDevice: jest.fn(() => ({ isDesktop: true })), })); -jest.mock('../../../../../components/ModalProvider', () => ({ +jest.mock('../../../../../../components/ModalProvider', () => ({ useModal: jest.fn(() => ({ getModalState: jest.fn(() => 'bvi'), })), })); -jest.mock('../../../../../components/Base/WalletLink', () => ({ +jest.mock('../../../../../../components/Base/WalletLink', () => ({ WalletLink: ({ children }: { children: React.ReactNode }) => {children}, })); const mockOnChange = jest.fn(); -describe('CFDPasswordModalTnc', () => { - const defaultProps: TCFDPasswordModalTncProps = { +describe('MT5PasswordModalTnc', () => { + const defaultProps: TMT5PasswordModalTncProps = { checked: false, onChange: mockOnChange, - platform: 'mt5', - product: 'zero_spread', }; it('renders correctly', () => { - render(); - expect(screen.getByTestId('dt_wallets_tnc_checkbox')).toBeInTheDocument(); - expect(screen.getByTestId('dt_wallets_tnc_inline_message')).toBeInTheDocument(); + render(); + expect(screen.getByTestId('dt_wallets_mt5_tnc_checkbox')).toBeInTheDocument(); }); it('displays correct text content', () => { - render(); - expect(screen.getByText(/You are adding your Deriv MT5/i)).toBeInTheDocument(); - expect(screen.getByText(/I confirm and accept/i)).toBeInTheDocument(); + render(); + expect(screen.getByText("I confirm and accept Deriv (BVI) Ltd's")).toBeInTheDocument(); }); it('handles checkbox change', () => { - render(); - const checkbox = screen.getByTestId('dt_wallets_tnc_checkbox'); + render(); + const checkbox = screen.getByTestId('dt_wallets_mt5_tnc_checkbox'); fireEvent.click(checkbox); expect(mockOnChange).toHaveBeenCalledTimes(1); }); it('renders the terms and conditions link', () => { - render(); + render(); const link = screen.getByText('terms and conditions'); expect(link).toHaveAttribute('href', 'https://example.com'); }); - - it('uses the correct platform and product titles', () => { - render(); - expect(screen.getByText(/MT5.*Zero Spread/)).toBeInTheDocument(); - }); }); diff --git a/packages/wallets/src/features/cfd/screens/components/MT5PasswordModalTnc/index.ts b/packages/wallets/src/features/cfd/screens/components/MT5PasswordModalTnc/index.ts new file mode 100644 index 000000000000..fe47194df6f4 --- /dev/null +++ b/packages/wallets/src/features/cfd/screens/components/MT5PasswordModalTnc/index.ts @@ -0,0 +1 @@ +export { default as MT5PasswordModalTnc } from './MT5PasswordModalTnc'; diff --git a/packages/wallets/src/features/cfd/screens/components/index.ts b/packages/wallets/src/features/cfd/screens/components/index.ts new file mode 100644 index 000000000000..14f20a9a7f16 --- /dev/null +++ b/packages/wallets/src/features/cfd/screens/components/index.ts @@ -0,0 +1,2 @@ +export * from './MT5LicenceMessage'; +export * from './MT5PasswordModalTnc'; diff --git a/packages/wallets/src/features/cfd/types.ts b/packages/wallets/src/features/cfd/types.ts new file mode 100644 index 000000000000..2a980dbf86d8 --- /dev/null +++ b/packages/wallets/src/features/cfd/types.ts @@ -0,0 +1,19 @@ +/* eslint-disable camelcase */ +/* + TODO: Remove these types once API types for client_kyc_status is available for mt5_login_list and trading_platform_available_accounts from BE +*/ +import { THooks } from '../../types'; +import { JURISDICTION } from './constants'; + +type TStatuses = 'expired' | 'none' | 'pending' | 'rejected' | 'suspected' | 'verified'; + +export type TModifiedMT5Accounts = THooks.SortedMT5Accounts & { + client_kyc_status: { + poa_status: TStatuses; + poi_status: TStatuses; + valid_tin: 0 | 1; + }; + licence_number: string; + regulatory_authority: string; + shortcode: typeof JURISDICTION[keyof typeof JURISDICTION]; +}; diff --git a/packages/wallets/src/features/cfd/utils/index.ts b/packages/wallets/src/features/cfd/utils/index.ts new file mode 100644 index 000000000000..04bca77e0dec --- /dev/null +++ b/packages/wallets/src/features/cfd/utils/index.ts @@ -0,0 +1 @@ +export * from './utils'; diff --git a/packages/wallets/src/features/cfd/utils/utils.ts b/packages/wallets/src/features/cfd/utils/utils.ts new file mode 100644 index 000000000000..541202eec661 --- /dev/null +++ b/packages/wallets/src/features/cfd/utils/utils.ts @@ -0,0 +1,32 @@ +import { TModifiedMT5Accounts } from '../types'; + +const requiredDocumentStatuses = ['expired', 'none', 'rejected', 'suspected']; + +export const getClientVerification = (account: TModifiedMT5Accounts) => { + const hasOverallStatus = 'status' in account; + const overallStatus = account.status; + const hasClientKycStatus = 'client_kyc_status' in account; + const documentStatuses = account.client_kyc_status; + + const hasPoiStatus = hasClientKycStatus && 'poi_status' in documentStatuses; + const hasPoaStatus = hasClientKycStatus && 'poa_status' in documentStatuses; + const hasTinStatus = hasClientKycStatus && 'valid_tin' in documentStatuses; + + const isPoiRequired = hasPoiStatus && requiredDocumentStatuses.includes(documentStatuses.poi_status); + const isPoaRequired = hasPoaStatus && requiredDocumentStatuses.includes(documentStatuses.poa_status); + const isTinRequired = hasTinStatus && !documentStatuses.valid_tin; + + return { + hasClientKycStatus, + hasOverallStatus, + hasPoaStatus, + hasPoiStatus, + hasTinStatus, + isPoaRequired, + isPoiRequired, + isTinRequired, + isVerificationRequired: isPoiRequired || isPoaRequired || isTinRequired, + overallStatus, + statuses: documentStatuses, + }; +};