diff --git a/packages/account/package.json b/packages/account/package.json index cfd2445d9f8d..6feecfbc8024 100644 --- a/packages/account/package.json +++ b/packages/account/package.json @@ -31,6 +31,7 @@ "@deriv/api-types": "^1.0.94", "@deriv/components": "^1.0.0", "@deriv/shared": "^1.0.0", + "@deriv/stores":"^1.0.0", "@deriv/translations": "^1.0.0", "bowser": "^2.9.0", "classnames": "^2.2.6", diff --git a/packages/account/src/App.tsx b/packages/account/src/App.tsx index 0a666cb80bf1..f658afe8facd 100644 --- a/packages/account/src/App.tsx +++ b/packages/account/src/App.tsx @@ -1,27 +1,27 @@ import React from 'react'; import Routes from './Containers/routes'; import ResetTradingPassword from './Containers/reset-trading-password'; -import { MobxContentProvider } from './Stores/connect'; -import initStore from './Stores/init-store'; -import TCoreStore from './Stores/index'; +import { setWebsocket } from '@deriv/shared'; +import { StoreProvider } from '@deriv/stores'; +import { TCoreStores } from '@deriv/stores/types'; -// TODO: add correct types for stores and WS after implementing them +// TODO: add correct types for WS after implementing them type TAppProps = { passthrough: { - root_store: TCoreStore; + root_store: TCoreStores; WS: Record; }; }; const App = ({ passthrough }: TAppProps) => { const { root_store, WS } = passthrough; - initStore(root_store, WS); + setWebsocket(WS); return ( - + - + ); }; diff --git a/packages/account/src/Components/Routes/__tests__/binary-link.spec.tsx b/packages/account/src/Components/Routes/__tests__/binary-link.spec.tsx index 450cbb9b6aa3..7c47af226cd7 100644 --- a/packages/account/src/Components/Routes/__tests__/binary-link.spec.tsx +++ b/packages/account/src/Components/Routes/__tests__/binary-link.spec.tsx @@ -6,12 +6,6 @@ import { PlatformContext } from '@deriv/shared'; import { findRouteByPath } from '../helpers'; import BinaryLink from '../binary-link'; -jest.mock('Stores/connect', () => ({ - __esModule: true, - default: 'mockedDefaultExport', - connect: () => Component => Component, -})); - jest.mock('../helpers', () => ({ findRouteByPath: jest.fn(() => '/test/path'), normalizePath: jest.fn(() => '/test/path'), diff --git a/packages/account/src/Components/Routes/__tests__/binary-routes.spec.tsx b/packages/account/src/Components/Routes/__tests__/binary-routes.spec.tsx index c57920aaecb8..3938f7960aea 100644 --- a/packages/account/src/Components/Routes/__tests__/binary-routes.spec.tsx +++ b/packages/account/src/Components/Routes/__tests__/binary-routes.spec.tsx @@ -5,12 +5,6 @@ import { render, screen } from '@testing-library/react'; import { PlatformContext } from '@deriv/shared'; import BinaryRoutes from '../binary-routes'; -jest.mock('Stores/connect', () => ({ - __esModule: true, - default: 'mockedDefaultExport', - connect: () => Component => Component, -})); - jest.mock('../route-with-sub-routes', () => jest.fn(() =>
RouteWithSubRoutes
)); jest.mock('Constants/routes-config', () => () => [{}]); diff --git a/packages/account/src/Components/account-limits/__tests__/account-limits.spec.tsx b/packages/account/src/Components/account-limits/__tests__/account-limits.spec.tsx index bc0f8b7f825c..07c98028a783 100644 --- a/packages/account/src/Components/account-limits/__tests__/account-limits.spec.tsx +++ b/packages/account/src/Components/account-limits/__tests__/account-limits.spec.tsx @@ -3,12 +3,7 @@ import { screen, render } from '@testing-library/react'; import { formatMoney, isDesktop, isMobile, PlatformContext } from '@deriv/shared'; import AccountLimits from '../account-limits'; import { BrowserRouter } from 'react-router-dom'; - -jest.mock('Stores/connect.js', () => ({ - __esModule: true, - default: 'mockedDefaultExport', - connect: () => (Component: React.ReactElement) => Component, -})); +import { StoreProvider, mockStore } from '@deriv/stores'; jest.mock('@deriv/components', () => { const original_module = jest.requireActual('@deriv/components'); @@ -18,6 +13,11 @@ jest.mock('@deriv/components', () => { Loading: jest.fn(() => 'mockedLoading'), }; }); +jest.mock('@deriv/shared/src/services/ws-methods', () => ({ + __esModule: true, // this property makes it work, + default: 'mockedDefaultExport', + useWS: () => undefined, +})); jest.mock('@deriv/shared', () => ({ ...jest.requireActual('@deriv/shared'), @@ -31,110 +31,133 @@ jest.mock('Components/load-error-message', () => jest.fn(() => 'mockedLoadErrorM jest.mock('../account-limits-footer', () => jest.fn(() => 'mockedAccountLimitsFooter')); describe('', () => { - const props: React.ComponentProps = { - currency: 'AUD', - is_fully_authenticated: true, - is_switching: false, - is_virtual: false, + let store = mockStore({}); + const props = { overlay_ref: document.createElement('div'), - getLimits: jest.fn(() => Promise.resolve({ data: {} })), - account_limits: { - account_balance: 300000, - daily_transfers: { - dxtrade: { - allowed: 12, - available: 12, - }, - internal: { - allowed: 10, - available: 10, - }, - mt5: { - allowed: 10, - available: 10, - }, - }, - lifetime_limit: 13907.43, - market_specific: { - commodities: [ - { - name: 'Commodities', - payout_limit: 5000, - profile_name: 'moderate_risk', - turnover_limit: 50000, - }, - ], - cryptocurrency: [ - { - name: 'Cryptocurrencies', - payout_limit: 100.0, - profile_name: 'extreme_risk', - turnover_limit: 1000.0, - }, - ], - forex: [ - { - name: 'Smart FX', - payout_limit: 5000, - profile_name: 'moderate_risk', - turnover_limit: 50000, - }, - { - name: 'Major Pairs', - payout_limit: 20000, - profile_name: 'medium_risk', - turnover_limit: 100000, - }, - { - name: 'Minor Pairs', - payout_limit: 5000, - profile_name: 'moderate_risk', - turnover_limit: 50000, + }; + const mock = { + client: { + currency: 'AUD', + is_fully_authenticated: true, + is_switching: false, + is_virtual: false, + getLimits: jest.fn(() => Promise.resolve({ data: {} })), + account_limits: { + account_balance: 300000, + daily_transfers: { + dxtrade: { + allowed: 12, + available: 12, }, - ], - indices: [ - { - name: 'Stock Indices', - payout_limit: 20000, - profile_name: 'medium_risk', - turnover_limit: 100000, + internal: { + allowed: 10, + available: 10, }, - ], - synthetic_index: [ - { - name: 'Synthetic Indices', - payout_limit: 50000, - profile_name: 'low_risk', - turnover_limit: 500000, + mt5: { + allowed: 10, + available: 10, }, - ], + }, + lifetime_limit: 13907.43, + market_specific: { + commodities: [ + { + name: 'Commodities', + payout_limit: 5000, + profile_name: 'moderate_risk', + turnover_limit: 50000, + }, + ], + cryptocurrency: [ + { + name: 'Cryptocurrencies', + payout_limit: 100.0, + profile_name: 'extreme_risk', + turnover_limit: 1000.0, + }, + ], + forex: [ + { + name: 'Smart FX', + payout_limit: 5000, + profile_name: 'moderate_risk', + turnover_limit: 50000, + }, + { + name: 'Major Pairs', + payout_limit: 20000, + profile_name: 'medium_risk', + turnover_limit: 100000, + }, + { + name: 'Minor Pairs', + payout_limit: 5000, + profile_name: 'moderate_risk', + turnover_limit: 50000, + }, + ], + indices: [ + { + name: 'Stock Indices', + payout_limit: 20000, + profile_name: 'medium_risk', + turnover_limit: 100000, + }, + ], + synthetic_index: [ + { + name: 'Synthetic Indices', + payout_limit: 50000, + profile_name: 'low_risk', + turnover_limit: 500000, + }, + ], + }, + num_of_days: 30, + num_of_days_limit: 13907.43, + open_positions: 100, + payout: 50000, + remainder: 13907.43, + withdrawal_for_x_days_monetary: 0, + withdrawal_since_inception_monetary: 0, }, - num_of_days: 30, - num_of_days_limit: 13907.43, - open_positions: 100, - payout: 50000, - remainder: 13907.43, - withdrawal_for_x_days_monetary: 0, - withdrawal_since_inception_monetary: 0, }, }; - + store = mockStore(mock); it('should render the Loading component if is_switching is true', () => { - render(); + store = mockStore({ + client: { + is_switching: true, + }, + }); + render( + + + + ); expect(screen.getByText('mockedLoading')).toBeInTheDocument(); }); it('should render DemoMessage component if is_virtual is true', () => { - render(); + store = mockStore({ + client: { + is_switching: false, + is_virtual: true, + }, + }); + render( + + + + ); expect(screen.queryByTestId('dt_account_demo_message_wrapper')).toHaveClass('account__demo-message-wrapper'); expect(screen.getByText('mockedDemoMessage')).toBeInTheDocument(); }); it('should render LoadErrorMessage component if there is api_initial_load_error', () => { - render( - ', () => { num_of_days_limit: '', remainder: '', withdrawal_since_inception_monetary: '', - }} - /> + }, + is_switching: false, + is_virtual: false, + }, + }); + render( + + + ); expect(screen.getByText('mockedLoadErrorMessage')).toBeInTheDocument(); }); it('should render AccountLimits component', () => { - render(); + store = mockStore(mock); + render( + + + + ); expect(screen.queryByTestId('account_limits_data')).toBeInTheDocument(); }); it('should call setIsPopupOverlayShown fn ', () => { + store = mockStore(mock); const setIsPopupOverlayShown = jest.fn(); - render(); + render( + + + + ); expect(setIsPopupOverlayShown).toHaveBeenCalledTimes(1); }); it('should render Loading component if is_loading is true', () => { - render(); + render( + + + + ); expect(screen.queryByTestId('account_limits_data')).toBeInTheDocument(); }); it('should render AccountLimitsArticle component if should_show_article is true and is_from_derivgo is false in mobile mode', () => { (isMobile as jest.Mock).mockReturnValue(true); (isDesktop as jest.Mock).mockReturnValue(false); - render(); + render( + + + + ); expect(screen.getByRole('heading', { name: /account limits/i })).toBeInTheDocument(); expect( screen.queryByText(/to learn more about trading limits and how they apply, please go to the/i) @@ -180,9 +228,18 @@ describe('', () => { }); it('should render AccountLimitsArticle component if should_show_article is true and is_from_derivgo is true in mobile mode', () => { + store = mockStore({ + common: { + is_from_derivgo: true, + }, + }); (isMobile as jest.Mock).mockReturnValue(true); (isDesktop as jest.Mock).mockReturnValue(false); - render(); + render( + + + + ); expect(screen.getByRole('heading', { name: /account limits/i })).toBeInTheDocument(); expect( screen.queryByText(/to learn more about trading limits and how they apply, please go to the/i) @@ -190,14 +247,23 @@ describe('', () => { }); it('should not render AccountLimitsArticle component if should_show_article is false', () => { + store = mockStore(mock); (isMobile as jest.Mock).mockReturnValue(true); (isDesktop as jest.Mock).mockReturnValue(false); - render(); + render( + + + + ); expect(screen.queryByText('/account limits/i')).not.toBeInTheDocument(); }); it('should render Trading limits table and its trading limits contents properly', () => { - render(); + render( + + + + ); expect(screen.queryByTestId('account_limits_data')).toBeInTheDocument(); expect( @@ -218,13 +284,17 @@ describe('', () => { }); it('should render Maximum number of open positions- table cell and its contents properly', () => { - render(); + render( + + + + ); expect( screen.getByRole('cell', { name: /\*maximum number of open positions/i, }) ).toBeInTheDocument(); - const { open_positions } = props.account_limits; + const { open_positions } = store.client.account_limits; expect( screen.getByRole('cell', { name: open_positions?.toString(), @@ -233,13 +303,21 @@ describe('', () => { }); it('should call formatMoney', () => { - render(); - const { account_balance } = props.account_limits; - expect(formatMoney).toHaveBeenCalledWith(props.currency, account_balance, true); + render( + + + + ); + const { account_balance } = store.client.account_limits; + expect(formatMoney).toHaveBeenCalledWith(store.client.currency, account_balance, true); }); it('should render Trading limits table and its maximum daily turnover contents properly', () => { - render(); + render( + + + + ); expect(screen.queryByTestId('trading_daily_turnover_table')).toBeInTheDocument(); expect( screen.getByRole('columnheader', { @@ -259,12 +337,20 @@ describe('', () => { }); it('should not render withdrawal_limits_table is_app_settings is true', () => { - render(); + render( + + + + ); expect(screen.queryByTestId('withdrawal_limits_table')).not.toBeInTheDocument(); }); it('should render withdrawal_limits_table is_app_settings is false', () => { - render(); + render( + + + + ); expect(screen.queryByTestId('withdrawal_limits_table')).toBeInTheDocument(); expect( screen.getByRole('columnheader', { @@ -274,12 +360,20 @@ describe('', () => { }); it('withdrawal_limits_table should have a Limits header if is_fully_authenticated is true', () => { - render(); + render( + + + + ); expect(screen.getByTestId('withdrawal_limits_table')).toHaveTextContent('Limit'); }); it('show show withdrawal limit lifted message if is_fully_authenticated is true', () => { - render(); + render( + + + + ); expect( screen.getByRole('cell', { @@ -289,10 +383,17 @@ describe('', () => { }); it('withdrawal_limits_table should show `Total withdrawal limit` if is_fully_authenticated is false and is_appstore is true', () => { + store = mockStore({ + client: { + is_fully_authenticated: false, + }, + }); render( - + + + ); @@ -300,19 +401,33 @@ describe('', () => { }); it('withdrawal_limits_table should show `Total withdrawal allowed` when is_fully_authenticated is false and is_appstore is true', () => { + store = mockStore({ + client: { + is_fully_authenticated: false, + }, + }); render( - + + + ); expect(screen.getByText(/total withdrawal allowed/i)).toBeInTheDocument(); }); it('withdrawal_limits_table should show the verfiy button when is_fully_authenticated is false and is_appstore is true', () => { + store = mockStore({ + client: { + is_fully_authenticated: false, + }, + }); render( - + + + ); @@ -322,34 +437,48 @@ describe('', () => { name: /verify/i, }) ).toHaveAttribute('href', '/account/proof-of-identity'); - const { num_of_days_limit } = props.account_limits; - expect(formatMoney).toHaveBeenCalledWith(props.currency, num_of_days_limit, true); + const { num_of_days_limit } = store.client.account_limits; + expect(formatMoney).toHaveBeenCalledWith(store.client.currency, num_of_days_limit, true); }); it('withdrawal_limits_table should show total withdrawn and withdrawn remaining details', () => { + store = mockStore({ + client: { + is_fully_authenticated: false, + }, + }); render( - + + + ); - const { withdrawal_since_inception_monetary, remainder } = props.account_limits; + const { withdrawal_since_inception_monetary, remainder } = store.client.account_limits; expect(screen.getByText(/total withdrawn/i)).toBeInTheDocument(); - expect(formatMoney).toHaveBeenCalledWith(props.currency, withdrawal_since_inception_monetary, true); + expect(formatMoney).toHaveBeenCalledWith(store.client.currency, withdrawal_since_inception_monetary, true); expect(screen.getByText(/maximum withdrawal remaining/i)).toBeInTheDocument(); - expect(formatMoney).toHaveBeenCalledWith(props.currency, remainder, true); + expect(formatMoney).toHaveBeenCalledWith(store.client.currency, remainder, true); }); it('should show limit_notice message when is_appstore is true and is_fully_authenticated is false in mobile mode', () => { + store = mockStore({ + client: { + is_fully_authenticated: false, + }, + }); (isMobile as jest.Mock).mockReturnValue(true); (isDesktop as jest.Mock).mockReturnValue(false); render( - + + + ); @@ -357,12 +486,19 @@ describe('', () => { }); it('should not show limit_notice message when is_appstore is false and is_fully_authenticated is false', () => { + store = mockStore({ + client: { + is_fully_authenticated: false, + }, + }); (isMobile as jest.Mock).mockReturnValue(false); (isDesktop as jest.Mock).mockReturnValue(true); render( - + + + ); @@ -372,9 +508,14 @@ describe('', () => { }); it('should show AccountLimitsArticle when should_show_article and isDesktop is true', () => { + store = mockStore(mock); (isMobile as jest.Mock).mockReturnValue(false); (isDesktop as jest.Mock).mockReturnValue(true); - render(); + render( + + + + ); expect(screen.getByRole('heading', { name: /account limits/i })).toBeInTheDocument(); expect(screen.getByText(/these are default limits that we apply to your accounts\./i)).toBeInTheDocument(); expect( @@ -390,7 +531,11 @@ describe('', () => { it('should show AccountLimitsFooter if footer_ref is passed', () => { const footer = React.createRef(); - render(); + render( + + + + ); expect(screen.getByText(/mockedaccountlimitsfooter/i)).toBeInTheDocument(); }); }); diff --git a/packages/account/src/Components/account-limits/account-limits.tsx b/packages/account/src/Components/account-limits/account-limits.tsx index 78ad00173a5a..0bb41406c1ab 100644 --- a/packages/account/src/Components/account-limits/account-limits.tsx +++ b/packages/account/src/Components/account-limits/account-limits.tsx @@ -12,38 +12,13 @@ import AccountLimitsFooter from './account-limits-footer'; import AccountLimitsOverlay from './account-limits-overlay'; import AccountLimitsTableCell from './account-limits-table-cell'; import AccountLimitsTableHeader from './account-limits-table-header'; -import AccountLimitsTurnoverLimitRow, { TAccountLimitsCollection } from './account-limits-turnover-limit-row'; +import AccountLimitsTurnoverLimitRow from './account-limits-turnover-limit-row'; +import { observer, useStore } from '@deriv/stores'; import { FormikValues } from 'formik'; type TAccountLimits = { - account_limits: { - api_initial_load_error?: string; - open_positions?: React.ReactNode; - account_balance: string | number; - daily_transfers?: object; - payout: string | number; - lifetime_limit?: number; - market_specific: { - commodities: TAccountLimitsCollection[]; - cryptocurrency: TAccountLimitsCollection[]; - forex: TAccountLimitsCollection[]; - indices: TAccountLimitsCollection[]; - synthetic_index: TAccountLimitsCollection[]; - }; - num_of_days?: number; - num_of_days_limit: string | number; - remainder: string | number; - withdrawal_for_x_days_monetary?: number; - withdrawal_since_inception_monetary: string | number; - }; - currency: string; footer_ref?: React.RefObject; is_app_settings?: boolean; - getLimits: () => Promise<{ data: object }>; - is_fully_authenticated: boolean; - is_from_derivgo?: boolean; - is_switching: boolean; - is_virtual: boolean; overlay_ref: HTMLDivElement; setIsOverlayShown?: (is_overlay_shown?: boolean) => void; setIsPopupOverlayShown?: (is_popup_overlay_shown: boolean) => void; @@ -51,327 +26,332 @@ type TAccountLimits = { should_show_article?: boolean; }; -const AccountLimits = ({ - account_limits, - currency, - footer_ref, - getLimits, - is_app_settings, - is_fully_authenticated, - is_switching, - is_virtual, - overlay_ref, - is_from_derivgo, - setIsOverlayShown: setIsPopupOverlayShown, - should_bypass_scrollbars, - should_show_article, -}: TAccountLimits) => { - const isMounted = useIsMounted(); - const [is_loading, setLoading] = React.useState(false); - const [is_overlay_shown, setIsOverlayShown] = React.useState(false); - const { is_appstore } = React.useContext(PlatformContext); +const AccountLimits = observer( + ({ + footer_ref, + is_app_settings, + overlay_ref, + setIsOverlayShown: setIsPopupOverlayShown, + should_bypass_scrollbars, + should_show_article = true, + }: TAccountLimits) => { + const { client, common } = useStore(); + const { account_limits, currency, getLimits, is_fully_authenticated, is_virtual, is_switching } = client; + const { is_from_derivgo } = common; + const isMounted = useIsMounted(); + const [is_loading, setLoading] = React.useState(false); + const [is_overlay_shown, setIsOverlayShown] = React.useState(false); + const { is_appstore } = React.useContext(PlatformContext); - React.useEffect(() => { - if (is_virtual) { - setLoading(false); - } else { - getLimits().then(() => { - if (isMounted()) setLoading(false); - }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + React.useEffect(() => { + if (is_virtual) { + setLoading(false); + } else { + getLimits().then(() => { + if (isMounted()) setLoading(false); + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); - React.useEffect(() => { - if (!is_virtual && account_limits && is_loading) { - setLoading(false); - } - }, [account_limits, is_virtual, is_loading]); + React.useEffect(() => { + if (!is_virtual && account_limits && is_loading) { + setLoading(false); + } + }, [account_limits, is_virtual, is_loading]); - React.useEffect(() => { - if (typeof setIsPopupOverlayShown === 'function') { - setIsPopupOverlayShown(is_overlay_shown); - } - }, [is_overlay_shown, setIsPopupOverlayShown]); + React.useEffect(() => { + if (typeof setIsPopupOverlayShown === 'function') { + setIsPopupOverlayShown(is_overlay_shown); + } + }, [is_overlay_shown, setIsPopupOverlayShown]); - const toggleOverlay = () => setIsOverlayShown(!is_overlay_shown); + const toggleOverlay = () => setIsOverlayShown(!is_overlay_shown); - if (is_switching) { - return ; - } + if (is_switching) { + return ; + } - if (is_virtual) { - return ( -
- -
- ); - } + if (is_virtual) { + return ( +
+ +
+ ); + } - const { - api_initial_load_error, - open_positions, - account_balance, - payout, - market_specific, - num_of_days_limit, - remainder, - withdrawal_since_inception_monetary, - }: TAccountLimits['account_limits'] = account_limits; + const { + api_initial_load_error, + open_positions, + account_balance, + payout, + market_specific, + num_of_days_limit, + remainder, + withdrawal_since_inception_monetary, + } = account_limits; - if (api_initial_load_error) { - return ; - } + if (api_initial_load_error) { + return ; + } - if (is_switching || is_loading) { - return ; - } + if (is_switching || is_loading) { + return ; + } - const { commodities, forex, indices, synthetic_index } = { ...market_specific }; - const forex_ordered = forex - ?.slice() - .sort((a: FormikValues, b: FormikValues) => (a.name > b.name ? 1 : b.name > a.name ? -1 : 0)); - const derived_ordered = synthetic_index - ?.slice() - .sort((a: FormikValues, b: FormikValues) => (a.level > b.level ? 1 : -1)); + const { commodities, forex, indices, synthetic_index } = { ...market_specific }; + const forex_ordered = forex + ?.slice() + .sort((a: FormikValues, b: FormikValues) => (a.name > b.name ? 1 : b.name > a.name ? -1 : 0)); + const derived_ordered = synthetic_index + ?.slice() + .sort((a: FormikValues, b: FormikValues) => (a.level > b.level ? 1 : -1)); - const context_value: TAccountLimitsContext = { - currency, - footer_ref, - overlay_ref, - toggleOverlay, - }; + const context_value: TAccountLimitsContext = { + currency, + footer_ref, + overlay_ref, + toggleOverlay, + }; - return ( - -
-
- {should_show_article && isMobile() && } -
- - - - - - - - - - - - - - - ( - - )} - > - - - {open_positions} - - - ( - - )} - > - - - - {/* null or 0 are expected form BE when max balance limit is not set */} - {account_balance ? ( - formatMoney(currency, account_balance, true) - ) : ( - - )} - - - - ( - - )} - > - - - - {formatMoney(currency, payout, true)} - - - - - - - - - -
- - - - ( - - )} - > - - - - - - - - - - - - - -
- {/* We only show "Withdrawal Limits" on account-wide settings pages. */} - {!is_app_settings && ( - - - - - - - - {is_fully_authenticated && ( - - - + return ( + +
+
+ {should_show_article && isMobile() && ( + + )} +
+ +
+ + + + + + + + + + + + + ( + )} - - - - {is_fully_authenticated ? ( + > + + + + {open_positions} + + + + ( + + )} + > + + + + {/* null or 0 are expected form BE when max balance limit is not set */} + {account_balance ? ( + formatMoney(currency, account_balance, true) + ) : ( + + )} + + + + ( + + )} + > + + + + {formatMoney(currency, payout, true)} + + + + + + + + + +
+ + + + ( + + )} + > + + + + + + + + + + + + + +
+ {/* We only show "Withdrawal Limits" on account-wide settings pages. */} + {!is_app_settings && ( + + + - - - - {localize( - 'Your account is fully authenticated and your withdrawal limits have been lifted.' - )} - - - - + + + + {is_fully_authenticated && ( + + + + )} - ) : ( - + + + {is_fully_authenticated ? ( - {is_appstore ? ( - - ) : ( - - )} - {is_appstore && !is_fully_authenticated && ( - - - {localize( - 'To increase limit please verify your identity' - )} - - + + + {localize( + 'Your account is fully authenticated and your withdrawal limits have been lifted.' + )} + + + + + + ) : ( + + + + {is_appstore ? ( + + ) : ( + + )} + {is_appstore && !is_fully_authenticated && ( + - {localize('Verify')} + {localize( + 'To increase limit please verify your identity' + )} - - - )} - - - {formatMoney(currency, num_of_days_limit, true)} - - - - - - - - {formatMoney( - currency, - withdrawal_since_inception_monetary, - true - )} - - - - - - - - {formatMoney(currency, remainder, true)} - - - - )} - -
- {(!is_appstore || isMobile()) && ( -
- - {is_fully_authenticated ? ( - - ) : ( - + + + {localize('Verify')} + + + + )} + + + {formatMoney(currency, num_of_days_limit, true)} + + + + + + + + {formatMoney( + currency, + withdrawal_since_inception_monetary, + true + )} + + + + + + + + {formatMoney(currency, remainder, true)} + + + )} - -
- )} -
- )} -
+ + + {(!is_appstore || isMobile()) && ( +
+ + {is_fully_authenticated ? ( + + ) : ( + + )} + +
+ )} + + )} + +
+ {should_show_article && isDesktop() && } + {footer_ref && } + {is_overlay_shown && overlay_ref && }
- {should_show_article && isDesktop() && } - {footer_ref && } - {is_overlay_shown && overlay_ref && } - -
-
- ); -}; + + + ); + } +); export default AccountLimits; diff --git a/packages/account/src/Components/api-token/__tests__/api-token.spec.tsx b/packages/account/src/Components/api-token/_tests_/api-token.spec.js similarity index 84% rename from packages/account/src/Components/api-token/__tests__/api-token.spec.tsx rename to packages/account/src/Components/api-token/_tests_/api-token.spec.js index 0c6ff192bcd4..1243918e1153 100644 --- a/packages/account/src/Components/api-token/__tests__/api-token.spec.tsx +++ b/packages/account/src/Components/api-token/_tests_/api-token.spec.js @@ -1,7 +1,8 @@ import React from 'react'; import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; -import { getPropertyValue, isDesktop, isMobile, useIsMounted } from '@deriv/shared'; -import ApiToken, { TApiToken } from '../api-token'; +import { getPropertyValue, isDesktop, isMobile, useIsMounted, WS } from '@deriv/shared'; +import ApiToken from '../api-token'; +import { StoreProvider, mockStore } from '@deriv/stores'; jest.mock('@deriv/shared', () => ({ ...jest.requireActual('@deriv/shared'), @@ -10,6 +11,29 @@ jest.mock('@deriv/shared', () => ({ isMobile: jest.fn(() => false), useIsMounted: jest.fn().mockImplementation(() => () => true), })); +jest.mock('@deriv/shared/src/services/ws-methods', () => ({ + __esModule: true, // this property makes it work, + default: 'mockedDefaultExport', + WS: { + apiToken: jest.fn(() => + Promise.resolve({ + api_token: { + tokens: [], + }, + }) + ), + authorized: { + apiToken: jest.fn(() => + Promise.resolve({ + api_token: { + tokens: [], + }, + }) + ), + }, + }, + useWS: () => undefined, +})); jest.mock('@deriv/components', () => ({ ...jest.requireActual('@deriv/components'), @@ -48,13 +72,18 @@ describe('', () => { const your_access_description = "To access your mobile apps and other third-party apps, you'll first need to generate an API token."; - const mock_props: TApiToken = { + let store = mockStore(); + store = mockStore({ + client: { + is_switching: false, + }, + }); + const mock_props = { footer_ref: undefined, is_app_settings: false, - is_switching: false, overlay_ref: undefined, setIsOverlayShown: jest.fn(), - ws: { + WS: { apiToken: jest.fn(() => Promise.resolve({ api_token: { @@ -75,9 +104,13 @@ describe('', () => { }; it('should render ApiToken component without app_settings and footer', async () => { - render(); + render( + + + + ); - expect(mock_props.ws.authorized.apiToken).toHaveBeenCalled(); + expect(WS.authorized.apiToken).toHaveBeenCalled(); expect(await screen.findByText(admin_scope_description)).toBeInTheDocument(); expect(await screen.findByText(admin_scope_note)).toBeInTheDocument(); @@ -95,9 +128,13 @@ describe('', () => { it('should not render ApiToken component if is not mounted', () => { useIsMounted.mockImplementationOnce(() => () => false); - render(); + render( + + + + ); - expect(mock_props.ws.authorized.apiToken).toHaveBeenCalled(); + expect(WS.authorized.apiToken).toHaveBeenCalled(); expect(screen.getByText('Loading')).toBeInTheDocument(); expect(screen.queryByText(admin_scope_description)).not.toBeInTheDocument(); @@ -117,7 +154,11 @@ describe('', () => { isMobile.mockReturnValueOnce(true); isDesktop.mockReturnValueOnce(false); - render(); + render( + + + + ); expect(await screen.findByText(admin_scope_description)).toBeInTheDocument(); expect(await screen.findByText(admin_scope_note)).toBeInTheDocument(); @@ -134,7 +175,11 @@ describe('', () => { it('should render ApiToken component with app_settings', async () => { mock_props.is_app_settings = true; - render(); + render( + + + + ); await waitFor(() => { expect(screen.queryByText(our_access_description)).not.toBeInTheDocument(); @@ -150,7 +195,11 @@ describe('', () => { mock_props.footer_ref = footer_portal_root_el; mock_props.overlay_ref = overlay_portal_root_el; - render(); + render( + + + + ); expect(await screen.findByText(learn_more_title)).toBeInTheDocument(); expect(screen.queryByText(our_access_description)).not.toBeInTheDocument(); @@ -166,14 +215,18 @@ describe('', () => { }); it('should choose checkbox, enter a valid value and create token', async () => { - render(); + render( + + + + ); expect(screen.queryByText('New token name')).not.toBeInTheDocument(); - const checkboxes: HTMLInputElement[] = await screen.findAllByRole('checkbox'); + const checkboxes = await screen.findAllByRole('checkbox'); const create_btn = await screen.findByRole('button'); - const read_checkbox = checkboxes.find(card => card.name === 'read') as HTMLInputElement; // Typecasting it since find can return undefined as well - const token_name_input: HTMLInputElement = await screen.findByLabelText('Token name'); + const read_checkbox = checkboxes.find(card => card.name === 'read'); // Typecasting it since find can return undefined as well + const token_name_input = await screen.findByLabelText('Token name'); expect(checkboxes.length).toBe(5); expect(create_btn).toBeDisabled(); @@ -202,7 +255,7 @@ describe('', () => { const updated_token_name_input = await screen.findByLabelText('Token name'); expect(updated_token_name_input.value).toBe(''); - const createToken = mock_props.ws.apiToken; + const createToken = WS.apiToken; expect(createToken).toHaveBeenCalledTimes(1); }); @@ -226,7 +279,11 @@ describe('', () => { }, ]); - render(); + render( + + + + ); expect(await screen.findByText('First test token')).toBeInTheDocument(); expect(await screen.findByText('Last used')).toBeInTheDocument(); @@ -255,7 +312,7 @@ describe('', () => { expect(yes_btn_1).toBeInTheDocument(); fireEvent.click(yes_btn_1); - const deleteToken = mock_props.ws.authorized.apiToken; + const deleteToken = WS.authorized.apiToken; expect(deleteToken).toHaveBeenCalled(); await waitFor(() => { expect(yes_btn_1).not.toBeInTheDocument(); @@ -287,7 +344,11 @@ describe('', () => { }, ]); - render(); + render( + + + + ); expect(await screen.findByText('First test token')).toBeInTheDocument(); expect(screen.queryByText('FirstTokenID')).not.toBeInTheDocument(); @@ -349,7 +410,11 @@ describe('', () => { }, ]); - render(); + render( + + + + ); expect((await screen.findAllByText('Name')).length).toBe(3); expect((await screen.findAllByText('Last Used')).length).toBe(3); @@ -364,7 +429,7 @@ describe('', () => { }); it('should show token error if exists', async () => { - mock_props.ws.authorized.apiToken = jest.fn(() => + WS.authorized.apiToken = jest.fn(() => Promise.resolve({ api_token: { tokens: [] }, error: { message: 'New test error' }, @@ -373,7 +438,11 @@ describe('', () => { getPropertyValue.mockReturnValue('New test error'); - render(); + render( + + + + ); expect(await screen.findByText('New test error')).toBeInTheDocument(); }); diff --git a/packages/account/src/Components/api-token/api-token.tsx b/packages/account/src/Components/api-token/api-token.tsx index 0c6dbb2e9170..558e16a5725c 100644 --- a/packages/account/src/Components/api-token/api-token.tsx +++ b/packages/account/src/Components/api-token/api-token.tsx @@ -3,7 +3,7 @@ import classNames from 'classnames'; import { Formik, Form, Field, FormikValues, FormikErrors, FieldProps } from 'formik'; import { Timeline, Input, Button, ThemedScrollbars, Loading } from '@deriv/components'; import InlineNoteWithIcon from '../inline-note-with-icon'; -import { isDesktop, isMobile, getPropertyValue, useIsMounted } from '@deriv/shared'; +import { isDesktop, isMobile, getPropertyValue, useIsMounted, WS } from '@deriv/shared'; import { localize } from '@deriv/translations'; import LoadErrorMessage from 'Components/load-error-message'; import ApiTokenArticle from './api-token-article'; @@ -13,6 +13,7 @@ import ApiTokenOverlay from './api-token-overlay'; import ApiTokenTable from './api-token-table'; import ApiTokenContext from './api-token-context'; import { TToken } from 'Types'; +import { observer, useStore } from '@deriv/stores'; const MIN_TOKEN = 2; const MAX_TOKEN = 32; @@ -32,7 +33,6 @@ type AptTokenState = { export type TApiToken = { footer_ref: Element | DocumentFragment | undefined; is_app_settings: boolean; - is_switching: boolean; overlay_ref: | undefined | ((...args: unknown[]) => unknown) @@ -41,10 +41,11 @@ export type TApiToken = { }>; setIsOverlayShown: (is_overlay_shown: boolean | undefined) => void; // eslint-disable-next-line @typescript-eslint/no-explicit-any - ws: any; }; -const ApiToken = ({ footer_ref, is_app_settings, is_switching, overlay_ref, setIsOverlayShown, ws }: TApiToken) => { +const ApiToken = ({ footer_ref, is_app_settings, overlay_ref, setIsOverlayShown }: TApiToken) => { + const { client } = useStore(); + const { is_switching } = client; const isMounted = useIsMounted(); const prev_is_switching = React.useRef(is_switching); const [state, setState] = React.useReducer( @@ -122,9 +123,8 @@ const ApiToken = ({ footer_ref, is_app_settings, is_switching, overlay_ref, setI const selectedTokenScope = (values: FormikValues) => Object.keys(values).filter(item => item !== 'token_name' && values[item]); - const handleSubmit = async (values: FormikValues, { setSubmitting, setFieldError, resetForm }: any) => { - const token_response = await ws.apiToken({ + const token_response = await WS.apiToken({ api_token: 1, new_token: values.token_name, new_token_scopes: selectedTokenScope(values), @@ -163,14 +163,14 @@ const ApiToken = ({ footer_ref, is_app_settings, is_switching, overlay_ref, setI const getApiTokens = async () => { setState({ is_loading: true }); - const token_response = await ws.authorized.apiToken({ api_token: 1 }); + const token_response = await WS.authorized.apiToken({ api_token: 1 }); populateTokenResponse(token_response); }; const deleteToken = async (token: string) => { setState({ is_delete_loading: true }); - const token_response = await ws.authorized.apiToken({ api_token: 1, delete_token: token }); + const token_response = await WS.authorized.apiToken({ api_token: 1, delete_token: token }); populateTokenResponse(token_response); @@ -356,4 +356,4 @@ const ApiToken = ({ footer_ref, is_app_settings, is_switching, overlay_ref, setI ); }; -export default ApiToken; +export default observer(ApiToken); diff --git a/packages/account/src/Components/forms/personal-details-form.jsx b/packages/account/src/Components/forms/personal-details-form.jsx index a3f5aa7f0c83..bafdb0141ff8 100644 --- a/packages/account/src/Components/forms/personal-details-form.jsx +++ b/packages/account/src/Components/forms/personal-details-form.jsx @@ -221,9 +221,7 @@ const PersonalDetailsForm = ({ { setIsTaxResidencePopoverOpen(false); setIsTinPopoverOpen(true); - e.stopPropagation(); + if (e.target.tagName !== 'A') e.stopPropagation(); }} > ({ - __esModule: true, - default: 'mockedDefaultExport', - connect: () => Component => Component, -})); +import { mockStore, StoreProvider } from '@deriv/stores'; jest.mock('@deriv/components', () => { const original_module = jest.requireActual('@deriv/components'); @@ -20,27 +15,46 @@ describe('', () => { icon: 'string', message: 'title', }; + const store = mockStore(); it('should render the IconWithMessage component', () => { - render(); + render( + + + + ); expect(screen.getByText('mockedIcon')).toBeInTheDocument(); expect(screen.getByText('title')).toBeInTheDocument(); }); it('should not render the button component if has_button is false', () => { - render(); + render( + + + + ); const btn = screen.queryByTestId('icon-with-message-button'); expect(btn).not.toBeInTheDocument(); }); it('should show "Switch to real account" button label if user have real account', () => { - render(); + store.client.has_any_real_account = true; + render( + + + + ); const btn = screen.getByTestId('icon-with-message-button'); expect(btn).toBeInTheDocument(); expect(screen.getByText('Switch to real account')).toBeInTheDocument(); }); it('should show "Add a real account" button label if user doesnt have real account', () => { - render(); + store.client.has_any_real_account = false; + render( + + + + ); expect(screen.getByText('Add a real account')).toBeInTheDocument(); }); @@ -49,16 +63,16 @@ describe('', () => { icon: 'string', message: 'title', has_button: true, - has_real_account: true, }; + store.client.has_any_real_account = true; const toggleShouldShowRealAccountsList = jest.fn(); const toggleAccountsDialog = jest.fn(); + store.ui.toggleShouldShowRealAccountsList = toggleShouldShowRealAccountsList; + store.ui.toggleAccountsDialog = toggleAccountsDialog; render( - + + + ); const btn = screen.getByTestId('icon-with-message-button'); expect(btn).toBeInTheDocument(); diff --git a/packages/account/src/Components/icon-with-message/icon-with-message.tsx b/packages/account/src/Components/icon-with-message/icon-with-message.tsx index bdd939eaf56f..ce3ca078034d 100644 --- a/packages/account/src/Components/icon-with-message/icon-with-message.tsx +++ b/packages/account/src/Components/icon-with-message/icon-with-message.tsx @@ -3,26 +3,18 @@ import classNames from 'classnames'; import { Icon, Text, Button } from '@deriv/components'; import { isMobile, PlatformContext } from '@deriv/shared'; import { localize } from '@deriv/translations'; -import { connect } from 'Stores/connect'; -import RootStore from 'Stores/index'; +import { observer, useStore } from '@deriv/stores'; type TIconWithMessage = { icon: string; has_button?: boolean; - has_real_account?: boolean; message: string; - toggleAccountsDialog: (status?: boolean) => void; - toggleShouldShowRealAccountsList: (value: boolean) => void; }; -const IconWithMessage = ({ - has_button, - has_real_account, - icon, - message, - toggleAccountsDialog, - toggleShouldShowRealAccountsList, -}: TIconWithMessage) => { +const IconWithMessage = observer(({ has_button, icon, message }: TIconWithMessage) => { + const { client, ui } = useStore(); + const { has_any_real_account: has_real_account } = client; + const { toggleAccountsDialog, toggleShouldShowRealAccountsList } = ui; const { is_appstore } = React.useContext(PlatformContext); return ( @@ -53,10 +45,6 @@ const IconWithMessage = ({ )} ); -}; +}); -export default connect(({ client, ui }: RootStore) => ({ - has_real_account: client.has_any_real_account, - toggleAccountsDialog: ui.toggleAccountsDialog, - toggleShouldShowRealAccountsList: ui.toggleShouldShowRealAccountsList, -}))(IconWithMessage); +export default IconWithMessage; diff --git a/packages/account/src/Components/self-exclusion/__tests__/self-exclusion.spec.tsx b/packages/account/src/Components/self-exclusion/__tests__/self-exclusion.spec.tsx index 2453cecb7313..909dbbdfdb9a 100644 --- a/packages/account/src/Components/self-exclusion/__tests__/self-exclusion.spec.tsx +++ b/packages/account/src/Components/self-exclusion/__tests__/self-exclusion.spec.tsx @@ -3,46 +3,71 @@ import { act } from 'react-dom/test-utils'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import SelfExclusion from '../self-exclusion'; import { FormikValues } from 'formik'; +import { StoreProvider, mockStore } from '@deriv/stores'; +import { WS } from '@deriv/shared'; const portal_root = document.createElement('div'); document.body.appendChild(portal_root); -jest.mock('Stores/connect.js', () => ({ - __esModule: true, - default: 'mockedDefaultExport', - connect: () => (Component: React.Component) => Component, -})); - jest.mock('@deriv/shared', () => ({ ...jest.requireActual('@deriv/shared'), useIsMounted: jest.fn().mockImplementation(() => () => true), })); +jest.mock('@deriv/shared/src/services/ws-methods', () => ({ + __esModule: true, // this property makes it work, + default: 'mockedDefaultExport', + WS: { + authorized: { + getLimits: jest.fn(() => + Promise.resolve({ + get_limits: {}, + }) + ), + getSelfExclusion: jest.fn(() => + Promise.resolve({ + error: { message: '' }, + }) + ), + setSelfExclusion: jest.fn(() => + Promise.resolve({ + error: {}, + }) + ), + }, + }, + useWS: () => undefined, +})); jest.mock('../self-exclusion-modal', () => { const MockSelfExclusionModal = () =>
SelfExclusionModal
; return MockSelfExclusionModal; }); - +const mock = { + client: { + currency: '', + standpoint: { + svg: false, + }, + is_uk: false, + is_virtual: false, + is_switching: false, + landing_company_shortcode: '', + logout: jest.fn(), + }, + ui: { + is_tablet: false, + }, +}; +let store = mockStore(mock); describe('', () => { let mock_props = { - currency: '', footer_ref: undefined, is_app_settings: false, is_appstore: false, - is_cr: false, - is_eu: false, - is_mf: false, - is_mlt: false, - is_mx: false, - is_switching: false, - is_tablet: false, - is_uk: false, - is_virtual: false, is_wrapper_bypassed: false, - logout: jest.fn(), overlay_ref: document.createElement('div'), setIsOverlayShown: jest.fn(), - ws: { + WS: { authorized: { getLimits: () => Promise.resolve({ @@ -54,61 +79,52 @@ describe('', () => { }), setSelfExclusion: () => Promise.resolve({ - error: {}, + error: false, }), }, }, }; beforeEach(() => { + mock.client.currency = 'Test currency'; + store = mockStore(mock); mock_props = { - currency: 'Test currency', footer_ref: undefined, is_app_settings: false, is_appstore: false, - is_cr: false, - is_eu: false, - is_mf: false, - is_mlt: false, - is_mx: false, - is_switching: false, - is_tablet: false, - is_uk: false, - is_virtual: false, is_wrapper_bypassed: false, - logout: jest.fn(), overlay_ref: document.createElement('div'), setIsOverlayShown: jest.fn(), - ws: { - authorized: { - getLimits: () => - Promise.resolve({ - get_limits: {}, - }), - getSelfExclusion: () => - Promise.resolve({ - error: { message: '' }, - }), - setSelfExclusion: () => - Promise.resolve({ - error: {}, - }), - }, - }, }; }); + afterEach(() => { + jest.clearAllMocks(); + }); it('should render SelfExclusion component for virtual account', () => { - mock_props.is_virtual = true; + store = mockStore({ + client: { + is_virtual: true, + }, + }); - render(); + render( + + + + ); expect(screen.getByText('This feature is not available for demo accounts.')).toBeInTheDocument(); }); it('should render SelfExclusion component with SelfExclusionModal', async () => { + store = mockStore(mock); await act(async () => { - render(); + render( + + + + ); }); expect(screen.getByText('SelfExclusionModal')).toBeInTheDocument(); @@ -120,22 +136,40 @@ describe('', () => { }); it('should render SelfExclusion component with error', async () => { - mock_props.ws.authorized.getSelfExclusion = () => + WS.authorized.getSelfExclusion = jest.fn(() => Promise.resolve({ error: { message: 'Test getSelfExclusion response error' }, - }); + }) + ); await act(async () => { - render(); + render( + + + + ); }); expect(screen.queryByText('Test getSelfExclusion response error')).toBeInTheDocument(); }); it('Should trigger session_duration_limit input and show error if the value is greater than 60480 or does not show if less than 60480', async () => { - mock_props.is_eu = true; + store = mockStore({ + client: { + is_eu: true, + }, + }); + WS.authorized.getSelfExclusion = jest.fn(() => + Promise.resolve({ + error: { message: '' }, + }) + ); - render(); + render( + + + + ); const inputs = await screen.findAllByRole('textbox'); const session_duration_limit_input = inputs.find( @@ -163,8 +197,17 @@ describe('', () => { it('Should trigger exclude_until input and show error depends on input value', async () => { (Date.now as jest.Mock) = jest.fn(() => new Date('2022-02-03')); - - render(); + WS.authorized.getSelfExclusion = jest.fn(() => + Promise.resolve({ + error: { message: '' }, + }) + ); + store = mockStore(mock); + render( + + + + ); const inputs = await screen.findAllByRole('textbox'); const exclude_until_input = inputs.find((input: FormikValues) => input.name === 'exclude_until'); @@ -187,13 +230,24 @@ describe('', () => { }); it('should trigger inputs with data, add new data, and show error wih invalid input data', async () => { - mock_props.ws.authorized.setSelfExclusion = () => + store = mockStore(mock); + WS.authorized.getSelfExclusion = jest.fn(() => + Promise.resolve({ + error: { message: '' }, + }) + ); + WS.authorized.setSelfExclusion = jest.fn(() => Promise.resolve({ error: { message: 'Test setSelfExclusion response error' }, - }); + }) + ); await act(async () => { - render(); + render( + + + + ); }); expect(screen.getByText('Your stake and loss limits')).toBeInTheDocument(); @@ -262,15 +316,19 @@ describe('', () => { }); it('should trigger inputs with correct data set timeout limit and logout', async () => { - mock_props.ws.authorized.setSelfExclusion = () => + WS.authorized.setSelfExclusion = jest.fn(() => Promise.resolve({ error: false, - }); - - const logout = mock_props.logout; + }) + ); + const logout = store.client.logout; await act(async () => { - render(); + render( + + + + ); }); expect(screen.getByText('Your stake and loss limits')).toBeInTheDocument(); diff --git a/packages/account/src/Components/self-exclusion/self-exclusion.tsx b/packages/account/src/Components/self-exclusion/self-exclusion.tsx index 0fbfc605ef18..2df2503ac0dd 100644 --- a/packages/account/src/Components/self-exclusion/self-exclusion.tsx +++ b/packages/account/src/Components/self-exclusion/self-exclusion.tsx @@ -8,6 +8,7 @@ import { getCurrencyDisplayCode, validNumber, useIsMounted, + WS, } from '@deriv/shared'; import { localize } from '@deriv/translations'; import DemoMessage from 'Components/demo-message'; @@ -18,26 +19,15 @@ import SelfExclusionModal from './self-exclusion-modal'; import SelfExclusionWrapper from './self-exclusion-wrapper'; import SelfExclusionForm from './self-exclusion-form'; import { FormikHelpers, FormikValues } from 'formik'; +import { observer, useStore } from '@deriv/stores'; type TSelfExclusion = { - currency: string; footer_ref?: React.RefObject; - is_app_settings: boolean; - is_cr: boolean; is_appstore: boolean; - is_eu: boolean; - is_mf: boolean; - is_mlt: boolean; - is_mx: boolean; - is_switching: boolean; - is_tablet: boolean; - is_uk: boolean; - is_virtual: boolean; + is_app_settings: boolean; is_wrapper_bypassed: boolean; - logout: () => void; overlay_ref: HTMLDivElement; setIsOverlayShown?: React.Dispatch>; - ws: FormikValues; }; type TExclusionData = { @@ -83,25 +73,20 @@ type TResponse = { }; const SelfExclusion = ({ - currency, footer_ref, is_app_settings, - is_cr, is_appstore, - is_eu, - is_mf, - is_mlt, - is_mx, - is_switching, - is_tablet, - is_uk, - is_virtual, - is_wrapper_bypassed, - logout, overlay_ref, setIsOverlayShown, - ws, }: TSelfExclusion) => { + const { client, ui } = useStore(); + const { currency, is_virtual, is_switching, standpoint, is_eu, is_uk, logout, landing_company_shortcode } = client; + const { is_tablet } = ui; + const is_wrapper_bypassed = false; + const is_mlt = landing_company_shortcode === 'malta'; + const is_mf = landing_company_shortcode === 'maltainvest'; + const is_mx = landing_company_shortcode === 'iom'; + const is_cr = standpoint.svg; const exclusion_fields_settings = Object.freeze({ max_number: 9999999999999, max_open_positions: 999999999, @@ -196,7 +181,6 @@ const SelfExclusion = ({ }, [state.show_article, setIsOverlayShown]); const resetState = () => setState(initial_state); - const validateFields = (values: FormikValues) => { const errors: Record = {}; @@ -309,7 +293,6 @@ const SelfExclusion = ({ }); return errors; }; - const handleSubmit = async (values: FormikValues, { setSubmitting }: FormikHelpers) => { const need_logout_exclusions = ['exclude_until', 'timeout_until']; const string_exclusions = ['exclude_until']; @@ -325,7 +308,7 @@ const SelfExclusion = ({ request[attr] = string_exclusions.includes(attr) ? values[attr] : +values[attr]; }); - ws.authorized.setSelfExclusion(request).then((response: TResponse) => resolve(response)); + WS.authorized.setSelfExclusion(request).then((response: TResponse) => resolve(response)); }); if (has_need_logout) { @@ -404,7 +387,7 @@ const SelfExclusion = ({ const getSelfExclusion = () => { setState({ is_loading: true }); - ws.authorized.getSelfExclusion({ get_self_exclusion: 1 }).then((self_exclusion_response: FormikValues) => { + WS.authorized.getSelfExclusion({ get_self_exclusion: 1 }).then((self_exclusion_response: FormikValues) => { populateExclusionResponse(self_exclusion_response); }); }; @@ -412,7 +395,7 @@ const SelfExclusion = ({ const getLimits = () => { setState({ is_loading: true }); - ws.authorized.getLimits({ get_limits: 1 }).then((limits: FormikValues) => { + WS.authorized.getLimits({ get_limits: 1 }).then((limits: FormikValues) => { exclusion_limits.current = limits; }); }; @@ -484,7 +467,7 @@ const SelfExclusion = ({ return ( - {/* Only show the modal in non-"" views, others will + {/* Only show the modal in non-"" views, others will use the overlay provided by */} {!is_app_settings && } @@ -494,4 +477,4 @@ const SelfExclusion = ({ ); }; -export default SelfExclusion; +export default observer(SelfExclusion); diff --git a/packages/account/src/Containers/account.jsx b/packages/account/src/Containers/account.jsx index f1fb09a3fcaf..5c035d7a13ae 100644 --- a/packages/account/src/Containers/account.jsx +++ b/packages/account/src/Containers/account.jsx @@ -1,14 +1,13 @@ import 'Styles/account.scss'; - -import { FadeWrapper, Icon, Loading, PageOverlay, Text, VerticalTab } from '@deriv/components'; import { PlatformContext, getSelectedRoute, isMobile, matchRoute, routes as shared_routes } from '@deriv/shared'; import PropTypes from 'prop-types'; import React from 'react'; -import { connect } from 'Stores/connect'; +import { withRouter } from 'react-router-dom'; +import { VerticalTab, FadeWrapper, PageOverlay, Loading, Text, Icon } from '@deriv/components'; +import { observer, useStore } from '@deriv/stores'; import { flatten } from '../Helpers/flatten'; import { localize } from '@deriv/translations'; import { useHistory } from 'react-router'; -import { withRouter } from 'react-router-dom'; const AccountLogout = ({ logout, history }) => { return ( @@ -104,23 +103,20 @@ const PageOverlayWrapper = ({ ); }; -const Account = ({ - active_account_landing_company, - history, - is_from_derivgo, - is_logged_in, - is_logging_in, - is_pending_proof_of_ownership, - is_virtual, - is_visible, - location, - logout, - platform, - routeBackInApp, - routes, - should_allow_authentication, - toggleAccount, -}) => { +const Account = observer(({ history, location, routes }) => { + const { client, common, ui } = useStore(); + const { + is_virtual, + is_logged_in, + is_logging_in, + is_risky_client, + is_pending_proof_of_ownership, + landing_company_shortcode, + should_allow_authentication, + logout, + } = client; + const { is_from_derivgo, routeBackInApp, platform } = common; + const { toggleAccountSettings, is_account_settings_visible } = ui; const { is_appstore } = React.useContext(PlatformContext); const subroutes = flatten(routes.map(i => i.subroutes)); let list_groups = [...routes]; @@ -133,17 +129,17 @@ const Account = ({ const onClickClose = React.useCallback(() => routeBackInApp(history), [routeBackInApp, history]); React.useEffect(() => { - toggleAccount(true); - }, [toggleAccount]); + toggleAccountSettings(true); + }, [toggleAccountSettings]); routes.forEach(menu_item => { menu_item.subroutes.forEach(route => { if (route.path === shared_routes.financial_assessment) { - route.is_disabled = is_virtual; + route.is_disabled = is_virtual || (landing_company_shortcode === 'maltainvest' && !is_risky_client); } if (route.path === shared_routes.trading_assessment) { - route.is_disabled = is_virtual || active_account_landing_company !== 'maltainvest'; + route.is_disabled = is_virtual || landing_company_shortcode !== 'maltainvest'; } if (route.path === shared_routes.proof_of_identity || route.path === shared_routes.proof_of_address) { @@ -169,7 +165,11 @@ const Account = ({ const selected_route = getSelectedRoute({ routes: subroutes, pathname: location.pathname }); return ( - +
); -}; +}); Account.propTypes = { active_account_landing_company: PropTypes.string, history: PropTypes.object, - is_from_derivgo: PropTypes.bool, - is_logged_in: PropTypes.bool, - is_logging_in: PropTypes.bool, - is_pending_proof_of_ownership: PropTypes.bool, - is_virtual: PropTypes.bool, - is_visible: PropTypes.bool, location: PropTypes.object, - logout: PropTypes.func, - platform: PropTypes.string, - routeBackInApp: PropTypes.func, routes: PropTypes.arrayOf(PropTypes.object), - should_allow_authentication: PropTypes.bool, - toggleAccount: PropTypes.func, }; -export default connect(({ client, common, ui }) => ({ - active_account_landing_company: client.landing_company_shortcode, - is_from_derivgo: common.is_from_derivgo, - is_logged_in: client.is_logged_in, - is_logging_in: client.is_logging_in, - is_pending_proof_of_ownership: client.is_pending_proof_of_ownership, - is_virtual: client.is_virtual, - is_visible: ui.is_account_settings_visible, - logout: client.logout, - platform: common.platform, - routeBackInApp: common.routeBackInApp, - should_allow_authentication: client.should_allow_authentication, - toggleAccount: ui.toggleAccountSettings, -}))(withRouter(Account)); +export default withRouter(Account); diff --git a/packages/account/src/Containers/reset-trading-password.jsx b/packages/account/src/Containers/reset-trading-password.jsx index 94709bbcd8f2..76669b31cb1f 100644 --- a/packages/account/src/Containers/reset-trading-password.jsx +++ b/packages/account/src/Containers/reset-trading-password.jsx @@ -1,54 +1,32 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { useLocation } from 'react-router-dom'; import { CFD_PLATFORMS } from '@deriv/shared'; -import { connect } from 'Stores/connect'; +import { observer, useStore } from '@deriv/stores'; import ResetTradingPasswordModal from '../Components/reset-trading-password-modal'; -const ResetTradingPassword = ({ - enableApp, - disableApp, - toggleResetTradingPasswordModal, - mt5_verification_code, - dxtrade_verification_code, - is_visible, - is_loading, -}) => { +const ResetTradingPassword = observer(() => { + const { ui, client } = useStore(); + const { enableApp, disableApp, is_visible, is_loading, setResetTradingPasswordModalOpen } = ui; const location = useLocation(); const query_params = new URLSearchParams(location.search); const cfd_platform = /^trading_platform_(.*)_password_reset$/.exec(query_params.get('action') || '')?.[1]; const [platform] = React.useState(cfd_platform); - const verification_code = platform === CFD_PLATFORMS.MT5 ? mt5_verification_code : dxtrade_verification_code; + const verification_code = + platform === CFD_PLATFORMS.MT5 + ? client.verification_code.trading_platform_mt5_password_reset + : client.verification_code.trading_platform_dxtrade_password_reset; return ( ); -}; +}); -ResetTradingPassword.propTypes = { - disableApp: PropTypes.func, - enableApp: PropTypes.func, - is_loading: PropTypes.bool, - is_visible: PropTypes.bool, - toggleResetTradingPasswordModal: PropTypes.func, - mt5_verification_code: PropTypes.string, - dxtrade_verification_code: PropTypes.string, -}; - -export default connect(({ ui, client }) => ({ - disableApp: ui.disableApp, - enableApp: ui.enableApp, - is_loading: ui.is_loading, - is_visible: ui.is_reset_trading_password_modal_visible, - toggleResetTradingPasswordModal: ui.setResetTradingPasswordModalOpen, - mt5_verification_code: client.verification_code.trading_platform_mt5_password_reset, - dxtrade_verification_code: client.verification_code.trading_platform_dxtrade_password_reset, -}))(ResetTradingPassword); +export default ResetTradingPassword; diff --git a/packages/account/src/Containers/routes.jsx b/packages/account/src/Containers/routes.jsx index ac85dcb0e31d..49b81f854432 100644 --- a/packages/account/src/Containers/routes.jsx +++ b/packages/account/src/Containers/routes.jsx @@ -1,41 +1,26 @@ -import { PropTypes as MobxPropTypes } from 'mobx-react'; import PropTypes from 'prop-types'; import React from 'react'; import { withRouter } from 'react-router'; import { BinaryRoutes } from 'Components/Routes'; -import { connect } from 'Stores/connect'; +import { observer, useStore } from '@deriv/stores'; import ErrorComponent from 'Components/error-component'; -const Routes = props => { +const Routes = observer(props => { + const { client, common } = useStore(); + const { is_logged_in, is_logging_in } = client; + const { error } = common; if (props.has_error) { - return ; + return ; } - return ( - - ); -}; + return ; +}); Routes.propTypes = { - error: MobxPropTypes.objectOrObservableObject, - has_error: PropTypes.bool, - is_logged_in: PropTypes.bool, - is_logging_in: PropTypes.bool, is_virtual: PropTypes.bool, passthrough: PropTypes.object, }; // need to wrap withRouter around connect // to prevent updates on from being blocked -export default withRouter( - connect(({ client, common }) => ({ - is_logged_in: client.is_logged_in, - is_logging_in: client.is_logging_in, - error: common.error, - has_error: common.has_error, - }))(Routes) -); +export default withRouter(Routes); diff --git a/packages/account/src/Sections/Assessment/FinancialAssessment/financial-assessment.jsx b/packages/account/src/Sections/Assessment/FinancialAssessment/financial-assessment.jsx index 88684b5f67ba..1ca1e3ca9c35 100644 --- a/packages/account/src/Sections/Assessment/FinancialAssessment/financial-assessment.jsx +++ b/packages/account/src/Sections/Assessment/FinancialAssessment/financial-assessment.jsx @@ -1,6 +1,5 @@ import classNames from 'classnames'; import React from 'react'; -import { PropTypes } from 'prop-types'; import { Formik } from 'formik'; import { useHistory, withRouter } from 'react-router'; import { @@ -17,7 +16,7 @@ import { } from '@deriv/components'; import { routes, isMobile, isDesktop, platforms, PlatformContext, WS } from '@deriv/shared'; import { localize, Localize } from '@deriv/translations'; -import { connect } from 'Stores/connect'; +import { observer, useStore } from '@deriv/stores'; import LeaveConfirm from 'Components/leave-confirm'; import IconMessageContent from 'Components/icon-message-content'; import DemoMessage from 'Components/demo-message'; @@ -175,20 +174,22 @@ const SubmittedPage = ({ platform, routeBackInApp }) => { ); }; -const FinancialAssessment = ({ - is_authentication_needed, - is_financial_account, - is_mf, - is_svg, - is_trading_experience_incomplete, - is_financial_information_incomplete, - is_virtual, - platform, - refreshNotifications, - routeBackInApp, - setFinancialAndTradingAssessment, - updateAccountStatus, -}) => { +const FinancialAssessment = observer(() => { + const { client, common, notifications } = useStore(); + const { + landing_company_shortcode, + is_virtual, + is_financial_account, + is_trading_experience_incomplete, + is_svg, + setFinancialAndTradingAssessment, + updateAccountStatus, + is_authentication_needed, + is_financial_information_incomplete, + } = client; + const { platform, routeBackInApp } = common; + const { refreshNotifications } = notifications; + const is_mf = landing_company_shortcode === 'maltainvest'; const history = useHistory(); const { is_appstore } = React.useContext(PlatformContext); const [is_loading, setIsLoading] = React.useState(true); @@ -1005,34 +1006,6 @@ const FinancialAssessment = ({ ); -}; - -FinancialAssessment.propTypes = { - is_authentication_needed: PropTypes.bool, - is_financial_account: PropTypes.bool, - is_mf: PropTypes.bool, - is_svg: PropTypes.bool, - is_trading_experience_incomplete: PropTypes.bool, - is_financial_information_incomplete: PropTypes.bool, - is_virtual: PropTypes.bool, - platform: PropTypes.string, - refreshNotifications: PropTypes.func, - routeBackInApp: PropTypes.func, - setFinancialAndTradingAssessment: PropTypes.func, - updateAccountStatus: PropTypes.func, -}; +}); -export default connect(({ client, common, notifications }) => ({ - is_authentication_needed: client.is_authentication_needed, - is_financial_account: client.is_financial_account, - is_mf: client.landing_company_shortcode === 'maltainvest', - is_svg: client.is_svg, - is_financial_information_incomplete: client.is_financial_information_incomplete, - is_trading_experience_incomplete: client.is_trading_experience_incomplete, - is_virtual: client.is_virtual, - platform: common.platform, - refreshNotifications: notifications.refreshNotifications, - routeBackInApp: common.routeBackInApp, - setFinancialAndTradingAssessment: client.setFinancialAndTradingAssessment, - updateAccountStatus: client.updateAccountStatus, -}))(withRouter(FinancialAssessment)); +export default withRouter(FinancialAssessment); diff --git a/packages/account/src/Sections/Assessment/TradingAssessment/trading-assessment.jsx b/packages/account/src/Sections/Assessment/TradingAssessment/trading-assessment.jsx index f3f91274c509..242ac860951b 100644 --- a/packages/account/src/Sections/Assessment/TradingAssessment/trading-assessment.jsx +++ b/packages/account/src/Sections/Assessment/TradingAssessment/trading-assessment.jsx @@ -16,7 +16,7 @@ import { } from '@deriv/components'; import FormFooter from 'Components/form-footer'; import { isMobile, routes, WS } from '@deriv/shared'; -import { connect } from 'Stores/connect'; +import { observer, useStore } from '@deriv/stores'; import { useHistory, withRouter } from 'react-router'; import { Formik, Form } from 'formik'; @@ -35,7 +35,9 @@ const populateData = form_data => { }; }; -const TradingAssessment = ({ is_virtual, setFinancialAndTradingAssessment }) => { +const TradingAssessment = observer(() => { + const { client } = useStore(); + const { is_virtual, setFinancialAndTradingAssessment } = client; const history = useHistory(); const [is_loading, setIsLoading] = React.useState(true); const [is_btn_loading, setIsBtnLoading] = React.useState(false); @@ -282,9 +284,6 @@ const TradingAssessment = ({ is_virtual, setFinancialAndTradingAssessment }) => }} ); -}; +}); -export default connect(({ client }) => ({ - is_virtual: client.is_virtual, - setFinancialAndTradingAssessment: client.setFinancialAndTradingAssessment, -}))(withRouter(TradingAssessment)); +export default withRouter(TradingAssessment); diff --git a/packages/account/src/Sections/Profile/LanguageSettings/language-settings.tsx b/packages/account/src/Sections/Profile/LanguageSettings/language-settings.tsx index 8a97f4f2f697..cb0b7eea8b27 100644 --- a/packages/account/src/Sections/Profile/LanguageSettings/language-settings.tsx +++ b/packages/account/src/Sections/Profile/LanguageSettings/language-settings.tsx @@ -2,20 +2,15 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { Button, DesktopWrapper } from '@deriv/components'; import { localize, getAllowedLanguages } from '@deriv/translations'; -import { connect } from 'Stores/connect'; import FormSubHeader from 'Components/form-sub-header'; -import TCoreStore from '../../../Stores'; import { Formik, FormikHandlers, FormikHelpers, FormikValues } from 'formik'; import FormFooter from 'Components/form-footer'; import LanguageRadioButton from 'Components/language-settings'; +import { observer, useStore } from '@deriv/stores'; -type TLanguageSettings = { - current_language: string; - changeSelectedLanguage: (lang: string) => void; - isCurrentLanguage: (lang: string) => boolean; -}; - -const LanguageSettings = ({ changeSelectedLanguage, current_language, isCurrentLanguage }: TLanguageSettings) => { +const LanguageSettings = observer(() => { + const { common } = useStore(); + const { changeSelectedLanguage, current_language, isCurrentLanguage } = common; const { i18n } = useTranslation(); const allowed_language_keys: string[] = Object.keys(getAllowedLanguages()); const initial_values = { language_code: current_language }; @@ -69,10 +64,6 @@ const LanguageSettings = ({ changeSelectedLanguage, current_language, isCurrentL }} ); -}; +}); -export default connect(({ common }: TCoreStore) => ({ - changeSelectedLanguage: common.changeSelectedLanguage, - current_language: common.current_language, - isCurrentLanguage: common.isCurrentLanguage, -}))(LanguageSettings); +export default LanguageSettings; diff --git a/packages/account/src/Sections/Profile/PersonalDetails/__tests__/personal-details.spec.js b/packages/account/src/Sections/Profile/PersonalDetails/__tests__/personal-details.spec.js index 5908e032cb17..1f54dcae50be 100644 --- a/packages/account/src/Sections/Profile/PersonalDetails/__tests__/personal-details.spec.js +++ b/packages/account/src/Sections/Profile/PersonalDetails/__tests__/personal-details.spec.js @@ -4,6 +4,7 @@ import { cleanup, render, waitForElementToBeRemoved, waitFor } from '@testing-li import { createBrowserHistory } from 'history'; import { Router } from 'react-router'; import { PersonalDetailsForm } from '../personal-details.jsx'; +import { StoreProvider, mockStore } from '@deriv/stores'; afterAll(cleanup); @@ -15,9 +16,11 @@ jest.mock('@deriv/shared/src/services/ws-methods', () => ({ return Promise.resolve([...payload]); }, }, + useWS: () => undefined, })); describe('', () => { + let store = mockStore(); const history = createBrowserHistory(); it('should_render_successfully', async () => { @@ -32,20 +35,36 @@ describe('', () => { value: 'value', }, ]; + store = mockStore({ + client: { + account_settings: { + email_consent: 1, + }, + is_virtual: false, + states_list: residence_list, + residence_list: residence_list, + has_residence: true, + getChangeableFields: () => [], + fetchResidenceList: fetchResidenceList, + fetchStatesList: fetchStatesList, + }, + }); + + // store.client.fetchResidenceList = fetchResidenceList; + // store.client.fetchStatesList = fetchStatesList; + // store.client.residence_list = residence_list; + // store.client.account_settings = { + // email_consent: 1, + // }; + // store.client.is_virtual = false; + // store.client.states_list = residence_list; + // store.client.getChangeableFields = () => []; + // store.client.has_residence = true; const screen = render( - []} - is_virtual={false} - states_list={residence_list} - /> + + + ); await waitForElementToBeRemoved(() => screen.container.querySelector('.account__initial-loader')); diff --git a/packages/account/src/Sections/Profile/PersonalDetails/personal-details.jsx b/packages/account/src/Sections/Profile/PersonalDetails/personal-details.jsx index b7b352190d0a..3978f839086f 100644 --- a/packages/account/src/Sections/Profile/PersonalDetails/personal-details.jsx +++ b/packages/account/src/Sections/Profile/PersonalDetails/personal-details.jsx @@ -39,14 +39,14 @@ import { removeEmptyPropertiesFromObject, } from '@deriv/shared'; import { Localize, localize } from '@deriv/translations'; +import { observer, useStore } from '@deriv/stores'; +import LeaveConfirm from 'Components/leave-confirm'; +import FormFooter from 'Components/form-footer'; import FormBody from 'Components/form-body'; import FormBodySection from 'Components/form-body-section'; -import FormFooter from 'Components/form-footer'; import FormSubHeader from 'Components/form-sub-header'; -import LeaveConfirm from 'Components/leave-confirm'; import LoadErrorMessage from 'Components/load-error-message'; import POAAddressMismatchHintBox from 'Components/poa-address-mismatch-hint-box'; -import { connect } from 'Stores/connect'; import { getEmploymentStatusList } from 'Sections/Assessment/FinancialAssessment/financial-information-list'; import { validateName, validate } from 'Helpers/utils'; @@ -96,31 +96,7 @@ const TaxResidenceSelect = ({ field, errors, setFieldValue, values, is_changeabl ); -export const PersonalDetailsForm = ({ - authentication_status, - is_eu, - is_mf, - is_uk, - is_svg, - is_virtual, - residence_list, - states_list, - current_landing_company, - refreshNotifications, - showPOAAddressMismatchSuccessNotification, - showPOAAddressMismatchFailureNotification, - Notifications, - fetchResidenceList, - fetchStatesList, - has_residence, - account_settings, - getChangeableFields, - history, - is_social_signup, - updateAccountStatus, - has_poa_address_mismatch, - is_language_changing, -}) => { +export const PersonalDetailsForm = observer(({ history }) => { const [is_loading, setIsLoading] = React.useState(true); const [is_state_loading, setIsStateLoading] = useStateCallback(false); @@ -128,7 +104,38 @@ export const PersonalDetailsForm = ({ const [is_btn_loading, setIsBtnLoading] = React.useState(false); const [is_submit_success, setIsSubmitSuccess] = useStateCallback(false); + const { client, notifications, ui, common } = useStore(); + + const { + authentication_status, + is_eu, + landing_company_shortcode, + is_uk, + is_svg, + is_virtual, + residence_list, + states_list, + current_landing_company, + fetchResidenceList, + fetchStatesList, + has_residence, + account_settings, + getChangeableFields, + updateAccountStatus, + is_social_signup, + account_status, + } = client; + + const { + refreshNotifications, + showPOAAddressMismatchSuccessNotification, + showPOAAddressMismatchFailureNotification, + } = notifications; + const { Notifications } = ui; + const { is_language_changing } = common; + const is_mf = landing_company_shortcode === 'maltainvest'; + const has_poa_address_mismatch = account_status.status?.includes('poa_address_mismatch'); const [rest_state, setRestState] = React.useState({ show_form: true, errors: false, @@ -141,7 +148,6 @@ export const PersonalDetailsForm = ({ }); const { is_appstore } = React.useContext(PlatformContext); - const isMounted = useIsMounted(); React.useEffect(() => { @@ -1272,55 +1278,10 @@ export const PersonalDetailsForm = ({ )} ); -}; +}); PersonalDetailsForm.propTypes = { - authentication_status: PropTypes.object, - is_eu: PropTypes.bool, - is_mf: PropTypes.bool, - is_uk: PropTypes.bool, - is_svg: PropTypes.bool, - is_virtual: PropTypes.bool, - residence_list: PropTypes.arrayOf(PropTypes.object), - states_list: PropTypes.array, - refreshNotifications: PropTypes.func, - showPOAAddressMismatchSuccessNotification: PropTypes.func, - showPOAAddressMismatchFailureNotification: PropTypes.func, - Notifications: PropTypes.node, - fetchResidenceList: PropTypes.func, - fetchStatesList: PropTypes.func, - has_residence: PropTypes.bool, - account_settings: PropTypes.object, - getChangeableFields: PropTypes.func, - current_landing_company: PropTypes.object, history: PropTypes.object, - is_social_signup: PropTypes.bool, - updateAccountStatus: PropTypes.func, - has_poa_address_mismatch: PropTypes.bool, - is_language_changing: PropTypes.bool, }; -export default connect(({ client, notifications, ui, common }) => ({ - account_settings: client.account_settings, - authentication_status: client.authentication_status, - has_residence: client.has_residence, - getChangeableFields: client.getChangeableFields, - current_landing_company: client.current_landing_company, - is_eu: client.is_eu, - is_mf: client.landing_company_shortcode === 'maltainvest', - is_svg: client.is_svg, - is_uk: client.is_uk, - is_virtual: client.is_virtual, - residence_list: client.residence_list, - states_list: client.states_list, - fetchResidenceList: client.fetchResidenceList, - fetchStatesList: client.fetchStatesList, - is_social_signup: client.is_social_signup, - refreshNotifications: notifications.refreshNotifications, - showPOAAddressMismatchSuccessNotification: notifications.showPOAAddressMismatchSuccessNotification, - showPOAAddressMismatchFailureNotification: notifications.showPOAAddressMismatchFailureNotification, - Notifications: ui.notification_messages_ui, - updateAccountStatus: client.updateAccountStatus, - has_poa_address_mismatch: client.account_status.status?.includes('poa_address_mismatch'), - is_language_changing: common.is_language_changing, -}))(withRouter(PersonalDetailsForm)); +export default withRouter(PersonalDetailsForm); diff --git a/packages/account/src/Sections/Security/AccountClosed/account-closed.jsx b/packages/account/src/Sections/Security/AccountClosed/account-closed.jsx index d3e553109ee5..e3379dfa2279 100644 --- a/packages/account/src/Sections/Security/AccountClosed/account-closed.jsx +++ b/packages/account/src/Sections/Security/AccountClosed/account-closed.jsx @@ -2,9 +2,11 @@ import React from 'react'; import { Modal, Text } from '@deriv/components'; import { Localize } from '@deriv/translations'; import { getStaticUrl, PlatformContext } from '@deriv/shared'; -import { connect } from 'Stores/connect'; +import { observer, useStore } from '@deriv/stores'; -const AccountClosed = ({ logout }) => { +const AccountClosed = observer(() => { + const { client } = useStore(); + const { logout } = client; const [is_modal_open, setModalState] = React.useState(true); const [timer, setTimer] = React.useState(10); const { is_appstore } = React.useContext(PlatformContext); @@ -38,8 +40,6 @@ const AccountClosed = ({ logout }) => { ); -}; +}); -export default connect(({ client }) => ({ - logout: client.logout, -}))(AccountClosed); +export default AccountClosed; diff --git a/packages/account/src/Sections/Security/AccountLimits/account-limits.jsx b/packages/account/src/Sections/Security/AccountLimits/account-limits.jsx index 8428842a49f2..5a0771faf09b 100644 --- a/packages/account/src/Sections/Security/AccountLimits/account-limits.jsx +++ b/packages/account/src/Sections/Security/AccountLimits/account-limits.jsx @@ -1,15 +1,4 @@ import AccountLimits from 'Components/account-limits/account-limits'; import 'Components/account-limits/account-limits.scss'; -import { connect } from 'Stores/connect'; -export default connect(({ client, common, ui }) => ({ - account_limits: client.account_limits, - currency: client.currency, - getLimits: client.getLimits, - is_fully_authenticated: client.is_fully_authenticated, - is_from_derivgo: common.is_from_derivgo, - is_virtual: client.is_virtual, - is_switching: client.is_switching, - should_show_article: true, - toggleAccountsDialog: ui.toggleAccountsDialog, -}))(AccountLimits); +export default AccountLimits; diff --git a/packages/account/src/Sections/Security/ApiToken/api-token.jsx b/packages/account/src/Sections/Security/ApiToken/api-token.jsx index 05363454eef6..cb02ee645088 100644 --- a/packages/account/src/Sections/Security/ApiToken/api-token.jsx +++ b/packages/account/src/Sections/Security/ApiToken/api-token.jsx @@ -1,6 +1,4 @@ -import { WS } from '@deriv/shared'; -import { connect } from 'Stores/connect'; import ApiToken from 'Components/api-token/api-token'; import 'Components/api-token/api-token.scss'; -export default connect(({ client }) => ({ is_switching: client.is_switching, ws: WS }))(ApiToken); +export default ApiToken; diff --git a/packages/account/src/Sections/Security/ClosingAccount/__tests__/closing-account-reason.spec.js b/packages/account/src/Sections/Security/ClosingAccount/__tests__/closing-account-reason.spec.js index 49ff2da25f9f..ca7527b3c459 100644 --- a/packages/account/src/Sections/Security/ClosingAccount/__tests__/closing-account-reason.spec.js +++ b/packages/account/src/Sections/Security/ClosingAccount/__tests__/closing-account-reason.spec.js @@ -1,14 +1,10 @@ import React from 'react'; import { act, render, screen, waitFor, fireEvent, userEvent } from '@testing-library/react'; import ClosingAccountReason from '../closing-account-reason'; - -jest.mock('Stores/connect', () => ({ - __esModule: true, - default: 'mockedDefaultExport', - connect: () => Component => Component, -})); +import { mockStore, StoreProvider } from '@deriv/stores'; describe('', () => { + let store = mockStore(); beforeAll(() => { const modal_root_el = document.createElement('div'); modal_root_el.setAttribute('id', 'modal_root'); @@ -21,14 +17,22 @@ describe('', () => { }); test('Should render properly', async () => { - render(); + render( + + + + ); await waitFor(() => { screen.getAllByText(/Please tell us why you’re leaving/i); }); }); test('Should be disabled when no reason has been selected', async () => { - render(); + render( + + + + ); // clicking the checkbox twice to select and unselect fireEvent.click(screen.getByRole('checkbox', { name: /I have other financial priorities./i })); @@ -43,7 +47,11 @@ describe('', () => { }); test('should reduce remaining chars', async () => { - render(); + render( + + + + ); expect(screen.getByText(/Remaining characters: 110/i)).toBeInTheDocument(); diff --git a/packages/account/src/Sections/Security/ClosingAccount/closing-account-reason.jsx b/packages/account/src/Sections/Security/ClosingAccount/closing-account-reason.jsx index d78f78ff2f73..7a2eeeba0d55 100644 --- a/packages/account/src/Sections/Security/ClosingAccount/closing-account-reason.jsx +++ b/packages/account/src/Sections/Security/ClosingAccount/closing-account-reason.jsx @@ -4,7 +4,7 @@ import classNames from 'classnames'; import { routes, PlatformContext, WS } from '@deriv/shared'; import { localize } from '@deriv/translations'; import { FormSubmitButton, Modal, Icon, Loading, Text, Button } from '@deriv/components'; -import { connect } from 'Stores/connect'; +import { observer, useStore } from '@deriv/stores'; import AccountHasPendingConditions from './account-has-balance.jsx'; import ClosingAccountReasonFrom from './closing-account-reason-form.jsx'; @@ -73,7 +73,9 @@ const GeneralErrorContent = ({ message, onClick }) => ( const character_limit_no = 110; const max_allowed_reasons = 3; -const ClosingAccountReason = ({ onBackClick, mt5_login_list, client_accounts, dxtrade_accounts_list }) => { +const ClosingAccountReason = observer(({ onBackClick }) => { + const { client } = useStore(); + const { dxtrade_accounts_list, mt5_login_list, account_list } = client; const { is_appstore } = React.useContext(PlatformContext); const [is_account_closed, setIsAccountClosed] = React.useState(false); const [is_loading, setIsLoading] = React.useState(false); @@ -228,7 +230,7 @@ const ClosingAccountReason = ({ onBackClick, mt5_login_list, client_accounts, dx @@ -239,10 +241,6 @@ const ClosingAccountReason = ({ onBackClick, mt5_login_list, client_accounts, dx
); -}; +}); -export default connect(({ client }) => ({ - client_accounts: client.account_list, - mt5_login_list: client.mt5_login_list, - dxtrade_accounts_list: client.dxtrade_accounts_list, -}))(ClosingAccountReason); +export default ClosingAccountReason; diff --git a/packages/account/src/Sections/Security/ClosingAccount/closing-account-steps.jsx b/packages/account/src/Sections/Security/ClosingAccount/closing-account-steps.jsx index fb138cfd3f5b..51a7215a66bc 100644 --- a/packages/account/src/Sections/Security/ClosingAccount/closing-account-steps.jsx +++ b/packages/account/src/Sections/Security/ClosingAccount/closing-account-steps.jsx @@ -1,12 +1,14 @@ import React from 'react'; import classNames from 'classnames'; -import { connect } from 'Stores/connect'; +import { observer, useStore } from '@deriv/stores'; import { localize, Localize } from '@deriv/translations'; import { Link } from 'react-router-dom'; import { Button, Text, StaticUrl } from '@deriv/components'; import { PlatformContext } from '@deriv/shared'; -const ClosingAccountSteps = ({ redirectToReasons, is_from_derivgo }) => { +const ClosingAccountSteps = observer(({ redirectToReasons }) => { + const { common } = useStore(); + const { is_from_derivgo } = common; const { is_appstore } = React.useContext(PlatformContext); return ( @@ -84,5 +86,5 @@ const ClosingAccountSteps = ({ redirectToReasons, is_from_derivgo }) => { )} ); -}; -export default connect(({ common }) => ({ is_from_derivgo: common.is_from_derivgo }))(ClosingAccountSteps); +}); +export default ClosingAccountSteps; diff --git a/packages/account/src/Sections/Security/LoginHistory/login-history.jsx b/packages/account/src/Sections/Security/LoginHistory/login-history.jsx index 449c4e253e09..099b5c5d9139 100644 --- a/packages/account/src/Sections/Security/LoginHistory/login-history.jsx +++ b/packages/account/src/Sections/Security/LoginHistory/login-history.jsx @@ -1,11 +1,10 @@ -import PropTypes from 'prop-types'; import React from 'react'; import classNames from 'classnames'; import { Loading, Table, Text, ThemedScrollbars } from '@deriv/components'; import Bowser from 'bowser'; import { convertDateFormat, isMobile, isDesktop, PlatformContext, WS } from '@deriv/shared'; +import { observer, useStore } from '@deriv/stores'; import { localize } from '@deriv/translations'; -import { connect } from 'Stores/connect'; import LoadErrorMessage from 'Components/load-error-message'; const API_FETCH_LIMIT = 50; @@ -168,7 +167,9 @@ const ListCell = ({ title, text, className, right }) => ( ); -const LoginHistory = ({ is_switching }) => { +const LoginHistory = observer(() => { + const { client } = useStore(); + const { is_switching } = client; const [is_loading, setLoading] = React.useState(true); const [error, setError] = React.useState(''); const [data, setData] = React.useState([]); @@ -197,12 +198,6 @@ const LoginHistory = ({ is_switching }) => { {data.length ? : null} ); -}; - -LoginHistory.propTypes = { - is_switching: PropTypes.bool, -}; +}); -export default connect(({ client }) => ({ - is_switching: client.is_switching, -}))(LoginHistory); +export default LoginHistory; diff --git a/packages/account/src/Sections/Security/Passwords/deriv-email.jsx b/packages/account/src/Sections/Security/Passwords/deriv-email.jsx index c2c85c1492a9..a962f2526c33 100644 --- a/packages/account/src/Sections/Security/Passwords/deriv-email.jsx +++ b/packages/account/src/Sections/Security/Passwords/deriv-email.jsx @@ -8,9 +8,11 @@ import { Button, Text, Input } from '@deriv/components'; import FormSubHeader from 'Components/form-sub-header'; import SentEmailModal from 'Components/sent-email-modal'; import UnlinkAccountModal from 'Components/unlink-account-modal'; -import { connect } from 'Stores/connect'; +import { observer, useStore } from '@deriv/stores'; -const DerivEmail = ({ email, social_identity_provider, is_social_signup, is_from_derivgo }) => { +const DerivEmail = observer(({ email, social_identity_provider, is_social_signup }) => { + const { common } = useStore(); + const { is_from_derivgo } = common; const [is_unlink_account_modal_open, setIsUnlinkAccountModalOpen] = React.useState(false); const [is_send_email_modal_open, setIsSendEmailModalOpen] = React.useState(false); @@ -88,18 +90,13 @@ const DerivEmail = ({ email, social_identity_provider, is_social_signup, is_from ); -}; +}); DerivEmail.propTypes = { email: PropTypes.string, is_dark_mode_on: PropTypes.bool, is_social_signup: PropTypes.bool, - is_from_derivgo: PropTypes.bool, social_identity_provider: PropTypes.string, }; -export default withRouter( - connect(({ common }) => ({ - is_from_derivgo: common.is_from_derivgo, - }))(DerivEmail) -); +export default withRouter(DerivEmail); diff --git a/packages/account/src/Sections/Security/Passwords/passwords.jsx b/packages/account/src/Sections/Security/Passwords/passwords.jsx index 86f6dbcb1f58..719d784a160c 100644 --- a/packages/account/src/Sections/Security/Passwords/passwords.jsx +++ b/packages/account/src/Sections/Security/Passwords/passwords.jsx @@ -1,32 +1,36 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { Loading } from '@deriv/components'; -import { connect } from 'Stores/connect'; +import { observer, useStore } from '@deriv/stores'; import DerivPassword from './deriv-password.jsx'; import DerivEmail from './deriv-email.jsx'; import PasswordsPlatform from './passwords-platform.jsx'; -const Passwords = ({ - email, - is_dark_mode_on, - mt5_login_list, - is_social_signup, - dxtrade_accounts_list, - social_identity_provider, - is_eu_user, - financial_restricted_countries, - is_loading_dxtrade, - is_loading_mt5, - is_mt5_password_not_set, - is_dxtrade_password_not_set, - is_from_derivgo, -}) => { +const Passwords = observer(() => { const [is_loading, setIsLoading] = React.useState(true); + const { client, ui, common, traders_hub } = useStore(); + const { + is_populating_mt5_account_list, + is_populating_dxtrade_account_list, + is_social_signup, + email, + social_identity_provider, + mt5_login_list, + is_mt5_password_not_set, + dxtrade_accounts_list, + is_dxtrade_password_not_set, + } = client; + const { is_from_derivgo } = common; + const { is_eu_user, financial_restricted_countries } = traders_hub; + const { is_dark_mode_on } = ui; React.useEffect(() => { - if (is_loading_mt5 === false && is_loading_dxtrade === false && is_social_signup !== undefined) { + if ( + is_populating_mt5_account_list === false && + is_populating_dxtrade_account_list === false && + is_social_signup !== undefined + ) { setIsLoading(false); } - }, [is_loading_mt5, is_loading_dxtrade, is_social_signup]); + }, [is_populating_mt5_account_list, is_populating_dxtrade_account_list, is_social_signup]); if (is_loading) { return ; @@ -61,36 +65,6 @@ const Passwords = ({ )} ); -}; +}); -Passwords.propTypes = { - email: PropTypes.string, - is_dark_mode_on: PropTypes.bool, - dxtrade_accounts_list: PropTypes.array, - is_social_signup: PropTypes.bool, - mt5_login_list: PropTypes.array, - social_identity_provider: PropTypes.string, - is_loading_mt5: PropTypes.bool, - is_loading_dxtrade: PropTypes.bool, - is_eu_user: PropTypes.bool, - financial_restricted_countries: PropTypes.bool, - is_mt5_password_not_set: PropTypes.bool, - is_dxtrade_password_not_set: PropTypes.bool, - is_from_derivgo: PropTypes.bool, -}; - -export default connect(({ client, ui, common, traders_hub }) => ({ - email: client.email, - is_dark_mode_on: ui.is_dark_mode_on, - is_social_signup: client.is_social_signup, - mt5_login_list: client.mt5_login_list, - dxtrade_accounts_list: client.dxtrade_accounts_list, - social_identity_provider: client.social_identity_provider, - is_eu_user: traders_hub.is_eu_user, - financial_restricted_countries: traders_hub.financial_restricted_countries, - is_loading_mt5: client.is_populating_mt5_account_list, - is_loading_dxtrade: client.is_populating_dxtrade_account_list, - is_mt5_password_not_set: client.is_mt5_password_not_set, - is_dxtrade_password_not_set: client.is_dxtrade_password_not_set, - is_from_derivgo: common.is_from_derivgo, -}))(Passwords); +export default Passwords; diff --git a/packages/account/src/Sections/Security/SelfExclusion/self-exclusion.jsx b/packages/account/src/Sections/Security/SelfExclusion/self-exclusion.jsx index 76609ebb4e7b..e0f4928efa53 100644 --- a/packages/account/src/Sections/Security/SelfExclusion/self-exclusion.jsx +++ b/packages/account/src/Sections/Security/SelfExclusion/self-exclusion.jsx @@ -1,6 +1,5 @@ import React from 'react'; -import { PlatformContext, WS } from '@deriv/shared'; -import { connect } from 'Stores/connect'; +import { PlatformContext } from '@deriv/shared'; import SelfExclusionComponent from 'Components/self-exclusion/self-exclusion'; import 'Components/self-exclusion/self-exclusion.scss'; @@ -9,18 +8,4 @@ const SelfExclusion = props => { return ; }; -export default connect(({ client, ui }) => ({ - is_tablet: ui.is_tablet, - currency: client.currency, - is_virtual: client.is_virtual, - is_switching: client.is_switching, - is_cr: client.standpoint.svg, - is_eu: client.is_eu, - is_mlt: client.landing_company_shortcode === 'malta', - is_mf: client.landing_company_shortcode === 'maltainvest', - is_mx: client.landing_company_shortcode === 'iom', - is_uk: client.is_uk, - is_wrapper_bypassed: false, - logout: client.logout, - ws: WS, -}))(SelfExclusion); +export default SelfExclusion; diff --git a/packages/account/src/Sections/Security/TwoFactorAuthentication/two-factor-authentication.jsx b/packages/account/src/Sections/Security/TwoFactorAuthentication/two-factor-authentication.jsx index c18230f8aec6..4b805f37eec9 100644 --- a/packages/account/src/Sections/Security/TwoFactorAuthentication/two-factor-authentication.jsx +++ b/packages/account/src/Sections/Security/TwoFactorAuthentication/two-factor-authentication.jsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import classNames from 'classnames'; import React from 'react'; import QRCode from 'qrcode.react'; @@ -14,20 +13,16 @@ import { } from '@deriv/components'; import { getPropertyValue, isMobile, PlatformContext, WS } from '@deriv/shared'; import { localize, Localize } from '@deriv/translations'; -import { connect } from 'Stores/connect'; import LoadErrorMessage from 'Components/load-error-message'; import DigitForm from './digit-form.jsx'; import TwoFactorAuthenticationArticle from './two-factor-authentication-article.jsx'; +import { observer, useStore } from '@deriv/stores'; -const TwoFactorAuthentication = ({ - email_address, - is_switching, - setTwoFAStatus, - getTwoFAStatus, - has_enabled_two_fa, - Notifications, - setTwoFAChangedStatus, -}) => { +const TwoFactorAuthentication = observer(() => { + const { client, ui } = useStore(); + const { email_address, getTwoFAStatus, has_enabled_two_fa, is_switching, setTwoFAStatus, setTwoFAChangedStatus } = + client; + const { notification_messages_ui: Notifications } = ui; const [is_loading, setLoading] = React.useState(true); const [is_qr_loading, setQrLoading] = React.useState(false); const [error_message, setErrorMessage] = React.useState(''); @@ -205,24 +200,6 @@ const TwoFactorAuthentication = ({ ); -}; +}); -TwoFactorAuthentication.propTypes = { - email_address: PropTypes.string, - is_switching: PropTypes.bool, - setTwoFAStatus: PropTypes.func, - getTwoFAStatus: PropTypes.func, - has_enabled_two_fa: PropTypes.bool, - Notifications: PropTypes.node, - setTwoFAChangedStatus: PropTypes.func, -}; - -export default connect(({ client, ui }) => ({ - email_address: client.email_address, - is_switching: client.is_switching, - setTwoFAStatus: client.setTwoFAStatus, - getTwoFAStatus: client.getTwoFAStatus, - has_enabled_two_fa: client.has_enabled_two_fa, - Notifications: ui.notification_messages_ui, - setTwoFAChangedStatus: client.setTwoFAChangedStatus, -}))(TwoFactorAuthentication); +export default TwoFactorAuthentication; diff --git a/packages/account/src/Sections/Verification/ProofOfAddress/proof-of-address-form.jsx b/packages/account/src/Sections/Verification/ProofOfAddress/proof-of-address-form.jsx index aee2265a2ca7..b47e1d6190e6 100644 --- a/packages/account/src/Sections/Verification/ProofOfAddress/proof-of-address-form.jsx +++ b/packages/account/src/Sections/Verification/ProofOfAddress/proof-of-address-form.jsx @@ -24,7 +24,6 @@ import { getLocation, WS, } from '@deriv/shared'; -import { connect } from 'Stores/connect'; import FormFooter from 'Components/form-footer'; import FormBody from 'Components/form-body'; import FormBodySection from 'Components/form-body-section'; @@ -32,6 +31,7 @@ import FormSubHeader from 'Components/form-sub-header'; import LoadErrorMessage from 'Components/load-error-message'; import LeaveConfirm from 'Components/leave-confirm'; import FileUploaderContainer from 'Components/file-uploader-container'; +import { observer, useStore } from '@deriv/stores'; const validate = (errors, values) => (fn, arr, err_msg) => { arr.forEach(field => { @@ -53,18 +53,14 @@ const UploaderSideNote = () => ( ); -const ProofOfAddressForm = ({ - account_settings, - addNotificationByKey, - is_eu, - is_resubmit, - fetchResidenceList, - fetchStatesList, - onSubmit, - removeNotificationByKey, - removeNotificationMessage, - states_list, -}) => { +const ProofOfAddressForm = observer(({ is_resubmit, onSubmit }) => { + const { client, notifications } = useStore(); + const { account_settings, fetchResidenceList, fetchStatesList, is_eu, states_list } = client; + const { + addNotificationMessageByKey: addNotificationByKey, + removeNotificationMessage, + removeNotificationByKey, + } = notifications; const [document_file, setDocumentFile] = React.useState({ files: [], error_message: null }); const [is_loading, setIsLoading] = React.useState(true); const [form_values, setFormValues] = useStateCallback({}); @@ -458,28 +454,11 @@ const ProofOfAddressForm = ({ )} ); -}; +}); ProofOfAddressForm.propTypes = { - account_settings: PropTypes.object, - addNotificationByKey: PropTypes.func, - is_eu: PropTypes.bool, is_resubmit: PropTypes.bool, - fetchResidenceList: PropTypes.func, - fetchStatesList: PropTypes.func, onSubmit: PropTypes.func, - removeNotificationByKey: PropTypes.func, - removeNotificationMessage: PropTypes.func, - states_list: PropTypes.array, }; -export default connect(({ client, notifications }) => ({ - account_settings: client.account_settings, - is_eu: client.is_eu, - addNotificationByKey: notifications.addNotificationMessageByKey, - removeNotificationMessage: notifications.removeNotificationMessage, - removeNotificationByKey: notifications.removeNotificationByKey, - states_list: client.states_list, - fetchResidenceList: client.fetchResidenceList, - fetchStatesList: client.fetchStatesList, -}))(ProofOfAddressForm); +export default ProofOfAddressForm; diff --git a/packages/account/src/Sections/Verification/ProofOfAddress/proof-of-address.jsx b/packages/account/src/Sections/Verification/ProofOfAddress/proof-of-address.jsx index 7d558e882487..1b9e16ba3e4e 100644 --- a/packages/account/src/Sections/Verification/ProofOfAddress/proof-of-address.jsx +++ b/packages/account/src/Sections/Verification/ProofOfAddress/proof-of-address.jsx @@ -1,45 +1,26 @@ import DemoMessage from 'Components/demo-message'; import { PlatformContext } from '@deriv/shared'; import ProofOfAddressContainer from './proof-of-address-container.jsx'; -import { PropTypes } from 'prop-types'; import React from 'react'; -import { connect } from 'Stores/connect'; +import { observer, useStore } from '@deriv/stores'; -const ProofOfAddress = ({ - is_virtual, - is_mx_mlt, - is_switching, - has_restricted_mt5_account, - refreshNotifications, - app_routing_history, -}) => { +const ProofOfAddress = observer(() => { + const { client, notifications, common } = useStore(); + const { app_routing_history } = common; + const { is_virtual, landing_company_shortcode, has_restricted_mt5_account, is_switching } = client; + const { refreshNotifications } = notifications; const { is_appstore } = React.useContext(PlatformContext); if (is_virtual) return ; return ( ); -}; +}); -ProofOfAddress.propTypes = { - is_mx_mlt: PropTypes.bool, - is_switching: PropTypes.bool, - is_virtual: PropTypes.bool, - refreshNotifications: PropTypes.func, - has_restricted_mt5_account: PropTypes.bool, -}; - -export default connect(({ client, notifications, common }) => ({ - is_mx_mlt: client.landing_company_shortcode === 'iom' || client.landing_company_shortcode === 'malta', - is_switching: client.is_switching, - is_virtual: client.is_virtual, - refreshNotifications: notifications.refreshNotifications, - has_restricted_mt5_account: client.has_restricted_mt5_account, - app_routing_history: common.app_routing_history, -}))(ProofOfAddress); +export default ProofOfAddress; diff --git a/packages/account/src/Sections/Verification/ProofOfIdentity/proof-of-identity.jsx b/packages/account/src/Sections/Verification/ProofOfIdentity/proof-of-identity.jsx index ed975b1a027b..ebcbd6f37dcf 100644 --- a/packages/account/src/Sections/Verification/ProofOfIdentity/proof-of-identity.jsx +++ b/packages/account/src/Sections/Verification/ProofOfIdentity/proof-of-identity.jsx @@ -2,26 +2,25 @@ import { AutoHeightWrapper } from '@deriv/components'; import ProofOfIdentityContainer from './proof-of-identity-container.jsx'; import React from 'react'; import { changeMetaTagWithOG } from '@deriv/shared'; -import { connect } from 'Stores/connect'; +import { observer, useStore } from '@deriv/stores'; import { withRouter } from 'react-router-dom'; -const ProofOfIdentity = ({ - account_settings, - account_status, - app_routing_history, - fetchResidenceList, - getChangeableFields, - is_from_external, - is_switching, - is_virtual, - is_high_risk, - is_withdrawal_lock, - onStateChange, - refreshNotifications, - routeBackInApp, - should_allow_authentication, - updateAccountStatus, -}) => { +const ProofOfIdentity = observer(({ is_from_external, onStateChange }) => { + const { client, common, notifications } = useStore(); + const { + account_status, + account_settings, + fetchResidenceList, + getChangeableFields, + is_switching, + is_high_risk, + is_withdrawal_lock, + should_allow_authentication, + is_virtual, + updateAccountStatus, + } = client; + const { refreshNotifications } = notifications; + const { app_routing_history, routeBackInApp } = common; // next useEffect implements seo requirements React.useEffect(() => { const description_content = 'Submit your proof of identity documents to verify your account and start trading'; @@ -65,20 +64,6 @@ const ProofOfIdentity = ({ )} ); -}; +}); -export default connect(({ client, common, notifications }) => ({ - account_settings: client.account_settings, - account_status: client.account_status, - app_routing_history: common.app_routing_history, - fetchResidenceList: client.fetchResidenceList, - getChangeableFields: client.getChangeableFields, - is_switching: client.is_switching, - is_virtual: client.is_virtual, - is_high_risk: client.is_high_risk, - is_withdrawal_lock: client.is_withdrawal_lock, - refreshNotifications: notifications.refreshNotifications, - routeBackInApp: common.routeBackInApp, - should_allow_authentication: client.should_allow_authentication, - updateAccountStatus: client.updateAccountStatus, -}))(withRouter(ProofOfIdentity)); +export default withRouter(ProofOfIdentity); diff --git a/packages/account/src/Sections/Verification/ProofOfOwnership/__test__/proof-of-ownership.spec.js b/packages/account/src/Sections/Verification/ProofOfOwnership/__test__/proof-of-ownership.spec.js index f79ea6d2e251..6bf1b94608e2 100644 --- a/packages/account/src/Sections/Verification/ProofOfOwnership/__test__/proof-of-ownership.spec.js +++ b/packages/account/src/Sections/Verification/ProofOfOwnership/__test__/proof-of-ownership.spec.js @@ -2,80 +2,93 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import { ProofOfOwnership } from '../proof-of-ownership.jsx'; import test_data from './test-data'; +import { StoreProvider, mockStore } from '@deriv/stores'; describe('proof-of-ownership.jsx', () => { let ownership_temp; beforeAll(() => { ownership_temp = test_data; }); + const ProofOfOwnershipScreen = () => { + return ( + + + + ); + }; + let store = mockStore(); it('should render no poo required status page', () => { - render( - - ); + }, + }, + }); + render(); + const element = screen.getByText("Your proof of ownership isn't required.", { exact: true }); expect(element).toBeInTheDocument(); }); it('should render poo verified status page', () => { - render( - - ); + }, + }, + }); + render(); + const element = screen.getByText('Proof of ownership verification passed.', { exact: true }); expect(element).toBeInTheDocument(); }); it('should render poo submitted status page', () => { - render( - - ); + }, + }, + }); + render(); + const element = screen.getByText('We’ve received your proof of ownership.', { exact: true }); expect(element).toBeInTheDocument(); }); it('should render poo rejected status page', () => { - render( - - ); + }, + }, + }); + render(); + const element = screen.getByTestId('dt_try-again-button', { exact: true }); expect(element).toBeInTheDocument(); }); it('should render ProofOfOwnershipForm', () => { - render( - - ); + }, + }, + }); + render(); expect(screen.getByTestId('dt_poo_form', { exact: true })).toBeInTheDocument(); }); }); diff --git a/packages/account/src/Sections/Verification/ProofOfOwnership/proof-of-ownership.jsx b/packages/account/src/Sections/Verification/ProofOfOwnership/proof-of-ownership.jsx index e8c45c71728e..b0ce9a0cde8d 100644 --- a/packages/account/src/Sections/Verification/ProofOfOwnership/proof-of-ownership.jsx +++ b/packages/account/src/Sections/Verification/ProofOfOwnership/proof-of-ownership.jsx @@ -1,19 +1,17 @@ import React, { useEffect, useState } from 'react'; import { withRouter } from 'react-router-dom'; -import { connect } from 'Stores/connect'; import ProofOfOwnershipForm from './proof-of-ownership-form.jsx'; import { POONotRequired, POOVerified, POORejetced, POOSubmitted } from 'Components/poo/statuses'; import { Loading } from '@deriv/components'; import { POO_STATUSES } from './constants/constants'; import getPaymentMethodsConfig from './payment-method-config.js'; +import { observer, useStore } from '@deriv/stores'; -export const ProofOfOwnership = ({ - account_status, - client_email, - is_dark_mode, - refreshNotifications, - updateAccountStatus, -}) => { +export const ProofOfOwnership = observer(() => { + const { client, notifications, ui } = useStore(); + const { account_status, email: client_email, updateAccountStatus } = client; + const { refreshNotifications } = notifications; + const { is_dark_mode_on: is_dark_mode } = ui; const cards = account_status?.authentication?.ownership?.requests; const [status, setStatus] = useState(POO_STATUSES.none); const grouped_payment_method_data = React.useMemo(() => { @@ -68,12 +66,6 @@ export const ProofOfOwnership = ({ return ; // Proof of ownership rejected } return ; -}; +}); -export default connect(({ client, notifications, ui }) => ({ - account_status: client.account_status, - client_email: client.email, - is_dark_mode: ui.is_dark_mode_on, - refreshNotifications: notifications.refreshNotifications, - updateAccountStatus: client.updateAccountStatus, -}))(withRouter(ProofOfOwnership)); +export default withRouter(ProofOfOwnership); diff --git a/packages/account/src/Stores/connect.js b/packages/account/src/Stores/connect.js deleted file mode 100644 index 4ef42c8d18b6..000000000000 --- a/packages/account/src/Stores/connect.js +++ /dev/null @@ -1,31 +0,0 @@ -import { useObserver } from 'mobx-react'; -import React from 'react'; - -const isClassComponent = Component => - !!(typeof Component === 'function' && Component.prototype && Component.prototype.isReactComponent); - -export const MobxContent = React.createContext(null); - -function injectStorePropsToComponent(propsToSelectFn, BaseComponent) { - const Component = own_props => { - const store = React.useContext(MobxContent); - - let ObservedComponent = BaseComponent; - - if (isClassComponent(BaseComponent)) { - const FunctionalWrapperComponent = props => ; - ObservedComponent = FunctionalWrapperComponent; - } - - return useObserver(() => ObservedComponent({ ...own_props, ...propsToSelectFn(store, own_props) })); - }; - - Component.displayName = BaseComponent.name; - return Component; -} - -export const MobxContentProvider = ({ store, children }) => { - return {children}; -}; - -export const connect = propsToSelectFn => Component => injectStorePropsToComponent(propsToSelectFn, Component); diff --git a/packages/account/src/Stores/index.ts b/packages/account/src/Stores/index.ts deleted file mode 100644 index 9797a63b3b03..000000000000 --- a/packages/account/src/Stores/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -export type TCoreStore = { - client: Record; - common: Record; - ui: Record; - gtm: Record; - rudderstack: Record; - pushwoosh: Record; -}; - -export default class RootStore { - public client: Record; - public common: Record; - public ui: Record; - public gtm: Record; - public rudderstack: Record; - public pushwoosh: Record; - - constructor(core_store: TCoreStore) { - this.client = core_store.client; - this.common = core_store.common; - this.ui = core_store.ui; - this.gtm = core_store.gtm; - this.rudderstack = core_store.rudderstack; - this.pushwoosh = core_store.pushwoosh; - } -} diff --git a/packages/account/src/Stores/init-store.js b/packages/account/src/Stores/init-store.js deleted file mode 100644 index 78df16195a36..000000000000 --- a/packages/account/src/Stores/init-store.js +++ /dev/null @@ -1,12 +0,0 @@ -import { configure } from 'mobx'; -import { setWebsocket } from '@deriv/shared'; -import RootStore from 'Stores'; - -configure({ enforceActions: 'observed' }); - -const initStore = (core_store, websocket) => { - setWebsocket(websocket); - return new RootStore(core_store); -}; - -export default initStore; diff --git a/packages/appstore/src/components/cfds-listing/cfds-listing.scss b/packages/appstore/src/components/cfds-listing/cfds-listing.scss index 67ba01a6d30a..f8e0fb77bd50 100644 --- a/packages/appstore/src/components/cfds-listing/cfds-listing.scss +++ b/packages/appstore/src/components/cfds-listing/cfds-listing.scss @@ -1324,11 +1324,18 @@ @for $column_count from 1 through 7 { .cfd-accounts-compare-modal__row-with-columns-count-#{$column_count} { - grid-template-columns: repeat($column_count, 1fr); + @if $column_count != 5 { + grid-template-columns: repeat($column_count, 1fr); + } @else if $column_count == 5 { + grid-template-columns: repeat($column_count - 1, 1fr); + @include mobile { + grid-template-columns: repeat($column_count, 1fr); + } + } } } &__table-header { - grid-template-columns: 0.9fr 1.39fr 2.74fr; + grid-template-columns: 1fr 2fr; &__pre-appstore { grid-template-columns: 11rem 36rem 46rem 13.5rem; } @@ -1527,6 +1534,9 @@ width: unset; height: unset; } + &__table-header { + width: calc(250vw / 7 * 3); + } } @for $column_count from 1 through 7 { .cfd-accounts-compare-modal__row-with-columns-count-#{$column_count} { diff --git a/packages/appstore/src/components/modals/account-type-modal/account-type-modal.tsx b/packages/appstore/src/components/modals/account-type-modal/account-type-modal.tsx index cf3a99bfc425..bd10965f0c96 100644 --- a/packages/appstore/src/components/modals/account-type-modal/account-type-modal.tsx +++ b/packages/appstore/src/components/modals/account-type-modal/account-type-modal.tsx @@ -109,19 +109,21 @@ const MT5AccountTypeModal = () => { const is_swapfree_available = useHasSwapFreeAccount(); const set_account_type = () => { - switch (account_type_card) { - case 'Derived': + const localizedAccountType = localize(account_type_card); + + switch (localizedAccountType) { + case localize('Derived'): setAccountType({ category: 'real', type: 'synthetic' }); break; - case 'Swap-Free': - setAccountType({ category: 'real', type: 'all' }); + case localize('Financial'): + setAccountType({ category: 'real', type: 'financial' }); break; - case 'Financial': default: - setAccountType({ category: 'real', type: 'financial' }); + setAccountType({ category: 'real', type: 'all' }); break; } }; + return (
}> diff --git a/packages/bot-skeleton/jest.config.js b/packages/bot-skeleton/jest.config.js new file mode 100644 index 000000000000..26391d325ed4 --- /dev/null +++ b/packages/bot-skeleton/jest.config.js @@ -0,0 +1,19 @@ +const baseConfigForPackages = require('../../jest.config.base'); + +module.exports = { + ...baseConfigForPackages, + clearMocks: true, + moduleNameMapper: { + '\\.s(c|a)ss$': '/../../__mocks__/styleMock.js', + '^.+\\.svg$': '/../../__mocks__/styleMock.js', + '^Constants/(.*)$': '/src/constants/$1', + '^Scratch/(.*)$': '/src/scratch/$1', + '^Services/(.*)$': '/src/services/$1', + '^Utils/(.*)$': '/src/utils/$1', + }, + // remove later + testRegex: 'packages/bot-skeleton/src/services/api/__tests__/datadog-middleware.spec.ts', + globals: { + __webpack_public_path__: '/', + }, +}; diff --git a/packages/bot-skeleton/src/services/api/__tests__/datadog-middleware.spec.ts b/packages/bot-skeleton/src/services/api/__tests__/datadog-middleware.spec.ts new file mode 100644 index 000000000000..1a05a301c2c4 --- /dev/null +++ b/packages/bot-skeleton/src/services/api/__tests__/datadog-middleware.spec.ts @@ -0,0 +1,146 @@ +import APIMiddleware, { REQUESTS } from '../api-middleware'; +import { datadogLogs } from '@datadog/browser-logs'; + +jest.mock('@datadog/browser-logs', () => { + return { + ...jest.requireActual('@datadog/browser-logs'), + datadogLogs: { + init: jest.fn(), + logger: { + info: jest.fn(), + }, + }, + }; +}); + +describe('APIMiddleware', () => { + let api_middleware: APIMiddleware; + const info = jest.fn(); + const mockMeasure = jest.fn(() => ({ startTime: 0 })); + const clearMarks = jest.fn(); + const clearMeasures = jest.fn(); + const mockSendRequestsStatistic = jest.fn(); + + const measure_object = { + name: 'time', + startTime: 15288.20000004768, + duration: 133, + detail: null, + isBotRunning: false, + }; + + beforeEach(() => { + Object.defineProperty(window, 'performance', { + value: { + mark: jest.fn(), + measure: mockMeasure, + getEntriesByName: jest.fn().mockReturnValue([{ name: 'entry_name' }]), + logger: { + info: jest.fn().mockReturnValue([{ measure: 'measure_name' }, measure_object]), + }, + mockSendRequestsStatistic, + clearMeasures, + clearMarks, + }, + }); + + api_middleware = new APIMiddleware(); + }); + + it('Should get measure for each request, invoke method log(), clear measures', () => { + const spyLog = jest.spyOn(api_middleware, 'log'); + + api_middleware.sendRequestsStatistic(false); + + REQUESTS.forEach(request_name => { + expect(spyLog).toHaveBeenCalledWith([{ name: 'entry_name' }], false, request_name); + }); + expect(clearMeasures).toBeCalledTimes(1); + }); + + it('Should log info if measures are there', () => { + const datadog_logs = { + name: 'time', + startTimeDate: 15288.20000004768, + duration: 133, + detail: null, + isBotRunning: false, + }; + + const spyDatalogsInfo = jest.spyOn(datadogLogs.logger, 'info'); + + api_middleware.log([datadog_logs], false); + + expect(spyDatalogsInfo).toHaveBeenCalledWith(datadog_logs.name, { ...measure_object }); + }); + + it('Should not log info if measures are absent', () => { + api_middleware.log([], false); + expect(info).toBeCalledTimes(0); + }); + + it('GetRequestType', () => { + const spyGetRequestType = jest.spyOn(api_middleware, 'getRequestType'); + const request_type = { authorize: 1 }; + const result = api_middleware.getRequestType(request_type); + REQUESTS.forEach(type => { + if (type in request_type) { + expect(spyGetRequestType).toHaveBeenCalledWith(request_type); + expect(result).toBeDefined(); + } + }); + }); + + it('Should invoke the method defineMeasure()', async () => { + const spydefineMeasure = jest.spyOn(api_middleware, 'defineMeasure'); + const response_promise = new Promise((res, rej) => res({ authorize: 1 })); + + await api_middleware.sendIsCalled({ response_promise, args: [{ authorize: 1 }] }); + + expect(spydefineMeasure).toHaveBeenCalledWith('authorize'); + }); + + describe('Define measure', () => { + it('Should define measure of history API call', () => { + const spydefineMeasure = jest.spyOn(api_middleware, 'defineMeasure'); + const result = api_middleware.defineMeasure('history'); + + expect(spydefineMeasure).toHaveBeenCalledWith('history'); + expect(mockMeasure).toHaveBeenCalledWith('ticks_history', 'ticks_history_start', 'ticks_history_end'); + expect(result).toBeDefined(); + }); + + it('Should define measure of proposal API call', () => { + const spydefineMeasure = jest.spyOn(api_middleware, 'defineMeasure'); + + const result = api_middleware.defineMeasure('proposal'); + + expect(spydefineMeasure).toHaveBeenCalledWith('proposal'); + expect(mockMeasure).toHaveBeenCalledWith('run-proposal', 'bot-start', 'first_proposal_end'); + expect(clearMarks).toBeCalledTimes(1); + expect(result).toBeDefined(); + }); + + it('Should define measure for API calls except of proposal and history', () => { + const spydefineMeasure = jest.spyOn(api_middleware, 'defineMeasure'); + + REQUESTS.forEach(request_name => { + if (request_name !== 'proposal' && request_name !== 'history') { + const result = api_middleware.defineMeasure(request_name); + expect(spydefineMeasure).toHaveBeenCalledWith(request_name); + expect(mockMeasure).toHaveBeenCalledWith( + `${request_name}`, + `${request_name}_start`, + `${request_name}_end` + ); + expect(result).toBeDefined(); + } + }); + }); + }); + + it('Should be added the method sendRequestsStatistic to window', () => { + expect(window).not.toBeUndefined(); + expect(mockSendRequestsStatistic).not.toBeUndefined(); + }); +}); diff --git a/packages/bot-skeleton/src/services/api/api-middleware.js b/packages/bot-skeleton/src/services/api/api-middleware.js new file mode 100644 index 000000000000..595a2bf427ef --- /dev/null +++ b/packages/bot-skeleton/src/services/api/api-middleware.js @@ -0,0 +1,149 @@ +import { datadogLogs } from '@datadog/browser-logs'; +import { formatDate, formatTime } from '@deriv/shared'; + +const DATADOG_CLIENT_TOKEN = process.env.DATADOG_CLIENT_TOKEN ?? ''; +const isProduction = process.env.CIRCLE_JOB === 'release_production'; +const isStaging = process.env.CIRCLE_JOB === 'release_staging'; + +let dataDogSessionSampleRate = 0; +let dataDogVersion = ''; +let dataDogEnv = ''; + +if (isProduction) { + dataDogVersion = `deriv-app-${process.env.CIRCLE_TAG}`; + dataDogSessionSampleRate = +process.env.DATADOG_SESSION_SAMPLE_RATE ?? 10; + dataDogEnv = 'production'; +} else if (isStaging) { + dataDogVersion = `deriv-app-staging-v${formatDate(new Date(), 'YYYYMMDD')}-${formatTime(Date.now(), 'HH:mm')}`; + dataDogSessionSampleRate = 100; + dataDogEnv = 'staging'; +} + +datadogLogs.init({ + clientToken: DATADOG_CLIENT_TOKEN, + site: 'datadoghq.eu', + forwardErrorsToLogs: false, + service: 'Dbot', + sessionSampleRate: dataDogSessionSampleRate, + version: dataDogVersion, + env: dataDogEnv, +}); + +export const REQUESTS = [ + 'authorize', + 'balance', + 'active_symbols', + 'transaction', + 'ticks_history', + 'forget', + 'proposal_open_contract', + 'proposal', + 'buy', + 'exchange_rates', + 'trading_times', + 'time', + 'get_account_status', + 'get_settings', + 'payout_currencies', + 'website_status', + 'get_financial_assessment', + 'mt5_login_list', + 'get_self_exclusion', + 'landing_company', + 'get_limits', + 'paymentagent_list', + 'platform', + 'trading_platform_available_accounts', + 'trading_platform_accounts', + 'statement', + 'landing_company_details', + 'contracts_for', + 'residence_list', + 'account_security', + 'p2p_advertiser_info', + 'platform', + 'history', + 'amount', + 'run-proposal', +]; + +class APIMiddleware { + constructor(config) { + this.config = config; + this.debounced_calls = {}; + this.addGlobalMethod(); + } + + getRequestType = request => { + let req_type; + REQUESTS.forEach(type => { + if (type in request && !req_type) req_type = type; + }); + + return req_type; + }; + + log = (measures = [], is_bot_running) => { + if (measures && measures.length) { + measures.forEach(measure => { + datadogLogs.logger.info(measure.name, { + name: measure.name, + startTime: measure.startTimeDate, + duration: measure.duration, + detail: measure.detail, + isBotRunning: is_bot_running, + }); + }); + } + }; + + defineMeasure = res_type => { + if (res_type) { + let measure; + if (res_type === 'proposal') { + performance.mark('first_proposal_end'); + if (performance.getEntriesByName('bot-start', 'mark').length) { + measure = performance.measure('run-proposal', 'bot-start', 'first_proposal_end'); + performance.clearMarks('bot-start'); + } + } + if (res_type === 'history') { + performance.mark('ticks_history_end'); + measure = performance.measure('ticks_history', 'ticks_history_start', 'ticks_history_end'); + } else { + performance.mark(`${res_type}_end`); + measure = performance.measure(`${res_type}`, `${res_type}_start`, `${res_type}_end`); + } + return (measure.startTimeDate = new Date(Date.now() - measure.startTime)); + } + return false; + }; + + sendIsCalled = ({ response_promise, args: [request] }) => { + const req_type = this.getRequestType(request); + if (req_type) performance.mark(`${req_type}_start`); + response_promise.then(res => { + const res_type = this.getRequestType(res); + if (res_type) { + this.defineMeasure(res_type); + } + }); + return response_promise; + }; + + sendRequestsStatistic = is_bot_running => { + REQUESTS.forEach(req_type => { + const measure = performance.getEntriesByName(req_type); + if (measure && measure.length) { + this.log(measure, is_bot_running, req_type); + } + }); + performance.clearMeasures(); + }; + + addGlobalMethod() { + if (window) window.sendRequestsStatistic = this.sendRequestsStatistic; + } +} + +export default APIMiddleware; diff --git a/packages/bot-skeleton/src/services/api/appId.js b/packages/bot-skeleton/src/services/api/appId.js index e56a9931fbea..d745eaedda9f 100644 --- a/packages/bot-skeleton/src/services/api/appId.js +++ b/packages/bot-skeleton/src/services/api/appId.js @@ -1,12 +1,14 @@ import DerivAPIBasic from '@deriv/deriv-api/dist/DerivAPIBasic'; import { getAppId, getSocketURL, website_name } from '@deriv/shared'; import { getLanguage } from '@deriv/translations'; +import APIMiddleware from './api-middleware'; export const generateDerivApiInstance = () => { const socket_url = `wss://${getSocketURL()}/websockets/v3?app_id=${getAppId()}&l=${getLanguage()}&brand=${website_name.toLowerCase()}`; const deriv_socket = new WebSocket(socket_url); const deriv_api = new DerivAPIBasic({ connection: deriv_socket, + middleware: new APIMiddleware({}), }); return deriv_api; }; diff --git a/packages/bot-web-ui/package.json b/packages/bot-web-ui/package.json index c2e9dc401c09..6456cc12b46b 100644 --- a/packages/bot-web-ui/package.json +++ b/packages/bot-web-ui/package.json @@ -67,6 +67,7 @@ "webpack-cli": "^4.7.2" }, "dependencies": { + "@datadog/browser-logs": "^4.36.0", "@deriv/bot-skeleton": "^1.0.0", "@deriv/components": "^1.0.0", "@deriv/deriv-charts": "1.2.2", diff --git a/packages/bot-web-ui/src/stores/app-store.js b/packages/bot-web-ui/src/stores/app-store.js index 80f03c5e6e91..442359df96e9 100644 --- a/packages/bot-web-ui/src/stores/app-store.js +++ b/packages/bot-web-ui/src/stores/app-store.js @@ -23,11 +23,18 @@ export default class AppStore { this.core = core; this.dbot_store = null; this.api_helpers_store = null; + this.timer = null; } onMount() { - const { blockly_store } = this.root_store; + const { blockly_store, run_panel } = this.root_store; const { client, common } = this.core; + + this.timer = setInterval(() => { + window.sendRequestsStatistic(run_panel?.is_running); + performance.clearMeasures(); + }, 10000); + this.showDigitalOptionsMaltainvestError(client, common); blockly_store.setLoading(true); @@ -78,6 +85,10 @@ export default class AppStore { ui.setAccountSwitcherDisabledMessage(false); ui.setPromptHandler(false); + + if (this.timer) clearInterval(this.timer); + window.sendRequestsStatistic(false); + performance.clearMeasures(); } onBeforeUnload = event => { diff --git a/packages/bot-web-ui/src/stores/run-panel-store.js b/packages/bot-web-ui/src/stores/run-panel-store.js index 7f2902308324..e4f53be5f6da 100644 --- a/packages/bot-web-ui/src/stores/run-panel-store.js +++ b/packages/bot-web-ui/src/stores/run-panel-store.js @@ -147,6 +147,10 @@ export default class RunPanelStore { } async onRunButtonClick() { + performance.mark('bot-start'); + + window.sendRequestsStatistic(false); + performance.clearMeasures(); const { summary_card, route_prompt_dialog, self_exclusion } = this.root_store; const { client, ui } = this.core; const is_ios = mobileOSDetect() === 'iOS'; @@ -230,6 +234,8 @@ export default class RunPanelStore { if (this.error_type) { this.error_type = undefined; } + window.sendRequestsStatistic(true); + performance.clearMeasures(); } onClearStatClick() { diff --git a/packages/bot-web-ui/webpack.config.js b/packages/bot-web-ui/webpack.config.js index 4b25cddd325a..473580b22df5 100644 --- a/packages/bot-web-ui/webpack.config.js +++ b/packages/bot-web-ui/webpack.config.js @@ -119,6 +119,14 @@ module.exports = function (env) { 'process.env.GD_CLIENT_ID': JSON.stringify(process.env.GD_CLIENT_ID), 'process.env.GD_API_KEY': JSON.stringify(process.env.GD_API_KEY), 'process.env.GD_APP_ID': JSON.stringify(process.env.GD_APP_ID), + 'process.env.DATADOG_APPLICATION_ID': JSON.stringify(process.env.DATADOG_APPLICATION_ID), + 'process.env.DATADOG_CLIENT_TOKEN': JSON.stringify(process.env.DATADOG_CLIENT_TOKEN), + 'process.env.DATADOG_SESSION_REPLAY_SAMPLE_RATE': JSON.stringify( + process.env.DATADOG_SESSION_REPLAY_SAMPLE_RATE + ), + 'process.env.DATADOG_SESSION_SAMPLE_RATE': JSON.stringify(process.env.DATADOG_SESSION_SAMPLE_RATE), + 'process.env.CIRCLE_TAG': JSON.stringify(process.env.CIRCLE_TAG), + 'process.env.CIRCLE_JOB': JSON.stringify(process.env.CIRCLE_JOB), }), new CleanWebpackPlugin(), new MiniCssExtractPlugin({ diff --git a/packages/cashier/src/modules/cashier-onboarding/components/cashier-onboarding-side-notes/cashier-onboarding-side-note-fiat.tsx b/packages/cashier/src/modules/cashier-onboarding/components/cashier-onboarding-side-notes/cashier-onboarding-side-note-fiat.tsx index 4b5d68af2d36..309197583ce4 100644 --- a/packages/cashier/src/modules/cashier-onboarding/components/cashier-onboarding-side-notes/cashier-onboarding-side-note-fiat.tsx +++ b/packages/cashier/src/modules/cashier-onboarding/components/cashier-onboarding-side-notes/cashier-onboarding-side-note-fiat.tsx @@ -6,9 +6,10 @@ import { Localize, localize } from '@deriv/translations'; import { SideNoteCard } from '../../../../components/side-note-card'; const CashierOnboardingSideNoteFiat: React.FC = observer(() => { - const { client, ui } = useStore(); + const { client, ui, common } = useStore(); const { currency, loginid, is_eu, is_low_risk } = client; const { is_mobile } = ui; + const { is_from_derivgo } = common; const currency_code = getCurrencyDisplayCode(currency); const regulation_text = is_eu ? 'EU' : 'non-EU'; @@ -30,10 +31,10 @@ const CashierOnboardingSideNoteFiat: React.FC = observer(() => { components={[ window.LC_API.open_chat_window()} + className={!is_from_derivgo && 'cashier-onboarding-side-notes__link'} + onClick={() => (!is_from_derivgo ? window.LC_API.open_chat_window() : null)} />, ]} /> diff --git a/packages/cfd/src/Constants/jurisdiction-contents/jurisdiction-bvi-contents.ts b/packages/cfd/src/Constants/jurisdiction-contents/jurisdiction-bvi-contents.ts index 546b1f32420c..ec8472c84c21 100644 --- a/packages/cfd/src/Constants/jurisdiction-contents/jurisdiction-bvi-contents.ts +++ b/packages/cfd/src/Constants/jurisdiction-contents/jurisdiction-bvi-contents.ts @@ -8,7 +8,7 @@ export const getJurisdictionBviContents = (): TJurisdictionCardItems => ({ { key: 'assets', title: localize('Assets'), - description: localize('Synthetics, Basket indices and Derived FX'), + description: localize('Synthetics, Baskets and Derived FX'), title_indicators: { type: 'displayText', display_text: localize('40+'), diff --git a/packages/cfd/src/Constants/jurisdiction-contents/jurisdiction-svg-contents.ts b/packages/cfd/src/Constants/jurisdiction-contents/jurisdiction-svg-contents.ts index d854b6a1e0ed..c264d4e1ce26 100644 --- a/packages/cfd/src/Constants/jurisdiction-contents/jurisdiction-svg-contents.ts +++ b/packages/cfd/src/Constants/jurisdiction-contents/jurisdiction-svg-contents.ts @@ -8,7 +8,7 @@ export const getJurisdictionSvgContents = (): TJurisdictionCardItems => ({ { key: 'assets', title: localize('Assets'), - description: localize('Synthetics, Basket indices and Derived FX'), + description: localize('Synthetics, Baskets and Derived FX'), title_indicators: { type: 'displayText', display_text: localize('40+'), diff --git a/packages/cfd/src/Constants/jurisdiction-contents/jurisdiction-vanuatu-contents.ts b/packages/cfd/src/Constants/jurisdiction-contents/jurisdiction-vanuatu-contents.ts index 282d1dbc987c..72684d4eb236 100644 --- a/packages/cfd/src/Constants/jurisdiction-contents/jurisdiction-vanuatu-contents.ts +++ b/packages/cfd/src/Constants/jurisdiction-contents/jurisdiction-vanuatu-contents.ts @@ -8,7 +8,7 @@ export const getJurisdictionVanuatuContents = (): TJurisdictionCardItems => ({ { key: 'assets', title: localize('Assets'), - description: localize('Synthetics, Basket indices and Derived FX'), + description: localize('Synthetics, Baskets and Derived FX'), title_indicators: { type: 'displayText', display_text: localize('40+'), diff --git a/packages/cfd/src/Containers/jurisdiction-modal/__test__/jurisdiction-card.spec.tsx b/packages/cfd/src/Containers/jurisdiction-modal/__test__/jurisdiction-card.spec.tsx index 3f5a83307187..331130b1010d 100644 --- a/packages/cfd/src/Containers/jurisdiction-modal/__test__/jurisdiction-card.spec.tsx +++ b/packages/cfd/src/Containers/jurisdiction-modal/__test__/jurisdiction-card.spec.tsx @@ -185,7 +185,7 @@ describe('JurisdictionCard', () => { expect(screen.getByText('St. Vincent & Grenadines')).toBeInTheDocument(); expect(screen.getByText('Assets')).toBeInTheDocument(); expect(screen.getByText('40+')).toBeInTheDocument(); - expect(screen.getByText('Synthetics, Basket indices and Derived FX')).toBeInTheDocument(); + expect(screen.getByText('Synthetics, Baskets and Derived FX')).toBeInTheDocument(); expect(screen.getByText('Leverage')).toBeInTheDocument(); expect(screen.getByText('1:1000')).toBeInTheDocument(); expect(screen.getByText('Verifications')).toBeInTheDocument(); diff --git a/packages/cfd/src/Containers/jurisdiction-modal/__test__/jurisdiction-modal-content.spec.tsx b/packages/cfd/src/Containers/jurisdiction-modal/__test__/jurisdiction-modal-content.spec.tsx index a4c14c36278c..4caaf5f558bc 100644 --- a/packages/cfd/src/Containers/jurisdiction-modal/__test__/jurisdiction-modal-content.spec.tsx +++ b/packages/cfd/src/Containers/jurisdiction-modal/__test__/jurisdiction-modal-content.spec.tsx @@ -182,7 +182,7 @@ describe('JurisdictionModalContent', () => { it('should display content of 3 types of jurisdiction correctly for synthetics account', () => { render(); expect(screen.getAllByText('Assets')).toHaveLength(3); - expect(screen.getAllByText('Synthetics, Basket indices and Derived FX')).toHaveLength(3); + expect(screen.getAllByText('Synthetics, Baskets and Derived FX')).toHaveLength(3); expect(screen.getAllByText('40+')).toHaveLength(3); expect(screen.getAllByText('Leverage')).toHaveLength(3); expect(screen.getAllByText('1:1000')).toHaveLength(3); @@ -327,7 +327,7 @@ describe('JurisdictionModalContent', () => { expect(screen.getByText('Regulator/EDR')).toBeInTheDocument(); expect(screen.getByText('Deriv (SVG) LLC (company no. 273 LLC 2020)')).toBeInTheDocument(); expect(screen.getByText('40+')).toBeInTheDocument(); - expect(screen.getByText('Synthetics, Basket indices and Derived FX')).toBeInTheDocument(); + expect(screen.getByText('Synthetics, Baskets and Derived FX')).toBeInTheDocument(); }); it('should display cfd-jurisdiction-card--all__wrapper in class name', () => { diff --git a/packages/cfd/src/Containers/jurisdiction-modal/jurisdiction-modal.tsx b/packages/cfd/src/Containers/jurisdiction-modal/jurisdiction-modal.tsx index 317868656e5a..54405437302e 100644 --- a/packages/cfd/src/Containers/jurisdiction-modal/jurisdiction-modal.tsx +++ b/packages/cfd/src/Containers/jurisdiction-modal/jurisdiction-modal.tsx @@ -20,7 +20,7 @@ const JurisdictionModal = ({ const modal_title = show_eu_related_content ? localize('Choose a jurisdiction for your Deriv MT5 CFDs account') : localize('Choose a jurisdiction for your Deriv MT5 {{account_type}} account', { - account_type: getMT5Title(account_type.type), + account_type: localize(getMT5Title(account_type.type)), }); return ( diff --git a/packages/cfd/src/Containers/mt5-compare-table-content.tsx b/packages/cfd/src/Containers/mt5-compare-table-content.tsx index 40ab17dff174..95d2865eccf6 100644 --- a/packages/cfd/src/Containers/mt5-compare-table-content.tsx +++ b/packages/cfd/src/Containers/mt5-compare-table-content.tsx @@ -35,6 +35,7 @@ const Row = ({ content_flag, is_high_risk_for_mt5, CFDs_restricted_countries, + financial_restricted_countries, is_preappstore_restricted_cr_demo_account, }: TCompareAccountRowProps) => { const is_leverage_row = id === 'leverage'; @@ -53,10 +54,18 @@ const Row = ({ if (is_platform_row && is_pre_appstore_setting && CFDs_restricted_countries) { values.synthetic_bvi = { text: 'MT5' }; } - if (CFDs_restricted_countries) { + if (is_leverage_row) values.synthetic_bvi = { text: localize('Up to 1:1000') }; delete values.derivx; } + if (is_platform_row && financial_restricted_countries) { + values.financial_svg = { text: localize('MT5') }; + if ('financial_labuan' in values) values.financial_labuan = { text: localize('MT5') }; + } + // As we only show one account for Demo + if (content_flag === ContentFlag.CR_DEMO) { + delete values.financial_labuan; + } if (is_pre_appstore_setting && is_preappstore_restricted_cr_demo_account) { delete values.synthetic_bvi; @@ -183,6 +192,7 @@ const DMT5CompareModalContent = ({ is_eu_user, no_MF_account, CFDs_restricted_countries, + financial_restricted_countries, }: TDMT5CompareModalContentProps) => { const [has_submitted_personal_details, setHasSubmittedPersonalDetails] = React.useState(false); @@ -510,6 +520,7 @@ const DMT5CompareModalContent = ({ content_flag={content_flag} is_high_risk_for_mt5={is_high_risk_for_mt5} CFDs_restricted_countries={CFDs_restricted_countries} + financial_restricted_countries={financial_restricted_countries} is_preappstore_restricted_cr_demo_account={ is_preappstore_restricted_cr_demo_account } @@ -598,4 +609,5 @@ export default connect(({ modules, client, common, ui, traders_hub }: RootStore) is_eu_user: traders_hub.is_eu_user, no_MF_account: traders_hub.no_MF_account, CFDs_restricted_countries: traders_hub.CFDs_restricted_countries, + financial_restricted_countries: traders_hub.financial_restricted_countries, }))(DMT5CompareModalContent); diff --git a/packages/cfd/src/Containers/props.types.ts b/packages/cfd/src/Containers/props.types.ts index bddf68a8dc58..dee5ea971e20 100644 --- a/packages/cfd/src/Containers/props.types.ts +++ b/packages/cfd/src/Containers/props.types.ts @@ -327,6 +327,7 @@ export type TCompareAccountRowProps = TCompareAccountContentProps & { pre_appstore_class: string; is_high_risk_for_mt5: boolean; CFDs_restricted_countries: string[]; + financial_restricted_countries: string[]; is_preappstore_restricted_cr_demo_account: boolean; }; @@ -377,6 +378,7 @@ export type TDMT5CompareModalContentProps = { is_eu_user: boolean; no_MF_account: boolean; CFDs_restricted_countries: string[]; + financial_restricted_countries: string[]; }; export type TCFDDbviOnboardingProps = { diff --git a/packages/components/src/components/input-field/input-field.jsx b/packages/components/src/components/input-field/input-field.jsx index 79eb76bf6430..f7019cca2928 100644 --- a/packages/components/src/components/input-field/input-field.jsx +++ b/packages/components/src/components/input-field/input-field.jsx @@ -133,7 +133,7 @@ const InputField = ({ if (max_is_disabled) return; let increment_value; - const current_value = +(local_value || value); + const current_value = local_value || value; const decimal_places = current_value ? getDecimals(current_value) : 0; const is_crypto = !!currency && isCryptocurrency(currency); @@ -149,8 +149,9 @@ const InputField = ({ parseFloat(current_value || 0) + parseFloat(1 * 10 ** (0 - (decimal_point_change || decimal_places))); increment_value = parseFloat(new_value).toFixed(decimal_point_change || decimal_places); } else { - increment_value = parseFloat((current_value || 0) + 1).toFixed(decimal_places); + increment_value = parseFloat((+current_value || 0) + 1).toFixed(decimal_places); } + updateValue(increment_value, !!long_press_step); }; @@ -185,6 +186,7 @@ const InputField = ({ if (is_negative_disabled && decrement_value < 0) { return; } + updateValue(decrement_value, !!long_press_step); }; diff --git a/packages/components/src/components/mobile-dialog/mobile-dialog.tsx b/packages/components/src/components/mobile-dialog/mobile-dialog.tsx index 1f4c1a83f6c6..4407f7942071 100644 --- a/packages/components/src/components/mobile-dialog/mobile-dialog.tsx +++ b/packages/components/src/components/mobile-dialog/mobile-dialog.tsx @@ -66,8 +66,8 @@ const MobileDialog = (props: React.PropsWithChildren) => { // sometimes input is covered by virtual keyboard on mobile chrome, uc browser const handleClick = (e: React.MouseEvent) => { - e.stopPropagation(); const target = e.target as HTMLInputElement; + if (target.tagName !== 'A') e.stopPropagation(); if (target.tagName === 'INPUT' && target.type === 'number') { const scrollToTarget = () => scrollToElement(e.currentTarget, target); window.addEventListener('resize', scrollToTarget, false); diff --git a/packages/core/src/App/Containers/RealAccountSignup/real-account-signup.jsx b/packages/core/src/App/Containers/RealAccountSignup/real-account-signup.jsx index 125580a2ca35..51acaa22d6fe 100644 --- a/packages/core/src/App/Containers/RealAccountSignup/real-account-signup.jsx +++ b/packages/core/src/App/Containers/RealAccountSignup/real-account-signup.jsx @@ -213,7 +213,7 @@ const RealAccountSignup = ({ local_props.state_value.error_message || local_props.state_value.error_code?.message_to_client } code={local_props.state_value.error_code} - onConfirm={onErrorConfirm} + onConfirm={() => onErrorConfirm(local_props.state_value.error_code)} /> ), title: () => localize('Add a real account'), @@ -421,10 +421,10 @@ const RealAccountSignup = ({ redirectToLegacyPlatform(); }; - const onErrorConfirm = () => { + const onErrorConfirm = err_code => { setParams({ active_modal_index: - current_action === 'multi' + current_action === 'multi' || err_code === 'CurrencyTypeNotAllowed' ? modal_pages_indices.add_or_manage_account : modal_pages_indices.account_wizard, }); diff --git a/packages/core/src/App/Containers/RealAccountSignup/signup-error-content.jsx b/packages/core/src/App/Containers/RealAccountSignup/signup-error-content.jsx index c25878e51f9b..9be688fa45ae 100644 --- a/packages/core/src/App/Containers/RealAccountSignup/signup-error-content.jsx +++ b/packages/core/src/App/Containers/RealAccountSignup/signup-error-content.jsx @@ -98,6 +98,7 @@ const TryAgain = ({ text, onConfirm }) => ( const ErrorCTA = ({ code, onConfirm }) => { switch (code) { + case 'DuplicateCurrency': case 'CurrencyTypeNotAllowed': return ; case 'DuplicateAccount': diff --git a/packages/p2p/src/components/modal-manager/modals/email-verification-modal/__tests__/email-verification-modal.spec.js b/packages/p2p/src/components/modal-manager/modals/email-verification-modal/__tests__/email-verification-modal.spec.js new file mode 100644 index 000000000000..f5ac738cd655 --- /dev/null +++ b/packages/p2p/src/components/modal-manager/modals/email-verification-modal/__tests__/email-verification-modal.spec.js @@ -0,0 +1,79 @@ +import React from 'react'; +import { screen, render } from '@testing-library/react'; +import EmailVerificationModal from '../email-verification-modal'; +import userEvent from '@testing-library/user-event'; + +let mock_store; + +const el_modal = document.createElement('div'); + +jest.mock('Stores', () => ({ + ...jest.requireActual('Stores'), + useStores: jest.fn(() => mock_store), +})); + +jest.mock('Components/modal-manager/modal-manager-context', () => ({ + ...jest.requireActual('Components/modal-manager/modal-manager-context'), + useModalManagerContext: jest.fn(() => ({ + hideModal: jest.fn(), + is_modal_open: true, + })), +})); + +describe('EmailVerificationModal />', () => { + beforeEach(() => { + mock_store = { + order_store: { + confirmOrderRequest: jest.fn(), + order_information: { + id: 1, + }, + }, + }; + }); + beforeAll(() => { + el_modal.setAttribute('id', 'modal_root'); + document.body.appendChild(el_modal); + }); + afterAll(() => { + document.body.removeChild(el_modal); + }); + + it('should render EmailVerificationModal', () => { + render(); + + expect(screen.getByText('Check your email')).toBeInTheDocument(); + expect( + screen.getByText('Hit the link in the email we sent you to authorise this transaction.') + ).toBeInTheDocument(); + expect(screen.getByText('The link will expire in 10 minutes.')).toBeInTheDocument(); + }); + + it('should be able to click on didn`t receive email and setShouldShowReasonsIfNoEmail should be passing true', () => { + const setShouldShowReasonsIfNoEmailMock = jest.spyOn(React, 'useState'); + setShouldShowReasonsIfNoEmailMock.mockImplementation(initialValue => [initialValue, jest.fn()]); + + render(); + + const didntReceiveEmailText = screen.getByText("Didn't receive the email?"); + + userEvent.click(didntReceiveEmailText); + + expect(setShouldShowReasonsIfNoEmailMock).toHaveBeenCalled(); + }); + + // TODO: Add other checks for hideModal and setShouldShowReasonsIfNoEmail to be called when refactoring this component + it('should call confirmOrderRequest when clicking on Resend Email button', () => { + jest.spyOn(React, 'useState').mockImplementationOnce(() => React.useState(true)); + + render(); + + const resendEmail = screen.getByRole('button', { name: 'Resend email' }); + + userEvent.click(resendEmail); + + expect(mock_store.order_store.confirmOrderRequest).toHaveBeenCalledWith( + mock_store.order_store.order_information.id + ); + }); +}); diff --git a/packages/p2p/src/components/modal-manager/modals/email-verification-modal/email-verification-modal.jsx b/packages/p2p/src/components/modal-manager/modals/email-verification-modal/email-verification-modal.jsx index 929b1be95175..78c7abf95d98 100644 --- a/packages/p2p/src/components/modal-manager/modals/email-verification-modal/email-verification-modal.jsx +++ b/packages/p2p/src/components/modal-manager/modals/email-verification-modal/email-verification-modal.jsx @@ -15,7 +15,7 @@ const EmailVerificationModal = ( const { order_store } = useStores(); const { hideModal, is_modal_open } = useModalManagerContext(); const [should_show_reasons_if_no_email, setShouldShowReasonsIfNoEmail] = React.useState(false); - const { confirmOrderRequest, order_information, user_email_address } = order_store; + const { confirmOrderRequest, order_information } = order_store; return ( - ]} - values={{ user_email_address }} - /> + + + + {/* TODO: Uncomment when time is available in BE response */} - + { verification_pending === 0 && !is_buy_order_for_user && status_string !== 'Expired' && + status_string !== 'Under dispute' && order_store.error_code !== api_error_codes.EXCESSIVE_VERIFICATION_REQUESTS ) { showModal({ key: 'EmailLinkExpiredModal' }, { should_stack_modal: isMobile() }); diff --git a/packages/shared/src/utils/helpers/barrier.ts b/packages/shared/src/utils/helpers/barrier.ts index 079e7e7ccfe1..f5d7c5429a27 100644 --- a/packages/shared/src/utils/helpers/barrier.ts +++ b/packages/shared/src/utils/helpers/barrier.ts @@ -61,6 +61,6 @@ export const getAccumulatorBarriers = ( }; export const getBarrierPipSize = (barrier: string) => { - if (Math.floor(+barrier) === +barrier || barrier.length < 1 || +barrier % 1 === 0 || isNaN(+barrier)) return 0; - return barrier.toString().split('.')[1].length || 0; + if (barrier.length < 1 || isNaN(+barrier)) return 0; + return barrier.split('.')[1]?.length || 0; }; diff --git a/packages/stores/src/mockStore.ts b/packages/stores/src/mockStore.ts index 5a4ae83af7a2..4183faa31f9a 100644 --- a/packages/stores/src/mockStore.ts +++ b/packages/stores/src/mockStore.ts @@ -5,11 +5,28 @@ const mock = (): TStores & { is_mock: boolean } => { return { is_mock: true, client: { + fetchResidenceList: jest.fn(), + fetchStatesList: jest.fn(), + getChangeableFields: jest.fn(), + residence_list: [ + { + text: 'Text', + value: 'value', + }, + ], + states_list: [ + { + text: 'Text', + value: 'value', + }, + ], account_settings: {}, accounts: {}, + is_social_signup: false, active_account_landing_company: '', trading_platform_available_accounts: [], account_limits: { + account_balance: 300000, daily_transfers: { dxtrade: { allowed: 0, @@ -24,6 +41,68 @@ const mock = (): TStores & { is_mock: boolean } => { available: 0, }, }, + lifetime_limit: 13907.43, + market_specific: { + commodities: [ + { + name: 'Commodities', + payout_limit: 5000, + profile_name: 'moderate_risk', + turnover_limit: 50000, + }, + ], + cryptocurrency: [ + { + name: 'Cryptocurrencies', + payout_limit: 100.0, + profile_name: 'extreme_risk', + turnover_limit: 1000.0, + }, + ], + forex: [ + { + name: 'Smart FX', + payout_limit: 5000, + profile_name: 'moderate_risk', + turnover_limit: 50000, + }, + { + name: 'Major Pairs', + payout_limit: 20000, + profile_name: 'medium_risk', + turnover_limit: 100000, + }, + { + name: 'Minor Pairs', + payout_limit: 5000, + profile_name: 'moderate_risk', + turnover_limit: 50000, + }, + ], + indices: [ + { + name: 'Stock Indices', + payout_limit: 20000, + profile_name: 'medium_risk', + turnover_limit: 100000, + }, + ], + synthetic_index: [ + { + name: 'Synthetic Indices', + payout_limit: 50000, + profile_name: 'low_risk', + turnover_limit: 500000, + }, + ], + }, + num_of_days: 30, + num_of_days_limit: 13907.43, + open_positions: 100, + payout: 50000, + remainder: 13907.43, + withdrawal_for_x_days_monetary: 0, + withdrawal_since_inception_monetary: 0, }, account_status: { authentication: { @@ -49,6 +128,7 @@ const mock = (): TStores & { is_mock: boolean } => { document: { status: 'verified', }, + identity: { services: { idv: { @@ -113,7 +193,8 @@ const mock = (): TStores & { is_mock: boolean } => { current_fiat_currency: '', cfd_score: 0, setCFDScore: jest.fn(), - getLimits: jest.fn(), + getLimits: jest.fn(() => Promise.resolve({ get_limits: {} })), + has_any_real_account: false, has_active_real_account: false, has_logged_out: false, has_maltainvest_account: false, @@ -123,6 +204,9 @@ const mock = (): TStores & { is_mock: boolean } => { is_deposit_lock: false, is_dxtrade_allowed: false, is_eu: false, + is_uk: false, + has_residence: false, + is_fully_authenticated: false, is_financial_account: false, is_financial_information_incomplete: false, is_low_risk: false, @@ -149,7 +233,11 @@ const mock = (): TStores & { is_mock: boolean } => { responseTradingPlatformAccountsList: jest.fn(), standpoint: { iom: '', + svg: '', malta: '', + maltainvest: '', + gaming_company: '', + financial_company: '', }, switchAccount: jest.fn(), verification_code: { @@ -194,7 +282,6 @@ const mock = (): TStores & { is_mock: boolean } => { setTwoFAStatus: jest.fn(), has_changed_two_fa: false, setTwoFAChangedStatus: jest.fn(), - has_any_real_account: false, real_account_creation_unlock_date: 0, setPrevAccountType: jest.fn(), }, @@ -211,6 +298,8 @@ const mock = (): TStores & { is_mock: boolean } => { redirectOnClick: jest.fn(), setError: jest.fn(), }, + current_language: 'EN', + isCurrentLanguage: jest.fn(), is_from_derivgo: false, has_error: false, platform: '', @@ -218,7 +307,6 @@ const mock = (): TStores & { is_mock: boolean } => { routeTo: jest.fn(), changeCurrentLanguage: jest.fn(), changeSelectedLanguage: jest.fn(), - current_language: 'EN', is_network_online: false, server_time: undefined, is_language_changing: false, @@ -258,7 +346,9 @@ const mock = (): TStores & { is_mock: boolean } => { toggleReports: jest.fn(), setSubSectionIndex: jest.fn(), sub_section_index: 0, + toggleShouldShowRealAccountsList: jest.fn(), toggleReadyToDepositModal: jest.fn(), + is_tablet: false, is_ready_to_deposit_modal_visible: false, is_real_acc_signup_on: false, is_need_real_account_for_cashier_modal_visible: false, @@ -308,6 +398,7 @@ const mock = (): TStores & { is_mock: boolean } => { setP2POrderProps: jest.fn(), showAccountSwitchToRealNotification: jest.fn(), setP2PRedirectTo: jest.fn(), + addNotificationMessageByKey: jest.fn(), }, portfolio: { active_positions: [], diff --git a/packages/stores/types.ts b/packages/stores/types.ts index 81c6c886a067..b79e405754ac 100644 --- a/packages/stores/types.ts +++ b/packages/stores/types.ts @@ -6,6 +6,8 @@ import type { GetLimits, GetSettings, LogOutResponse, + ResidenceList, + StatesList, ProposalOpenContract, } from '@deriv/api-types'; import type { Moment } from 'moment'; @@ -18,6 +20,33 @@ type TPopulateSettingsExtensionsMenuItem = { value: (props: T) => JSX.Element; }; +type TAccountLimitsCollection = { + level?: string; + name: string; + payout_limit: number; + profile_name: string; + turnover_limit: number; +}; +type TAccount_limits = { + api_initial_load_error?: string; + open_positions?: React.ReactNode; + account_balance: string | number; + daily_transfers?: object; + payout: string | number; + lifetime_limit?: number; + market_specific: { + commodities: TAccountLimitsCollection[]; + cryptocurrency: TAccountLimitsCollection[]; + forex: TAccountLimitsCollection[]; + indices: TAccountLimitsCollection[]; + synthetic_index: TAccountLimitsCollection[]; + }; + num_of_days?: number; + num_of_days_limit: string | number; + remainder: string | number; + withdrawal_for_x_days_monetary?: number; + withdrawal_since_inception_monetary: string | number; +}; type TAccount = NonNullable[0] & { balance?: number; }; @@ -128,6 +157,9 @@ type TNotification = | ((excluded_until: number) => TNotificationMessage); type TClientStore = { + fetchResidenceList: () => Promise; + fetchStatesList: () => Promise; + getChangeableFields: () => string[]; accounts: { [k: string]: TActiveAccount }; active_accounts: TActiveAccount[]; active_account_landing_company: string; @@ -145,8 +177,11 @@ type TClientStore = { cfd_score: number; setCFDScore: (score: number) => void; currency: string; + residence_list: ResidenceList; + states_list: StatesList; current_currency_type?: string; current_fiat_currency?: string; + has_any_real_account: boolean; getLimits: () => Promise<{ get_limits?: GetLimits }>; has_active_real_account: boolean; has_logged_out: boolean; @@ -156,6 +191,9 @@ type TClientStore = { is_deposit_lock: boolean; is_dxtrade_allowed: boolean; is_eu: boolean; + is_uk: boolean; + is_social_signup: boolean; + has_residence: boolean; is_authorize: boolean; is_financial_account: boolean; is_financial_information_incomplete: boolean; @@ -191,7 +229,11 @@ type TClientStore = { }) => DetailsOfEachMT5Loginid[]; standpoint: { iom: string; + svg: string; malta: string; + maltainvest: string; + gaming_company: string; + financial_company: string; }; setAccountStatus: (status?: GetAccountStatus) => void; setBalanceOtherAccounts: (balance: number) => void; @@ -230,7 +272,7 @@ type TClientStore = { setTwoFAStatus: (status: boolean) => void; has_changed_two_fa: boolean; setTwoFAChangedStatus: (status: boolean) => void; - has_any_real_account: boolean; + is_fully_authenticated: boolean; real_account_creation_unlock_date: number; setPrevAccountType: (account_type: string) => void; }; @@ -249,6 +291,7 @@ type TCommonStoreError = { }; type TCommonStore = { + isCurrentLanguage(language_code: string): boolean; error: TCommonStoreError; has_error: boolean; is_from_derivgo: boolean; @@ -276,6 +319,8 @@ type TUiStore = { is_reports_visible: boolean; is_language_settings_modal_on: boolean; is_mobile: boolean; + sub_section_index: number; + toggleShouldShowRealAccountsList: (value: boolean) => void; openRealAccountSignup: ( value: 'maltainvest' | 'svg' | 'add_crypto' | 'choose' | 'add_fiat' | 'set_currency' | 'manage' ) => void; @@ -285,7 +330,6 @@ type TUiStore = { setReportsTabIndex: (value: number) => void; setIsClosingCreateRealAccountModal: (value: boolean) => void; setRealAccountSignupEnd: (status: boolean) => void; - sub_section_index: number; setSubSectionIndex: (index: number) => void; shouldNavigateAfterChooseCrypto: (value: string) => void; toggleAccountsDialog: () => void; @@ -293,6 +337,7 @@ type TUiStore = { toggleLanguageSettingsModal: () => void; toggleReadyToDepositModal: () => void; toggleSetCurrencyModal: () => void; + is_tablet: boolean; removeToast: (key: string) => void; is_ready_to_deposit_modal_visible: boolean; reports_route_tab_index: number; @@ -331,12 +376,13 @@ type TMenuStore = { }; type TNotificationStore = { + addNotificationMessageByKey: (key: string) => void; addNotificationMessage: (message: TNotification) => void; client_notifications: object; filterNotificationMessages: () => void; refreshNotifications: () => void; - removeNotificationByKey: (obj: { key: string }) => void; - removeNotificationMessage: (obj: { key: string; should_show_again?: boolean }) => void; + removeNotificationByKey: (key: string) => void; + removeNotificationMessage: (key: string, should_show_again?: boolean) => void; setP2POrderProps: () => void; showAccountSwitchToRealNotification: (loginid: string, currency: string) => void; setP2PRedirectTo: () => void; diff --git a/packages/trader/src/App/Components/Elements/ContractAudit/__tests__/contract-details.spec.js b/packages/trader/src/App/Components/Elements/ContractAudit/__tests__/contract-details.spec.js new file mode 100644 index 000000000000..5dc927b50738 --- /dev/null +++ b/packages/trader/src/App/Components/Elements/ContractAudit/__tests__/contract-details.spec.js @@ -0,0 +1,80 @@ +import React from 'react'; +import { screen, render } from '@testing-library/react'; +import ContractDetails from '../contract-details'; + +describe('ContractDetails', () => { + const contract_info = { + account_id: 73816028, + barrier: '1460.00', + barrier_count: 1, + bid_price: 1.9, + buy_price: 2, + contract_id: 210660718708, + contract_type: 'VANILLALONGCALL', + currency: 'USD', + current_spot: 1458.01, + current_spot_display_value: '1458.01', + current_spot_time: 1686895544, + date_expiry: 1687046399, + date_settlement: 1687046400, + date_start: 1686895542, + display_name: 'Volatility 100 (1s) Index', + entry_spot: 1458.17, + entry_spot_display_value: '1458.17', + entry_tick: 1458.17, + entry_tick_display_value: '1458.17', + entry_tick_time: 1686895541, + expiry_time: 1687046399, + id: '1c1fd73a-daeb-05df-47f3-f70aa09146e4', + is_expired: 0, + is_forward_starting: 0, + is_intraday: 0, + is_path_dependent: 0, + is_settleable: 0, + is_sold: 0, + is_valid_to_cancel: 0, + is_valid_to_sell: 1, + longcode: 'Your payout will be 0.04958 for each point above 1460.00 at expiry time', + number_of_contracts: 0.04958, + profit: -0.1, + profit_percentage: -5, + purchase_time: 1686895542, + shortcode: 'VANILLALONGCALL_1HZ100V_2.00_1686895542_1687046399_1460000000_0.04958_1686895541', + status: 'open', + transaction_ids: { buy: 420381262708 }, + underlying: '1HZ100V', + }; + + it('renders the ContractAuditItems specific to Vanilla component when is_vanilla is true', () => { + const wrapper = render( + + ); + expect(wrapper.queryAllByTestId('dt_bt_label')).toHaveLength(2); + }); + + it('renders the Payout per point label when is_vanilla is true', () => { + render( + + ); + + expect(screen.getByText('Payout per point')).toBeInTheDocument(); + }); +}); diff --git a/packages/trader/src/App/Components/Elements/ContractAudit/contract-audit-item.jsx b/packages/trader/src/App/Components/Elements/ContractAudit/contract-audit-item.jsx index 067b3883c65e..efd400b90269 100644 --- a/packages/trader/src/App/Components/Elements/ContractAudit/contract-audit-item.jsx +++ b/packages/trader/src/App/Components/Elements/ContractAudit/contract-audit-item.jsx @@ -4,7 +4,7 @@ import { formatDate, formatTime } from '@deriv/shared'; import { Text } from '@deriv/components'; const ContractAuditItem = ({ icon, id, label, timestamp, value, value2 }) => ( -
+
{icon &&
{icon}
}
diff --git a/packages/trader/src/App/Components/Elements/ContractAudit/contract-details.jsx b/packages/trader/src/App/Components/Elements/ContractAudit/contract-details.jsx index 9547a6725a85..d13461c7c3e4 100644 --- a/packages/trader/src/App/Components/Elements/ContractAudit/contract-details.jsx +++ b/packages/trader/src/App/Components/Elements/ContractAudit/contract-details.jsx @@ -4,7 +4,6 @@ import { Money, Icon, ThemedScrollbars } from '@deriv/components'; import { localize } from '@deriv/translations'; import { epochToMoment, - formatMoney, toGMTFormat, getCancellationPrice, isAccumulatorContract, @@ -103,11 +102,8 @@ const ContractDetails = ({ contract_end_time, contract_info, duration, duration_ id='dt_bt_label' icon={} label={localize('Payout per point')} - value={ - `${formatMoney(currency, number_of_contracts, true)} ${getCurrencyDisplayCode( - currency - )}` || ' - ' - } + value={`${number_of_contracts} ${getCurrencyDisplayCode(currency)}` || ' - '} + should_format={!is_vanilla} /> )} diff --git a/packages/trader/src/Modules/Trading/Components/Form/Purchase/contract-info.jsx b/packages/trader/src/Modules/Trading/Components/Form/Purchase/contract-info.jsx index fd0089c488a3..b33394dea8b3 100644 --- a/packages/trader/src/Modules/Trading/Components/Form/Purchase/contract-info.jsx +++ b/packages/trader/src/Modules/Trading/Components/Form/Purchase/contract-info.jsx @@ -25,6 +25,7 @@ export const ValueMovement = ({ })} currency={currency} show_currency={show_currency} + should_format={!is_vanilla} /> )}
@@ -86,12 +87,7 @@ const ContractInfo = ({ if (['VANILLALONGCALL', 'VANILLALONGPUT'].includes(type)) { return ( ]} - values={{ - trade_type: type === 'VANILLALONGCALL' ? localize('above') : localize('below'), - title: type === 'VANILLALONGCALL' ? localize('Call') : localize('Put'), - }} + i18n_default_text='The payout at expiry is equal to the payout per point multiplied by the difference between the final price and the strike price.' /> ); } @@ -162,16 +158,7 @@ const ContractInfo = ({ zIndex={9999} message={ ]} - values={{ - trade_type: - type === 'VANILLALONGCALL' - ? localize('above') - : localize('below'), - title: - type === 'VANILLALONGCALL' ? localize('Call') : localize('Put'), - }} + i18n_default_text='The payout at expiry is equal to the payout per point multiplied by the difference between the final price and the strike price.' /> } /> diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/advanced-duration.jsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/advanced-duration.jsx index 1fea7a11d937..2d52cecf4291 100644 --- a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/advanced-duration.jsx +++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/advanced-duration.jsx @@ -7,6 +7,7 @@ import { toMoment, hasIntradayDurationUnit } from '@deriv/shared'; import RangeSlider from 'App/Components/Form/RangeSlider'; import TradingDatePicker from '../../DatePicker'; import TradingTimePicker from '../../TimePicker'; +import ExpiryText from './expiry-text.jsx'; import { observer, useStore } from '@deriv/stores'; import { useTraderStore } from 'Stores/useTraderStores'; @@ -14,13 +15,14 @@ const AdvancedDuration = observer( ({ advanced_duration_unit, advanced_expiry_type, - duration_units_list, - duration_t, changeDurationUnit, - getDurationFromUnit, + duration_t, + duration_units_list, expiry_date, + expiry_epoch, expiry_list, expiry_type, + getDurationFromUnit, number_input_props, onChange, onChangeUiStore, @@ -30,7 +32,7 @@ const AdvancedDuration = observer( }) => { const { ui } = useStore(); const { current_focus, setCurrentFocus } = ui; - const { contract_expiry_type, validation_errors } = useTraderStore(); + const { contract_expiry_type, is_vanilla, validation_errors } = useTraderStore(); let is_24_hours_contract = false; @@ -52,6 +54,8 @@ const AdvancedDuration = observer( onChangeUiStore({ name, value }); }; + const has_error = !!validation_errors?.duration?.length; + return ( <> {expiry_list.length > 1 && ( @@ -92,6 +96,9 @@ const AdvancedDuration = observer( is_24_hours_contract={is_24_hours_contract} /> )} + {advanced_duration_unit === 'd' && is_vanilla && ( + + )} {advanced_duration_unit !== 't' && advanced_duration_unit !== 'd' && ( // validation_errors={validation_errors.end_time} TODO: add validation_errors for end time } + {!is_24_hours_contract && is_vanilla && ( + + )}
)} diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration-mobile.jsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration-mobile.jsx index d6622b27a3ad..c53ae8a02a1c 100644 --- a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration-mobile.jsx +++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration-mobile.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Tabs, TickPicker, Numpad, RelativeDatepicker } from '@deriv/components'; +import { Tabs, TickPicker, Numpad, RelativeDatepicker, Text } from '@deriv/components'; import { isEmptyObject, addComma, getDurationMinMaxValues } from '@deriv/shared'; import { Localize, localize } from '@deriv/translations'; @@ -90,16 +90,19 @@ const Ticks = observer( const Numbers = observer( ({ - setDurationError, basis_option, - toggleModal, + contract_expiry = 'intraday', duration_unit_option, + duration_values, + expiry_epoch, has_amount_error, - contract_expiry = 'intraday', + is_vanilla, payout_value, - stake_value, selected_duration, + setDurationError, setSelectedDuration, + stake_value, + toggleModal, }) => { const { ui } = useStore(); const { addToast } = ui; @@ -113,6 +116,7 @@ const Numbers = observer( } = useTraderStore(); const { value: duration_unit } = duration_unit_option; const [min, max] = getDurationMinMaxValues(duration_min_max, contract_expiry, duration_unit); + const [has_error, setHasError] = React.useState(false); const validateDuration = value => { const localized_message = ( @@ -127,17 +131,21 @@ const Numbers = observer( if (parseInt(value) < min || parseInt(selected_duration) > max) { addToast({ key: 'duration_error', content: localized_message, type: 'error', timeout: 2000 }); setDurationError(true); + setHasError(true); return 'error'; } else if (parseInt(value) > max) { addToast({ key: 'duration_error', content: localized_message, type: 'error', timeout: 2000 }); + setHasError(true); return 'error'; } else if (value.toString().length < 1) { addToast({ key: 'duration_error', content: localized_message, type: 'error', timeout: 2000 }); setDurationError(true); + setHasError(true); return false; } setDurationError(false); + setHasError(false); return true; }; @@ -158,6 +166,16 @@ const Numbers = observer( toggleModal(); }; + const setExpiryDate = (epoch, duration) => { + let expiry_date = new Date((epoch - (trade_duration * 24 * 60 * 60)) * 1000); + + if (duration) { + expiry_date = new Date(expiry_date.getTime() + (duration) * 24 * 60 * 60 * 1000); + } + + return expiry_date.toUTCString().replace('GMT', 'GMT +0').substring(5).replace(/(\d{2}) (\w{3} \d{4})/, '$1 $2,'); + } + const onNumberChange = num => { setSelectedDuration(duration_unit, num); validateDuration(num); @@ -165,6 +183,14 @@ const Numbers = observer( return (
+ {is_vanilla && ( + + + + )} { const { duration_units_list, duration_min_max, duration_unit, basis: trade_basis } = useTraderStore(); const duration_values = { @@ -315,6 +343,9 @@ const Duration = observer( setSelectedDuration={setSelectedDuration} stake_value={stake_value} payout_value={payout_value} + expiry_epoch={expiry_epoch} + is_vanilla={is_vanilla} + duration_values={duration_values} /> { duration_min_max, expiry_type, expiry_date, + expiry_epoch, expiry_time, start_date, market_open_times, @@ -34,26 +35,27 @@ const DurationWrapper = observer(() => { } = useTraderStore(); const duration_props = { - advanced_expiry_type, advanced_duration_unit, - getDurationFromUnit, - is_advanced_duration, - onChangeUiStore, - simple_duration_unit, + advanced_expiry_type, contract_expiry_type, - duration, - duration_unit, + contract_type, + duration_min_max, duration_t, + duration_unit, duration_units_list, - duration_min_max, - expiry_type, + duration, expiry_date, + expiry_epoch, expiry_time, - start_date, + expiry_type, + getDurationFromUnit, + is_advanced_duration, market_open_times, onChange, onChangeMultiple, - contract_type, + onChangeUiStore, + simple_duration_unit, + start_date, }; const hasDurationUnit = (duration_type, is_advanced) => { diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration.jsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration.jsx index 3cac13bb55ac..82eec441f0a0 100644 --- a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration.jsx +++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/Duration/duration.jsx @@ -14,27 +14,28 @@ import SimpleDuration from './simple-duration.jsx'; const Duration = ({ advanced_duration_unit, advanced_expiry_type, - duration, + contract_type, + duration_t, duration_unit, duration_units_list, - duration_t, + duration, expiry_date, + expiry_epoch, expiry_time, expiry_type, getDurationFromUnit, hasDurationUnit, is_advanced_duration, is_minimized, - min_value, + market_open_times, max_value, + min_value, onChange, - onChangeUiStore, onChangeMultiple, - simple_duration_unit, + onChangeUiStore, server_time, + simple_duration_unit, start_date, - market_open_times, - contract_type, }) => { React.useEffect(() => { if (contract_type === 'vanilla') { @@ -163,12 +164,13 @@ const Duration = ({ <> {is_advanced_duration && ( { + const formatted_date = + expiry_epoch && !has_error + ? new Date(expiry_epoch * 1000) + .toUTCString() + .replace('GMT', 'GMT +0') + .substring(5) + .replace(/(\d{2}) (\w{3} \d{4})/, '$1 $2,') + : ''; + + return ( + + + + ); +}; + +export default ExpiryText; diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/amount.jsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/amount.jsx index 8432a61225e1..978d26394d8d 100644 --- a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/amount.jsx +++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/amount.jsx @@ -99,10 +99,6 @@ const Amount = observer(({ is_minimized, is_nativepicker }) => { return ( ); - } else if (contract_type === 'vanilla') { - return ( - - ); } return null; }; diff --git a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/strike.jsx b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/strike.jsx index e7a894956939..3190606654e9 100644 --- a/packages/trader/src/Modules/Trading/Components/Form/TradeParams/strike.jsx +++ b/packages/trader/src/Modules/Trading/Components/Form/TradeParams/strike.jsx @@ -75,13 +75,13 @@ const Strike = observer(() => { header={localize('Strike price')} header_tooltip={ ]} values={{ trade_type: vanilla_trade_type === 'VANILLALONGCALL' - ? localize('For Call') - : localize('For Put'), + ? localize('Call') + : localize('Put'), payout_status: vanilla_trade_type === 'VANILLALONGCALL' ? localize('above') : localize('below'), }} diff --git a/packages/trader/src/Modules/Trading/Containers/strike-param-modal.jsx b/packages/trader/src/Modules/Trading/Containers/strike-param-modal.jsx index a83225974255..435dc5a80bda 100644 --- a/packages/trader/src/Modules/Trading/Containers/strike-param-modal.jsx +++ b/packages/trader/src/Modules/Trading/Containers/strike-param-modal.jsx @@ -24,13 +24,13 @@ const StrikeParamModal = ({ is_open, toggleModal, strike, onChange, name, strike is_bubble_hover_enabled message={ ]} values={{ trade_type: vanilla_trade_type === 'VANILLALONGCALL' - ? localize('For Call') - : localize('For Put'), + ? localize('Call') + : localize('Put'), payout_status: vanilla_trade_type === 'VANILLALONGCALL' ? localize('above') : localize('below'), }} diff --git a/packages/trader/src/Modules/Trading/Containers/trade-params-mobile.jsx b/packages/trader/src/Modules/Trading/Containers/trade-params-mobile.jsx index 382cb95b7e0b..8a5873c816a9 100644 --- a/packages/trader/src/Modules/Trading/Containers/trade-params-mobile.jsx +++ b/packages/trader/src/Modules/Trading/Containers/trade-params-mobile.jsx @@ -1,6 +1,6 @@ import 'Sass/app/modules/trading-mobile.scss'; -import { Div100vhContainer, Modal, Money, Tabs, ThemedScrollbars, usePreventIOSZoom, Popover } from '@deriv/components'; +import { Div100vhContainer, Modal, Money, Tabs, ThemedScrollbars, usePreventIOSZoom } from '@deriv/components'; import AmountMobile from 'Modules/Trading/Components/Form/TradeParams/amount-mobile.jsx'; import Barrier from 'Modules/Trading/Components/Form/TradeParams/barrier.jsx'; @@ -10,7 +10,7 @@ import { observer, useStore } from '@deriv/stores'; import { useTraderStore } from 'Stores/useTraderStores'; import React from 'react'; import classNames from 'classnames'; -import { localize, Localize } from '@deriv/translations'; +import { localize } from '@deriv/translations'; const DEFAULT_DURATION = Object.freeze({ t: 5, @@ -34,7 +34,7 @@ const TradeParamsModal = observer(({ is_open, toggleModal }) => { const { client, ui } = useStore(); const { currency } = client; const { enableApp, disableApp } = ui; - const { amount, form_components, duration, duration_unit, duration_units_list, is_vanilla } = useTraderStore(); + const { amount, form_components, duration, duration_unit, duration_units_list } = useTraderStore(); // eslint-disable-next-line react-hooks/exhaustive-deps const getDefaultDuration = React.useCallback(makeGetDefaultDuration(duration, duration_unit), []); @@ -91,28 +91,6 @@ const TradeParamsModal = observer(({ is_open, toggleModal }) => { const isVisible = component_key => form_components.includes(component_key); - const setTooltipContent = () => { - if (is_vanilla && state.trade_param_tab_idx === 1) - return ( -
- - } - classNameWrapper='trade-params--modal-wrapper' - classNameBubble='trade-params--modal-wrapper__content--vanilla' - /> -
- ); - return null; - }; - return ( { toggleModal={toggleModal} height='auto' width='calc(100vw - 32px)' - renderTitle={setTooltipContent} > @@ -197,7 +174,7 @@ const TradeParamsMobile = observer( h_duration, d_duration, }) => { - const { basis_list, basis, is_vanilla } = useTraderStore(); + const { basis_list, basis, is_vanilla, expiry_epoch } = useTraderStore(); const getDurationText = () => { const duration = duration_units_list.find(d => d.value === duration_unit); return `${duration_value} ${ @@ -270,6 +247,8 @@ const TradeParamsMobile = observer( d_duration={d_duration} stake_value={stake_value} payout_value={payout_value} + is_vanilla={is_vanilla} + expiry_epoch={expiry_epoch} />
)} diff --git a/packages/trader/src/Modules/Trading/Containers/trade.jsx b/packages/trader/src/Modules/Trading/Containers/trade.jsx index ef6c38c08555..1f9d85ea068a 100644 --- a/packages/trader/src/Modules/Trading/Containers/trade.jsx +++ b/packages/trader/src/Modules/Trading/Containers/trade.jsx @@ -57,7 +57,7 @@ const Trade = observer(() => { should_show_multipliers_onboarding, is_dark_mode_on: is_dark_theme, } = ui; - const { is_eu, is_virtual } = client; + const { is_eu } = client; const { network_status } = common; const [digits, setDigits] = React.useState([]); @@ -224,11 +224,7 @@ const Trade = observer(() => { 0 && - network_status.class === 'online' && - // TODO: delete the below line for releasing ACCU trade for real - (is_virtual || !form_components.includes('accumulator')) + is_trade_enabled && form_components.length > 0 && network_status.class === 'online' } />
diff --git a/packages/trader/src/Stores/Modules/Trading/trade-store.js b/packages/trader/src/Stores/Modules/Trading/trade-store.js index 96d4fa4a9086..9988ef8bef74 100644 --- a/packages/trader/src/Stores/Modules/Trading/trade-store.js +++ b/packages/trader/src/Stores/Modules/Trading/trade-store.js @@ -78,10 +78,11 @@ export default class TradeStore extends BaseStore { // Duration duration = 5; + duration_min_max = {}; duration_unit = ''; duration_units_list = []; - duration_min_max = {}; expiry_date = ''; + expiry_epoch = ''; expiry_time = ''; expiry_type = 'duration'; @@ -223,6 +224,7 @@ export default class TradeStore extends BaseStore { duration: observable, expiration: observable, expiry_date: observable, + expiry_epoch: observable, expiry_time: observable, expiry_type: observable, form_components: observable, @@ -265,8 +267,8 @@ export default class TradeStore extends BaseStore { strike_price_choices: observable, symbol: observable, take_profit: observable, - ticks_history_stats: observable, tick_size_barrier: observable, + ticks_history_stats: observable, trade_types: observable, accountSwitcherListener: action.bound, barrier_pipsize: computed, @@ -550,23 +552,10 @@ export default class TradeStore extends BaseStore { await Symbol.onChangeSymbolAsync(this.symbol); runInAction(() => { const contract_categories = ContractType.getContractCategories(); - //TODO yauheni, maryia - delete this 'if' statement when accumulators are allowed for real account, should leave 'else' box - if ( - this.is_accumulator && - !this.root_store.client.is_virtual && - contract_categories.contract_types_list.Accumulators - ) { - delete contract_categories.contract_types_list.Accumulators; - this.processNewValuesAsync({ - ...contract_categories, - ...ContractType.getContractType(contract_categories.contract_types_list), - }); - } else { - this.processNewValuesAsync({ - ...contract_categories, - ...ContractType.getContractType(contract_categories.contract_types_list, this.contract_type), - }); - } + this.processNewValuesAsync({ + ...contract_categories, + ...ContractType.getContractType(contract_categories.contract_types_list, this.contract_type), + }); this.processNewValuesAsync(ContractType.getContractValues(this)); }); } @@ -1031,11 +1020,6 @@ export default class TradeStore extends BaseStore { } this.debouncedProposal(); } - - //TODO yauheni, maryia - delete this 'if' statement when accumulators are allowed for real account - if (!this.root_store.client.is_virtual) { - delete this.contract_types_list.Accumulators; - } } get is_synthetics_available() { @@ -1130,6 +1114,7 @@ export default class TradeStore extends BaseStore { // add/update expiration or date_expiry for crypto indices from proposal const date_expiry = response.proposal?.date_expiry; + this.expiry_epoch = date_expiry || this.expiry_epoch; if (!response.error && !!date_expiry && this.is_crypto_multiplier) { this.expiration = date_expiry; diff --git a/packages/trader/src/sass/app/_common/components/contract-type-dialog.scss b/packages/trader/src/sass/app/_common/components/contract-type-dialog.scss index bbfbb34b4947..73fca5a74fe2 100644 --- a/packages/trader/src/sass/app/_common/components/contract-type-dialog.scss +++ b/packages/trader/src/sass/app/_common/components/contract-type-dialog.scss @@ -103,8 +103,6 @@ } } -.dc-mobile-dialog { - &__content { - height: 100%; - } +.contract-type-widget__header ~ .dc-mobile-dialog__content { + height: 100%; } diff --git a/packages/trader/src/sass/app/modules/trading-mobile.scss b/packages/trader/src/sass/app/modules/trading-mobile.scss index a587d08e0e5c..ffa70084d1fc 100644 --- a/packages/trader/src/sass/app/modules/trading-mobile.scss +++ b/packages/trader/src/sass/app/modules/trading-mobile.scss @@ -256,11 +256,11 @@ &__amount { &-keypad { width: 100%; - padding: 1.6rem; + padding: 2.4rem 1.6rem 1.6rem; height: auto; - margin-top: 0.8rem; margin-bottom: 0.8rem; display: flex; + flex-direction: column; align-items: center; justify-content: center; @@ -511,7 +511,7 @@ left: 2.4rem !important; &__content { - max-width: 32.8rem; + max-width: calc(min(32.8rem, 85vw)); left: 2.5rem; &--vanilla { top: -0.9rem; diff --git a/packages/trader/src/sass/app/modules/trading.scss b/packages/trader/src/sass/app/modules/trading.scss index 94070d4292cd..7aaaaec72b4a 100644 --- a/packages/trader/src/sass/app/modules/trading.scss +++ b/packages/trader/src/sass/app/modules/trading.scss @@ -342,7 +342,7 @@ &-modal { @include mobile { &--vanilla { - max-width: 33rem; + max-width: calc(min(33rem, 85vw)); } } } @@ -781,6 +781,16 @@ } } +.expiry-text-container { + margin-top: 0.4rem; + + &--mobile { + margin: 0 0 0.8rem; + position: relative; + top: -0.8rem; + } +} + /** @define dc-collapsible */ @include mobile { .dc-collapsible {