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..9916fc1d8c78 --- /dev/null +++ b/packages/api-v2/src/hooks/__tests__/useSortedMT5Accounts.spec.ts @@ -0,0 +1,302 @@ +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: 'stp', + 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_virtual: false, + landing_company_short: 'vanuatu', + product: 'standard', + }, + { + is_added: true, + is_virtual: false, + landing_company_short: 'vanuatu', + product: 'financial', + }, + { + is_added: false, + is_default_jurisdiction: 'true', + product: 'stp', + shortcode: 'vanuatu', + }, + { + is_added: false, + is_default_jurisdiction: 'true', + product: 'swap_free', + shortcode: 'svg', + }, + { + is_added: true, + is_virtual: false, + landing_company_short: 'bvi', + product: 'zero_spread', + }, + ]); + }); + + 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_virtual: false, + landing_company_short: 'maltainvest', + product: 'financial', + }, + ]); + }); + + 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', + 'stp', + 'swap_free', + 'zero_spread', + ]); + }); + + 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', + }, + { + is_virtual: false, + landing_company_short: 'vanuatu', + product: 'stp', + }, + ], + }); + + const { result } = renderHook(() => useSortedMT5Accounts()); + + expect(result.current.data).toEqual([ + { + is_added: true, + is_virtual: false, + landing_company_short: 'vanuatu', + product: 'standard', + }, + { + is_added: true, + is_virtual: false, + landing_company_short: 'vanuatu', + product: 'financial', + }, + { + is_added: true, + is_virtual: false, + landing_company_short: 'vanuatu', + product: 'stp', + }, + { + is_added: true, + is_virtual: false, + landing_company_short: 'svg', + product: 'swap_free', + }, + { + is_added: true, + is_virtual: false, + landing_company_short: 'bvi', + product: 'zero_spread', + }, + ]); + }); +}); diff --git a/packages/api-v2/src/hooks/useSortedMT5Accounts.ts b/packages/api-v2/src/hooks/useSortedMT5Accounts.ts index 7db93f7d010f..4634c5674865 100644 --- a/packages/api-v2/src/hooks/useSortedMT5Accounts.ts +++ b/packages/api-v2/src/hooks/useSortedMT5Accounts.ts @@ -28,79 +28,39 @@ const useSortedMT5Accounts = (regulation?: string) => { : account.landing_company_short !== 'maltainvest') ); - return filtered_available_accounts?.map(available_account => { - const created_account = filtered_mt5_accounts?.find(account => { + const available_accounts = filtered_available_accounts + .filter(available => { return ( - available_account.market_type === account.market_type && - available_account.shortcode === account.landing_company_short + !filtered_mt5_accounts.find(added => added.product === available.product) && + // @ts-expect-error type for is_default_jurisdiction is unavailable in mt5_login_list and trading_platform_available_accounts + available.is_default_jurisdiction === 'true' ); - }); - - if (created_account) - return { - ...created_account, - /** Determine if the account is added or not */ - is_added: true, - } as const; - - return { - ...available_account, - /** Determine if the account is added or not */ - is_added: false, - } as const; - }); + }) + //@ts-expect-error needs backend type + .filter(account => !activeAccount?.is_virtual || account.product !== 'stp'); + + const combined_accounts = [ + ...available_accounts.map(account => ({ ...account, is_added: false })), + ...filtered_mt5_accounts.map(account => ({ ...account, is_added: true })), + ]; + return combined_accounts; }, [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]); - - // 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/wallets/src/components/Base/ModalStepWrapper/ModalStepWrapper.scss b/packages/wallets/src/components/Base/ModalStepWrapper/ModalStepWrapper.scss index a9329b132510..a5e9cb9c3453 100644 --- a/packages/wallets/src/components/Base/ModalStepWrapper/ModalStepWrapper.scss +++ b/packages/wallets/src/components/Base/ModalStepWrapper/ModalStepWrapper.scss @@ -62,6 +62,7 @@ background: var(--general-main-2, #ffffff); border-bottom: 0.2rem solid #f2f3f4; z-index: 1; + border-radius: 0.8rem 0.8rem 0 0; &-close-icon { cursor: pointer; @@ -74,6 +75,7 @@ z-index: 99; width: 100%; padding: 0 2rem; + border-radius: 0; } } @@ -81,6 +83,10 @@ height: 100%; width: 100%; overflow-y: scroll; + + &--disable-scroll { + overflow: unset; + } } &__content { diff --git a/packages/wallets/src/components/Base/ModalStepWrapper/ModalStepWrapper.tsx b/packages/wallets/src/components/Base/ModalStepWrapper/ModalStepWrapper.tsx index 81c98ba63589..b74b45711ead 100644 --- a/packages/wallets/src/components/Base/ModalStepWrapper/ModalStepWrapper.tsx +++ b/packages/wallets/src/components/Base/ModalStepWrapper/ModalStepWrapper.tsx @@ -83,7 +83,12 @@ const ModalStepWrapper: FC> = ({ /> )} -
+
{children} {!shouldFixedFooter &&
}
diff --git a/packages/wallets/src/features/cashier/constants/constants.ts b/packages/wallets/src/features/cashier/constants/constants.ts index e11ac6513c6b..017e356a4e6c 100644 --- a/packages/wallets/src/features/cashier/constants/constants.ts +++ b/packages/wallets/src/features/cashier/constants/constants.ts @@ -15,7 +15,7 @@ interface TDefinedMT5LandingCompanyDetails { interface TMT5MarketTypeDetails extends TMT5MarketTypeDetailsCommon { landingCompany?: Record; - product?: Record; + product?: Partial>; } interface TMT5MarketTypeDetailsCommon { @@ -82,6 +82,12 @@ export const MT5MarketTypeDetails: Record { ).toBe(MT5MarketTypeDetails.financial.landingCompany?.svg.title); }); + it('returns correct name for MT5 financial STP account', () => { + expect( + getAccountName({ + accountCategory: 'trading', + accountType: PlatformDetails.mt5.name, + landingCompanyName: 'svg', + mt5MarketType: MT5MarketTypeDetails.financial.name, + product: 'stp', + }) + ).toBe(MT5MarketTypeDetails.financial.product?.stp?.title); + }); + it('returns correct name for MT5 synthetic account', () => { expect( getAccountName({ diff --git a/packages/wallets/src/features/cashier/helpers/helpers.ts b/packages/wallets/src/features/cashier/helpers/helpers.ts index b76b9698b2b1..aafd1a158872 100644 --- a/packages/wallets/src/features/cashier/helpers/helpers.ts +++ b/packages/wallets/src/features/cashier/helpers/helpers.ts @@ -9,7 +9,7 @@ type TGetAccountNameProps = { displayCurrencyCode?: THooks.CurrencyConfig['display_code']; landingCompanyName: TWalletLandingCompanyName; mt5MarketType?: TMarketTypes.SortedMT5Accounts; - product?: THooks.AvailableMT5Accounts['product']; + product?: THooks.AvailableMT5Accounts['product'] | 'stp'; }; //TODO: remove this function when market_type will be added to transfer_between_accounts response in API @@ -37,6 +37,11 @@ export const getAccountName = ({ mt5MarketType, product, }: TGetAccountNameProps) => { + const MT5FinancialTitle = + product === 'stp' + ? MT5MarketTypeDetails.financial.product?.stp?.title + : MT5MarketTypeDetails.financial.landingCompany?.svg.title; + switch (accountCategory) { case 'wallet': return localize('{{currency}} Wallet', { currency: displayCurrencyCode }); @@ -60,13 +65,13 @@ export const getAccountName = ({ 'svg' | 'virtual' > ) - ? MT5MarketTypeDetails.financial.landingCompany?.svg.title + ? MT5FinancialTitle : MT5MarketTypeDetails.financial.landingCompany?.malta.title; case MT5MarketTypeDetails.synthetic.name: return MT5MarketTypeDetails.synthetic.title; case MT5MarketTypeDetails.all.name: if (product === PRODUCT.ZEROSPREAD) { - return MT5MarketTypeDetails.all.product?.zero_spread.title; + return MT5MarketTypeDetails.all.product?.zero_spread?.title; } return MT5MarketTypeDetails.all.title; default: 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 b0618c3901e9..000000000000 --- a/packages/wallets/src/features/cfd/components/CFDPasswordModalTnc/CFDPasswordModalTnc.scss +++ /dev/null @@ -1,13 +0,0 @@ -.wallets-cfd-modal-tnc { - display: flex; - flex-direction: column; - gap: 1.6rem; - - &__message { - max-width: 44rem; - } - - @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 d4026be040d6..000000000000 --- a/packages/wallets/src/features/cfd/components/CFDPasswordModalTnc/CFDPasswordModalTnc.tsx +++ /dev/null @@ -1,59 +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/CFDPlatformsListAccounts/CFDPlatformsListAccounts.tsx b/packages/wallets/src/features/cfd/components/CFDPlatformsListAccounts/CFDPlatformsListAccounts.tsx index 68f97577d784..0a3ea97a7b3c 100644 --- a/packages/wallets/src/features/cfd/components/CFDPlatformsListAccounts/CFDPlatformsListAccounts.tsx +++ b/packages/wallets/src/features/cfd/components/CFDPlatformsListAccounts/CFDPlatformsListAccounts.tsx @@ -9,6 +9,7 @@ import { AvailableDxtradeAccountsList, AvailableMT5AccountsList, } from '../../flows'; +import { TAddedMT5Account, TAvailableMT5Account } from '../../types'; import './CFDPlatformsListAccounts.scss'; const CFDPlatformsListAccounts: React.FC = () => { @@ -55,10 +56,18 @@ const CFDPlatformsListAccounts: React.FC = () => {
{mt5AccountsList?.map((account, index) => { if (account.is_added) - return ; + return ( + + ); return ( - + ); })} {!isRestricted && ( 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..eabf13023033 100644 --- a/packages/wallets/src/features/cfd/components/PlatformStatusBadge/PlatformStatusBadge.tsx +++ b/packages/wallets/src/features/cfd/components/PlatformStatusBadge/PlatformStatusBadge.tsx @@ -3,15 +3,15 @@ import { useTradingPlatformStatus } from '@deriv/api-v2'; import { LegacyWarningIcon } from '@deriv/quill-icons'; import { useTranslations } from '@deriv-com/translations'; import { Badge, Text } from '@deriv-com/ui'; -import { THooks } from '../../../../types'; import type { TAccount } from '../../../cashier/modules/Transfer/types'; import { MT5_ACCOUNT_STATUS, TRADING_PLATFORM_STATUS } from '../../constants'; +import { TAddedMT5Account } from '../../types'; type TProps = { badgeSize: ComponentProps['badgeSize']; cashierAccount?: TAccount; className?: ComponentProps['className']; - mt5Account?: THooks.MT5AccountsList; + mt5Account?: TAddedMT5Account; }; 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..d74137d0ca37 100644 --- a/packages/wallets/src/features/cfd/constants.tsx +++ b/packages/wallets/src/features/cfd/constants.tsx @@ -31,14 +31,17 @@ const swapFreeDetails = (localize: ReturnType['localize' export const getMarketTypeDetails = ( localize: ReturnType['localize'], - product?: THooks.AvailableMT5Accounts['product'] + product?: THooks.AvailableMT5Accounts['product'] | 'stp' ) => ({ all: product === PRODUCT.ZEROSPREAD ? zeroSpreadDetails(localize) : swapFreeDetails(localize), financial: { - description: localize('CFDs on financial instruments'), + description: + product === 'stp' + ? localize('Direct access to market prices') + : localize('CFDs on financial instruments'), icon: , - title: 'Financial', + title: product === 'stp' ? 'Financial STP' : 'Financial', }, synthetic: { description: localize('CFDs on derived and financial instruments'), @@ -185,12 +188,24 @@ 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_REQUIRED: 'poa_required', + + // eslint-disable-next-line sort-keys + POA_PENDING: 'poa_pending', + 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.scss b/packages/wallets/src/features/cfd/flows/MT5/AddedMT5AccountsList/AddedMT5AccountsList.scss index b2a54c389c1a..80a851dae2be 100644 --- a/packages/wallets/src/features/cfd/flows/MT5/AddedMT5AccountsList/AddedMT5AccountsList.scss +++ b/packages/wallets/src/features/cfd/flows/MT5/AddedMT5AccountsList/AddedMT5AccountsList.scss @@ -94,5 +94,8 @@ &--pending { opacity: 0.5; } + &--disabled { + opacity: 0.48; + } } } 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 d6d4f84ae9ba..dab700346633 100644 --- a/packages/wallets/src/features/cfd/flows/MT5/AddedMT5AccountsList/AddedMT5AccountsList.tsx +++ b/packages/wallets/src/features/cfd/flows/MT5/AddedMT5AccountsList/AddedMT5AccountsList.tsx @@ -1,161 +1,67 @@ -import React, { useMemo, useState } from 'react'; +import React, { useState } 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 { useTranslations } from '@deriv-com/translations'; +import { Text } from '@deriv-com/ui'; import { WalletDisabledAccountModal, WalletStatusBadge } from '../../../../../components'; 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 { TAddedMT5Account } from '../../../types'; +import { useAddedMT5Account } from './hooks'; import './AddedMT5AccountsList.scss'; type TProps = { - account: THooks.MT5AccountsList; -}; - -type TTradingAccountJurisdictionStatusInfoProps = { - isAccountDisabled?: boolean; - isJurisdictionFailure?: boolean; - isJurisdictionPending?: boolean; - selectedJurisdiction: THooks.MT5AccountsList['landing_company_short']; -}; - -const TradingAccountJurisdictionStatusInfo: React.FC = ({ - isAccountDisabled, - isJurisdictionFailure, - isJurisdictionPending, - selectedJurisdiction, -}) => { - const { show } = useModal(); - if (isAccountDisabled) { - return ; - } - if (isJurisdictionPending) { - return ( - - } - > - - - - - ); - } - - if (isJurisdictionFailure) { - return ( - - } - > - - - show(, { - defaultRootId: 'wallets_modal_root', - }) - } - />, - ]} - i18n_default_text='Verification failed <0>Why?' - /> - - - ); - } - - return null; + account: TAddedMT5Account; }; const AddedMT5AccountsList: React.FC = ({ account }) => { - const [shouldShowDisabledAccountModal, setShouldShowDisabledAccountModal] = useState(false); - 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 { getPlatformStatus } = useTradingPlatformStatus(); - const platformStatus = getPlatformStatus(account.platform); + const { accountDetails, isAccountDisabled, isServerMaintenance, kycStatus, showMT5TradeModal, showPlatformStatus } = + useAddedMT5Account(account); - const hasPlatformStatus = - account.status === TRADING_PLATFORM_STATUS.UNAVAILABLE || - account.status === MT5_ACCOUNT_STATUS.UNDER_MAINTENANCE || - platformStatus === TRADING_PLATFORM_STATUS.MAINTENANCE; + const { show } = useModal(); + const [showDisabledAccountModal, setShowDisabledAccountModal] = useState(false); - const isServerMaintenance = - platformStatus === TRADING_PLATFORM_STATUS.MAINTENANCE || - account.status === MT5_ACCOUNT_STATUS.UNDER_MAINTENANCE; - const showPlatformStatus = hasPlatformStatus && !(jurisdictionStatus.is_pending || jurisdictionStatus.is_failed); - // @ts-expect-error The enabled property exists, but the api-types are invalid - const isAccountDisabled = !account?.rights?.enabled; - const shouldShowBalance = !(jurisdictionStatus.is_failed || jurisdictionStatus.is_pending) && !isAccountDisabled; return ( <> { if (isAccountDisabled) { - return setShouldShowDisabledAccountModal(true); + setShowDisabledAccountModal(true); + 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} = ({ account }) => { })} >
- {title} + {accountDetails.title}
- {shouldShowBalance && ( + {!isAccountDisabled && !kycStatus && ( {account.display_balance} )} - {account.display_login} - + {!isAccountDisabled && kycStatus && ( + + show(, { + defaultRootId: 'wallets_modal_root', + }) + } + variant={kycStatus} + /> + )} + {isAccountDisabled && }
{showPlatformStatus ? ( @@ -194,7 +105,7 @@ const AddedMT5AccountsList: React.FC = ({ account }) => { mt5Account={account} /> ) : ( -
+
{isRtl ? ( ) : ( @@ -207,8 +118,8 @@ const AddedMT5AccountsList: React.FC = ({ account }) => { setShouldShowDisabledAccountModal(false)} + isVisible={showDisabledAccountModal} + onClose={() => setShowDisabledAccountModal(false)} /> ); diff --git a/packages/wallets/src/features/cfd/flows/MT5/AddedMT5AccountsList/__tests__/AddedMT5AccountsList.spec.tsx b/packages/wallets/src/features/cfd/flows/MT5/AddedMT5AccountsList/__tests__/AddedMT5AccountsList.spec.tsx index 7157576d8451..1b5f66f84d52 100644 --- a/packages/wallets/src/features/cfd/flows/MT5/AddedMT5AccountsList/__tests__/AddedMT5AccountsList.spec.tsx +++ b/packages/wallets/src/features/cfd/flows/MT5/AddedMT5AccountsList/__tests__/AddedMT5AccountsList.spec.tsx @@ -1,160 +1,220 @@ 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('../../../../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
; + }), })); -jest.mock('../../../../../../components/ModalProvider', () => ({ - useModal: jest.fn(), +jest.mock('../../../../../../components', () => ({ + ...jest.requireActual('../../../../../../components'), + WalletDisabledAccountModal: jest.fn(props => { + mockPropsFn(props); + return
WalletDisabledAccountModal
; + }), + WalletStatusBadge: jest.fn(props => { + mockPropsFn(props); + return
WalletStatusBadge
; + }), })); +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', - rights: { enabled: true }, - 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 () => { + 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( - // @ts-expect-error - since this is a mock, we only need partial properties of the account - - ); + expect(screen.getByText('ClientVerificationModal')).toBeInTheDocument(); }); }); - it('shows TradingPlatformStatusModal when platform is under maintenance', async () => { - (useTradingPlatformStatus as jest.Mock).mockReturnValue({ - getPlatformStatus: jest.fn().mockReturnValue('maintenance'), + it('shows the disabled badge when the account MT5 account is disabled', () => { + (useAddedMT5Account as jest.Mock).mockReturnValue({ + ...mockUseAddedMT5AccountData, + isAccountDisabled: true, + isServerMaintenance: true, + showPlatformStatus: true, }); // @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('WalletStatusBadge')).toBeInTheDocument(); + expect(mockPropsFn).toBeCalledWith({ + badgeSize: 'md', + padding: 'tight', + status: 'disabled', }); }); - 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 }), + 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 }); - expect(screen.getByText('Pending verification')).toBeInTheDocument(); - }); + userEvent.click(screen.getByTestId('dt_wallets_trading_account_card')); - it('displays verification failed message when verification has failed', () => { - (useJurisdictionStatus as jest.Mock).mockReturnValue({ - getVerificationStatus: jest.fn().mockReturnValue({ is_failed: true, is_pending: false }), + await waitFor(() => { + expect(screen.getByText('TradingPlatformStatusModal')).toBeInTheDocument(); + expect(mockPropsFn).toBeCalledWith({ + isServerMaintenance: true, + }); }); - // @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 the WalletDisabledAccountModal when a disabled account MT5 account is clicked', async () => { + (useAddedMT5Account as jest.Mock).mockReturnValue({ + ...mockUseAddedMT5AccountData, + isAccountDisabled: true, + isServerMaintenance: true, + showPlatformStatus: true, }); // @ts-expect-error - since this is a mock, we only need partial properties of the account - render(); - - const link = screen.getByText('Why?'); - userEvent.click(link); + render(, { wrapper }); await waitFor(() => { - expect(mockShow).toHaveBeenCalledWith(, { - defaultRootId: 'wallets_modal_root', - }); + userEvent.click(screen.getByText('WalletStatusBadge')); }); - }); - it('shows WalletStatusBadge when account is disabled', () => { - // @ts-expect-error - since this is a mock, we only need partial properties of the account - render(); - - expect(screen.getByText('Disabled')).toBeInTheDocument(); - }); - - it('opens WalletDisabledAccountModal when disabled account card is clicked', async () => { - // @ts-expect-error - since this is a mock, we only need partial properties of the account - render(); - - const card = screen.getByTestId('dt_wallets_trading_account_card'); - await userEvent.click(card); - - expect(screen.getByText('Contact us via live chat for more details.')).toBeInTheDocument(); - - const closeButton = screen.getByTestId('dt-close-icon'); - expect(closeButton).toBeInTheDocument(); - await userEvent.click(closeButton); - - expect(screen.queryByText('Contact us via live chat for more details.')).not.toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText('WalletDisabledAccountModal')).toBeInTheDocument(); + }); }); }); 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..0d7af35c6215 --- /dev/null +++ b/packages/wallets/src/features/cfd/flows/MT5/AddedMT5AccountsList/hooks/__tests__/useAddedMT5Account.spec.ts @@ -0,0 +1,111 @@ +import { useTradingPlatformStatus } from '@deriv/api-v2'; +import { cleanup } from '@testing-library/react'; +import { renderHook } from '@testing-library/react-hooks'; +import { getMarketTypeDetails } from '../../../../../constants'; +import { TAddedMT5Account } 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: '', +} as TAddedMT5Account; + +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)); + + 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)); + + expect(result.current.isServerMaintenance).toEqual(true); + }); + + it('isServerMaintenance is `true` when account status is `under_maintenance`', () => { + const { result } = renderHook(() => useAddedMT5Account({ ...mockAccount, status: 'under_maintenance' })); + + 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' })); + + 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' })); + + 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' })); + + 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' })); + + 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)); + + expect(result.current.showMT5TradeModal).toEqual(true); + }); + + it('showPlatformStatus is `true` when account status is `unavailable`', () => { + const { result } = renderHook(() => useAddedMT5Account({ ...mockAccount, status: 'unavailable' })); + + expect(result.current.showPlatformStatus).toEqual(true); + }); + + it('showPlatformStatus is `true` when account status is `under_maintenance`', () => { + const { result } = renderHook(() => useAddedMT5Account({ ...mockAccount, status: 'under_maintenance' })); + + 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)); + + 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..e02189eab358 --- /dev/null +++ b/packages/wallets/src/features/cfd/flows/MT5/AddedMT5AccountsList/hooks/useAddedMT5Account.ts @@ -0,0 +1,61 @@ +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 { TAddedMT5Account } from '../../../../types'; + +type TBadgeVariations = Partial['variant']> | undefined; + +const getClientKycStatus = (status: TAddedMT5Account['status']): TBadgeVariations => { + switch (status) { + case MT5_ACCOUNT_STATUS.POA_FAILED: + case MT5_ACCOUNT_STATUS.PROOF_FAILED: + return 'failed'; + case MT5_ACCOUNT_STATUS.VERIFICATION_PENDING: + case MT5_ACCOUNT_STATUS.POA_PENDING: + return 'in_review'; + case MT5_ACCOUNT_STATUS.NEEDS_VERIFICATION: + case MT5_ACCOUNT_STATUS.POA_REQUIRED: + return 'needs_verification'; + default: + } +}; + +const useAddedMT5Account = (account: TAddedMT5Account) => { + const { localize } = useTranslations(); + + // @ts-expect-error The enabled property exists, but the api-types are invalid + const isAccountDisabled = !account.rights?.enabled; + + 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, + isAccountDisabled, + 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 310168f8c1a3..3d6c53ed996e 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,22 @@ -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 { TAvailableMT5Account } 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: TAvailableMT5Account; }; const AvailableMT5AccountsList: React.FC = ({ account }) => { @@ -31,10 +28,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); const onButtonClick = useCallback(() => { if (hasUnavailableAccount) return show(); @@ -46,58 +44,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..275e56b693ac --- /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: 39.2rem; + + @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..7ec3a86c1f77 --- /dev/null +++ b/packages/wallets/src/features/cfd/modals/ClientVerificationModal/ClientVerificationModal.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { Localize, useTranslations } from '@deriv-com/translations'; +import { Text, useDevice } from '@deriv-com/ui'; +import { ModalStepWrapper } from '../../../../components'; +import DerivLightUserVerificationIcon from '../../../../public/images/ic-deriv-light-user-verification.svg'; +import { TModifiedMT5Account } from '../../types'; +import { DocumentsList } from './components'; +import './ClientVerificationModal.scss'; + +type TClientVerificationModal = { + account: TModifiedMT5Account; +}; + +const ClientVerificationModal: React.FC = ({ account }) => { + const { localize } = useTranslations(); + const { isMobile } = useDevice(); + + 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..0b1c91731fdb --- /dev/null +++ b/packages/wallets/src/features/cfd/modals/ClientVerificationModal/components/DocumentsList/DocumentsList.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { useHistory } from 'react-router-dom'; +import { TModifiedMT5Account } 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: TModifiedMT5Account; +}; + +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, + hasRequiredTin, + hasTinStatus, + isPoaRequired, + isPoiRequired, + isTinRequired, + statuses, + } = getClientVerification(account); + + const shouldShowTin = hasRequiredTin && hasTinStatus && isTinRequired; + + return ( +
+ {hasPoiStatus && ( + history.push('/account/proof-of-identity')} + title={localize('Proof of identity')} + /> + )} + {hasPoaStatus && ( + { + localStorage.setItem('mt5_poa_status', statuses.poa_status); + // @ts-expect-error the following link is not part of wallets routes config + history.push('/account/proof-of-address'); + }} + title={localize('Proof of address')} + /> + )} + {shouldShowTin && ( + history.push('/account/personal-details')} + title={localize('Additional information')} + /> + )} +
+ ); +}; + +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..b1b914df8a28 --- /dev/null +++ b/packages/wallets/src/features/cfd/modals/ClientVerificationModal/components/DocumentsList/__tests__/DocumentsList.spec.tsx @@ -0,0 +1,173 @@ +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('poa 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('`Additional information` tile is not rendered', () => { + render( + + ); + + expect(screen.queryByText('Additional information')).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 `Additional information` tile redirects to correct page', async () => { + render( + + ); + + const additionalInfoTile = screen.getByText('Additional information'); + userEvent.click(additionalInfoTile); + + 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..5d94c13d85ef --- /dev/null +++ b/packages/wallets/src/features/cfd/modals/ClientVerificationModal/components/DocumentsList/components/DocumentTile/DocumentTile.tsx @@ -0,0 +1,32 @@ +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 43bfbaea7bbb..f3168081fb81 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, useSettings, @@ -13,21 +12,19 @@ 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 { TAvailableMT5Account } from '../../types'; import MT5AccountAdded from '../MT5AccountAdded/MT5AccountAdded'; import { PasswordLimitExceededModal } from '../PasswordLimitExceededModal'; import { MT5PasswordModalFooter, SuccessModalFooter } from './MT5PasswordModalFooters'; import './MT5PasswordModal.scss'; type TProps = { + account: TAvailableMT5Account; isVirtual?: boolean; - marketType: TMarketTypes.SortedMT5Accounts; - platform: TPlatforms.All; - product?: THooks.AvailableMT5Accounts['product']; }; export type TPlatformPasswordChange = { @@ -35,8 +32,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 as TAvailableMT5Account).shortcode === JURISDICTION.SVG || isVirtual + ); const { data: createMT5AccountData, error: createMT5AccountError, @@ -51,7 +51,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, @@ -67,10 +66,13 @@ 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 isDemo = activeWalletData?.is_virtual; const { platform: mt5Platform, title } = PlatformDetails.mt5; - const selectedJurisdiction = isDemo ? JURISDICTION.SVG : getModalState('selectedJurisdiction'); + const selectedJurisdiction = isVirtual ? JURISDICTION.SVG : getModalState('selectedJurisdiction'); const isLoading = accountStatusLoading || createMT5AccountLoading || tradingPlatformPasswordChangeLoading; @@ -86,7 +88,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({ @@ -104,7 +106,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 @@ -128,7 +130,7 @@ const MT5PasswordModal: React.FC = ({ isVirtual, marketType, platform, p }, [ availableMT5AccountsData, createMT5AccountMutate, - isDemo, + isVirtual, isMT5PasswordNotSet, marketType, mt5Platform, @@ -151,12 +153,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) => { @@ -172,7 +174,7 @@ const MT5PasswordModal: React.FC = ({ isVirtual, marketType, platform, p const renderTitle = useCallback(() => { const accountAction = isMT5PasswordNotSet ? localize('Create a') : localize('Enter your'); - const accountTitle = isDemo ? localize('demo {{title}}', { title }) : title; + const accountTitle = isVirtual ? localize('demo {{title}}', { title }) : title; return updateMT5Password ? localize('{{title}} latest password requirements', { title }) @@ -180,13 +182,13 @@ const MT5PasswordModal: React.FC = ({ isVirtual, marketType, platform, p accountAction, accountTitle, }); - }, [isMT5PasswordNotSet, isDemo, localize, title, updateMT5Password]); + }, [isMT5PasswordNotSet, isVirtual, localize, title, updateMT5Password]); const renderFooter = useCallback(() => { if (createMT5AccountSuccess) return (
- +
); @@ -231,7 +233,7 @@ const MT5PasswordModal: React.FC = ({ isVirtual, marketType, platform, p }, [ createMT5AccountLoading, createMT5AccountSuccess, - isDemo, + isVirtual, isDesktop, isMT5PasswordNotSet, title, @@ -259,6 +261,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} /> ); @@ -285,9 +287,10 @@ const MT5PasswordModal: React.FC = ({ isVirtual, marketType, platform, p return ( = ({ isVirtual, marketType, platform, p }, [ isLoading, 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 sendEmailVerification, ]); + 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(); @@ -62,13 +63,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.scss b/packages/wallets/src/features/cfd/screens/EnterPassword/EnterPassword.scss index 1e4692d8b610..f4b672c20890 100644 --- a/packages/wallets/src/features/cfd/screens/EnterPassword/EnterPassword.scss +++ b/packages/wallets/src/features/cfd/screens/EnterPassword/EnterPassword.scss @@ -5,13 +5,15 @@ border-radius: 0.8rem; background: var(--system-light-8-primary-background, #fff); box-shadow: 0rem 0rem 2.4rem 0rem rgba(0, 0, 0, 0.25); + max-width: 40rem; + width: 100%; @include mobile-or-tablet-screen { padding-inline: 1.6rem; padding-block: 0; box-shadow: none; height: 100%; - width: 100%; + max-width: 60rem; } &__container { diff --git a/packages/wallets/src/features/cfd/screens/EnterPassword/EnterPassword.tsx b/packages/wallets/src/features/cfd/screens/EnterPassword/EnterPassword.tsx index 5b8c25c0b00c..8c909b9a44d6 100644 --- a/packages/wallets/src/features/cfd/screens/EnterPassword/EnterPassword.tsx +++ b/packages/wallets/src/features/cfd/screens/EnterPassword/EnterPassword.tsx @@ -5,11 +5,14 @@ import { Button, Text, useDevice } from '@deriv-com/ui'; import { WalletPasswordFieldLazy } from '../../../../components/Base'; import { THooks, TMarketTypes, TPlatforms } from '../../../../types'; import { validPassword, validPasswordMT5 } from '../../../../utils/password-validation'; -import { CFDPasswordModalTnc } from '../../components/CFDPasswordModalTnc'; -import { CFD_PLATFORMS, getMarketTypeDetails, PlatformDetails, PRODUCT } from '../../constants'; +import { CFD_PLATFORMS, getMarketTypeDetails, JURISDICTION, PlatformDetails } from '../../constants'; +import { TAvailableMT5Account } from '../../types'; +import { MT5LicenceMessage, MT5PasswordModalTnc } from '../components'; import './EnterPassword.scss'; +// Note: this component requires a proper refactor to remove props for keys available under the `account` prop type TProps = { + account?: TAvailableMT5Account; 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..2e4982189039 100644 --- a/packages/wallets/src/features/cfd/screens/MT5TradeScreen/MT5TradeScreen.tsx +++ b/packages/wallets/src/features/cfd/screens/MT5TradeScreen/MT5TradeScreen.tsx @@ -8,6 +8,7 @@ import { WalletBadge, WalletListCardBadge } from '../../../../components'; import { useModal } from '../../../../components/ModalProvider'; import { THooks } from '../../../../types'; import { CFD_PLATFORMS, getMarketTypeDetails, getServiceMaintenanceMessages, PlatformDetails } from '../../constants'; +import { TAddedMT5Account } from '../../types'; import MT5DesktopRedirectOption from './MT5TradeLink/MT5DesktopRedirectOption'; import MT5MobileRedirectOption from './MT5TradeLink/MT5MobileRedirectOption'; import { MT5TradeDetailsItem } from './MT5TradeDetailsItem'; @@ -15,7 +16,7 @@ import { MT5TradeLink } from './MT5TradeLink'; import './MT5TradeScreen.scss'; type MT5TradeScreenProps = { - mt5Account?: THooks.MT5AccountsList; + mt5Account?: TAddedMT5Account; }; const MT5TradeScreen: FC = ({ mt5Account }) => { @@ -89,6 +90,14 @@ const MT5TradeScreen: FC = ({ mt5Account }) => { return details?.login; }, [details, dxtradePlatform, mt5Platform, platform]); + const shouldShowBadge = + !activeWalletData?.is_virtual && + details && + 'product' in details && + //@ts-expect-error needs backend type + details.product !== 'stp' && + details.landing_company_name !== 'labuan'; + const migrationMessage = useMemo(() => { if (platform === mt5Platform && !activeWalletData?.is_virtual) { switch ( @@ -137,7 +146,7 @@ const MT5TradeScreen: FC = ({ mt5Account }) => { {platform === mt5Platform ? marketTypeTitle : platformTitle}{' '} - {!activeWalletData?.is_virtual && ( + {shouldShowBadge && ( {details?.landing_company_short?.toUpperCase()} )} {activeWalletData?.is_virtual && } 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..8b68d3233535 --- /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 { TAvailableMT5Account } from '../../../types'; +import './MT5LicenceMessage.scss'; + +type TMT5LicenseMessageProps = { + account: TAvailableMT5Account; +}; + +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..80635ca2ec68 --- /dev/null +++ b/packages/wallets/src/features/cfd/types.ts @@ -0,0 +1,24 @@ +/* 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'; + +type TStatuses = 'expired' | 'none' | 'pending' | 'rejected' | 'suspected' | 'verified'; + +export type TModifiedMT5Account = THooks.SortedMT5Accounts & { + client_kyc_status: { + poa_status: TStatuses; + poi_status: TStatuses; + required_tin: 0 | 1; + valid_tin: 0 | 1; + }; + licence_number: string; + regulatory_authority: string; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type ObjectWithKeyInUnion = T extends any ? (K extends keyof T ? T : never) : never; + +export type TAvailableMT5Account = ObjectWithKeyInUnion; +export type TAddedMT5Account = ObjectWithKeyInUnion; 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..a34521f148a3 --- /dev/null +++ b/packages/wallets/src/features/cfd/utils/utils.ts @@ -0,0 +1,31 @@ +import { TAddedMT5Account, TAvailableMT5Account } from '../types'; + +const requiredDocumentStatuses = ['expired', 'none', 'rejected', 'suspected']; + +export const getClientVerification = (account: TAddedMT5Account | TAvailableMT5Account) => { + 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 hasRequiredTin = hasClientKycStatus && 'required_tin' in documentStatuses; + + const isPoiRequired = hasPoiStatus && requiredDocumentStatuses.includes(documentStatuses.poi_status); + const isPoaRequired = hasPoaStatus && requiredDocumentStatuses.includes(documentStatuses.poa_status); + const isTinRequired = + hasTinStatus && hasRequiredTin && Boolean(documentStatuses.required_tin) && !documentStatuses.valid_tin; + + return { + hasClientKycStatus, + hasPoaStatus, + hasPoiStatus, + hasRequiredTin, + hasTinStatus, + isPoaRequired, + isPoiRequired, + isTinRequired, + isVerificationRequired: isPoiRequired || isPoaRequired || isTinRequired, + statuses: documentStatuses, + }; +}; diff --git a/packages/wallets/src/public/images/ic-deriv-light-user-verification.svg b/packages/wallets/src/public/images/ic-deriv-light-user-verification.svg new file mode 100644 index 000000000000..0e16c052bd57 --- /dev/null +++ b/packages/wallets/src/public/images/ic-deriv-light-user-verification.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/wallets/src/types.ts b/packages/wallets/src/types.ts index a95ffbaa2250..bd78aa567c74 100644 --- a/packages/wallets/src/types.ts +++ b/packages/wallets/src/types.ts @@ -110,9 +110,9 @@ export type TIconTypes = Record; export type TCurrencyIconTypes = Record; -export type TProductForMarketDetails = NonNullable< - Exclude ->; +export type TProductForMarketDetails = + | NonNullable> + | 'stp'; export type TTranslations = ReturnType;