diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 63a10a301db9..3f18b757f105 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,8 +35,6 @@ jobs: run: npx tsc --project packages/stores/tsconfig.json -noEmit - name: Check TypeScript for @deriv/wallets run: npx tsc --project packages/wallets/tsconfig.json -noEmit - - name: Check TypeScript for @deriv/cashier-v2 - run: npx tsc --project packages/cashier-v2/tsconfig.json -noEmit - name: Check ESLint for @deriv/wallets run: npx eslint --fix --ignore-path packages/wallets/.eslintignore --config packages/wallets/.eslintrc.js packages/wallets - name: Check ESLint for @deriv/cashier-v2 diff --git a/packages/api-v2/src/hooks/useDerivAccountsList.ts b/packages/api-v2/src/hooks/useDerivAccountsList.ts index 366b9ced8a38..830134596172 100644 --- a/packages/api-v2/src/hooks/useDerivAccountsList.ts +++ b/packages/api-v2/src/hooks/useDerivAccountsList.ts @@ -1,8 +1,6 @@ import { useMemo } from 'react'; import useAuthorize from './useAuthorize'; -import useBalance from './useBalance'; import useCurrencyConfig from './useCurrencyConfig'; -import { displayMoney } from '../utils'; import useAuthorizedQuery from '../useAuthorizedQuery'; import { getAccountListWithAuthToken } from '@deriv/utils'; @@ -18,8 +16,6 @@ const useDerivAccountsList = () => { staleTime: Infinity, } ); - - const { data: balance_data } = useBalance(); const { getConfig } = useCurrencyConfig(); const account_list = account_list_data?.account_list; @@ -58,29 +54,9 @@ const useDerivAccountsList = () => { }); }, [account_list_data, account_list, authorize_data?.loginid, getConfig]); - // Add balance to each account - const modified_accounts_with_balance = useMemo( - () => - modified_accounts?.map(account => { - const balance = balance_data?.accounts?.[account.loginid]?.balance || 0; - - return { - ...account, - /** The balance of the account. */ - balance, - /** The balance of the account in currency format. */ - display_balance: displayMoney(balance, account.currency_config?.display_code || 'USD', { - fractional_digits: account.currency_config?.fractional_digits, - preferred_language: authorize_data?.preferred_language, - }), - }; - }), - [balance_data?.accounts, modified_accounts, authorize_data?.preferred_language] - ); - return { /** The list of accounts for the current user. */ - data: modified_accounts_with_balance, + data: modified_accounts, ...rest, }; }; diff --git a/packages/api-v2/src/hooks/useWalletAccountsList.ts b/packages/api-v2/src/hooks/useWalletAccountsList.ts index c1412f5940c0..bb605b540339 100644 --- a/packages/api-v2/src/hooks/useWalletAccountsList.ts +++ b/packages/api-v2/src/hooks/useWalletAccountsList.ts @@ -1,13 +1,10 @@ import { useMemo } from 'react'; import useAuthorize from './useAuthorize'; -import useBalance from './useBalance'; import useCurrencyConfig from './useCurrencyConfig'; -import { displayMoney } from '../utils'; /** A custom hook that gets the list of all wallet accounts for the current user. */ const useWalletAccountsList = () => { const { data: authorize_data, ...rest } = useAuthorize(); - const { data: balance_data } = useBalance(); const { getConfig } = useCurrencyConfig(); // Filter out non-wallet accounts. @@ -57,31 +54,11 @@ const useWalletAccountsList = () => { }); }, [filtered_accounts, getConfig]); - // Add balance to each wallet account - const modified_accounts_with_balance = useMemo( - () => - modified_accounts?.map(wallet => { - const balance = balance_data?.accounts?.[wallet.loginid]?.balance || 0; - - return { - ...wallet, - /** The balance of the wallet account. */ - balance, - /** The balance of the wallet account in currency format. */ - display_balance: displayMoney(balance, wallet.currency_config?.display_code || 'USD', { - fractional_digits: wallet.currency_config?.fractional_digits, - preferred_language: authorize_data?.preferred_language, - }), - }; - }), - [balance_data?.accounts, modified_accounts, authorize_data?.preferred_language] - ); - // Sort wallet accounts alphabetically by fiat, crypto, then virtual. const sorted_accounts = useMemo(() => { - if (!modified_accounts_with_balance) return; + if (!modified_accounts) return; - return [...modified_accounts_with_balance].sort((a, b) => { + return [...modified_accounts].sort((a, b) => { if (a.is_virtual !== b.is_virtual) { return a.is_virtual ? 1 : -1; } else if (a.currency_config?.is_crypto !== b.currency_config?.is_crypto) { @@ -90,7 +67,7 @@ const useWalletAccountsList = () => { return (a.currency || 'USD').localeCompare(b.currency || 'USD'); }); - }, [modified_accounts_with_balance]); + }, [modified_accounts]); return { /** The list of wallet accounts for the current user. */ diff --git a/packages/api-v2/src/utils/display-money.ts b/packages/api-v2/src/utils/display-money.ts index 3d8e6e37452f..2f2bfa614169 100644 --- a/packages/api-v2/src/utils/display-money.ts +++ b/packages/api-v2/src/utils/display-money.ts @@ -5,8 +5,8 @@ type TCurrency = NonNullable['data']['currency'] type TPreferredLanguage = ReturnType['data']['preferred_language']; export const displayMoney = ( - amount: number, - currency: TCurrency, + amount = 0, + currency: TCurrency = '', options?: { fractional_digits?: number; preferred_language?: TPreferredLanguage; diff --git a/packages/wallets/component-tests/crypto-payment-redirection.spec.tsx b/packages/wallets/component-tests/crypto-payment-redirection.spec.tsx index db524a834d03..7249360d1af9 100644 --- a/packages/wallets/component-tests/crypto-payment-redirection.spec.tsx +++ b/packages/wallets/component-tests/crypto-payment-redirection.spec.tsx @@ -6,6 +6,7 @@ import { mockCryptoWithdraw } from './mocks/mockCryptoWithdraw'; import { mockGetAccountTypes } from './mocks/mockGetAccountTypes'; import { mockProposalOpenContract } from './mocks/mockProposalOpenContract'; import mockWalletsAuthorize, { DEFAULT_WALLET_ACCOUNTS } from './mocks/mockWalletsAuthorize'; +import { mockAccountList } from './mocks/mockAccountList'; test.describe('Wallets - Crypto withdrawal', () => { test.beforeEach(async ({ baseURL, page }) => { @@ -20,6 +21,7 @@ test.describe('Wallets - Crypto withdrawal', () => { mockCryptoConfig, mockProposalOpenContract, mockBalance, + mockAccountList, ], page, state: { diff --git a/packages/wallets/component-tests/menu.spec.tsx b/packages/wallets/component-tests/menu.spec.tsx index a74e7d66eb95..203d32535f52 100644 --- a/packages/wallets/component-tests/menu.spec.tsx +++ b/packages/wallets/component-tests/menu.spec.tsx @@ -5,6 +5,7 @@ import { mockCryptoConfig } from './mocks/mockCryptoConfig'; import { mockGetAccountTypes } from './mocks/mockGetAccountTypes'; import { mockProposalOpenContract } from './mocks/mockProposalOpenContract'; import mockWalletsAuthorize, { DEFAULT_WALLET_ACCOUNTS } from './mocks/mockWalletsAuthorize'; +import { mockAccountList } from './mocks/mockAccountList'; test.describe('Wallets - Traders Hub', () => { test('render USD wallet balance', async ({ baseURL, page }) => { @@ -18,6 +19,7 @@ test.describe('Wallets - Traders Hub', () => { mockCryptoConfig, mockProposalOpenContract, mockBalance, + mockAccountList, ], page, state: { diff --git a/packages/wallets/component-tests/mocks/mockAccountList.ts b/packages/wallets/component-tests/mocks/mockAccountList.ts new file mode 100644 index 000000000000..6c9c7e08ac58 --- /dev/null +++ b/packages/wallets/component-tests/mocks/mockAccountList.ts @@ -0,0 +1,66 @@ +import { Context } from '@deriv/integration/src/utils/mocks/mocks'; + +const TEMP_ACCOUNT_LIST = [ + { + account_category: 'trading', + account_type: 'standard', + broker: 'CR', + created_at: 1720591930, + currency: 'USD', + currency_type: 'fiat', + is_disabled: 0, + is_virtual: 0, + landing_company_name: 'svg', + linked_to: [ + { + loginid: 'CRW1003', + platform: 'dwallet', + }, + ], + loginid: 'CR90000243', + }, + { + account_category: 'wallet', + account_type: 'doughflow', + broker: 'CRW', + created_at: 1720591899, + currency: 'USD', + currency_type: 'fiat', + is_disabled: 0, + is_virtual: 0, + landing_company_name: 'svg', + linked_to: [ + { + loginid: 'CR90000243', + platform: 'dtrade', + }, + ], + loginid: 'CRW1003', + }, + { + account_category: 'wallet', + account_type: 'virtual', + broker: 'VRW', + created_at: 1720591899, + currency: 'USD', + currency_type: 'fiat', + is_disabled: 0, + is_virtual: 1, + landing_company_name: 'virtual', + linked_to: [], + loginid: 'VRW1004', + }, +]; + +export function mockAccountList(context: Context) { + if (!('account_list' in context.request)) { + return; + } + + context.response = { + account_list: TEMP_ACCOUNT_LIST, + echo_req: context.request, + msg_type: 'account_list', + req_id: context.req_id, + }; +} diff --git a/packages/wallets/component-tests/wallets-carousel-content.spec.tsx b/packages/wallets/component-tests/wallets-carousel-content.spec.tsx index f7c766dcf5c2..fee9c793a4f9 100644 --- a/packages/wallets/component-tests/wallets-carousel-content.spec.tsx +++ b/packages/wallets/component-tests/wallets-carousel-content.spec.tsx @@ -5,6 +5,7 @@ import { mockGetAccountTypes } from './mocks/mockGetAccountTypes'; import { mockProposalOpenContract } from './mocks/mockProposalOpenContract'; import mockWalletsAuthorize, { DEFAULT_WALLET_ACCOUNTS } from './mocks/mockWalletsAuthorize'; import mockWalletsLoggedIn from './mocks/mockWalletsLoggedIn'; +import { mockAccountList } from './mocks/mockAccountList'; const CAROUSEL_SELECTOR = '.wallets-carousel-content__cards .wallets-card:nth-child(1)'; @@ -47,6 +48,7 @@ test.describe('Wallets - Mobile carousel', () => { mockGetAccountTypes, mockCryptoConfig, mockProposalOpenContract, + mockAccountList, ], page: mobilePage, state: { diff --git a/packages/wallets/src/AppContent.tsx b/packages/wallets/src/AppContent.tsx index 8c2801274b30..dd067ac31790 100644 --- a/packages/wallets/src/AppContent.tsx +++ b/packages/wallets/src/AppContent.tsx @@ -1,6 +1,8 @@ import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { useDerivAccountsList } from '@deriv/api-v2'; import { Analytics } from '@deriv-com/analytics'; +import useAllBalanceSubscription from './hooks/useAllBalanceSubscription'; import { defineViewportHeight } from './utils/utils'; import { WalletLanguageSidePanel } from './components'; import { Router } from './routes'; @@ -9,6 +11,19 @@ import './AppContent.scss'; const AppContent: React.FC = () => { const [isPanelOpen, setIsPanelOpen] = useState(false); const { i18n } = useTranslation(); + const { isSubscribed, subscribeToAllBalance, unsubscribeFromAllBalance } = useAllBalanceSubscription(); + const { data: derivAccountList, isRefetching } = useDerivAccountsList(); + + useEffect(() => { + if ((derivAccountList?.length ?? 0) > 0 && !isRefetching && !isSubscribed) { + subscribeToAllBalance(); + } + return () => { + if (isSubscribed) { + unsubscribeFromAllBalance(); + } + }; + }, [derivAccountList?.length, isRefetching, isSubscribed, subscribeToAllBalance, unsubscribeFromAllBalance]); useEffect(() => { const handleShortcutKey = (event: globalThis.KeyboardEvent) => { diff --git a/packages/wallets/src/components/AccountsList/AccountsList.tsx b/packages/wallets/src/components/AccountsList/AccountsList.tsx index 72326a5d357c..4ecb454ff2c9 100644 --- a/packages/wallets/src/components/AccountsList/AccountsList.tsx +++ b/packages/wallets/src/components/AccountsList/AccountsList.tsx @@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next'; import { Divider, Tab, Tabs } from '@deriv-com/ui'; import { CFDPlatformsList } from '../../features'; import useDevice from '../../hooks/useDevice'; -import { TSubscribedBalance } from '../../types'; import { OptionsAndMultipliersListing } from '../OptionsAndMultipliersListing'; import './AccountsList.scss'; @@ -11,11 +10,10 @@ const tabs = ['CFDs', 'Options']; type TProps = { accountsActiveTabIndex?: number; - balance: TSubscribedBalance['balance']; onTabClickHandler?: React.Dispatch>; }; -const AccountsList: FC = ({ accountsActiveTabIndex, balance, onTabClickHandler }) => { +const AccountsList: FC = ({ accountsActiveTabIndex, onTabClickHandler }) => { const { isMobile } = useDevice(); const { t } = useTranslation(); @@ -34,7 +32,7 @@ const AccountsList: FC = ({ accountsActiveTabIndex, balance, onTabClickH - + @@ -47,7 +45,7 @@ const AccountsList: FC = ({ accountsActiveTabIndex, balance, onTabClickH - + ); diff --git a/packages/wallets/src/components/AccountsList/__tests__/AccountsList.spec.tsx b/packages/wallets/src/components/AccountsList/__tests__/AccountsList.spec.tsx index e7f9ab15bd2d..42110a94a4b0 100644 --- a/packages/wallets/src/components/AccountsList/__tests__/AccountsList.spec.tsx +++ b/packages/wallets/src/components/AccountsList/__tests__/AccountsList.spec.tsx @@ -4,7 +4,6 @@ import { APIProvider } from '@deriv/api-v2'; import { render, screen } from '@testing-library/react'; import WalletsAuthProvider from '../../../AuthProvider'; import useDevice from '../../../hooks/useDevice'; -import { TSubscribedBalance } from '../../../types'; import { ModalProvider } from '../../ModalProvider'; import AccountsList from '../AccountsList'; @@ -33,36 +32,6 @@ const wrapper = ({ children }: PropsWithChildren) => ( ); -const mockBalanceData: TSubscribedBalance['balance'] = { - data: { - accounts: { - 1234567: { - balance: 1000.0, - converted_amount: 1000.0, - currency: 'USD', - demo_account: 0, - status: 1, - type: 'deriv', - }, - 7654321: { - balance: 1.0, - converted_amount: 1.0, - currency: 'BTC', - demo_account: 1, - status: 1, - type: 'deriv', - }, - }, - balance: 9990, - currency: 'USD', - loginid: 'CRW1314', - }, - error: undefined, - isIdle: false, - isLoading: false, - isSubscribed: false, -}; - describe('AccountsList', () => { it('should render account list in mobile view', () => { mockUseDevice.mockReturnValue({ @@ -71,7 +40,7 @@ describe('AccountsList', () => { isTablet: false, }); - render(, { + render(, { wrapper, }); @@ -87,10 +56,9 @@ describe('AccountsList', () => { isTablet: false, }); - render(, { + render(, { wrapper, }); - expect(screen.getByText('CFDs')).toBeInTheDocument(); expect(screen.getAllByText('Options')[0]).toBeInTheDocument(); @@ -110,12 +78,9 @@ describe('AccountsList', () => { isMobile: true, isTablet: false, }); - render( - , - { - wrapper, - } - ); + render(, { + wrapper, + }); screen.getAllByText('Options')[0].click(); expect(onTabClickHandler).toHaveBeenCalledWith(1); @@ -127,7 +92,7 @@ describe('AccountsList', () => { isMobile: false, isTablet: false, }); - render(, { wrapper }); + render(, { wrapper }); expect(screen.getByTestId('dt_desktop_accounts_list')).toBeInTheDocument(); expect(screen.getByText('CFDs')).toBeInTheDocument(); @@ -140,9 +105,11 @@ describe('AccountsList', () => { isMobile: true, isTablet: false, }); - render(, { + + render(, { wrapper, }); + expect(mockWalletTourGuide); }); @@ -152,9 +119,11 @@ describe('AccountsList', () => { isMobile: true, isTablet: false, }); - render(, { + + render(, { wrapper, }); + expect(mockWalletTourGuide); }); }); diff --git a/packages/wallets/src/components/DerivAppsSection/DerivAppsGetAccount.tsx b/packages/wallets/src/components/DerivAppsSection/DerivAppsGetAccount.tsx index eadf3f2adf7c..e8e4758d1273 100644 --- a/packages/wallets/src/components/DerivAppsSection/DerivAppsGetAccount.tsx +++ b/packages/wallets/src/components/DerivAppsSection/DerivAppsGetAccount.tsx @@ -6,8 +6,10 @@ import { useInvalidateQuery, useSettings, } from '@deriv/api-v2'; +import { displayMoney } from '@deriv/api-v2/src/utils'; import { toMoment } from '@deriv/utils'; import { CFDSuccess } from '../../features/cfd/screens/CFDSuccess'; +import useAllBalanceSubscription from '../../hooks/useAllBalanceSubscription'; import useDevice from '../../hooks/useDevice'; import useSyncLocalStorageClientAccounts from '../../hooks/useSyncLocalStorageClientAccounts'; import { ModalStepWrapper, WalletButton, WalletText } from '../Base'; @@ -30,8 +32,9 @@ const DerivAppsGetAccount: React.FC = () => { const { addTradingAccountToLocalStorage } = useSyncLocalStorageClientAccounts(); const invalidate = useInvalidateQuery(); - const { data: activeLinkedToTradingAccount, isLoading: isActiveLinkedToTradingAccountLoading } = - useActiveLinkedToTradingAccount(); + const { isLoading: isActiveLinkedToTradingAccountLoading } = useActiveLinkedToTradingAccount(); + + const { data: balanceData } = useAllBalanceSubscription(); const createTradingAccount = async () => { if (!activeWallet?.is_virtual) { @@ -57,6 +60,14 @@ const DerivAppsGetAccount: React.FC = () => { useEffect(() => { if (isAccountCreationSuccess) { + const displayBalance = displayMoney( + balanceData?.[activeWallet?.loginid ?? '']?.balance, + activeWallet?.currency, + { + fractional_digits: activeWallet?.currency_config?.fractional_digits, + } + ); + show( } @@ -65,7 +76,7 @@ const DerivAppsGetAccount: React.FC = () => { > } title={`Your Options account is ready`} /> diff --git a/packages/wallets/src/components/DerivAppsSection/DerivAppsSection.tsx b/packages/wallets/src/components/DerivAppsSection/DerivAppsSection.tsx index 8cf14ec5237d..ddad18ad55de 100644 --- a/packages/wallets/src/components/DerivAppsSection/DerivAppsSection.tsx +++ b/packages/wallets/src/components/DerivAppsSection/DerivAppsSection.tsx @@ -1,18 +1,13 @@ import React from 'react'; import { useActiveLinkedToTradingAccount } from '@deriv/api-v2'; -import { TSubscribedBalance } from '../../types'; import { DerivAppsGetAccount } from './DerivAppsGetAccount'; import { DerivAppsTradingAccount } from './DerivAppsTradingAccount'; import './DerivAppsSection.scss'; -const DerivAppsSection: React.FC = ({ balance }) => { +const DerivAppsSection = () => { const { data: activeLinkedToTradingAccount } = useActiveLinkedToTradingAccount(); - return activeLinkedToTradingAccount?.loginid ? ( - - ) : ( - - ); + return activeLinkedToTradingAccount?.loginid ? : ; }; export default DerivAppsSection; diff --git a/packages/wallets/src/components/DerivAppsSection/DerivAppsTradingAccount.tsx b/packages/wallets/src/components/DerivAppsSection/DerivAppsTradingAccount.tsx index 39ecf57089af..551905e17b0d 100644 --- a/packages/wallets/src/components/DerivAppsSection/DerivAppsTradingAccount.tsx +++ b/packages/wallets/src/components/DerivAppsSection/DerivAppsTradingAccount.tsx @@ -3,19 +3,20 @@ import { useHistory } from 'react-router-dom'; import { useActiveLinkedToTradingAccount, useActiveWalletAccount, useAuthorize } from '@deriv/api-v2'; import { displayMoney } from '@deriv/api-v2/src/utils'; import { LabelPairedArrowUpArrowDownSmBoldIcon } from '@deriv/quill-icons'; +import useAllBalanceSubscription from '../../hooks/useAllBalanceSubscription'; import useDevice from '../../hooks/useDevice'; -import { TSubscribedBalance } from '../../types'; import { WalletText } from '../Base'; import { WalletListCardBadge } from '../WalletListCardBadge'; import { WalletMarketIcon } from '../WalletMarketIcon'; -const DerivAppsTradingAccount: React.FC = ({ balance }) => { +const DerivAppsTradingAccount = () => { const { isMobile } = useDevice(); const history = useHistory(); const { data: authorizeData } = useAuthorize(); - const { data: balanceData, isLoading } = balance; const { data: activeWallet } = useActiveWalletAccount(); const { data: activeLinkedToTradingAccount } = useActiveLinkedToTradingAccount(); + const { data: balanceData, isLoading: isBalanceLoading } = useAllBalanceSubscription(); + const balance = balanceData?.[activeLinkedToTradingAccount?.loginid ?? '']?.balance; return (
@@ -27,18 +28,14 @@ const DerivAppsTradingAccount: React.FC = ({ balance }) => { Options {activeWallet?.is_virtual && }
- {isLoading ? ( + {isBalanceLoading ? (
) : ( - {displayMoney( - balanceData?.accounts?.[activeLinkedToTradingAccount?.loginid ?? '']?.balance || 0, - activeLinkedToTradingAccount?.currency_config?.display_code || 'USD', - { - fractional_digits: activeLinkedToTradingAccount?.currency_config?.fractional_digits, - preferred_language: authorizeData?.preferred_language, - } - )} + {displayMoney(balance, activeLinkedToTradingAccount?.currency_config?.display_code, { + fractional_digits: activeLinkedToTradingAccount?.currency_config?.fractional_digits, + preferred_language: authorizeData?.preferred_language, + })} )} diff --git a/packages/wallets/src/components/DesktopWalletsList/DesktopWalletsList.tsx b/packages/wallets/src/components/DesktopWalletsList/DesktopWalletsList.tsx index de7d1c0aaf4b..3ca54664fbf9 100644 --- a/packages/wallets/src/components/DesktopWalletsList/DesktopWalletsList.tsx +++ b/packages/wallets/src/components/DesktopWalletsList/DesktopWalletsList.tsx @@ -1,13 +1,12 @@ import React from 'react'; import { useActiveWalletAccount } from '@deriv/api-v2'; -import { TSubscribedBalance } from '../../types'; import { AccountsList } from '../AccountsList'; import { WalletsCardLoader } from '../SkeletonLoader'; import { WalletListCard } from '../WalletListCard'; import { WalletsContainer } from '../WalletsContainer'; import './DesktopWalletsList.scss'; -const DesktopWalletsList: React.FC = ({ balance }) => { +const DesktopWalletsList = () => { const { data: activeWallet, isInitializing } = useActiveWalletAccount(); return ( @@ -16,9 +15,9 @@ const DesktopWalletsList: React.FC = ({ balance }) => { {!isInitializing && ( } + renderHeader={() => } > - + )}
diff --git a/packages/wallets/src/components/OptionsAndMultipliersListing/OptionsAndMultipliersListing.tsx b/packages/wallets/src/components/OptionsAndMultipliersListing/OptionsAndMultipliersListing.tsx index 15c56ed301b7..4d5c40bc35ea 100644 --- a/packages/wallets/src/components/OptionsAndMultipliersListing/OptionsAndMultipliersListing.tsx +++ b/packages/wallets/src/components/OptionsAndMultipliersListing/OptionsAndMultipliersListing.tsx @@ -6,14 +6,13 @@ import { LabelPairedChevronRightCaptionRegularIcon } from '@deriv/quill-icons'; import { optionsAndMultipliersContent } from '../../constants/constants'; import useDevice from '../../hooks/useDevice'; import { TRoute } from '../../routes/Router'; -import { TSubscribedBalance } from '../../types'; import { WalletLink, WalletText } from '../Base'; import { DerivAppsSection } from '../DerivAppsSection'; import { TradingAccountCard } from '../TradingAccountCard'; import LinkTitle from './LinkTitle'; import './OptionsAndMultipliersListing.scss'; -const OptionsAndMultipliersListing: React.FC = ({ balance }) => { +const OptionsAndMultipliersListing = () => { const { isMobile } = useDevice(); const history = useHistory(); const { data: activeLinkedToTradingAccount } = useActiveLinkedToTradingAccount(); @@ -36,7 +35,7 @@ const OptionsAndMultipliersListing: React.FC = ({ balance }) /> - +
{optionsAndMultipliersContent.map(account => { diff --git a/packages/wallets/src/components/OptionsAndMultipliersListing/__test__/OptionsAndMultipliersListing.spec.tsx b/packages/wallets/src/components/OptionsAndMultipliersListing/__test__/OptionsAndMultipliersListing.spec.tsx index 2286f532321c..c17055a63119 100644 --- a/packages/wallets/src/components/OptionsAndMultipliersListing/__test__/OptionsAndMultipliersListing.spec.tsx +++ b/packages/wallets/src/components/OptionsAndMultipliersListing/__test__/OptionsAndMultipliersListing.spec.tsx @@ -21,18 +21,6 @@ jest.mock('../../DerivAppsSection', () => ({ DerivAppsSection: () =>
DerivAppsSection
, })); -const mockBalance = { - data: { - balance: 100, - currency: 'USD', - }, - error: undefined, - isIdle: false, - isLoading: false, - isSubscribed: false, - unsubscribe: jest.fn(), -}; - const wrapper = ({ children }: PropsWithChildren) => ( @@ -46,7 +34,7 @@ describe('OptionsAndMultipliersListing', () => { (useActiveLinkedToTradingAccount as jest.Mock).mockReturnValue({ data: { loginid: 'MX-12345' }, }); - render(, { wrapper }); + render(, { wrapper }); expect(screen.getByText('DerivAppsSection')).toBeInTheDocument(); expect(screen.getAllByTestId('dt_wallets_trading_account_card')[0]).toBeInTheDocument(); expect(screen.getAllByTestId('dt_wallet_icon')[0]).toBeInTheDocument(); @@ -57,7 +45,7 @@ describe('OptionsAndMultipliersListing', () => { (useActiveLinkedToTradingAccount as jest.Mock).mockReturnValue({ data: { loginid: undefined }, }); - render(, { wrapper }); + render(, { wrapper }); expect(screen.getAllByTestId('dt_wallets_trading_account_card')[0]).toBeDisabled(); expect(screen.queryByTestId('dt_label_paired_chevron')).not.toBeInTheDocument(); }); diff --git a/packages/wallets/src/components/WalletCard/WalletCard.tsx b/packages/wallets/src/components/WalletCard/WalletCard.tsx index d5e8edb95465..b5e1db16929c 100644 --- a/packages/wallets/src/components/WalletCard/WalletCard.tsx +++ b/packages/wallets/src/components/WalletCard/WalletCard.tsx @@ -1,6 +1,6 @@ import React, { ComponentProps } from 'react'; import classNames from 'classnames'; -import { useBalance } from '@deriv/api-v2'; +import useAllBalanceSubscription from '../../hooks/useAllBalanceSubscription'; import { WalletText } from '../Base'; import { WalletCurrencyIcon } from '../WalletCurrencyIcon'; import { WalletGradientBackground } from '../WalletGradientBackground'; @@ -23,7 +23,7 @@ const WalletCard: React.FC = ({ isDemo, onClick, }) => { - const { isLoading } = useBalance(); + const { isLoading: isBalanceLoading } = useAllBalanceSubscription(); return (
diff --git a/packages/wallets/src/components/WalletListCardDetails/__tests__/WalletListCardDetails.spec.tsx b/packages/wallets/src/components/WalletListCardDetails/__tests__/WalletListCardDetails.spec.tsx index 91928596a7d4..9575764ffef2 100644 --- a/packages/wallets/src/components/WalletListCardDetails/__tests__/WalletListCardDetails.spec.tsx +++ b/packages/wallets/src/components/WalletListCardDetails/__tests__/WalletListCardDetails.spec.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { useActiveWalletAccount } from '@deriv/api-v2'; import { render, screen } from '@testing-library/react'; -import { TSubscribedBalance } from '../../../types'; import WalletListCardDetails from '../WalletListCardDetails'; jest.mock('@deriv/api-v2', () => ({ @@ -28,43 +27,13 @@ jest.mock('../../WalletListCardDropdown/WalletListCardDropdown', () => ({ default: jest.fn(() =>
Mocked WalletListCardDropdown
), })); -const mockBalanceData: TSubscribedBalance['balance'] = { - data: { - accounts: { - 1234567: { - balance: 1000.0, - converted_amount: 1000.0, - currency: 'USD', - demo_account: 0, - status: 1, - type: 'deriv', - }, - 7654321: { - balance: 1.0, - converted_amount: 1.0, - currency: 'BTC', - demo_account: 1, - status: 1, - type: 'deriv', - }, - }, - balance: 9990, - currency: 'USD', - loginid: 'CRW1314', - }, - error: undefined, - isIdle: false, - isLoading: false, - isSubscribed: false, -}; - describe('WalletListCardDetails', () => { beforeEach(() => { jest.clearAllMocks(); }); it('should render with default components correctly for real account', () => { - render(); + render(); expect(screen.getByText('Mocked WalletListCardActions')).toBeInTheDocument(); expect(screen.getByText('Mocked WalletListCardBalance')).toBeInTheDocument(); @@ -77,7 +46,7 @@ describe('WalletListCardDetails', () => { is_virtual: true, }, }); - render(); + render(); expect(screen.getByText('Mocked WalletListCardActions')).toBeInTheDocument(); expect(screen.getByText('Mocked WalletListCardBalance')).toBeInTheDocument(); diff --git a/packages/wallets/src/components/WalletListCardDropdown/WalletListCardDropdown.scss b/packages/wallets/src/components/WalletListCardDropdown/WalletListCardDropdown.scss index bd6e2d79b54a..156fec775290 100644 --- a/packages/wallets/src/components/WalletListCardDropdown/WalletListCardDropdown.scss +++ b/packages/wallets/src/components/WalletListCardDropdown/WalletListCardDropdown.scss @@ -2,6 +2,11 @@ position: relative; cursor: pointer; + &__balance-loader { + width: 14.1rem; + height: 2rem; + } + .wallets-textfield__field { cursor: pointer; } diff --git a/packages/wallets/src/components/WalletListCardDropdown/WalletListCardDropdown.tsx b/packages/wallets/src/components/WalletListCardDropdown/WalletListCardDropdown.tsx index 9b2740cef9b1..c2742d58a9a3 100644 --- a/packages/wallets/src/components/WalletListCardDropdown/WalletListCardDropdown.tsx +++ b/packages/wallets/src/components/WalletListCardDropdown/WalletListCardDropdown.tsx @@ -5,8 +5,9 @@ import { useEventListener, useOnClickOutside } from 'usehooks-ts'; import { useActiveWalletAccount, useWalletAccountsList } from '@deriv/api-v2'; import { displayMoney } from '@deriv/api-v2/src/utils'; import { LabelPairedChevronDownLgFillIcon } from '@deriv/quill-icons'; +import useAllBalanceSubscription from '../../hooks/useAllBalanceSubscription'; import useWalletAccountSwitcher from '../../hooks/useWalletAccountSwitcher'; -import { THooks, TSubscribedBalance } from '../../types'; +import { THooks } from '../../types'; import reactNodeToString from '../../utils/react-node-to-string'; import { WalletText, WalletTextField } from '../Base'; import { WalletCurrencyIcon } from '../WalletCurrencyIcon'; @@ -19,14 +20,14 @@ type WalletList = { text: React.ReactNode; }[]; -const WalletListCardDropdown: React.FC = ({ balance }) => { +const WalletListCardDropdown = () => { const { data: wallets } = useWalletAccountsList(); const { data: activeWallet } = useActiveWalletAccount(); const switchWalletAccount = useWalletAccountSwitcher(); const { t } = useTranslation(); const dropdownRef = useRef(null); - const { data: balanceData } = balance; + const { data: balanceData, isLoading: isBalanceLoading } = useAllBalanceSubscription(); const loginId = activeWallet?.loginid; const [inputWidth, setInputWidth] = useState('auto'); const [isOpen, setIsOpen] = useState(false); @@ -130,18 +131,25 @@ const WalletListCardDropdown: React.FC = ({ balance }) => { - - - + ) : ( + + + + )} diff --git a/packages/wallets/src/components/WalletListCardDropdown/__tests__/WalletListCardDropdown.spec.tsx b/packages/wallets/src/components/WalletListCardDropdown/__tests__/WalletListCardDropdown.spec.tsx index bb5c5be9f757..f149157024cf 100644 --- a/packages/wallets/src/components/WalletListCardDropdown/__tests__/WalletListCardDropdown.spec.tsx +++ b/packages/wallets/src/components/WalletListCardDropdown/__tests__/WalletListCardDropdown.spec.tsx @@ -1,7 +1,8 @@ -import React from 'react'; -import { useActiveWalletAccount, useWalletAccountsList } from '@deriv/api-v2'; +import React, { PropsWithChildren } from 'react'; +import { APIProvider, useActiveWalletAccount, useWalletAccountsList } from '@deriv/api-v2'; import { fireEvent, render, screen } from '@testing-library/react'; -import { TSubscribedBalance } from '../../../types'; +import WalletsAuthProvider from '../../../AuthProvider'; +import useAllBalanceSubscription from '../../../hooks/useAllBalanceSubscription'; import WalletListCardDropdown from '../WalletListCardDropdown'; import '@testing-library/jest-dom'; @@ -10,6 +11,12 @@ jest.mock('@deriv/api-v2', () => ({ useActiveWalletAccount: jest.fn(), useWalletAccountsList: jest.fn(), })); +jest.mock('../../../hooks/useAllBalanceSubscription', () => + jest.fn(() => ({ + data: undefined, + isLoading: false, + })) +); const mockSwitchAccount = jest.fn(); @@ -18,42 +25,22 @@ jest.mock('../../../hooks/useWalletAccountSwitcher', () => ({ default: jest.fn(() => mockSwitchAccount), })); -const mockBalanceData: TSubscribedBalance['balance'] = { - data: { - accounts: { - 1234567: { - balance: 1000.0, - converted_amount: 1000.0, - currency: 'USD', - demo_account: 0, - status: 1, - type: 'deriv', - }, - 7654321: { - balance: 1.0, - converted_amount: 1.0, - currency: 'BTC', - demo_account: 1, - status: 1, - type: 'deriv', - }, - }, - balance: 9990, - currency: 'USD', - loginid: 'CRW1314', - }, - error: undefined, - isIdle: false, - isLoading: false, - isSubscribed: false, -}; +const mockUseAllBalanceSubscription = useAllBalanceSubscription as jest.MockedFunction< + typeof useAllBalanceSubscription +>; + +const wrapper = ({ children }: PropsWithChildren) => ( + + {children} + +); describe('WalletListCardDropdown', () => { beforeEach(() => { jest.clearAllMocks(); (useActiveWalletAccount as jest.Mock).mockReturnValue({ data: { - loginid: '1234567', + loginid: 'CR1', }, }); (useWalletAccountsList as jest.Mock).mockReturnValue({ @@ -61,29 +48,27 @@ describe('WalletListCardDropdown', () => { { currency: 'USD', currency_config: { fractional_digits: 2 }, - display_balance: '1000.00', is_virtual: false, - loginid: '1234567', + loginid: 'CR1', }, { currency: 'BTC', currency_config: { fractional_digits: 8 }, - display_balance: '1.0000000', is_virtual: false, - loginid: '7654321', + loginid: 'BTC1', }, ], }); }); it('renders with correct default data', () => { - render(); + render(, { wrapper }); expect(screen.getByDisplayValue('USD Wallet')).toBeInTheDocument(); }); it('switches to selected account on click of the list item', () => { - render(); + render(, { wrapper }); const input = screen.getByDisplayValue('USD Wallet'); fireEvent.click(input); @@ -94,16 +79,26 @@ describe('WalletListCardDropdown', () => { const btcWallet = screen.getByText('BTC Wallet'); fireEvent.click(btcWallet); - expect(mockSwitchAccount).toHaveBeenCalledWith('7654321'); + expect(mockSwitchAccount).toHaveBeenCalledWith('BTC1'); expect(screen.getByDisplayValue('BTC Wallet')).toBeInTheDocument(); }); it('displays correct wallet details with balance in items list', () => { - render(); + (mockUseAllBalanceSubscription as jest.Mock).mockReturnValue({ + data: { + BTC1: { + balance: '1.0000000', + }, + CR1: { + balance: '1000.00', + }, + }, + isLoading: false, + }); + render(, { wrapper }); const input = screen.getByDisplayValue('USD Wallet'); fireEvent.click(input); - expect(screen.getByText('BTC Wallet')).toBeInTheDocument(); expect(screen.getByText('1.00000000 BTC')).toBeInTheDocument(); expect(screen.getByText('USD Wallet')).toBeInTheDocument(); @@ -115,7 +110,7 @@ describe('WalletListCardDropdown', () => { data: null, }); - render(); + render(, { wrapper }); expect(screen.queryByDisplayValue('USD Wallet')).not.toBeInTheDocument(); }); @@ -123,13 +118,13 @@ describe('WalletListCardDropdown', () => { it('handles case where wallets data is empty', () => { (useWalletAccountsList as jest.Mock).mockReturnValue({ data: [] }); - render(); + render(, { wrapper }); expect(screen.queryByDisplayValue('USD Wallet')).not.toBeInTheDocument(); }); it('closes the dropdown when clicking outside', () => { - render(); + render(, { wrapper }); const input = screen.getByDisplayValue('USD Wallet'); fireEvent.click(input); @@ -141,7 +136,7 @@ describe('WalletListCardDropdown', () => { }); it('closes the dropdown when pressing the escape key', () => { - render(); + render(, { wrapper }); const input = screen.getByDisplayValue('USD Wallet'); fireEvent.click(input); diff --git a/packages/wallets/src/components/WalletsCarousel/WalletsCarousel.tsx b/packages/wallets/src/components/WalletsCarousel/WalletsCarousel.tsx index e6e073631159..e3a292e14969 100644 --- a/packages/wallets/src/components/WalletsCarousel/WalletsCarousel.tsx +++ b/packages/wallets/src/components/WalletsCarousel/WalletsCarousel.tsx @@ -2,30 +2,34 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; import { useLocation } from 'react-router-dom'; import { useActiveWalletAccount } from '@deriv/api-v2'; import { displayMoney } from '@deriv/api-v2/src/utils'; -import { TSubscribedBalance } from '../../types'; +import useAllBalanceSubscription from '../../hooks/useAllBalanceSubscription'; import { AccountsList } from '../AccountsList'; import { WalletsCarouselContent } from '../WalletsCarouselContent'; import { WalletsCarouselHeader } from '../WalletsCarouselHeader'; import './WalletsCarousel.scss'; -const WalletsCarousel: React.FC = ({ balance }) => { +const WalletsCarousel = () => { const { data: activeWallet, isLoading: isActiveWalletLoading } = useActiveWalletAccount(); const [hideWalletsCarouselHeader, setHideWalletsCarouselHeader] = useState(true); const contentRef = useRef(null); const location = useLocation(); const [accountsActiveTabIndex, setAccountsActiveTabIndex] = useState(location.state?.accountsActiveTabIndex ?? 0); - const { data: balanceData, isLoading: isBalanceLoading } = balance; + const { data: balanceData, isLoading: isBalanceLoading } = useAllBalanceSubscription(); const displayedBalance = useMemo(() => { - return displayMoney?.( - balanceData?.accounts?.[activeWallet?.loginid ?? '']?.balance ?? 0, - activeWallet?.currency || '', - { - fractional_digits: activeWallet?.currency_config?.fractional_digits, - } - ); - }, [balanceData, activeWallet]); + if (isBalanceLoading) return; + + return displayMoney(balanceData?.[activeWallet?.loginid ?? '']?.balance, activeWallet?.currency, { + fractional_digits: activeWallet?.currency_config?.fractional_digits, + }); + }, [ + isBalanceLoading, + balanceData, + activeWallet?.loginid, + activeWallet?.currency, + activeWallet?.currency_config?.fractional_digits, + ]); // useEffect hook to handle event for hiding/displaying WalletsCarouselHeader // walletsCarouselHeader will be displayed when WalletsCarouselContent is almost out of viewport @@ -60,8 +64,8 @@ const WalletsCarousel: React.FC = ({ balance }) => { balance={displayedBalance} currency={activeWallet?.currency || 'USD'} hidden={hideWalletsCarouselHeader} + isBalanceLoading={isBalanceLoading} isDemo={activeWallet?.is_virtual} - isLoading={isBalanceLoading} /> )}
@@ -70,7 +74,6 @@ const WalletsCarousel: React.FC = ({ balance }) => {
diff --git a/packages/wallets/src/components/WalletsCarouselContent/WalletsCarouselContent.tsx b/packages/wallets/src/components/WalletsCarouselContent/WalletsCarouselContent.tsx index 5ad82927fd57..4046ebb2d962 100644 --- a/packages/wallets/src/components/WalletsCarouselContent/WalletsCarouselContent.tsx +++ b/packages/wallets/src/components/WalletsCarouselContent/WalletsCarouselContent.tsx @@ -1,13 +1,9 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import useEmblaCarousel, { EmblaCarouselType, EmblaEventType } from 'embla-carousel-react'; import { useHistory } from 'react-router-dom'; -import { - useActiveWalletAccount, - useBalanceSubscription, - useCurrencyConfig, - useMobileCarouselWalletsList, -} from '@deriv/api-v2'; +import { useActiveWalletAccount, useCurrencyConfig, useMobileCarouselWalletsList } from '@deriv/api-v2'; import { displayMoney } from '@deriv/api-v2/src/utils'; +import useAllBalanceSubscription from '../../hooks/useAllBalanceSubscription'; import useWalletAccountSwitcher from '../../hooks/useWalletAccountSwitcher'; import { THooks } from '../../types'; import { ProgressBar, WalletText } from '../Base'; @@ -37,13 +33,7 @@ const WalletsCarouselContent: React.FC = ({ accountsActiveTabIndex }) => const { data: walletAccountsList, isLoading: isWalletAccountsListLoading } = useMobileCarouselWalletsList(); const { data: activeWallet, isLoading: isActiveWalletLoading } = useActiveWalletAccount(); - const { - data: balanceData, - isLoading: isBalanceLoading, - isSubscribed, - subscribe, - unsubscribe, - } = useBalanceSubscription(); + const { data: balanceData } = useAllBalanceSubscription(); const { isLoading: isCurrencyConfigLoading } = useCurrencyConfig(); const [selectedLoginId, setSelectedLoginId] = useState(''); @@ -56,6 +46,16 @@ const WalletsCarouselContent: React.FC = ({ accountsActiveTabIndex }) => const transitionNodes = useRef([]); const transitionFactor = useRef(0); + const getBalance = ( + loginid: string, + currency?: string, + wallet?: ReturnType['data'] + ) => { + return displayMoney(balanceData?.[loginid]?.balance, currency, { + fractional_digits: wallet?.currency_config?.fractional_digits, + }); + }; + // sets the transition nodes to be scaled const setTransitionNodes = useCallback((walletsCarouselEmblaApi: EmblaCarouselType) => { // find and store all available wallet card containers for the transition nodes @@ -181,18 +181,11 @@ const WalletsCarouselContent: React.FC = ({ accountsActiveTabIndex }) => if (index !== -1) { walletsCarouselEmblaApi?.scrollTo(index); } - if (isSubscribed) unsubscribe(); - subscribe({ loginid: selectedLoginId }); }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedLoginId, walletAccountsList]); - // unsubscribe to the balance call if the whole component unmounts - useEffect(() => { - return () => unsubscribe(); - }, [unsubscribe]); - // initial loading useEffect(() => { if (walletsCarouselEmblaApi && isInitialDataLoaded) { @@ -254,17 +247,9 @@ const WalletsCarouselContent: React.FC = ({ accountsActiveTabIndex }) => {walletAccountsList?.map((account, index) => ( = ({ balance, currency, hidden, isDemo, isLoading }) => { +const WalletsCarouselHeader: React.FC = ({ balance, currency, hidden, isBalanceLoading, isDemo }) => { const history = useHistory(); return ( @@ -25,7 +25,7 @@ const WalletsCarouselHeader: React.FC = ({ balance, currency, hidden, is {currency} Wallet - {isLoading ? ( + {isBalanceLoading ? (
{ }); it('should display loader when balance is loading', () => { - render(); + render(); expect(screen.getByTestId('dt_wallets_carousel_header_balance_loader')).toBeInTheDocument(); }); diff --git a/packages/wallets/src/features/cashier/components/WalletCashierHeader/WalletCashierHeader.tsx b/packages/wallets/src/features/cashier/components/WalletCashierHeader/WalletCashierHeader.tsx index a781692976d0..936789dd687d 100644 --- a/packages/wallets/src/features/cashier/components/WalletCashierHeader/WalletCashierHeader.tsx +++ b/packages/wallets/src/features/cashier/components/WalletCashierHeader/WalletCashierHeader.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useRef } from 'react'; import classNames from 'classnames'; import { useHistory, useLocation } from 'react-router-dom'; -import { useActiveWalletAccount, useBalanceSubscription } from '@deriv/api-v2'; +import { useActiveWalletAccount } from '@deriv/api-v2'; import { displayMoney } from '@deriv/api-v2/src/utils'; import { LabelPairedArrowsRotateMdRegularIcon, @@ -13,6 +13,7 @@ import { } from '@deriv/quill-icons'; import { WalletCurrencyIcon, WalletGradientBackground, WalletText } from '../../../../components'; import { WalletListCardBadge } from '../../../../components/WalletListCardBadge'; +import useAllBalanceSubscription from '../../../../hooks/useAllBalanceSubscription'; import useDevice from '../../../../hooks/useDevice'; import i18n from '../../../../translations/i18n'; import './WalletCashierHeader.scss'; @@ -64,7 +65,7 @@ const virtualAccountTabs = [ const WalletCashierHeader: React.FC = ({ hideWalletDetails }) => { const { data: activeWallet } = useActiveWalletAccount(); - const { data: balanceData, isLoading, subscribe, unsubscribe } = useBalanceSubscription(); + const { data: balanceData, isLoading: isBalanceLoading } = useAllBalanceSubscription(); const { isMobile } = useDevice(); const activeTabRef = useRef(null); const history = useHistory(); @@ -80,15 +81,6 @@ const WalletCashierHeader: React.FC = ({ hideWalletDetails }) => { } }, [location.pathname, isMobile]); - useEffect(() => { - subscribe({ - loginid: activeWallet?.loginid, - }); - return () => { - unsubscribe(); - }; - }, [activeWallet?.loginid, subscribe, unsubscribe]); - return ( = ({ hideWalletDetails }) => { {isDemo && }
- {isLoading ? ( -
+ {isBalanceLoading ? ( +
) : ( - {displayMoney?.(balanceData?.balance ?? 0, activeWallet?.currency || '', { - fractional_digits: activeWallet?.currency_config?.fractional_digits, - })} + {displayMoney( + balanceData?.[activeWallet?.loginid ?? '']?.balance, + activeWallet?.currency, + { + fractional_digits: activeWallet?.currency_config?.fractional_digits, + } + )} )}
diff --git a/packages/wallets/src/features/cashier/components/WalletCashierHeader/__tests__/WalletCashierHeader.spec.tsx b/packages/wallets/src/features/cashier/components/WalletCashierHeader/__tests__/WalletCashierHeader.spec.tsx index 5651ab69f4a3..87616568ab06 100644 --- a/packages/wallets/src/features/cashier/components/WalletCashierHeader/__tests__/WalletCashierHeader.spec.tsx +++ b/packages/wallets/src/features/cashier/components/WalletCashierHeader/__tests__/WalletCashierHeader.spec.tsx @@ -1,14 +1,14 @@ import React from 'react'; -import { APIProvider, useActiveWalletAccount, useBalanceSubscription } from '@deriv/api-v2'; -import { render, screen, waitFor } from '@testing-library/react'; +import { APIProvider, useActiveWalletAccount } from '@deriv/api-v2'; +import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import WalletsAuthProvider from '../../../../../AuthProvider'; +import useAllBalanceSubscription from '../../../../../hooks/useAllBalanceSubscription'; import WalletCashierHeader from '../WalletCashierHeader'; jest.mock('@deriv/api-v2', () => ({ ...jest.requireActual('@deriv/api-v2'), useActiveWalletAccount: jest.fn(), - useBalanceSubscription: jest.fn(), })); const mockPush = jest.fn(); @@ -19,6 +19,8 @@ jest.mock('react-router-dom', () => ({ useLocation: () => ({ pathname: '/' }), })); +jest.mock('../../../../../hooks/useAllBalanceSubscription', () => jest.fn()); + const wrapper: React.FC = ({ children }) => ( {children} @@ -33,12 +35,12 @@ describe('', () => { loginid: 'CR1', }, }); - (useBalanceSubscription as jest.Mock).mockReturnValue({ + (useAllBalanceSubscription as jest.Mock).mockReturnValue({ data: { - balance: 10, + CR1: { + balance: 10, + }, }, - subscribe: jest.fn(), - unsubscribe: jest.fn(), }); }); @@ -72,41 +74,6 @@ describe('', () => { expect(screen.getByText('Demo')).toBeInTheDocument(); }); - it('should subscribe to the balance call when the header mounts', () => { - const mockSubscribe = jest.fn(); - - (useBalanceSubscription as jest.Mock).mockReturnValue({ - data: { - balance: 10, - }, - subscribe: mockSubscribe, - unsubscribe: jest.fn(), - }); - - render(, { wrapper }); - - expect(mockSubscribe).toBeCalledWith({ loginid: 'CR1' }); - }); - - it('should unsubscribe from the balance call when the header unmounts', async () => { - const mockUnsubscribe = jest.fn(); - - (useBalanceSubscription as jest.Mock).mockReturnValue({ - data: { - balance: 10, - }, - subscribe: jest.fn(), - unsubscribe: mockUnsubscribe, - }); - - const { unmount } = render(, { wrapper }); - - unmount(); - await waitFor(() => { - expect(mockUnsubscribe).toBeCalled(); - }); - }); - it('should display real transfer tabs - Deposit, Withdraw, Transfer, Transaction', () => { render(, { wrapper }); diff --git a/packages/wallets/src/features/cashier/flows/WalletWithdrawal/WalletWithdrawal.tsx b/packages/wallets/src/features/cashier/flows/WalletWithdrawal/WalletWithdrawal.tsx index 0a0fb405958a..788f6acbcf76 100644 --- a/packages/wallets/src/features/cashier/flows/WalletWithdrawal/WalletWithdrawal.tsx +++ b/packages/wallets/src/features/cashier/flows/WalletWithdrawal/WalletWithdrawal.tsx @@ -1,23 +1,17 @@ import React, { useEffect, useState } from 'react'; -import { useActiveWalletAccount, useAuthorize, useBalance } from '@deriv/api-v2'; +import { useActiveWalletAccount, useAuthorize } from '@deriv/api-v2'; import { Loader } from '@deriv-com/ui'; +import useAllBalanceSubscription from '../../../../hooks/useAllBalanceSubscription'; import { WithdrawalCryptoModule, WithdrawalFiatModule, WithdrawalVerificationModule } from '../../modules'; import { WithdrawalNoBalance } from '../../screens'; const WalletWithdrawal = () => { const { switchAccount } = useAuthorize(); const { data: activeWallet } = useActiveWalletAccount(); - const { data: balanceData, isLoading, isRefetching, refetch } = useBalance(); + const { data: balanceData, isLoading: isBalanceLoading } = useAllBalanceSubscription(); const [verificationCode, setVerificationCode] = useState(''); const [resendEmail, setResendEmail] = useState(false); - const isBalanceLoading = isLoading && !isRefetching; - - useEffect(() => { - refetch(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - useEffect(() => { const queryParams = new URLSearchParams(location.search); const loginidQueryParam = queryParams.get('loginid'); @@ -53,11 +47,7 @@ const WalletWithdrawal = () => { return ; } - if ( - balanceData.accounts && - !isBalanceLoading && - balanceData.accounts[activeWallet?.loginid ?? 'USD'].balance <= 0 - ) { + if (balanceData && !isBalanceLoading && balanceData[activeWallet?.loginid ?? 'USD'].balance <= 0) { return ; } diff --git a/packages/wallets/src/features/cashier/flows/WalletWithdrawal/__tests__/WalletWithdrawal.spec.tsx b/packages/wallets/src/features/cashier/flows/WalletWithdrawal/__tests__/WalletWithdrawal.spec.tsx index 7a4cec144864..6ad2999d1c56 100644 --- a/packages/wallets/src/features/cashier/flows/WalletWithdrawal/__tests__/WalletWithdrawal.spec.tsx +++ b/packages/wallets/src/features/cashier/flows/WalletWithdrawal/__tests__/WalletWithdrawal.spec.tsx @@ -1,6 +1,7 @@ import React, { PropsWithChildren } from 'react'; -import { useActiveWalletAccount, useBalance } from '@deriv/api-v2'; +import { useActiveWalletAccount } from '@deriv/api-v2'; import { render, screen } from '@testing-library/react'; +import useAllBalanceSubscription from '../../../../../hooks/useAllBalanceSubscription'; import { CashierLocked, WithdrawalLocked } from '../../../modules'; import WalletWithdrawal from '../WalletWithdrawal'; @@ -43,8 +44,35 @@ jest.mock('@deriv/api-v2', () => ({ useBalance: jest.fn(), })); +jest.mock('../../../../../hooks/useAllBalanceSubscription', () => + jest.fn(() => ({ + data: { + CR42069: { + balance: 100, + converted_amount: 100, + currency: 'USD', + demo_account: 0, + status: 1, + type: 'deriv', + }, + CR69420: { + balance: 50, + converted_amount: 50, + currency: 'USD', + demo_account: 0, + status: 1, + type: 'deriv', + }, + }, + isLoading: false, + setBalanceData: jest.fn(), + })) +); + const mockUseActiveWalletAccount = useActiveWalletAccount as jest.MockedFunction; -const mockUseBalance = useBalance as jest.Mock; +const mockUseAllBalanceSubscription = useAllBalanceSubscription as jest.MockedFunction< + typeof useAllBalanceSubscription +>; const wrapper = ({ children }: PropsWithChildren) => ( @@ -60,17 +88,6 @@ describe('WalletWithdrawal', () => { value: new URL('http://localhost/redirect?verification=1234&loginid=CR42069'), writable: true, }); - mockUseBalance.mockReturnValue({ - data: { - accounts: { - CR42069: { balance: 100 }, - CR69420: { balance: 50 }, - }, - }, - isLoading: false, - isRefetching: false, - refetch: jest.fn(), - }); }); afterEach(() => { @@ -84,7 +101,6 @@ describe('WalletWithdrawal', () => { mockUseActiveWalletAccount.mockReturnValue({ // @ts-expect-error - since this is a mock, we only need partial properties of the hook data: { - balance: 100, currency: 'USD', loginid: 'CR69420', }, @@ -101,7 +117,6 @@ describe('WalletWithdrawal', () => { mockUseActiveWalletAccount.mockReturnValue({ // @ts-expect-error - since this is a mock, we only need partial properties of the hook data: { - balance: 100, currency: 'USD', loginid: 'CR42069', }, @@ -121,7 +136,6 @@ describe('WalletWithdrawal', () => { mockUseActiveWalletAccount.mockReturnValue({ // @ts-expect-error - since this is a mock, we only need partial properties of the hook data: { - balance: 100, currency: 'USD', loginid: 'CR42069', }, @@ -135,7 +149,6 @@ describe('WalletWithdrawal', () => { mockUseActiveWalletAccount.mockReturnValue({ // @ts-expect-error - since this is a mock, we only need partial properties of the hook data: { - balance: 100, currency: 'USD', loginid: 'CR42069', }, @@ -149,7 +162,6 @@ describe('WalletWithdrawal', () => { it('should render withdrawal crypto module if withdrawal is for crypto wallet', async () => { mockUseActiveWalletAccount.mockReturnValue({ data: { - balance: 100, currency: 'BTC', // @ts-expect-error - since this is a mock, we only need partial properties of the hook currency_config: { is_crypto: true }, @@ -165,9 +177,7 @@ describe('WalletWithdrawal', () => { it('should show loader if verification code is activeWallet data has not been received yet', () => { // @ts-expect-error - since this is a mock, we only need partial properties of the hook mockUseActiveWalletAccount.mockReturnValue({}); - mockUseBalance.mockReturnValue({ - refetch: jest.fn(), - }); + (mockUseAllBalanceSubscription as jest.Mock).mockReturnValue({ data: undefined, isLoading: true }); render(, { wrapper }); expect(screen.getByText('Loading...')).toBeInTheDocument(); @@ -176,7 +186,6 @@ describe('WalletWithdrawal', () => { it('should test if WithdrawalNoBalance screen is rendered if the wallet balance has zero balance', () => { mockUseActiveWalletAccount.mockReturnValue({ data: { - balance: 0, currency: 'BTC', // @ts-expect-error - since this is a mock, we only need partial properties of the hook currency_config: { is_crypto: true }, @@ -184,15 +193,17 @@ describe('WalletWithdrawal', () => { }, }); - mockUseBalance.mockReturnValue({ + (mockUseAllBalanceSubscription as jest.Mock).mockReturnValue({ data: { - accounts: { - CR42069: { balance: 0 }, + CR42069: { + balance: 0, + converted_amount: 0, + currency: 'BTC', + demo_account: 0, + status: 1, + type: 'deriv', }, }, - isLoading: false, - isRefetching: false, - refetch: jest.fn(), }); render(, { wrapper }); diff --git a/packages/wallets/src/features/cashier/modules/Transfer/components/TransferReceipt/__tests__/TransferReceipt.spec.tsx b/packages/wallets/src/features/cashier/modules/Transfer/components/TransferReceipt/__tests__/TransferReceipt.spec.tsx index ac6e07e2827a..6d6e77b97cef 100644 --- a/packages/wallets/src/features/cashier/modules/Transfer/components/TransferReceipt/__tests__/TransferReceipt.spec.tsx +++ b/packages/wallets/src/features/cashier/modules/Transfer/components/TransferReceipt/__tests__/TransferReceipt.spec.tsx @@ -49,9 +49,6 @@ const ACCOUNTS = [ jest.mock('@deriv/api-v2', () => ({ ...jest.requireActual('@deriv/api-v2'), - useBalance: jest.fn(() => ({ - isLoading: false, - })), useTransferBetweenAccounts: jest.fn(() => ({ data: { accounts: ACCOUNTS }, })), @@ -102,6 +99,13 @@ jest.mock('../../../provider', () => ({ })), })); +jest.mock('../../../../../../../hooks/useAllBalanceSubscription', () => + jest.fn(() => ({ + data: undefined, + isLoading: false, + })) +); + const wrapper = ({ children }: PropsWithChildren) => { return ( diff --git a/packages/wallets/src/features/cashier/modules/Transfer/hooks/useExtendedTransferAccountProperties.ts b/packages/wallets/src/features/cashier/modules/Transfer/hooks/useExtendedTransferAccountProperties.ts index 9e9885daabd4..93eb1865338d 100644 --- a/packages/wallets/src/features/cashier/modules/Transfer/hooks/useExtendedTransferAccountProperties.ts +++ b/packages/wallets/src/features/cashier/modules/Transfer/hooks/useExtendedTransferAccountProperties.ts @@ -26,7 +26,7 @@ const useExtendedTransferAccountProperties = ( landingCompanyName: activeWallet?.landing_company_name as TWalletLandingCompanyName, mt5MarketType: getMarketType(account.mt5_group), }); - const displayBalance = displayMoney(Number(account.balance) || 0, currencyConfig?.display_code || 'USD', { + const displayBalance = displayMoney(Number(account.balance), currencyConfig?.display_code, { fractional_digits: currencyConfig?.fractional_digits, preferred_language: authorizeData?.preferred_language, }); diff --git a/packages/wallets/src/features/cashier/modules/WithdrawalCrypto/components/WithdrawalCryptoForm/components/WithdrawalCryptoAmountConverter/WithdrawalCryptoAmountConverter.tsx b/packages/wallets/src/features/cashier/modules/WithdrawalCrypto/components/WithdrawalCryptoForm/components/WithdrawalCryptoAmountConverter/WithdrawalCryptoAmountConverter.tsx index 91380a888cb3..bc52e5520bfb 100644 --- a/packages/wallets/src/features/cashier/modules/WithdrawalCrypto/components/WithdrawalCryptoForm/components/WithdrawalCryptoAmountConverter/WithdrawalCryptoAmountConverter.tsx +++ b/packages/wallets/src/features/cashier/modules/WithdrawalCrypto/components/WithdrawalCryptoForm/components/WithdrawalCryptoAmountConverter/WithdrawalCryptoAmountConverter.tsx @@ -1,8 +1,10 @@ import React, { useState } from 'react'; import classNames from 'classnames'; import { Field, FieldProps, useFormikContext } from 'formik'; +import { displayMoney } from '@deriv/api-v2/src/utils'; import { LegacyArrowRight2pxIcon } from '@deriv/quill-icons'; import { WalletTextField } from '../../../../../../../../components'; +import useAllBalanceSubscription from '../../../../../../../../hooks/useAllBalanceSubscription'; import { useWithdrawalCryptoContext } from '../../../../provider'; import type { TWithdrawalForm } from '../../../../types'; import { validateCryptoInput, validateFiatInput } from '../../../../utils'; @@ -21,10 +23,15 @@ const WithdrawalCryptoAmountConverter: React.FC = () => { const [isCryptoInputActive, setIsCryptoInputActive] = useState(true); const { errors, setValues } = useFormikContext(); + const { data: balanceData } = useAllBalanceSubscription(); + const balance = balanceData?.[activeWallet?.loginid ?? '']?.balance ?? 0; + const displayBalance = displayMoney(balance, activeWallet?.currency, { + fractional_digits: activeWallet?.currency_config?.fractional_digits, + }); const onChangeCryptoInput = (e: React.ChangeEvent) => { const convertedValue = !validateCryptoInput( - activeWallet, + { balance, currency: activeWallet?.currency ?? '', displayBalance }, fractionalDigits, isClientVerified, accountLimits?.remainder ?? 0, @@ -59,7 +66,7 @@ const WithdrawalCryptoAmountConverter: React.FC = () => { name='cryptoAmount' validate={(value: string) => validateCryptoInput( - activeWallet, + { balance, currency: activeWallet?.currency ?? '', displayBalance }, fractionalDigits, isClientVerified, accountLimits?.remainder ?? 0, diff --git a/packages/wallets/src/features/cashier/modules/WithdrawalCrypto/components/WithdrawalCryptoForm/components/WithdrawalCryptoAmountConverter/__tests__/WithdrawalCryptoAmountConverter.spec.tsx b/packages/wallets/src/features/cashier/modules/WithdrawalCrypto/components/WithdrawalCryptoForm/components/WithdrawalCryptoAmountConverter/__tests__/WithdrawalCryptoAmountConverter.spec.tsx index 9a76f6c1b107..029c6a47e899 100644 --- a/packages/wallets/src/features/cashier/modules/WithdrawalCrypto/components/WithdrawalCryptoForm/components/WithdrawalCryptoAmountConverter/__tests__/WithdrawalCryptoAmountConverter.spec.tsx +++ b/packages/wallets/src/features/cashier/modules/WithdrawalCrypto/components/WithdrawalCryptoForm/components/WithdrawalCryptoAmountConverter/__tests__/WithdrawalCryptoAmountConverter.spec.tsx @@ -1,9 +1,11 @@ import React from 'react'; import { Formik } from 'formik'; import { act, fireEvent, render, screen } from '@testing-library/react'; +import WalletsAuthProvider from '../../../../../../../../../AuthProvider'; import { useWithdrawalCryptoContext } from '../../../../../provider'; import { validateCryptoInput, validateFiatInput } from '../../../../../utils'; import WithdrawalCryptoAmountConverter from '../WithdrawalCryptoAmountConverter'; +import { APIProvider } from '@deriv/api-v2'; jest.mock('../../../../../utils', () => ({ ...jest.requireActual('../../../../../utils'), @@ -24,17 +26,21 @@ const mockValidateFiatInput = validateFiatInput as jest.Mock; const wrapper: React.FC = ({ children }) => { return ( - - {children} - + + + + {children} + + + ); }; diff --git a/packages/wallets/src/features/cashier/modules/WithdrawalCrypto/components/WithdrawalCryptoForm/components/WithdrawalCryptoPercentageSelector/WithdrawalCryptoPercentageSelector.tsx b/packages/wallets/src/features/cashier/modules/WithdrawalCrypto/components/WithdrawalCryptoForm/components/WithdrawalCryptoPercentageSelector/WithdrawalCryptoPercentageSelector.tsx index 1b0c7c66195e..1f5acd375478 100644 --- a/packages/wallets/src/features/cashier/modules/WithdrawalCrypto/components/WithdrawalCryptoForm/components/WithdrawalCryptoPercentageSelector/WithdrawalCryptoPercentageSelector.tsx +++ b/packages/wallets/src/features/cashier/modules/WithdrawalCrypto/components/WithdrawalCryptoForm/components/WithdrawalCryptoPercentageSelector/WithdrawalCryptoPercentageSelector.tsx @@ -1,6 +1,8 @@ import React from 'react'; import { useFormikContext } from 'formik'; +import { displayMoney } from '@deriv/api-v2/src/utils'; import { WalletsPercentageSelector, WalletText } from '../../../../../../../../components'; +import useAllBalanceSubscription from '../../../../../../../../hooks/useAllBalanceSubscription'; import { useWithdrawalCryptoContext } from '../../../../provider'; import { TWithdrawalForm } from '../../../../types'; import { validateCryptoInput, validateFiatInput } from '../../../../utils'; @@ -11,19 +13,33 @@ const WithdrawalCryptoPercentageSelector: React.FC = () => { const { accountLimits, activeWallet, cryptoConfig, fractionalDigits, getConvertedFiatAmount, isClientVerified } = useWithdrawalCryptoContext(); + const { data: balanceData } = useAllBalanceSubscription(); + const activeWalletBalance = balanceData?.[activeWallet?.loginid ?? '']?.balance ?? 0; + const activeWalletDisplayBalance = displayMoney( + balanceData?.[activeWallet?.loginid ?? '']?.balance, + activeWallet?.currency, + { + fractional_digits: activeWallet?.currency_config?.fractional_digits, + } + ); + const getPercentageMessage = (value: string) => { const amount = parseFloat(value); - if (!activeWallet?.balance || !activeWallet.display_balance) return; + if (!activeWalletBalance || !activeWalletDisplayBalance) return; - if (amount <= activeWallet.balance) { - const percentage = Math.round((amount * 100) / activeWallet.balance); - return `${percentage}% of available balance (${activeWallet.display_balance})`; + if (amount <= activeWalletBalance) { + const percentage = Math.round((amount * 100) / activeWalletBalance); + return `${percentage}% of available balance (${activeWalletDisplayBalance})`; } }; const isValidInput = !validateCryptoInput( - activeWallet, + { + balance: activeWalletBalance, + currency: activeWallet?.currency ?? '', + displayBalance: activeWalletDisplayBalance, + }, fractionalDigits, isClientVerified, accountLimits?.remainder ?? 0, @@ -35,19 +51,23 @@ const WithdrawalCryptoPercentageSelector: React.FC = () => {
{ - if (activeWallet?.balance) { + if (activeWalletBalance) { const fraction = percentage / 100; - const cryptoAmount = (activeWallet.balance * fraction).toFixed(fractionalDigits.crypto); + const cryptoAmount = (activeWalletBalance * fraction).toFixed(fractionalDigits.crypto); const fiatAmount = !validateCryptoInput( - activeWallet, + { + balance: activeWalletBalance, + currency: activeWallet?.currency ?? '', + displayBalance: activeWalletDisplayBalance, + }, fractionalDigits, isClientVerified, accountLimits?.remainder ?? 0, diff --git a/packages/wallets/src/features/cashier/modules/WithdrawalCrypto/components/WithdrawalCryptoForm/components/WithdrawalCryptoPercentageSelector/__tests__/WithdrawalCryptoPercentageSelector.spec.tsx b/packages/wallets/src/features/cashier/modules/WithdrawalCrypto/components/WithdrawalCryptoForm/components/WithdrawalCryptoPercentageSelector/__tests__/WithdrawalCryptoPercentageSelector.spec.tsx index 9f4ea6ed43fc..d8c47e808abe 100644 --- a/packages/wallets/src/features/cashier/modules/WithdrawalCrypto/components/WithdrawalCryptoForm/components/WithdrawalCryptoPercentageSelector/__tests__/WithdrawalCryptoPercentageSelector.spec.tsx +++ b/packages/wallets/src/features/cashier/modules/WithdrawalCrypto/components/WithdrawalCryptoForm/components/WithdrawalCryptoPercentageSelector/__tests__/WithdrawalCryptoPercentageSelector.spec.tsx @@ -1,6 +1,7 @@ import React from 'react'; import * as formik from 'formik'; import { render, screen } from '@testing-library/react'; +import useAllBalanceSubscription from '../../../../../../../../../hooks/useAllBalanceSubscription'; import { useWithdrawalCryptoContext } from '../../../../../provider'; import WithdrawalCryptoPercentageSelector from '../WithdrawalCryptoPercentageSelector'; @@ -9,19 +10,32 @@ jest.mock('../../../../../provider', () => ({ useWithdrawalCryptoContext: jest.fn(), })); +jest.mock('../../../../../../../../../hooks/useAllBalanceSubscription', () => + jest.fn(() => ({ + data: undefined, + isLoading: false, + })) +); + const mockUseFormikContext = jest.spyOn(formik, 'useFormikContext') as jest.Mock; const mockUseWithdrawalCryptoContext = useWithdrawalCryptoContext as jest.Mock; +const mockUseAllBalanceSubscription = useAllBalanceSubscription as jest.MockedFunction< + typeof useAllBalanceSubscription +>; describe('', () => { beforeEach(() => { + (mockUseAllBalanceSubscription as jest.Mock).mockReturnValue({ + data: { CR1: { balance: 10 } }, + }); mockUseWithdrawalCryptoContext.mockReturnValue({ accountLimits: { remainder: 0, }, activeWallet: { - balance: 10, currency: 'BTC', - display_balance: '10.00000000 BTC', + currency_config: { fractional_digits: 8 }, + loginid: 'CR1', }, cryptoConfig: { minimum_withdrawal: 1, diff --git a/packages/wallets/src/features/cashier/modules/WithdrawalCrypto/utils/__tests__/withdrawalCryptoValidators.spec.ts b/packages/wallets/src/features/cashier/modules/WithdrawalCrypto/utils/__tests__/withdrawalCryptoValidators.spec.ts index b935973da9d6..d5e1ab2a272f 100644 --- a/packages/wallets/src/features/cashier/modules/WithdrawalCrypto/utils/__tests__/withdrawalCryptoValidators.spec.ts +++ b/packages/wallets/src/features/cashier/modules/WithdrawalCrypto/utils/__tests__/withdrawalCryptoValidators.spec.ts @@ -1,11 +1,10 @@ -import { THooks } from '../../../../../../types'; import { validateCryptoAddress, validateCryptoInput, validateFiatInput } from '../withdrawalCryptoValidators'; describe('withdrawalCryptoValidator', () => { let mockValue: string, mockIsClientVerified: boolean, mockCryptoAddress: string, - mockActiveWallet: THooks.ActiveWalletAccount, + mockActiveWallet: Parameters[0], mockFractionalDigits: { crypto: number; fiat: number }, mockRemainder: number, mockMinimumWithdrawal: number; @@ -167,11 +166,10 @@ describe('withdrawalCryptoValidator', () => { }); it('should return `balanceLessThanMinWithdrawalLimit` error', () => { - //@ts-expect-error since this is a mock, we only need partial properties of data mockActiveWallet = { balance: 0.3, currency: 'BTC', - display_balance: '0.3000000 BTC', + displayBalance: '0.3000000 BTC', }; mockValue = '0.2000000'; mockMinimumWithdrawal = 0.5; diff --git a/packages/wallets/src/features/cashier/modules/WithdrawalCrypto/utils/withdrawalCryptoValidators.ts b/packages/wallets/src/features/cashier/modules/WithdrawalCrypto/utils/withdrawalCryptoValidators.ts index 740872d98cd4..f7e1c422f381 100644 --- a/packages/wallets/src/features/cashier/modules/WithdrawalCrypto/utils/withdrawalCryptoValidators.ts +++ b/packages/wallets/src/features/cashier/modules/WithdrawalCrypto/utils/withdrawalCryptoValidators.ts @@ -54,7 +54,11 @@ const checkIfInvalidInput = ( }; const validateCryptoInput = ( - activeWallet: TWithdrawalCryptoContext['activeWallet'], + activeWallet: { + balance: number; + currency: string; + displayBalance: string; + }, fractionalDigits: TWithdrawalCryptoContext['fractionalDigits'], isClientVerified: TWithdrawalCryptoContext['isClientVerified'], remainder: number, @@ -75,7 +79,7 @@ const validateCryptoInput = ( if (MIN_WITHDRAWAL_AMOUNT && activeWallet.balance < MIN_WITHDRAWAL_AMOUNT) { return helperMessageMapper.balanceLessThanMinWithdrawalLimit( - activeWallet.display_balance, + activeWallet.displayBalance, `${MIN_WITHDRAWAL_AMOUNT.toFixed(fractionalDigits.crypto)} ${activeWallet.currency}` ); } diff --git a/packages/wallets/src/hooks/__tests__/useAllBalanceSubscription.spec.ts b/packages/wallets/src/hooks/__tests__/useAllBalanceSubscription.spec.ts new file mode 100644 index 000000000000..d27cbb99e1b3 --- /dev/null +++ b/packages/wallets/src/hooks/__tests__/useAllBalanceSubscription.spec.ts @@ -0,0 +1,140 @@ +import { useAuthorize, useBalanceSubscription } from '@deriv/api-v2'; +import { renderHook } from '@testing-library/react-hooks'; +import useAllBalanceSubscription from '../useAllBalanceSubscription'; + +jest.mock('@deriv/api-v2', () => ({ + ...jest.requireActual('@deriv/api-v2'), + useAuthorize: jest.fn(), + useBalanceSubscription: jest.fn(), +})); + +const mockAuthorize = useAuthorize as jest.MockedFunction; +const mockUseBalanceSubscription = useBalanceSubscription as jest.MockedFunction; + +describe('useAllBalanceSubscription', () => { + it('does not subscribe to all balance when subscribeToAllBalance is called before authorize', () => { + const mockSubscribe = jest.fn(); + const mockUnsubscribe = jest.fn(); + (mockUseBalanceSubscription as jest.Mock).mockReturnValue({ + data: {}, + isLoading: false, + isSubscribed: false, + subscribe: mockSubscribe, + unsubscribe: mockUnsubscribe, + }); + (mockAuthorize as jest.Mock).mockReturnValue({ + isSuccess: false, + }); + const { result } = renderHook(() => useAllBalanceSubscription()); + const { subscribeToAllBalance } = result.current; + subscribeToAllBalance(); + expect(mockSubscribe).not.toHaveBeenCalled(); + }); + it('subscribes to all balance when subscribeToAllBalance is called', () => { + const mockSubscribe = jest.fn(); + const mockUnsubscribe = jest.fn(); + (mockUseBalanceSubscription as jest.Mock).mockReturnValue({ + data: {}, + isLoading: false, + isSubscribed: false, + subscribe: mockSubscribe, + unsubscribe: mockUnsubscribe, + }); + (mockAuthorize as jest.Mock).mockReturnValue({ + isSuccess: true, + }); + const { result } = renderHook(() => useAllBalanceSubscription()); + const { subscribeToAllBalance } = result.current; + subscribeToAllBalance(); + expect(mockSubscribe).toHaveBeenCalledWith({ + account: 'all', + }); + }); + it('does not set data before the subscription is successful', () => { + const mockSubscribe = jest.fn(); + const mockUnsubscribe = jest.fn(); + (mockUseBalanceSubscription as jest.Mock).mockReturnValue({ + data: { + accounts: { + CRW1: { + balance: 100, + currency: 'USD', + }, + }, + }, + isLoading: true, + isSubscribed: false, + subscribe: mockSubscribe, + unsubscribe: mockUnsubscribe, + }); + (mockAuthorize as jest.Mock).mockReturnValue({ + isSuccess: true, + }); + const { result } = renderHook(() => useAllBalanceSubscription()); + expect(result.current.data).toBeUndefined(); + expect(result.current.isLoading).toBeTruthy(); + }); + it('sets data when the initial subscription is successful', () => { + const mockSubscribe = jest.fn(); + const mockUnsubscribe = jest.fn(); + (mockUseBalanceSubscription as jest.Mock).mockReturnValue({ + data: { + accounts: { + CRW1: { + balance: 100, + currency: 'USD', + }, + }, + }, + isLoading: false, + isSubscribed: true, + subscribe: mockSubscribe, + unsubscribe: mockUnsubscribe, + }); + (mockAuthorize as jest.Mock).mockReturnValue({ + isSuccess: true, + }); + const { result } = renderHook(() => useAllBalanceSubscription()); + expect(result.current.data).toEqual({ + CRW1: { + balance: 100, + currency: 'USD', + }, + }); + }); + it('sets the data when the subsequent responses are received from the api', () => { + const mockSubscribe = jest.fn(); + const mockUnsubscribe = jest.fn(); + (mockUseBalanceSubscription as jest.Mock).mockReturnValue({ + data: { + balance: 9996, + currency: 'USD', + loginid: 'CRW1', + total: { + deriv: { + amount: 10000, + currency: 'USD', + }, + }, + }, + isLoading: false, + isSubscribed: true, + subscribe: mockSubscribe, + unsubscribe: mockUnsubscribe, + }); + (mockAuthorize as jest.Mock).mockReturnValue({ + isSuccess: true, + }); + const { result } = renderHook(() => useAllBalanceSubscription()); + expect(result.current.data).toEqual({ + CRW1: { + balance: 9996, + converted_amount: 9996, + currency: 'USD', + demo_account: 0, + status: 0, + type: 'deriv', + }, + }); + }); +}); diff --git a/packages/wallets/src/hooks/useAllBalanceSubscription.ts b/packages/wallets/src/hooks/useAllBalanceSubscription.ts new file mode 100644 index 000000000000..83ce1da0a235 --- /dev/null +++ b/packages/wallets/src/hooks/useAllBalanceSubscription.ts @@ -0,0 +1,66 @@ +import { useCallback, useEffect, useState } from 'react'; +import { useAuthorize, useBalanceSubscription } from '@deriv/api-v2'; +import Observable from '../utils/observable'; + +type TBalance = ReturnType['data']['accounts']; + +const balanceStore = new Observable(undefined); + +/** + * Custom hook that manages subscription to balance changes from `balanceStore`. + * Retrieves initial balance and subscribes to future updates. + * @returns An object containing the current balance and a function to update it. + * @example const { data: balanceData, subscribeToAllBalance } = useAllBalanceSubscription(); + */ +const useAllBalanceSubscription = () => { + const [balance, setBalance] = useState(balanceStore.get()); + const { + data: balanceData, + isLoading: isBalanceLoading, + isSubscribed, + subscribe, + unsubscribe, + } = useBalanceSubscription(); + const { isSuccess: isAuthorizeSuccess } = useAuthorize(); + useEffect(() => { + return balanceStore.subscribe(setBalance); // subscribe setBalance to the balance store and return the cleanup function + }, []); + + const subscribeToAllBalance = useCallback(() => { + if (!isAuthorizeSuccess) return; + subscribe({ + account: 'all', + }); + }, [isAuthorizeSuccess, subscribe]); + + useEffect(() => { + if (!isAuthorizeSuccess || isBalanceLoading || Object.entries(balanceData).length === 0) return; // don't update the balance if the user is not authorized, the balance is loading, or the balance data is empty (i.e. before the call to subscribe is made). + const oldBalance = balanceStore.get(); + let newBalance = balanceData.accounts; + if (!balanceData.accounts && balanceData.balance !== undefined && balanceData.loginid && balanceData.currency) { + const { balance, currency, loginid } = balanceData; + newBalance = { + ...oldBalance, + [loginid]: { + balance, + converted_amount: balance, + currency, + demo_account: oldBalance?.[loginid]?.demo_account ?? 0, + status: oldBalance?.[loginid]?.status ?? 0, + type: oldBalance?.[loginid]?.type ?? 'deriv', + }, + }; + } + balanceStore.set(newBalance); + }, [balanceData, isBalanceLoading, isAuthorizeSuccess]); + + return { + data: balance, + isLoading: !balance, + isSubscribed, + subscribeToAllBalance, + unsubscribeFromAllBalance: unsubscribe, + }; +}; + +export default useAllBalanceSubscription; diff --git a/packages/wallets/src/routes/WalletsListingRoute/WalletsListingRoute.tsx b/packages/wallets/src/routes/WalletsListingRoute/WalletsListingRoute.tsx index 561968b05f24..083f9a701ce6 100644 --- a/packages/wallets/src/routes/WalletsListingRoute/WalletsListingRoute.tsx +++ b/packages/wallets/src/routes/WalletsListingRoute/WalletsListingRoute.tsx @@ -1,5 +1,4 @@ -import React, { lazy, useEffect } from 'react'; -import { useAuthorize, useBalanceSubscription } from '@deriv/api-v2'; +import React, { lazy } from 'react'; import { WalletListHeader, WalletsAddMoreCarousel, @@ -16,28 +15,17 @@ const LazyDesktopWalletsList = lazy(() => import('../../components/DesktopWallet const WalletsListingRoute: React.FC = () => { const { isMobile } = useDevice(); - const { subscribe, unsubscribe, ...rest } = useBalanceSubscription(); - const { isSuccess } = useAuthorize(); - useEffect(() => { - if (!isSuccess) return; - subscribe({ - account: 'all', - }); - return () => { - unsubscribe(); - }; - }, [isSuccess, subscribe, unsubscribe]); return (
{isMobile ? ( }> - + ) : ( }> - + )} diff --git a/packages/wallets/src/types.ts b/packages/wallets/src/types.ts index 7a9a056f396a..ed4af7c3f869 100644 --- a/packages/wallets/src/types.ts +++ b/packages/wallets/src/types.ts @@ -8,7 +8,6 @@ import type { useAuthentication, useAuthorize, useAvailableMT5Accounts, - useBalanceSubscription, useCreateMT5Account, useCreateOtherCFDAccount, useCreateWallet, @@ -89,10 +88,6 @@ export namespace TDisplayBalance { export type CtraderAccountsList = THooks.CtraderAccountsList['display_balance']; export type DxtradeAccountsList = THooks.DxtradeAccountsList['display_balance']; export type MT5AccountsList = THooks.MT5AccountsList['display_balance']; - export type WalletAccountsList = THooks.WalletAccountsList['display_balance']; - export type ActiveWalletAccount = THooks.ActiveWalletAccount['display_balance']; - export type AccountsList = THooks.DerivAccountsList['display_balance']; - export type ActiveTradingAccount = THooks.ActiveTradingAccount['display_balance']; } export type TGenericSizes = '2xl' | '2xs' | '3xl' | '3xs' | '4xl' | '5xl' | '6xl' | 'lg' | 'md' | 'sm' | 'xl' | 'xs'; @@ -107,7 +102,3 @@ export type TWalletCarouselItem = Omit; export type TCurrencyIconTypes = Record; - -export type TSubscribedBalance = { - balance: Omit, 'subscribe' | 'unsubscribe'>; -}; diff --git a/packages/wallets/src/utils/observable.ts b/packages/wallets/src/utils/observable.ts new file mode 100644 index 000000000000..aaefd34f10b2 --- /dev/null +++ b/packages/wallets/src/utils/observable.ts @@ -0,0 +1,38 @@ +type Subscriber = (value: T) => void; + +class Observable { + private subscribers = new Set>(); + + constructor(private value: T) { + this.value = value; + } + + get(): T { + return this.value; + } + + set(newValue: T): void { + this.value = newValue; + + this.subscribers.forEach(listener => listener(this.value)); // notify all subscribers + } + + /** + * @description Subscribes to the observable + * @param subscriber the observer function + * @returns cleanup function to unsubscribe the subscriber when the component unmounts. + */ + subscribe(subscriber: Subscriber): () => void { + this.subscribers.add(subscriber); + + return () => this.unsubscribe(subscriber); // cleanup function to unsubscribe the subscriber when the component unmounts + } + + unsubscribe(subscriber: Subscriber): void { + this.subscribers.delete(subscriber); + } +} + +export { type Subscriber }; + +export default Observable;