mirror of
https://github.com/trezor/trezor-suite.git
synced 2026-02-20 00:33:07 +01:00
feat(suite-native): account picker with account types
This commit is contained in:
committed by
vytick
parent
31897ee1b2
commit
6af0ae992f
@@ -28,6 +28,7 @@ module.exports = {
|
||||
],
|
||||
setupFiles: [
|
||||
'<rootDir>/../../node_modules/@shopify/react-native-skia/jestSetup.js',
|
||||
'<rootDir>/../../node_modules/@shopify/flash-list/jestSetup.js',
|
||||
'<rootDir>/../../node_modules/react-native-gesture-handler/jestSetup.js',
|
||||
'<rootDir>/../../suite-native/test-utils/src/everstakeJestSetup.js',
|
||||
'<rootDir>/../../suite-native/test-utils/src/expoMock.js',
|
||||
|
||||
63
suite-native/module-trading/src/__fixtures__/accounts.json
Normal file
63
suite-native/module-trading/src/__fixtures__/accounts.json
Normal file
@@ -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
|
||||
}
|
||||
]
|
||||
@@ -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 (
|
||||
<AccountListBaseItem
|
||||
receiveAccount={receiveAccount}
|
||||
label={<AccountAddress address={address.address} form="full" />}
|
||||
isAddressDetail={true}
|
||||
info={
|
||||
<Text variant="hint" style={applyStyle(labelTextStyle)}>
|
||||
{address.path}
|
||||
</Text>
|
||||
}
|
||||
onPress={onPress}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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 (
|
||||
<Text
|
||||
variant="body"
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
style={applyStyle(labelTextStyle, {
|
||||
textColor: 'textDefault',
|
||||
flex,
|
||||
})}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={account.accountLabel}
|
||||
>
|
||||
<HStack
|
||||
alignItems="center"
|
||||
spacing="sp12"
|
||||
paddingVertical="sp12"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Box justifyContent="center">
|
||||
<CryptoIcon symbol={account.symbol} size="extraSmall" />
|
||||
</Box>
|
||||
{!info && (
|
||||
<Box flex={1}>
|
||||
<AccountListLabel label={label} flex={0} />
|
||||
</Box>
|
||||
)}
|
||||
<VStack flex={info ? 1 : 0} spacing={0}>
|
||||
<HStack alignItems="center" justifyContent="space-between">
|
||||
{info && <AccountListLabel label={label} flex={1} />}
|
||||
{shouldDisplayBalance && (
|
||||
<CryptoAmountFormatter
|
||||
value={cryptoValue}
|
||||
symbol={account.symbol}
|
||||
variant="body"
|
||||
style={applyStyle(amountTextStyle, {
|
||||
textColor: 'textDefault',
|
||||
})}
|
||||
accessibilityLabel={translate(
|
||||
'moduleTrading.accountScreen.balanceCrypto',
|
||||
)}
|
||||
isBalance={false}
|
||||
decimals={BASE_CRYPTO_MAX_DISPLAYED_DECIMALS}
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
<HStack style={applyStyle(bottomContentStyle, { hasSingleChildren: false })}>
|
||||
{info}
|
||||
{shouldDisplayBalance && cryptoValue && (
|
||||
<CryptoToFiatAmountFormatter
|
||||
value={cryptoValue}
|
||||
symbol={account.symbol}
|
||||
variant="hint"
|
||||
style={applyStyle(labelTextStyle, {
|
||||
textColor: 'textDefault',
|
||||
flex: 1,
|
||||
})}
|
||||
accessibilityLabel={translate(
|
||||
'moduleTrading.accountScreen.balanceFiat',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
</VStack>
|
||||
{shouldDisplayCaret && (
|
||||
<Box justifyContent="center">
|
||||
<Icon
|
||||
name="caretRight"
|
||||
color="textSecondaryHighlight"
|
||||
accessibilityHint={translate('moduleTrading.accountScreen.step2Hint')}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</HStack>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
@@ -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<TextStyle>(({ colors }, { textColor }) => ({
|
||||
color: colors[textColor],
|
||||
flex: 1,
|
||||
}));
|
||||
|
||||
const amountTextStyle = prepareNativeStyle<TextStyle>(({ 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 ? (
|
||||
<AccountAddress address={address.address} form="full" />
|
||||
) : (
|
||||
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
|
||||
) : (
|
||||
<DisplaySymbolFormatter value={symbol} areAmountUnitsEnabled={false} />
|
||||
|
||||
const typeBadge = formattedAccountType && (
|
||||
<Badge label={formattedAccountType} size="small" elevation="1" />
|
||||
);
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
<AccountListBaseItem
|
||||
receiveAccount={receiveAccount}
|
||||
label={account.accountLabel}
|
||||
isAddressDetail={false}
|
||||
info={typeBadge}
|
||||
onPress={onPress}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={account.accountLabel}
|
||||
>
|
||||
<HStack alignItems="center" spacing="sp12" paddingVertical="sp12">
|
||||
<Box justifyContent="center">
|
||||
<RoundedIcon symbol={symbol} />
|
||||
</Box>
|
||||
<VStack flex={1} spacing={0}>
|
||||
<HStack alignItems="center" justifyContent="space-between">
|
||||
<Text
|
||||
variant="body"
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
style={applyStyle(labelTextStyle, { textColor: 'textDefault' })}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
{shouldDisplayBalance && (
|
||||
<CryptoAmountFormatter
|
||||
value={cryptoValue}
|
||||
symbol={account.symbol}
|
||||
variant="body"
|
||||
style={applyStyle(amountTextStyle, {
|
||||
textColor: 'textDefault',
|
||||
})}
|
||||
accessibilityLabel={translate(
|
||||
'moduleTrading.accountScreen.balanceCrypto',
|
||||
)}
|
||||
isBalance={false}
|
||||
decimals={BASE_CRYPTO_MAX_DISPLAYED_DECIMALS}
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
<HStack alignItems="center" justifyContent="space-between">
|
||||
<Text
|
||||
variant="hint"
|
||||
style={applyStyle(labelTextStyle, { textColor: 'textSubdued' })}
|
||||
>
|
||||
{info}
|
||||
</Text>
|
||||
{shouldDisplayBalance && cryptoValue && (
|
||||
<CryptoToFiatAmountFormatter
|
||||
value={cryptoValue}
|
||||
symbol={account.symbol}
|
||||
variant="hint"
|
||||
style={applyStyle(labelTextStyle, { textColor: 'textSubdued' })}
|
||||
accessibilityLabel={translate(
|
||||
'moduleTrading.accountScreen.balanceFiat',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
</VStack>
|
||||
{shouldDisplayCaret && (
|
||||
<Box justifyContent="center">
|
||||
<Icon
|
||||
name="caretCircleRight"
|
||||
color="textSecondaryHighlight"
|
||||
accessibilityHint={translate('moduleTrading.accountScreen.step2Hint')}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</HStack>
|
||||
</Pressable>
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<NavigationProp>();
|
||||
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) => (
|
||||
<AccountListItem receiveAccount={item} onPress={() => onItemSelect(item)} symbol={symbol} />
|
||||
);
|
||||
const renderItem = (item: ReceiveAccount) =>
|
||||
pickerMode === 'account' ? (
|
||||
<AccountListItem receiveAccount={item} onPress={() => onItemSelect(item)} />
|
||||
) : (
|
||||
<AccountListAddressItem receiveAccount={item} onPress={() => 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 ? (
|
||||
<TradeAccountsListFooter
|
||||
hasTextualDivider={itemsCount > 0}
|
||||
onAddAccountTap={onAddAccountTap}
|
||||
/>
|
||||
<AccountsListFooter hasTextualDivider={itemsCount > 0} onAddAccountTap={onAddAccountTap} />
|
||||
) : null;
|
||||
|
||||
const filter = '';
|
||||
@@ -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 (
|
||||
@@ -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(
|
||||
<AccountListAddressItem receiveAccount={receiveAccount} onPress={onPressMock} />,
|
||||
);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -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(
|
||||
<AccountListItem symbol="btc" onPress={onPressMock} receiveAccount={receiveAccount} />,
|
||||
<AccountListItem onPress={onPressMock} receiveAccount={receiveAccount} />,
|
||||
);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<AccountsListProps>,
|
||||
preloadedState = defaultPreloadedState,
|
||||
) =>
|
||||
renderWithStoreProviderAsync(
|
||||
<AccountsList
|
||||
symbol="btc"
|
||||
pickerMode="account"
|
||||
onAddAccountTap={jest.fn()}
|
||||
onSetPickerMode={jest.fn()}
|
||||
{...props}
|
||||
/>,
|
||||
{ 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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<TradeAccountsListFooterProps>) =>
|
||||
describe('AccountsListFooter', () => {
|
||||
const renderAccountsListFooter = (props: Partial<AccountsListFooterProps>) =>
|
||||
renderWithBasicProvider(
|
||||
<TradeAccountsListFooter hasTextualDivider onAddAccountTap={jest.fn()} {...props} />,
|
||||
<AccountsListFooter hasTextualDivider onAddAccountTap={jest.fn()} {...props} />,
|
||||
);
|
||||
|
||||
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'));
|
||||
@@ -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' },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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 (
|
||||
<Screen header={<ScreenHeader content={title} closeActionType="close" />}>
|
||||
<TradeAccountsList
|
||||
<AccountsList
|
||||
symbol={symbol}
|
||||
pickerMode={pickerMode}
|
||||
onAddAccountTap={handleAddAccount}
|
||||
setPickerMode={setPickerMode}
|
||||
onSetPickerMode={setPickerMode}
|
||||
/>
|
||||
<AccountTypeDecisionBottomSheet
|
||||
coinName={symbol}
|
||||
|
||||
Reference in New Issue
Block a user