Skip to content

Commit 4a71f6e

Browse files
authored
Amina/account limits section for accounts v2 (deriv-com#14747)
* feat: draft * feat: draft * fix: table layout * fix: subcategory * fix: table value * fix: category * fix: side note * fix: content * fix: content * fix: content * fix: type * fix: resolve conflict * fix: resolve type * fix: fill color * fix: resolve commits * fix: remove eslint formating * fix: remove eslint formating * fix: resolve conflicts * fix: resolve commits * fix: conflict
1 parent b9bf489 commit 4a71f6e

File tree

13 files changed

+276
-6
lines changed

13 files changed

+276
-6
lines changed

packages/account-v2/src/components/DemoMessage/DemoMessage.tsx

+11-2
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,21 @@ import { DerivLightIcPoaLockIcon } from '@deriv/quill-icons';
44
import { Button } from '@deriv-com/ui';
55
import { IconWithMessage } from '../IconWithMessage';
66

7-
export const DemoMessage = () => {
7+
export const DemoMessage = ({ className }: { className?: string }) => {
88
const { data: authorizeData } = useAuthorize();
99
const hasRealAccount = authorizeData?.account_list?.some(account => account.is_virtual === 0);
1010
return (
1111
<IconWithMessage
12-
actionButton={<Button>{hasRealAccount ? 'Switch to real account' : 'Add a real account'}</Button>}
12+
actionButton={
13+
<Button
14+
onClick={() => {
15+
// [TODO]: Add action for switching to real account
16+
}}
17+
>
18+
{hasRealAccount ? 'Switch to real account' : 'Add a real account'}
19+
</Button>
20+
}
21+
className={className}
1322
icon={<DerivLightIcPoaLockIcon width={128} />}
1423
title='This feature is not available for demo accounts.'
1524
/>

packages/account-v2/src/components/IconWithMessage/IconWithMessage.tsx

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
import React, { ReactNode } from 'react';
22
import { Text } from '@deriv-com/ui';
3+
import { twMerge } from 'tailwind-merge';
34

45
type TIconWithMessage = {
56
actionButton?: ReactNode;
67
children?: ReactNode;
8+
className?: string;
79
icon: JSX.Element;
810
title: string;
911
};
1012

11-
export const IconWithMessage = ({ actionButton, children, icon, title }: TIconWithMessage) => (
12-
<div className='flex flex-col w-full'>
13+
export const IconWithMessage = ({ actionButton, children, className, icon, title }: TIconWithMessage) => (
14+
<div className={twMerge('flex flex-col w-full', className)}>
1315
{icon}
1416
<div className='grid justify-center gap-16 mt-24 mb-32'>
1517
<Text align='center' size='md' weight='bold'>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import React from 'react';
2+
import { SideNote, Text } from '@deriv-com/ui';
3+
4+
export const AccountLimitsSideNote = () => (
5+
<SideNote className='mx-16 h-fit md:w-[256px]' title='Account limits' titleSize='sm'>
6+
<Text size='xs'>These are default limits that we apply to your accounts</Text>
7+
</SideNote>
8+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import React from 'react';
2+
import { StandaloneCircleInfoRegularIcon } from '@deriv/quill-icons';
3+
import { Table, Text } from '@deriv-com/ui';
4+
import { TAccountLimitValues } from '../../types';
5+
import { CATEGORY } from '../../utils/accountLimitsUtils';
6+
7+
const RenderRow = ({ category, hintInfo, isLessProminent, title, value }: TAccountLimitValues) => {
8+
return (
9+
<div className='grid grid-flow-col justify-between'>
10+
<div>
11+
{title && (
12+
<Text
13+
className={category === CATEGORY.submarket ? 'px-16' : ''}
14+
color={isLessProminent ? 'less-prominent' : 'general'}
15+
size={category === CATEGORY.footer ? 'xs' : 'sm'}
16+
weight={category === CATEGORY.header ? 'bold' : ''}
17+
>
18+
{title}
19+
</Text>
20+
)}
21+
{hintInfo && (
22+
<span className='px-8'>
23+
<StandaloneCircleInfoRegularIcon className='fill-solid-grey-1' iconSize='sm' />
24+
</span>
25+
)}
26+
</div>
27+
{value && (
28+
<Text size='sm' weight={category === CATEGORY.header ? 'bold' : ''}>
29+
{value}
30+
</Text>
31+
)}
32+
</div>
33+
);
34+
};
35+
36+
export const AccountLimitsTable = ({ accountLimitValues }: { accountLimitValues: TAccountLimitValues[] }) => (
37+
<Table
38+
data={accountLimitValues}
39+
isFetching={false}
40+
loadMoreFunction={() => {
41+
//[TODO]: Add load more function
42+
}}
43+
rowRender={RenderRow}
44+
/>
45+
);

packages/account-v2/src/containers/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export { AccountClosureForm } from './AccountClosureForm/AccountClosureForm';
22
export { AccountClosureSteps } from './AccountClosureSteps/AccountClosureSteps';
3+
export { AccountLimitsSideNote } from './AccountLimitsContainer/AccountLimitsSideNote';
34
export { LoginHistoryTable } from './LoginHistoryTable/LoginHistoryTable';
45
export { LoginHistoryTableCard } from './LoginHistoryTable/LoginHistoryTableCard';
56
export { MaskCardModal } from './MaskCardModal/MaskCardModal';

packages/account-v2/src/hooks/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export { useAccountLimitsData } from './useAccountLimits';
12
export { useManualForm } from './useManualForm';
23
export { usePaymentMethodDetails } from './usePaymentMethodDetails';
34
export { usePOAInfo } from './usePOAInfo';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { useMemo } from 'react';
2+
import { useAccountLimits, useAccountStatus, useActiveAccount } from '@deriv/api-v2';
3+
import { TCurrency } from '../types';
4+
import { getAccountLimitValues } from '../utils/accountLimitsUtils';
5+
6+
export const useAccountLimitsData = () => {
7+
const { data: accountLimits, isLoading: isAccountLimitsLoading } = useAccountLimits();
8+
const { data: activeAccount, isLoading: isActiveAccountLoading } = useActiveAccount();
9+
const { data: accountStatus, isLoading: isAccountStatusLoading } = useAccountStatus();
10+
11+
const currency = (activeAccount?.currency as TCurrency) ?? 'USD';
12+
const isVirtual = activeAccount?.is_virtual;
13+
const { is_authenticated: isAuthenticated } = accountStatus || {};
14+
15+
const isDataLoading = isAccountLimitsLoading || isActiveAccountLoading || isAccountStatusLoading;
16+
17+
const formattedAccountLimits = useMemo(() => {
18+
if (!accountLimits) return [];
19+
return getAccountLimitValues(accountLimits, currency, isAuthenticated);
20+
}, [accountLimits, currency, isAuthenticated]);
21+
22+
return {
23+
accountLimits: formattedAccountLimits,
24+
isLoading: isDataLoading,
25+
isVirtual,
26+
};
27+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import React from 'react';
2+
import { Loader } from '@deriv-com/ui';
3+
import { AccountLimitsSideNote } from 'src/containers';
4+
import { AccountLimitsTable } from 'src/containers/AccountLimitsContainer/AccountLimitsTable';
5+
import { DemoMessage } from '../../components/DemoMessage';
6+
import { useAccountLimitsData } from '../../hooks/useAccountLimits';
7+
8+
export const AccountLimits = () => {
9+
const { accountLimits, isLoading, isVirtual } = useAccountLimitsData();
10+
if (isLoading) return <Loader isFullScreen={false} />;
11+
12+
if (isVirtual) {
13+
return <DemoMessage className='items-center' />;
14+
}
15+
16+
if (accountLimits.length) {
17+
return (
18+
<div className='grid md:grid-cols-[auto,256px] gap-16'>
19+
<AccountLimitsTable accountLimitValues={accountLimits} />
20+
<AccountLimitsSideNote />
21+
</div>
22+
);
23+
}
24+
return null;
25+
};

packages/account-v2/src/pages/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export { AccountClosure } from './AccountClosure/AccountClosure';
2+
export { AccountLimits } from './AccountLimits/AccountLimits';
23
export { ConnectedApps } from './ConnectedApps/ConnectedApps';
34
export { LoginHistory } from './LoginHistory/LoginHistory';
45
export { ManualUploadContainer } from './ManualUploadContainer/ManualUploadContainer';

packages/account-v2/src/router/routesConfig.tsx

+9-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,14 @@ import { TradingAssessmentForm } from '../containers/TradingAssessmentForm';
44
import { FinancialAssessmentForm } from '../modules/src/FinancialAssessment/FinancialAssessmentForm';
55
import { POAFormContainer } from '../modules/src/POAForm/POAFormContainer';
66
import { ProofOfIdentity } from '../modules/src/POI/POI';
7-
import { AccountClosure, ConnectedApps, LoginHistory, ProofOfOwnership, TwoFactorAuthentication } from '../pages';
7+
import {
8+
AccountClosure,
9+
AccountLimits,
10+
ConnectedApps,
11+
LoginHistory,
12+
ProofOfOwnership,
13+
TwoFactorAuthentication,
14+
} from '../pages';
815
import { DummyRoute } from './components/DummyRoute/DummyRoute';
916

1017
export const routes = [
@@ -59,7 +66,7 @@ export const routes = [
5966
routePath: ACCOUNT_V2_ROUTES.SelfExclusion,
6067
},
6168
{
62-
routeComponent: DummyRoute,
69+
routeComponent: AccountLimits,
6370
routeName: 'Account limits',
6471
routePath: ACCOUNT_V2_ROUTES.AccountLimits,
6572
},

packages/account-v2/src/types.ts

+11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useGetAccountStatus, useKycAuthStatus, useSettings } from '@deriv/api-v2';
2+
import { CurrencyConstants } from '@deriv-com/utils';
23
import {
34
AUTH_STATUS_CODES,
45
getPaymentMethodsConfig,
@@ -62,3 +63,13 @@ export type TProofOfOwnershipFormValue = Record<TPaymentMethod, Record<number |
6263
export type TPOIService = typeof POI_SERVICE[keyof typeof POI_SERVICE];
6364

6465
export type TIDVErrorStatusCode = typeof IDV_ERROR_CODES[keyof typeof IDV_ERROR_CODES]['code'];
66+
67+
export type TAccountLimitValues = {
68+
category?: string;
69+
hintInfo?: string;
70+
isLessProminent?: boolean;
71+
title?: string;
72+
value?: number | string;
73+
};
74+
75+
export type TCurrency = CurrencyConstants.Currency;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { GetLimits } from '@deriv/api-types';
2+
import { FormatUtils } from '@deriv-com/utils';
3+
import { TAccountLimitValues, TCurrency } from '../types';
4+
5+
export const CATEGORY = {
6+
footer: 'footer',
7+
header: 'header',
8+
submarket: 'submarket',
9+
} as const;
10+
11+
type TMarketSpecific = GetLimits['market_specific'];
12+
type TMarketSpecificData = Exclude<TMarketSpecific, undefined>[string];
13+
14+
const markets = ['commodities', 'forex', 'indices', 'synthetic_index'];
15+
16+
const getTradingLimitsTableData = (
17+
currency: TCurrency,
18+
payout: number,
19+
openPositions?: number,
20+
accountBalance?: number | null
21+
): TAccountLimitValues[] => [
22+
{
23+
category: CATEGORY.header,
24+
title: 'Trading limits',
25+
value: 'Limit',
26+
},
27+
{
28+
hintInfo:
29+
'Represents the maximum number of outstanding contracts in your portfolio. Each line in your portfolio counts for one open position. Once the maximum is reached, you will not be able to open new positions without closing an existing position first.',
30+
title: '*Maximum number of open positions',
31+
value: openPositions,
32+
},
33+
{
34+
hintInfo:
35+
'Represents the maximum amount of cash that you may hold in your account. If the maximum is reached, you will be asked to withdraw funds.',
36+
title: '*Maximum account cash balance',
37+
value: accountBalance ? FormatUtils.formatMoney(accountBalance, { currency }) : 'Not set',
38+
},
39+
{
40+
hintInfo:
41+
'Represents the maximum aggregate payouts on outstanding contracts in your portfolio. If the maximum is attained, you may not purchase additional contracts without first closing out existing positions.',
42+
title: 'Maximum aggregate payouts on open positions',
43+
value: FormatUtils.formatMoney(payout, { currency }),
44+
},
45+
{
46+
category: CATEGORY.footer,
47+
title: '*Any limits in your Self-exclusion settings will override these default limits.',
48+
},
49+
];
50+
51+
const getMarketValues = (collection: TMarketSpecificData, currency: TCurrency) => {
52+
const formattedCollection = collection?.slice().sort((a, b) => ((a?.level || '') > (b?.level || '') ? 1 : -1));
53+
return formattedCollection?.map(data => ({
54+
category: data?.level,
55+
title: data?.name,
56+
value: FormatUtils.formatMoney(data?.turnover_limit ?? 0, { currency }),
57+
}));
58+
};
59+
60+
const getMaximumDailyLimiitsTableData = (marketSpecific: TMarketSpecific, currency: TCurrency) => {
61+
if (!marketSpecific) return [];
62+
return [
63+
{
64+
category: CATEGORY.header,
65+
hintInfo: 'Represents the maximum volume of contracts that you may purchase in any given trading day.',
66+
title: 'Maximum daily turnover',
67+
value: 'Limit',
68+
},
69+
...markets.flatMap(market => (marketSpecific[market] ? getMarketValues(marketSpecific[market], currency) : [])),
70+
];
71+
};
72+
73+
const getWithdrawalLimitsTableData = (
74+
isAuthenticated: boolean,
75+
currency: TCurrency,
76+
numberOfDaysLimit?: number,
77+
withdrawalSinceInceptionMonetary?: number,
78+
remainder?: number
79+
) => [
80+
{
81+
category: CATEGORY.header,
82+
title: 'Withdrawal limits',
83+
value: 'Limit',
84+
},
85+
...(!isAuthenticated
86+
? [
87+
{
88+
title: 'Total withdrawal allowed',
89+
value: FormatUtils.formatMoney(numberOfDaysLimit ?? 0, { currency }),
90+
},
91+
{
92+
title: 'Total withdrawn',
93+
value: FormatUtils.formatMoney(withdrawalSinceInceptionMonetary ?? 0, { currency }),
94+
},
95+
{
96+
title: 'Maximum withdrawal remaining',
97+
value: FormatUtils.formatMoney(remainder ?? 0, { currency }),
98+
},
99+
]
100+
: []),
101+
{
102+
category: CATEGORY.footer,
103+
isLessProminent: true,
104+
title: isAuthenticated
105+
? 'Your account is fully authenticated and your withdrawal limits have been lifted'
106+
: 'Stated limits are subject to change without prior notice.',
107+
},
108+
];
109+
110+
export const getAccountLimitValues = (accountLimits: GetLimits, currency: TCurrency, isAuthenticated = false) => {
111+
const {
112+
account_balance: accountBalance,
113+
market_specific: marketSpecific,
114+
num_of_days_limit: numberOfDaysLimit,
115+
open_positions: openPositions,
116+
payout = 0,
117+
remainder,
118+
withdrawal_since_inception_monetary: withdrawalSinceInceptionMonetary,
119+
} = accountLimits;
120+
121+
return [
122+
...getTradingLimitsTableData(currency, payout, openPositions, accountBalance),
123+
...getMaximumDailyLimiitsTableData(marketSpecific, currency),
124+
...getWithdrawalLimitsTableData(
125+
isAuthenticated,
126+
currency,
127+
numberOfDaysLimit,
128+
withdrawalSinceInceptionMonetary,
129+
remainder
130+
),
131+
];
132+
};

packages/account-v2/src/utils/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './accountClosureUtils';
2+
export * from './accountLimitsUtils';
23
export * from './formattedLoginHistoryData';
34
export * from './idvFormUtils';
45
export * from './manualFormUtils';

0 commit comments

Comments
 (0)