From 6af0ae992ff645328f602cfa67222697d0f6da13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Vytick=20Vytrhl=C3=ADk?= Date: Mon, 17 Mar 2025 18:56:51 +0100 Subject: [PATCH] feat(suite-native): account picker with account types --- jest.config.native.js | 1 + .../src/__fixtures__/accounts.json | 63 ++++++ .../AccountSheet/AccountListAddressItem.tsx | 41 ++++ .../AccountSheet/AccountListBaseItem.tsx | 153 +++++++++++++++ .../general/AccountSheet/AccountListItem.tsx | 132 ++----------- ...TradeAccountsList.tsx => AccountsList.tsx} | 37 ++-- ...sListFooter.tsx => AccountsListFooter.tsx} | 6 +- .../__tests__/AccountListAddressItem.test.tsx | 128 +++++++++++++ .../__tests__/AccountListItem.comp.test.tsx | 67 +------ .../__tests__/AccountsList.comp.test.tsx | 180 ++++++++++++++++++ ...t.tsx => AccountsListFooter.comp.test.tsx} | 14 +- ....tsx => NoAccountsComponent.comp.test.tsx} | 0 .../useReceiveAccountsListData.test.tsx | 49 +---- .../screens/ReceiveAccountsPickerScreen.tsx | 6 +- 14 files changed, 628 insertions(+), 249 deletions(-) create mode 100644 suite-native/module-trading/src/__fixtures__/accounts.json create mode 100644 suite-native/module-trading/src/components/general/AccountSheet/AccountListAddressItem.tsx create mode 100644 suite-native/module-trading/src/components/general/AccountSheet/AccountListBaseItem.tsx rename suite-native/module-trading/src/components/general/AccountSheet/{TradeAccountsList.tsx => AccountsList.tsx} (79%) rename suite-native/module-trading/src/components/general/AccountSheet/{TradeAccountsListFooter.tsx => AccountsListFooter.tsx} (91%) create mode 100644 suite-native/module-trading/src/components/general/AccountSheet/__tests__/AccountListAddressItem.test.tsx create mode 100644 suite-native/module-trading/src/components/general/AccountSheet/__tests__/AccountsList.comp.test.tsx rename suite-native/module-trading/src/components/general/AccountSheet/__tests__/{TradeAccountsListFooter.comp.test.tsx => AccountsListFooter.comp.test.tsx} (53%) rename suite-native/module-trading/src/components/general/AccountSheet/__tests__/{NoAccountsComponent.test.tsx => NoAccountsComponent.comp.test.tsx} (100%) diff --git a/jest.config.native.js b/jest.config.native.js index b630a2493f..21eb316e51 100644 --- a/jest.config.native.js +++ b/jest.config.native.js @@ -28,6 +28,7 @@ module.exports = { ], setupFiles: [ '/../../node_modules/@shopify/react-native-skia/jestSetup.js', + '/../../node_modules/@shopify/flash-list/jestSetup.js', '/../../node_modules/react-native-gesture-handler/jestSetup.js', '/../../suite-native/test-utils/src/everstakeJestSetup.js', '/../../suite-native/test-utils/src/expoMock.js', diff --git a/suite-native/module-trading/src/__fixtures__/accounts.json b/suite-native/module-trading/src/__fixtures__/accounts.json new file mode 100644 index 0000000000..8b212c60b2 --- /dev/null +++ b/suite-native/module-trading/src/__fixtures__/accounts.json @@ -0,0 +1,63 @@ +[ + { + "symbol": "btc", + "accountLabel": "BTC Account #1", + "deviceState": "staticSessionId", + "key": "btc1", + "networkType": "bitcoin", + "accountType": "normal", + "addresses": { + "used": [ + { "address": "USED1", "path": "path_USED1", "balance": "10000000" }, + { "address": "USED2", "path": "path_USED2", "balance": "20000000" } + ], + "change": [{ "address": "CHANGE1", "path": "path_CHANGE1" }], + "unused": [ + { "address": "UNUSED1", "path": "path_UNUSED1" }, + { "address": "UNUSED2", "path": "path_UNUSED2" } + ] + }, + "visible": true + }, + { + "symbol": "btc", + "accountLabel": "BTC Account #2", + "deviceState": "staticSessionId", + "key": "btc2", + "networkType": "bitcoin", + "accountType": "legacy", + "addresses": { + "used": [], + "change": [], + "unused": [] + }, + "visible": true + }, + { + "symbol": "eth", + "accountLabel": "ETH Account #1", + "deviceState": "staticSessionId", + "key": "eth1", + "networkType": "ethereum", + "accountType": "normal", + "visible": true + }, + { + "symbol": "eth", + "accountLabel": "ETH Account #2", + "deviceState": "staticSessionId", + "key": "eth2", + "networkType": "ethereum", + "accountType": "legacy", + "visible": true + }, + { + "symbol": "eth", + "accountLabel": "ETH Account #3 HIDDEN", + "deviceState": "staticSessionId", + "key": "eth3", + "networkType": "ethereum", + "accountType": "legacy", + "visible": false + } +] diff --git a/suite-native/module-trading/src/components/general/AccountSheet/AccountListAddressItem.tsx b/suite-native/module-trading/src/components/general/AccountSheet/AccountListAddressItem.tsx new file mode 100644 index 0000000000..65e1051e93 --- /dev/null +++ b/suite-native/module-trading/src/components/general/AccountSheet/AccountListAddressItem.tsx @@ -0,0 +1,41 @@ +import { Text } from '@suite-native/atoms'; +import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; + +import { ReceiveAccount } from '../../../types'; +import { AccountAddress } from '../AccountAddress'; +import { AccountListBaseItem } from './AccountListBaseItem'; + +const labelTextStyle = prepareNativeStyle(utils => ({ + color: utils.colors.textSubdued, + flex: 1, +})); + +export type AccountListAddressItemProps = { + receiveAccount: ReceiveAccount; + onPress: () => void; +}; +export const AccountListAddressItem = ({ + receiveAccount, + onPress, +}: AccountListAddressItemProps) => { + const { applyStyle } = useNativeStyles(); + const { address } = receiveAccount; + + if (!address) { + return null; + } + + return ( + } + isAddressDetail={true} + info={ + + {address.path} + + } + onPress={onPress} + /> + ); +}; diff --git a/suite-native/module-trading/src/components/general/AccountSheet/AccountListBaseItem.tsx b/suite-native/module-trading/src/components/general/AccountSheet/AccountListBaseItem.tsx new file mode 100644 index 0000000000..dcdf986a81 --- /dev/null +++ b/suite-native/module-trading/src/components/general/AccountSheet/AccountListBaseItem.tsx @@ -0,0 +1,153 @@ +import { ReactNode } from 'react'; +import { Pressable } from 'react-native'; + +import { BASE_CRYPTO_MAX_DISPLAYED_DECIMALS } from '@suite-common/formatters'; +import { Box, HStack, Text, VStack } from '@suite-native/atoms'; +import { CryptoAmountFormatter, CryptoToFiatAmountFormatter } from '@suite-native/formatters'; +import { CryptoIcon, Icon } from '@suite-native/icons'; +import { useTranslate } from '@suite-native/intl'; +import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; + +import { ReceiveAccount } from '../../../types'; + +export type AccountListBaseItemProps = { + receiveAccount: ReceiveAccount; + label: ReactNode; + info: ReactNode; + isAddressDetail: boolean; + onPress: () => void; +}; + +type TextColor = 'textDefault' | 'textSubdued'; + +export const ACCOUNT_LIST_ITEM_HEIGHT = 68 as const; + +const labelTextStyle = prepareNativeStyle<{ textColor: TextColor; flex: number }>( + ({ colors }, { textColor, flex }) => ({ + color: colors[textColor], + flex, + }), +); + +const amountTextStyle = prepareNativeStyle<{ textColor: TextColor }>( + ({ colors }, { textColor }) => ({ + color: colors[textColor], + textAlign: 'right', + flex: 0, + }), +); + +const bottomContentStyle = prepareNativeStyle<{ hasSingleChildren: boolean }>( + (_, { hasSingleChildren }) => ({ + alignItems: 'center', + justifyContent: 'space-between', + extend: { + condition: hasSingleChildren, + style: { justifyContent: 'flex-end' }, + }, + }), +); + +const AccountListLabel = ({ label, flex }: { label: ReactNode; flex: number }) => { + const { applyStyle } = useNativeStyles(); + + return ( + + {label} + + ); +}; + +export const AccountListBaseItem = ({ + receiveAccount: { account, address }, + label, + info, + isAddressDetail, + onPress, +}: AccountListBaseItemProps) => { + const { applyStyle } = useNativeStyles(); + const { translate } = useTranslate(); + + const cryptoValue = isAddressDetail ? address?.balance ?? '0' : account.availableBalance; + + const shouldDisplayCaret = !isAddressDetail && !!account.addresses; + const shouldDisplayBalance = !isAddressDetail || address?.balance != null; + + return ( + + + + + + {!info && ( + + + + )} + + + {info && } + {shouldDisplayBalance && ( + + )} + + + {info} + {shouldDisplayBalance && cryptoValue && ( + + )} + + + {shouldDisplayCaret && ( + + + + )} + + + ); +}; diff --git a/suite-native/module-trading/src/components/general/AccountSheet/AccountListItem.tsx b/suite-native/module-trading/src/components/general/AccountSheet/AccountListItem.tsx index 14e3603329..51eca7169e 100644 --- a/suite-native/module-trading/src/components/general/AccountSheet/AccountListItem.tsx +++ b/suite-native/module-trading/src/components/general/AccountSheet/AccountListItem.tsx @@ -1,131 +1,33 @@ -import { Pressable } from 'react-native'; +import { useSelector } from 'react-redux'; -import { BASE_CRYPTO_MAX_DISPLAYED_DECIMALS, useFormatters } from '@suite-common/formatters'; -import { NetworkSymbol } from '@suite-common/wallet-config'; -import { Box, HStack, RoundedIcon, Text, VStack } from '@suite-native/atoms'; -import { CryptoAmountFormatter, CryptoToFiatAmountFormatter } from '@suite-native/formatters'; -import { Icon } from '@suite-native/icons'; -import { useTranslate } from '@suite-native/intl'; -import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; +import { AccountsRootState, selectFormattedAccountType } from '@suite-common/wallet-core'; +import { Badge } from '@suite-native/atoms'; +import { AccountListBaseItem } from './AccountListBaseItem'; import { ReceiveAccount } from '../../../types'; -import { AccountAddress } from '../AccountAddress'; export type AccountListItemProps = { - symbol: NetworkSymbol; receiveAccount: ReceiveAccount; onPress: () => void; }; -type TextStyle = { - textColor: 'textDefault' | 'textSubdued'; -}; - -export const ACCOUNT_LIST_ITEM_HEIGHT = 68 as const; - -const labelTextStyle = prepareNativeStyle(({ colors }, { textColor }) => ({ - color: colors[textColor], - flex: 1, -})); - -const amountTextStyle = prepareNativeStyle(({ colors }, { textColor }) => ({ - color: colors[textColor], - textAlign: 'right', - flex: 0, -})); - -export const AccountListItem = ({ - symbol, - receiveAccount: { account, address }, - onPress, -}: AccountListItemProps) => { - const { applyStyle } = useNativeStyles(); - const { translate } = useTranslate(); - const { DisplaySymbolFormatter } = useFormatters(); - - const isAddressDetail = !!address; - - const cryptoValue = isAddressDetail ? address.balance : account.availableBalance; - - const shouldDisplayCaret = !isAddressDetail && !!account.addresses; - const shouldDisplayBalance = !isAddressDetail || address?.balance != null; - const label = isAddressDetail ? ( - - ) : ( - account.accountLabel +export const AccountListItem = ({ receiveAccount, onPress }: AccountListItemProps) => { + const { account } = receiveAccount; + const formattedAccountType = useSelector((state: AccountsRootState) => + selectFormattedAccountType(state, account.key), ); - const info = isAddressDetail ? ( - address.path - ) : ( - + + const typeBadge = formattedAccountType && ( + ); return ( - - - - - - - - - {label} - - {shouldDisplayBalance && ( - - )} - - - - {info} - - {shouldDisplayBalance && cryptoValue && ( - - )} - - - {shouldDisplayCaret && ( - - - - )} - - + /> ); }; diff --git a/suite-native/module-trading/src/components/general/AccountSheet/TradeAccountsList.tsx b/suite-native/module-trading/src/components/general/AccountSheet/AccountsList.tsx similarity index 79% rename from suite-native/module-trading/src/components/general/AccountSheet/TradeAccountsList.tsx rename to suite-native/module-trading/src/components/general/AccountSheet/AccountsList.tsx index f51b7f44ef..29d81c3f03 100644 --- a/suite-native/module-trading/src/components/general/AccountSheet/TradeAccountsList.tsx +++ b/suite-native/module-trading/src/components/general/AccountSheet/AccountsList.tsx @@ -14,10 +14,12 @@ import { } from '@suite-native/navigation'; import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; -import { ACCOUNT_LIST_ITEM_HEIGHT, AccountListItem } from './AccountListItem'; +import { AccountListAddressItem } from './AccountListAddressItem'; +import { ACCOUNT_LIST_ITEM_HEIGHT } from './AccountListBaseItem'; +import { AccountListItem } from './AccountListItem'; +import { AccountsListFooter } from './AccountsListFooter'; import { AddressListEmptyComponent } from './AddressListEmptyComponent'; import { NoAccountsComponent } from './NoAccountsComponent'; -import { TradeAccountsListFooter } from './TradeAccountsListFooter'; import { ReceiveAccountsListMode, useReceiveAccountsListData, @@ -35,11 +37,11 @@ type NavigationProp = StackToStackCompositeNavigationProps< RootStackParamList >; -type TradeAccountsListProps = { +export type AccountsListProps = { symbol: NetworkSymbol; pickerMode: ReceiveAccountsListMode; onAddAccountTap: () => void; - setPickerMode: (mode: ReceiveAccountsListMode) => void; + onSetPickerMode: (mode: ReceiveAccountsListMode) => void; }; const DEFAULT_INSET_BOTTOM = 25; @@ -50,15 +52,15 @@ const contentContainerStyle = prepareNativeStyle<{ paddingBottom: Math.max(insetBottom, utils.spacings.sp16), })); -const keyExtractor = (item: ReceiveAccount) => +export const keyExtractor = (item: ReceiveAccount) => `${item.account.key}_${item.address?.address ?? 'address_undefined'}`; -export const TradeAccountsList = ({ +export const AccountsList = ({ symbol, pickerMode, onAddAccountTap, - setPickerMode, -}: TradeAccountsListProps) => { + onSetPickerMode, +}: AccountsListProps) => { const navigation = useNavigation(); const { applyStyle } = useNativeStyles(); const dispatch = useDispatch(); @@ -77,16 +79,19 @@ export const TradeAccountsList = ({ dispatch(setBuySelectedReceiveAccount({ selectedReceiveAccount: receiveAccount })); const hasAddresses = receiveAccount.account.addresses; if (receiveAccount.account && hasAddresses) { - setPickerMode('address'); + onSetPickerMode('address'); } if ((hasAddresses && receiveAccount.address) || !hasAddresses) { navigation.popToTop(); } }; - const renderItem = (item: ReceiveAccount) => ( - onItemSelect(item)} symbol={symbol} /> - ); + const renderItem = (item: ReceiveAccount) => + pickerMode === 'account' ? ( + onItemSelect(item)} /> + ) : ( + onItemSelect(item)} /> + ); const { data: internalData, @@ -99,17 +104,15 @@ export const TradeAccountsList = ({ keyExtractor, renderItem, noSingletonSectionHeader: true, - isLastItemRounded: isDeviceInViewOnlyMode, + isLastItemRounded: isDeviceInViewOnlyMode || pickerMode === 'address', }); + const insetBottom = Math.max(insetsBottom, DEFAULT_INSET_BOTTOM); const shouldHaveFooter = !isDeviceInViewOnlyMode && pickerMode === 'account'; const footer = shouldHaveFooter ? ( - 0} - onAddAccountTap={onAddAccountTap} - /> + 0} onAddAccountTap={onAddAccountTap} /> ) : null; const filter = ''; diff --git a/suite-native/module-trading/src/components/general/AccountSheet/TradeAccountsListFooter.tsx b/suite-native/module-trading/src/components/general/AccountSheet/AccountsListFooter.tsx similarity index 91% rename from suite-native/module-trading/src/components/general/AccountSheet/TradeAccountsListFooter.tsx rename to suite-native/module-trading/src/components/general/AccountSheet/AccountsListFooter.tsx index a3873fcaf7..cb094ca813 100644 --- a/suite-native/module-trading/src/components/general/AccountSheet/TradeAccountsListFooter.tsx +++ b/suite-native/module-trading/src/components/general/AccountSheet/AccountsListFooter.tsx @@ -11,15 +11,15 @@ const footerStyle = prepareNativeStyle(utils => ({ borderBottomRightRadius: utils.borders.radii.r16, })); -export type TradeAccountsListFooterProps = { +export type AccountsListFooterProps = { hasTextualDivider: boolean; onAddAccountTap: () => void; }; -export const TradeAccountsListFooter = ({ +export const AccountsListFooter = ({ hasTextualDivider, onAddAccountTap, -}: TradeAccountsListFooterProps) => { +}: AccountsListFooterProps) => { const { applyStyle } = useNativeStyles(); return ( diff --git a/suite-native/module-trading/src/components/general/AccountSheet/__tests__/AccountListAddressItem.test.tsx b/suite-native/module-trading/src/components/general/AccountSheet/__tests__/AccountListAddressItem.test.tsx new file mode 100644 index 0000000000..2e6b277001 --- /dev/null +++ b/suite-native/module-trading/src/components/general/AccountSheet/__tests__/AccountListAddressItem.test.tsx @@ -0,0 +1,128 @@ +import { Account } from '@suite-common/wallet-types'; +import { fireEvent, renderWithStoreProviderAsync } from '@suite-native/test-utils'; +import { Address } from '@trezor/blockchain-link-types'; + +import { ReceiveAccount } from '../../../../types'; +import { AccountListAddressItem } from '../AccountListAddressItem'; + +jest.mock('@suite-common/wallet-core', () => { + const fiatRate = { rate: 1e8 }; + + return { + ...jest.requireActual('@suite-common/wallet-core'), + selectFiatRatesByFiatRateKey: () => fiatRate, + }; +}); + +describe('AccountListAddressItem', () => { + const onPressMock = jest.fn(); + + const renderAccountListAddressItem = (receiveAccount: ReceiveAccount) => + renderWithStoreProviderAsync( + , + ); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should call onPress callback when pressed', async () => { + const receiveAccount: ReceiveAccount = { + account: { + key: 'btc1', + symbol: 'btc', + accountLabel: 'My BTC account', + availableBalance: '10000000', + } as unknown as Account, + address: { + address: 'BTC_address', + balance: '5000000', + } as unknown as Address, + }; + const { getByText } = await renderAccountListAddressItem(receiveAccount); + + fireEvent.press(getByText('BTC_address')); + + expect(onPressMock).toHaveBeenCalled(); + }); + + it('should not display caret for address addresses', async () => { + const receiveAccount: ReceiveAccount = { + account: { + key: 'btc1', + symbol: 'btc', + accountLabel: 'My BTC account', + availableBalance: '10000000', + } as unknown as Account, + address: { + address: 'BTC_address', + balance: '5000000', + } as unknown as Address, + }; + const { getByText, queryByAccessibilityHint } = + await renderAccountListAddressItem(receiveAccount); + + expect(getByText('BTC_address')).toBeDefined(); + expect(queryByAccessibilityHint('Select to display account addresses')).toBeNull(); + }); + + it('should display address', async () => { + const receiveAccount: ReceiveAccount = { + account: { + key: 'btc1', + symbol: 'btc', + accountLabel: 'My BTC account', + availableBalance: '10000000', + } as unknown as Account, + address: { + address: 'BTC_address', + balance: '5000000', + } as unknown as Address, + }; + const { getByText, queryByText, queryByAccessibilityHint, getByLabelText } = + await renderAccountListAddressItem(receiveAccount); + + expect(getByText('BTC_address')).toBeDefined(); + expect(queryByText('My BTC account')).toBeNull(); + expect(queryByAccessibilityHint('Select to display account addresses')).toBeNull(); + expect(getByLabelText('Balance in fiat')).toHaveTextContent('$5,000,000.00'); + expect(getByLabelText('Balance in crypto')).toHaveTextContent('0.05 BTC'); + }); + + it('should display zero balance', async () => { + const receiveAccount: ReceiveAccount = { + account: { + key: 'btc1', + symbol: 'btc', + accountLabel: 'My BTC account', + availableBalance: '10000000', + } as unknown as Account, + address: { + address: 'BTC_address', + balance: '0', + } as unknown as Address, + }; + const { getByLabelText } = await renderAccountListAddressItem(receiveAccount); + + expect(getByLabelText('Balance in fiat')).toHaveTextContent('$0.00'); + expect(getByLabelText('Balance in crypto')).toHaveTextContent('0 BTC'); + }); + + it('should not display balance when address has no balance', async () => { + const receiveAccount: ReceiveAccount = { + account: { + key: 'btc1', + symbol: 'btc', + accountLabel: 'My BTC account', + availableBalance: '10000000', + } as unknown as Account, + address: { + address: 'BTC_address', + } as unknown as Address, + }; + const { queryByLabelText } = await renderAccountListAddressItem(receiveAccount); + + expect(queryByLabelText('Balance in fiat')).toBeNull(); + expect(queryByLabelText('Balance in crypto')).toBeNull(); + }); +}); diff --git a/suite-native/module-trading/src/components/general/AccountSheet/__tests__/AccountListItem.comp.test.tsx b/suite-native/module-trading/src/components/general/AccountSheet/__tests__/AccountListItem.comp.test.tsx index 42fd9c10cf..8a0dcc344c 100644 --- a/suite-native/module-trading/src/components/general/AccountSheet/__tests__/AccountListItem.comp.test.tsx +++ b/suite-native/module-trading/src/components/general/AccountSheet/__tests__/AccountListItem.comp.test.tsx @@ -1,6 +1,5 @@ import { Account } from '@suite-common/wallet-types'; import { fireEvent, renderWithStoreProviderAsync } from '@suite-native/test-utils'; -import { Address } from '@trezor/blockchain-link-types'; import { ReceiveAccount } from '../../../../types'; import { AccountListItem } from '../AccountListItem'; @@ -19,7 +18,7 @@ describe('AccountListItem', () => { const renderAccountListItem = (receiveAccount: ReceiveAccount) => renderWithStoreProviderAsync( - , + , ); beforeEach(() => { @@ -37,12 +36,12 @@ describe('AccountListItem', () => { }; const { getByText } = await renderAccountListItem(receiveAccount); - fireEvent.press(getByText('BTC')); + fireEvent.press(getByText('My BTC account')); expect(onPressMock).toHaveBeenCalled(); }); - it('should render account name when no address is specified', async () => { + it('should render account name', async () => { const receiveAccount: ReceiveAccount = { account: { key: 'btc1', @@ -79,64 +78,4 @@ describe('AccountListItem', () => { expect(getByText('My BTC account')).toBeDefined(); expect(getByAccessibilityHint('Select to display account addresses')).toBeDefined(); }); - - it('should display address when specified', async () => { - const receiveAccount: ReceiveAccount = { - account: { - key: 'btc1', - symbol: 'btc', - accountLabel: 'My BTC account', - availableBalance: '10000000', - } as unknown as Account, - address: { - address: 'BTC_address', - balance: '5000000', - } as unknown as Address, - }; - const { getByText, queryByText, queryByAccessibilityHint, getByLabelText } = - await renderAccountListItem(receiveAccount); - - expect(getByText('BTC_address')).toBeDefined(); - expect(queryByText('My BTC account')).toBeNull(); - expect(queryByAccessibilityHint('Select to display account addresses')).toBeNull(); - expect(getByLabelText('Balance in fiat')).toHaveTextContent('$5,000,000.00'); - expect(getByLabelText('Balance in crypto')).toHaveTextContent('0.05 BTC'); - }); - - it('should display zero balance', async () => { - const receiveAccount: ReceiveAccount = { - account: { - key: 'btc1', - symbol: 'btc', - accountLabel: 'My BTC account', - availableBalance: '10000000', - } as unknown as Account, - address: { - address: 'BTC_address', - balance: '0', - } as unknown as Address, - }; - const { getByLabelText } = await renderAccountListItem(receiveAccount); - - expect(getByLabelText('Balance in fiat')).toHaveTextContent('$0.00'); - expect(getByLabelText('Balance in crypto')).toHaveTextContent('0 BTC'); - }); - - it('should not display balance when address has no balance', async () => { - const receiveAccount: ReceiveAccount = { - account: { - key: 'btc1', - symbol: 'btc', - accountLabel: 'My BTC account', - availableBalance: '10000000', - } as unknown as Account, - address: { - address: 'BTC_address', - } as unknown as Address, - }; - const { queryByLabelText } = await renderAccountListItem(receiveAccount); - - expect(queryByLabelText('Balance in fiat')).toBeNull(); - expect(queryByLabelText('Balance in crypto')).toBeNull(); - }); }); diff --git a/suite-native/module-trading/src/components/general/AccountSheet/__tests__/AccountsList.comp.test.tsx b/suite-native/module-trading/src/components/general/AccountSheet/__tests__/AccountsList.comp.test.tsx new file mode 100644 index 0000000000..119d2f8a30 --- /dev/null +++ b/suite-native/module-trading/src/components/general/AccountSheet/__tests__/AccountsList.comp.test.tsx @@ -0,0 +1,180 @@ +import { useNavigation } from '@react-navigation/native'; + +import { Account } from '@suite-common/wallet-types'; +import { fireEvent, renderWithStoreProviderAsync } from '@suite-native/test-utils'; +import { Address } from '@trezor/blockchain-link-types'; +import { StaticSessionId } from '@trezor/connect'; + +import fixturesAccounts from '../../../../__fixtures__/accounts.json'; +import { ReceiveAccount } from '../../../../types'; +import { AccountsList, AccountsListProps, keyExtractor } from '../AccountsList'; + +const accounts = fixturesAccounts as Account[]; +const defaultPreloadedState = { + device: { + selectedDevice: { + state: { + staticSessionId: 'staticSessionId' as StaticSessionId, + }, + connected: true, + available: true, + remember: true, + }, + }, + wallet: { accounts }, +}; + +const getStateMockup = (selectedAccount: ReceiveAccount) => ({ + ...defaultPreloadedState, + wallet: { + accounts: defaultPreloadedState.wallet.accounts, + tradingNew: { + buy: { + selectedReceiveAccount: selectedAccount, + }, + }, + }, +}); + +jest.mock('@react-navigation/native', () => ({ + ...jest.requireActual('@react-navigation/native'), + useNavigation: jest.fn(), +})); + +describe('AccountsList', () => { + const onSetPickerModeMock = jest.fn(); + const popToTop = jest.fn(); + + const renderComponent = ( + props: Partial, + preloadedState = defaultPreloadedState, + ) => + renderWithStoreProviderAsync( + , + { preloadedState }, + ); + + describe('renderItem', () => { + it('should display all accounts for given symbol', async () => { + const { getByText } = await renderComponent({ + symbol: 'btc', + pickerMode: 'account', + }); + + expect(getByText('BTC Account #1')).toBeTruthy(); + expect(getByText('BTC Account #2')).toBeTruthy(); + }); + + it('should display addresses when picker mode is set and account is selected', async () => { + const { getByText } = await renderComponent( + { + symbol: 'btc', + pickerMode: 'address', + }, + getStateMockup({ account: accounts[0] }), + ); + + const item = getByText('UNUSED1'); + expect(item).toBeTruthy(); + }); + }); + + describe('onItemSelect', () => { + it('should call onSetPickerMode when account is selected in account mode', async () => { + (useNavigation as jest.Mock).mockReturnValue({ popToTop }); + + const { getByText } = await renderComponent({ + symbol: 'btc', + pickerMode: 'account', + onSetPickerMode: onSetPickerModeMock, + }); + + const item = getByText('BTC Account #1'); + expect(item).toBeTruthy(); + + fireEvent.press(item); + + expect(onSetPickerModeMock).toHaveBeenCalledWith('address'); + expect(popToTop).not.toHaveBeenCalled(); + }); + + it('should popToTop when account is selected in account mode and there are no addresses', async () => { + (useNavigation as jest.Mock).mockReturnValue({ popToTop }); + + const { getByText } = await renderComponent({ + symbol: 'eth', + pickerMode: 'account', + onSetPickerMode: onSetPickerModeMock, + }); + + const item = getByText('ETH Account #1'); + expect(item).toBeTruthy(); + + fireEvent.press(item); + + expect(popToTop).toHaveBeenCalled(); + }); + + it('should handle address selection in address mode', async () => { + (useNavigation as jest.Mock).mockReturnValue({ popToTop }); + const { getByText } = await renderComponent( + { + symbol: 'btc', + pickerMode: 'address', + onSetPickerMode: onSetPickerModeMock, + }, + getStateMockup({ account: accounts[0] }), + ); + + const item = getByText('UNUSED1'); + expect(item).toBeTruthy(); + + fireEvent.press(item); + + expect(popToTop).toHaveBeenCalled(); + }); + }); + + describe('footer', () => { + it('should display footer with "Add new" button in "account" mode', async () => { + const { getByText } = await renderComponent({ + symbol: 'btc', + pickerMode: 'account', + }); + + expect(getByText('Add new')).toBeTruthy(); + }); + + it('should NOT display footer with "Add new" button in "address" mode', async () => { + const { queryByText } = await renderComponent( + { + symbol: 'btc', + pickerMode: 'address', + }, + getStateMockup({ account: accounts[0] }), + ); + + expect(queryByText('Add new')).toBeNull(); + }); + }); + + describe('keyExtractor', () => { + it('should use default string for undefined address', () => { + expect(keyExtractor({ account: accounts[0], address: undefined })).toBe( + 'btc1_address_undefined', + ); + }); + + it('should use address string for set address', () => { + expect( + keyExtractor({ account: accounts[0], address: { address: 'ADDRESS1' } as Address }), + ).toBe('btc1_ADDRESS1'); + }); + }); +}); diff --git a/suite-native/module-trading/src/components/general/AccountSheet/__tests__/TradeAccountsListFooter.comp.test.tsx b/suite-native/module-trading/src/components/general/AccountSheet/__tests__/AccountsListFooter.comp.test.tsx similarity index 53% rename from suite-native/module-trading/src/components/general/AccountSheet/__tests__/TradeAccountsListFooter.comp.test.tsx rename to suite-native/module-trading/src/components/general/AccountSheet/__tests__/AccountsListFooter.comp.test.tsx index 522bc2679f..9a88dbe39d 100644 --- a/suite-native/module-trading/src/components/general/AccountSheet/__tests__/TradeAccountsListFooter.comp.test.tsx +++ b/suite-native/module-trading/src/components/general/AccountSheet/__tests__/AccountsListFooter.comp.test.tsx @@ -1,28 +1,28 @@ import { act, fireEvent, renderWithBasicProvider } from '@suite-native/test-utils'; -import { TradeAccountsListFooter, TradeAccountsListFooterProps } from '../TradeAccountsListFooter'; +import { AccountsListFooter, AccountsListFooterProps } from '../AccountsListFooter'; -describe('TradeAccountsListFooter', () => { - const renderTradeAccountsListFooter = (props: Partial) => +describe('AccountsListFooter', () => { + const renderAccountsListFooter = (props: Partial) => renderWithBasicProvider( - , + , ); it('should not render "OR" when hasTextualDivider props is false', () => { - const { queryByText } = renderTradeAccountsListFooter({ hasTextualDivider: false }); + const { queryByText } = renderAccountsListFooter({ hasTextualDivider: false }); expect(queryByText('OR')).toBeNull(); }); it('should render "OR" when hasTextualDivider props is true', () => { - const { getByText } = renderTradeAccountsListFooter({ hasTextualDivider: true }); + const { getByText } = renderAccountsListFooter({ hasTextualDivider: true }); expect(getByText('OR')).toBeDefined(); }); it('should call onAddAccountTap callback on "Add new" button press', () => { const onAddAccountTap = jest.fn(); - const { getByText } = renderTradeAccountsListFooter({ onAddAccountTap }); + const { getByText } = renderAccountsListFooter({ onAddAccountTap }); act(() => { fireEvent.press(getByText('Add new')); diff --git a/suite-native/module-trading/src/components/general/AccountSheet/__tests__/NoAccountsComponent.test.tsx b/suite-native/module-trading/src/components/general/AccountSheet/__tests__/NoAccountsComponent.comp.test.tsx similarity index 100% rename from suite-native/module-trading/src/components/general/AccountSheet/__tests__/NoAccountsComponent.test.tsx rename to suite-native/module-trading/src/components/general/AccountSheet/__tests__/NoAccountsComponent.comp.test.tsx diff --git a/suite-native/module-trading/src/hooks/__tests__/useReceiveAccountsListData.test.tsx b/suite-native/module-trading/src/hooks/__tests__/useReceiveAccountsListData.test.tsx index 25a1e2d039..36e300a69f 100644 --- a/suite-native/module-trading/src/hooks/__tests__/useReceiveAccountsListData.test.tsx +++ b/suite-native/module-trading/src/hooks/__tests__/useReceiveAccountsListData.test.tsx @@ -3,6 +3,7 @@ import { Account } from '@suite-common/wallet-types'; import { PreloadedState, renderHookWithStoreProviderAsync } from '@suite-native/test-utils'; import { StaticSessionId } from '@trezor/connect'; +import accounts from '../../__fixtures__/accounts.json'; import { ReceiveAccountsListMode, useReceiveAccountsListData } from '../useReceiveAccountsListData'; describe('useReceiveAccountsListData', () => { @@ -14,42 +15,7 @@ describe('useReceiveAccountsListData', () => { }, }, }, - wallet: { - accounts: [ - { - symbol: 'btc', - accountLabel: 'BTC Account #1', - deviceState: 'staticSessionId', - key: 'btc1', - addresses: { - used: [{ address: 'USED1' }, { address: 'USED2' }], - change: [{ address: 'CHANGE1' }], - unused: [{ address: 'UNUSED1' }, { address: 'UNUSED2' }], - }, - visible: true, - }, - { - symbol: 'btc', - accountLabel: 'BTC Account #2', - deviceState: 'staticSessionId', - key: 'btc2', - addresses: { - used: [], - change: [], - unused: [], - }, - visible: true, - }, - { - symbol: 'eth', - accountLabel: 'ETH Account #1', - deviceState: 'staticSessionId', - addresses: undefined, - key: 'eth1', - visible: true, - }, - ] as unknown as Account[], - }, + wallet: { accounts: accounts as Account[] }, }; const renderUseReceiveAccountsListDataHook = ( @@ -104,7 +70,10 @@ describe('useReceiveAccountsListData', () => { { key: '', label: '', - data: [{ account: expect.objectContaining({ key: 'eth1' }) }], + data: [ + { account: expect.objectContaining({ key: 'eth1' }) }, + { account: expect.objectContaining({ key: 'eth2' }) }, + ], }, ]); }); @@ -149,7 +118,7 @@ describe('useReceiveAccountsListData', () => { data: [ { account: expect.objectContaining({ key: 'btc1' }), - address: { address: 'UNUSED1' }, + address: { address: 'UNUSED1', path: 'path_UNUSED1' }, }, ], }, @@ -159,11 +128,11 @@ describe('useReceiveAccountsListData', () => { data: [ { account: expect.objectContaining({ key: 'btc1' }), - address: { address: 'USED1' }, + address: { address: 'USED1', balance: '10000000', path: 'path_USED1' }, }, { account: expect.objectContaining({ key: 'btc1' }), - address: { address: 'USED2' }, + address: { address: 'USED2', balance: '20000000', path: 'path_USED2' }, }, ], }, diff --git a/suite-native/module-trading/src/screens/ReceiveAccountsPickerScreen.tsx b/suite-native/module-trading/src/screens/ReceiveAccountsPickerScreen.tsx index 5890d16d48..ed6753064d 100644 --- a/suite-native/module-trading/src/screens/ReceiveAccountsPickerScreen.tsx +++ b/suite-native/module-trading/src/screens/ReceiveAccountsPickerScreen.tsx @@ -17,7 +17,7 @@ import { TradingStackRoutes, } from '@suite-native/navigation'; -import { TradeAccountsList } from '../components/general/AccountSheet/TradeAccountsList'; +import { AccountsList } from '../components/general/AccountSheet/AccountsList'; import { ReceiveAccountsListMode } from '../hooks/useReceiveAccountsListData'; import { selectBuySelectedReceiveAccount } from '../tradingSlice'; @@ -56,11 +56,11 @@ export const ReceiveAccountsPickerScreen = () => { return ( }> -