diff --git a/packages/wallets/src/constants/errorCodes.ts b/packages/wallets/src/constants/errorCodes.ts index 17d2de56284f..68206194d249 100644 --- a/packages/wallets/src/constants/errorCodes.ts +++ b/packages/wallets/src/constants/errorCodes.ts @@ -7,6 +7,7 @@ export enum CryptoDepositErrorCodes { export enum CryptoWithdrawalErrorCodes { CryptoConnectionError = 'CryptoConnectionError', CryptoInvalidAddress = 'CryptoInvalidAddress', + CryptoLimitAgeVerified = 'CryptoLimitAgeVerified', InvalidToken = 'InvalidToken', SuspendedCurrency = 'CryptoSuspendedCurrency', SuspendedWithdrawal = 'CryptoDisabledCurrencyWithdrawal', diff --git a/packages/wallets/src/features/cashier/modules/Transfer/hooks/useTransferMessages/useTransferMessages.ts b/packages/wallets/src/features/cashier/modules/Transfer/hooks/useTransferMessages/useTransferMessages.ts index 9311b7bb711b..66318bf408d7 100644 --- a/packages/wallets/src/features/cashier/modules/Transfer/hooks/useTransferMessages/useTransferMessages.ts +++ b/packages/wallets/src/features/cashier/modules/Transfer/hooks/useTransferMessages/useTransferMessages.ts @@ -4,6 +4,7 @@ import { displayMoney as displayMoney_ } from '@deriv/api-v2/src/utils'; import { THooks } from '../../../../../../types'; import { TAccount, TInitialTransferFormValues, TMessageFnProps, TTransferMessage } from '../../types'; import { + countLimitMessageFn, cumulativeAccountLimitsMessageFn, insufficientBalanceMessageFn, lifetimeAccountLimitsBetweenWalletsMessageFn, @@ -56,6 +57,7 @@ const useTransferMessages = ({ const messages: TTransferMessage[] = []; messageFns.push(insufficientBalanceMessageFn); + messageFns.push(countLimitMessageFn); if (!isAccountVerified && isTransferBetweenWallets) { messageFns.push(lifetimeAccountLimitsBetweenWalletsMessageFn); diff --git a/packages/wallets/src/features/cashier/modules/Transfer/hooks/useTransferMessages/utils/countLimitsMessageFn.ts b/packages/wallets/src/features/cashier/modules/Transfer/hooks/useTransferMessages/utils/countLimitsMessageFn.ts new file mode 100644 index 000000000000..1823a8fea33d --- /dev/null +++ b/packages/wallets/src/features/cashier/modules/Transfer/hooks/useTransferMessages/utils/countLimitsMessageFn.ts @@ -0,0 +1,68 @@ +import { TMessageFnProps, TTransferMessage } from '../../../types'; + +let text: TTransferMessage['message']['text'], + type: TTransferMessage['type'], + values: TTransferMessage['message']['values']; + +const countLimitMessageFn = ({ activeWallet, limits, sourceAccount, targetAccount }: TMessageFnProps) => { + if (!targetAccount) return null; + + const isTransferBetweenWallets = + sourceAccount.account_category === 'wallet' && targetAccount.account_category === 'wallet'; + + const isDemoTransfer = activeWallet?.is_virtual; + + const keyAccountType = + [sourceAccount, targetAccount].find(acc => acc.account_category !== 'wallet')?.account_type ?? 'internal'; + + const platformKey = keyAccountType === 'standard' ? 'dtrade' : keyAccountType; + + const allowedCount = isDemoTransfer + ? //@ts-expect-error needs backend type + (limits?.daily_transfers?.virtual?.allowed as number) + : //@ts-expect-error needs backend type + (limits?.daily_transfers?.[platformKey]?.allowed as number); + + const availableCount = isDemoTransfer + ? //@ts-expect-error needs backend type + (limits?.daily_transfers?.virtual?.available as number) + : //@ts-expect-error needs backend type + (limits?.daily_transfers?.[platformKey]?.available as number); + + if (allowedCount === undefined || availableCount === undefined) return null; + + if (availableCount === 0 && isDemoTransfer) { + text = + 'You have reached your daily transfer limit of {{allowedCount}} transfers for your virtual funds. The limit will reset at 00:00 GMT.'; + values = { + allowedCount, + }; + type = 'error' as const; + + return { + message: { text, values }, + type, + }; + } + + if (availableCount === 0) { + text = isTransferBetweenWallets + ? 'You have reached your daily transfer limit of {{allowedCount}} transfers between your Wallets. The limit will reset at 00:00 GMT.' + : 'You have reached your daily transfer limit of {{allowedCount}} transfers between your {{sourceAccountName}} and {{targetAccountName}}. The limit will reset at 00:00 GMT.'; + values = { + allowedCount, + sourceAccountName: sourceAccount.accountName, + targetAccountName: targetAccount.accountName, + }; + type = 'error' as const; + + return { + message: { text, values }, + type, + }; + } + + return null; +}; + +export default countLimitMessageFn; diff --git a/packages/wallets/src/features/cashier/modules/Transfer/hooks/useTransferMessages/utils/index.ts b/packages/wallets/src/features/cashier/modules/Transfer/hooks/useTransferMessages/utils/index.ts index 6bacfb54c597..9553a9313fd6 100644 --- a/packages/wallets/src/features/cashier/modules/Transfer/hooks/useTransferMessages/utils/index.ts +++ b/packages/wallets/src/features/cashier/modules/Transfer/hooks/useTransferMessages/utils/index.ts @@ -1,9 +1,11 @@ +import countLimitMessageFn from './countLimitsMessageFn'; import cumulativeAccountLimitsMessageFn from './cumulativeAccountLimitsMessageFn'; import insufficientBalanceMessageFn from './insufficientBalanceMessageFn'; import lifetimeAccountLimitsBetweenWalletsMessageFn from './lifetimeAccountLimitsBetweenWalletsMessageFn'; import transferFeesBetweenWalletsMessageFn from './transferFeesBetweenWalletsMessageFn'; export { + countLimitMessageFn, cumulativeAccountLimitsMessageFn, insufficientBalanceMessageFn, lifetimeAccountLimitsBetweenWalletsMessageFn, diff --git a/packages/wallets/src/features/cashier/screens/WithdrawalErrorScreen/WithdrawalErrorScreen.tsx b/packages/wallets/src/features/cashier/screens/WithdrawalErrorScreen/WithdrawalErrorScreen.tsx index 8526b315d4d1..b134433d95b0 100644 --- a/packages/wallets/src/features/cashier/screens/WithdrawalErrorScreen/WithdrawalErrorScreen.tsx +++ b/packages/wallets/src/features/cashier/screens/WithdrawalErrorScreen/WithdrawalErrorScreen.tsx @@ -1,5 +1,6 @@ import React, { ComponentProps } from 'react'; import classNames from 'classnames'; +import { useHistory } from 'react-router-dom'; import { useActiveWalletAccount } from '@deriv/api-v2'; import { TSocketError } from '@deriv/api-v2/types'; import { WalletButton, WalletsErrorScreen } from '../../../../components'; @@ -23,6 +24,7 @@ type TErrorContent = { type TErrorCodeHandlers = Record; const WithdrawalErrorScreen: React.FC = ({ error, resetError, setResendEmail }) => { + const history = useHistory(); const { data } = useActiveWalletAccount(); const currency = data?.currency; @@ -51,6 +53,16 @@ const WithdrawalErrorScreen: React.FC = ({ error, resetError, setResendE onClick: resetError, title: 'Error', }, + [CryptoWithdrawalErrorCodes.CryptoLimitAgeVerified]: { + ...defaultContent, + buttonText: 'Verify identity', + buttonVariant: 'contained', + onClick: () => { + // @ts-expect-error the following link is not part of wallets routes config + history.push('/account/proof-of-identity'); + }, + title: 'Error', + }, [CryptoWithdrawalErrorCodes.SuspendedCurrency]: { ...defaultContent, buttonText: undefined, diff --git a/packages/wallets/src/features/cashier/screens/WithdrawalErrorScreen/__tests__/WithdrawalErrorScreen.spec.tsx b/packages/wallets/src/features/cashier/screens/WithdrawalErrorScreen/__tests__/WithdrawalErrorScreen.spec.tsx index 9863faf499b2..64790486418e 100644 --- a/packages/wallets/src/features/cashier/screens/WithdrawalErrorScreen/__tests__/WithdrawalErrorScreen.spec.tsx +++ b/packages/wallets/src/features/cashier/screens/WithdrawalErrorScreen/__tests__/WithdrawalErrorScreen.spec.tsx @@ -1,9 +1,15 @@ import React from 'react'; +import { useHistory } from 'react-router-dom'; import { useActiveWalletAccount } from '@deriv/api-v2'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import WithdrawalErrorScreen from '../WithdrawalErrorScreen'; +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useHistory: jest.fn(), +})); + jest.mock('@deriv/api-v2', () => ({ useActiveWalletAccount: jest.fn(), })); @@ -108,6 +114,37 @@ describe('WithdrawalErrorScreen', () => { expect(screen.queryByText('Try again')).not.toBeInTheDocument(); }); + it('should show correct withdrawal error screen for crypto age limit verified error', () => { + const error = { + code: 'CryptoLimitAgeVerified', + message: 'Crypto Limit Age Verified Error', + }; + + render(); + + expect(screen.getByText('Error')).toBeInTheDocument(); + expect(screen.getByText('Crypto Limit Age Verified Error')).toBeInTheDocument(); + expect(screen.queryByText('Verify identity')).toBeInTheDocument(); + }); + + it('should show redirect the user to the account/proof-of-identity when the user clicks on `Verify identity` after receiving crypto age limit verified error', () => { + const mockHistoryPush = jest.fn(); + (useHistory as jest.Mock).mockReturnValueOnce({ + push: mockHistoryPush, + }); + const error = { + code: 'CryptoLimitAgeVerified', + message: 'Crypto Limit Age Verified Error', + }; + + render(); + + const verifyIdentityButton = screen.getByText('Verify identity'); + userEvent.click(verifyIdentityButton); + + expect(mockHistoryPush).toBeCalledWith('/account/proof-of-identity'); + }); + it('should reload page when the user clicks on `Try again` button', () => { const reloadMock = jest.fn(); Object.defineProperty(window, 'location', {