diff --git a/suite-native/intl/src/en.ts b/suite-native/intl/src/en.ts
index 64f4ec51bed..3dd524785ff 100644
--- a/suite-native/intl/src/en.ts
+++ b/suite-native/intl/src/en.ts
@@ -1302,6 +1302,15 @@ export const en = {
emptyDescription:
'We couldn’t find a country matching your search. Try checking the spelling or exploring the list for the right option.',
},
+ accountSheet: {
+ addressEmptyTitle: 'No address found',
+ addressEmptyDescription:
+ 'We couldn’t find an address matching your search. Try checking the spelling or exploring the list for the right option.',
+ titleStep1: 'Pick account',
+ newAddress: 'New address',
+ usedAddresses: 'Used addresses',
+ step2Hint: 'Select to display account addresses',
+ },
defaultSearchLabel: 'Search',
notSelected: 'Not selected',
},
diff --git a/suite-native/module-trading/package.json b/suite-native/module-trading/package.json
index bfaca191ab6..64c0d49eb87 100644
--- a/suite-native/module-trading/package.json
+++ b/suite-native/module-trading/package.json
@@ -15,9 +15,11 @@
"@reduxjs/toolkit": "1.9.5",
"@suite-native/navigation": "workspace:*",
"@suite-native/test-utils": "workspace:*",
+ "@trezor/blockchain-link-types": "workspace:*",
"expo-linear-gradient": "^14.0.1",
"react": "18.2.0",
"react-native": "0.76.1",
- "react-native-reanimated": "3.16.7"
+ "react-native-reanimated": "3.16.7",
+ "react-redux": "8.0.7"
}
}
diff --git a/suite-native/module-trading/src/components/buy/BuyCard.tsx b/suite-native/module-trading/src/components/buy/BuyCard.tsx
index e9c91afbf27..ed93d7309b1 100644
--- a/suite-native/module-trading/src/components/buy/BuyCard.tsx
+++ b/suite-native/module-trading/src/components/buy/BuyCard.tsx
@@ -1,18 +1,13 @@
import { useFormatters } from '@suite-common/formatters';
import { Card, HStack, Text, VStack } from '@suite-native/atoms';
import { Icon } from '@suite-native/icons';
-import { Translation, useTranslate } from '@suite-native/intl';
+import { Translation } from '@suite-native/intl';
import { prepareNativeStyle, useNativeStyles } from '@trezor/styles';
+import { ReceiveAccountPicker } from './ReceiveAccountPicker';
import { TradeableAssetPicker } from './TradeableAssetPicker';
import { useTradeSheetControls } from '../../hooks/useTradeSheetControls';
import { TradeableAsset } from '../../types';
-import { TradingOverviewRow } from '../general/TradingOverviewRow';
-
-const notImplementedCallback = () => {
- // eslint-disable-next-line no-console
- console.log('Not implemented');
-};
const buySectionStyle = prepareNativeStyle(({ borders, colors, spacings }) => ({
borderBottomWidth: borders.widths.small,
@@ -21,7 +16,6 @@ const buySectionStyle = prepareNativeStyle(({ borders, colors, spacings }) => ({
}));
export const BuyCard = () => {
- const { translate } = useTranslate();
const { FiatAmountFormatter, CryptoAmountFormatter } = useFormatters();
const { applyStyle } = useNativeStyles();
@@ -55,20 +49,7 @@ export const BuyCard = () => {
-
-
-
- Bitcoin Vault
-
-
- 3QJmV3qfvL9SuYo34YihAf3sRCW3qSinyC
-
-
-
+
);
};
diff --git a/suite-native/module-trading/src/components/buy/ReceiveAccountPicker.tsx b/suite-native/module-trading/src/components/buy/ReceiveAccountPicker.tsx
new file mode 100644
index 00000000000..4e8e524c2aa
--- /dev/null
+++ b/suite-native/module-trading/src/components/buy/ReceiveAccountPicker.tsx
@@ -0,0 +1,74 @@
+import { Text, VStack } from '@suite-native/atoms';
+import { Translation, useTranslate } from '@suite-native/intl';
+
+import { useTradeSheetControls } from '../../hooks/useTradeSheetControls';
+import { ReceiveAccount, TradeableAsset } from '../../types';
+import { AccountSheet } from '../general/AccountSheet/AccountSheet';
+import { TradingOverviewRow } from '../general/TradingOverviewRow';
+
+type ReceiveAccountPickerProps = {
+ selectedAsset?: TradeableAsset;
+};
+type ReceiveAccountPickerSelectedAccountProps = {
+ selectedAccount?: ReceiveAccount;
+};
+
+const ReceiveAccountPickerRight = ({
+ selectedAccount,
+}: ReceiveAccountPickerSelectedAccountProps) => {
+ if (!selectedAccount) {
+ return (
+
+
+
+ );
+ }
+
+ const { account, address } = selectedAccount;
+
+ if (!address) {
+ return (
+
+
+ {account.accountLabel}
+
+
+ );
+ }
+
+ return (
+
+
+ {account.accountLabel}
+
+
+ {address.address}
+
+
+ );
+};
+
+export const ReceiveAccountPicker = ({ selectedAsset }: ReceiveAccountPickerProps) => {
+ const { translate } = useTranslate();
+
+ const { isSheetVisible, hideSheet, showSheet, setSelectedValue, selectedValue } =
+ useTradeSheetControls();
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+};
diff --git a/suite-native/module-trading/src/components/general/AccountSheet/AccountListItem.tsx b/suite-native/module-trading/src/components/general/AccountSheet/AccountListItem.tsx
new file mode 100644
index 00000000000..b3ac98ca816
--- /dev/null
+++ b/suite-native/module-trading/src/components/general/AccountSheet/AccountListItem.tsx
@@ -0,0 +1,110 @@
+import { Pressable } from 'react-native';
+
+import { useFormatters } from '@suite-common/formatters';
+import { NetworkSymbol } from '@suite-common/wallet-config';
+import { Box, HStack, RoundedIcon, Text, VStack } from '@suite-native/atoms';
+import { Icon } from '@suite-native/icons';
+import { useTranslate } from '@suite-native/intl';
+import { prepareNativeStyle, useNativeStyles } from '@trezor/styles';
+
+import { ReceiveAccount } from '../../../types';
+
+export type AccountListItemProps = {
+ symbol: NetworkSymbol;
+ receiveAccount: ReceiveAccount;
+ onPress: () => void;
+};
+
+export const ACCOUNT_LIST_ITEM_HEIGHT = 68 as const;
+
+const labelTextStyle = prepareNativeStyle(({ colors }) => ({
+ color: colors.textDefault,
+ flex: 1,
+}));
+
+const cryptoAmountTextStyle = prepareNativeStyle(({ colors }) => ({
+ color: colors.textDefault,
+ textAlign: 'right',
+ flex: 0,
+}));
+
+const networkStyle = prepareNativeStyle(({ colors }) => ({
+ color: colors.textSubdued,
+ flex: 1,
+}));
+
+const fiatStyle = prepareNativeStyle(({ colors }) => ({
+ color: colors.textSubdued,
+ textAlign: 'right',
+ flex: 0,
+}));
+
+export const AccountListItem = ({
+ symbol,
+ receiveAccount: { account, address },
+ onPress,
+}: AccountListItemProps) => {
+ const { applyStyle } = useNativeStyles();
+ const { translate } = useTranslate();
+ const { DisplaySymbolFormatter, FiatAmountFormatter, CryptoAmountFormatter } = useFormatters();
+
+ // TODO 16638 use selectAccountFiatBalance selector
+ const fiatValue = 987654;
+
+ const isAddressDetail = !!address;
+ const shouldDisplayCaret = !isAddressDetail && !!account.addresses;
+ const shouldDisplayBalance = !isAddressDetail || address?.balance != null;
+
+ return (
+
+
+
+
+
+
+
+
+ {address?.address ?? account.accountLabel}
+
+ {shouldDisplayBalance && (
+
+
+
+ )}
+
+
+
+
+
+ {shouldDisplayBalance && (
+
+
+
+ )}
+
+
+ {shouldDisplayCaret && (
+
+
+
+ )}
+
+
+ );
+};
diff --git a/suite-native/module-trading/src/components/general/AccountSheet/AccountSheet.tsx b/suite-native/module-trading/src/components/general/AccountSheet/AccountSheet.tsx
new file mode 100644
index 00000000000..5f4b539c84b
--- /dev/null
+++ b/suite-native/module-trading/src/components/general/AccountSheet/AccountSheet.tsx
@@ -0,0 +1,60 @@
+import { NetworkSymbol } from '@suite-common/wallet-config';
+
+import { useReceiveAccountsListData } from '../../../hooks/useReceiveAccountsListData';
+import { useSelectedAccount } from '../../../hooks/useSelectedAccount';
+import { TradingBottomSheetSectionList } from '../TradingBottomSheetSectionList';
+import { ACCOUNT_LIST_ITEM_HEIGHT, AccountListItem } from './AccountListItem';
+import { AccountSheetHeader } from './AccountSheetHeader';
+import { AddressListEmptyComponent } from './AddressListEmptyComponent';
+import { ReceiveAccount } from '../../../types';
+
+export type AccountSheetProps = {
+ isVisible: boolean;
+ onClose: () => void;
+ onAccountSelect: (account: ReceiveAccount) => void;
+ symbol: NetworkSymbol;
+};
+
+const keyExtractor = (item: ReceiveAccount) => `${item.account}_${item.address?.address}`;
+
+export const AccountSheet = ({
+ isVisible,
+ onClose,
+ onAccountSelect,
+ symbol,
+}: AccountSheetProps) => {
+ const { selectedAccount, clearSelectedAccount, onItemSelect } = useSelectedAccount({
+ onAccountSelect,
+ onClose,
+ isVisible,
+ });
+
+ // TODO 16638 should we display loading state instead of empty?
+ const data = useReceiveAccountsListData(symbol, selectedAccount) ?? [];
+
+ return (
+
+ isVisible={isVisible}
+ onClose={onClose}
+ handleComponent={() => (
+
+ )}
+ ListEmptyComponent={}
+ renderItem={item => (
+ onItemSelect(item)}
+ symbol={symbol}
+ />
+ )}
+ data={data}
+ estimatedItemSize={ACCOUNT_LIST_ITEM_HEIGHT}
+ keyExtractor={keyExtractor}
+ noSingletonSectionHeader
+ />
+ );
+};
diff --git a/suite-native/module-trading/src/components/general/AccountSheet/AccountSheetHeader.tsx b/suite-native/module-trading/src/components/general/AccountSheet/AccountSheetHeader.tsx
new file mode 100644
index 00000000000..d75506b4ce3
--- /dev/null
+++ b/suite-native/module-trading/src/components/general/AccountSheet/AccountSheetHeader.tsx
@@ -0,0 +1,50 @@
+import { Account } from '@suite-common/wallet-types';
+import { BottomSheetGrabber, VStack } from '@suite-native/atoms';
+import { Translation, useTranslate } from '@suite-native/intl';
+import { prepareNativeStyle, useNativeStyles } from '@trezor/styles';
+
+import { SearchableSheetHeader } from '../SearchableSheetHeader';
+import { SheetHeaderTitle } from '../SheetHeaderTitle';
+
+export type AccountSheetHeaderProps = {
+ onClose: () => void;
+ selectedAccount: undefined | Account;
+ clearSelectedAccount: () => void;
+};
+
+const wrapperStyle = prepareNativeStyle<{}>(({ spacings }) => ({
+ padding: spacings.sp16,
+ gap: spacings.sp16,
+}));
+
+export const AccountSheetHeader = ({
+ selectedAccount,
+ clearSelectedAccount,
+ onClose,
+}: AccountSheetHeaderProps) => {
+ const { applyStyle } = useNativeStyles();
+ const { translate } = useTranslate();
+
+ if (selectedAccount) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+ );
+};
diff --git a/suite-native/module-trading/src/components/general/AccountSheet/AddressListEmptyComponent.tsx b/suite-native/module-trading/src/components/general/AccountSheet/AddressListEmptyComponent.tsx
new file mode 100644
index 00000000000..e782f1d09a1
--- /dev/null
+++ b/suite-native/module-trading/src/components/general/AccountSheet/AddressListEmptyComponent.tsx
@@ -0,0 +1,11 @@
+import { Translation } from '@suite-native/intl';
+
+import { TradingEmptyComponent } from '../TradingEmptyComponent';
+
+// TODO 16638 we probably need empty state for step 1 as well
+export const AddressListEmptyComponent = () => (
+ }
+ description={}
+ />
+);
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
new file mode 100644
index 00000000000..412a6b6b1b6
--- /dev/null
+++ b/suite-native/module-trading/src/components/general/AccountSheet/__tests__/AccountListItem.comp.test.tsx
@@ -0,0 +1,137 @@
+import { Account } from '@suite-common/wallet-types';
+import { fireEvent, renderWithStore, waitFor } from '@suite-native/test-utils';
+import { Address } from '@trezor/blockchain-link-types';
+
+import { ReceiveAccount } from '../../../../types';
+import { AccountListItem, AccountListItemProps } from '../AccountListItem';
+
+describe('AccountListItem', () => {
+ const onPressMock = jest.fn();
+
+ const renderAccountListItem = async (receiveAccount: ReceiveAccount) => {
+ const props: AccountListItemProps = {
+ symbol: 'btc',
+ receiveAccount,
+ onPress: onPressMock,
+ };
+ const result = renderWithStore();
+
+ await waitFor(() => expect(result.getByText('BTC')).toBeDefined());
+
+ return result;
+ };
+
+ 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 Account,
+ };
+ const { getByText } = await renderAccountListItem(receiveAccount);
+
+ fireEvent.press(getByText('BTC'));
+
+ expect(onPressMock).toHaveBeenCalled();
+ });
+
+ it('should render account name when no address is specified', async () => {
+ const receiveAccount: ReceiveAccount = {
+ account: {
+ key: 'btc1',
+ symbol: 'btc',
+ accountLabel: 'My BTC account',
+ availableBalance: '10000000',
+ } as Account,
+ };
+ const { getByText, queryByAccessibilityHint } = await renderAccountListItem(receiveAccount);
+
+ expect(getByText('My BTC account')).toBeDefined();
+ expect(getByText('0.1 BTC')).toBeDefined();
+ expect(queryByAccessibilityHint('Select to display account addresses')).toBeNull();
+ });
+
+ it('should display caret when account defines addresses', async () => {
+ const receiveAccount: ReceiveAccount = {
+ account: {
+ key: 'btc1',
+ symbol: 'btc',
+ accountLabel: 'My BTC account',
+ availableBalance: '10000000',
+ addresses: {
+ change: [],
+ used: [],
+ unused: [],
+ },
+ } as unknown as Account,
+ };
+ const { getByText, getByAccessibilityHint } = await renderAccountListItem(receiveAccount);
+
+ 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 } =
+ await renderAccountListItem(receiveAccount);
+
+ expect(getByText('BTC_address')).toBeDefined();
+ expect(queryByText('My BTC account')).toBeNull();
+ expect(getByText('0.05 BTC')).toBeDefined();
+ expect(queryByAccessibilityHint('Select to display account addresses')).toBeNull();
+ });
+
+ 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 { getByText } = await renderAccountListItem(receiveAccount);
+
+ expect(getByText('0 BTC')).toBeDefined();
+ });
+
+ 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 { queryByText } = await renderAccountListItem(receiveAccount);
+
+ expect(queryByText('0 BTC')).toBeNull();
+ expect(queryByText('1 BTC')).toBeNull();
+ });
+});
diff --git a/suite-native/module-trading/src/components/general/SearchableSheetHeader.tsx b/suite-native/module-trading/src/components/general/SearchableSheetHeader.tsx
index 7f75f65cd4e..bbe3d295e83 100644
--- a/suite-native/module-trading/src/components/general/SearchableSheetHeader.tsx
+++ b/suite-native/module-trading/src/components/general/SearchableSheetHeader.tsx
@@ -2,6 +2,7 @@ import { ReactNode, useCallback, useState } from 'react';
import Animated, { FadeIn, FadeOut, LinearTransition } from 'react-native-reanimated';
import { BottomSheetGrabber, VStack } from '@suite-native/atoms';
+import { IconName } from '@suite-native/icons';
import { useTranslate } from '@suite-native/intl';
import { NativeStyleObject, prepareNativeStyle, useNativeStyles } from '@trezor/styles';
@@ -11,6 +12,7 @@ import { SheetHeaderTitle } from './SheetHeaderTitle';
export type SearchableSheetHeaderProps = {
onClose: () => void;
title: ReactNode;
+ leftButtonIcon?: IconName;
onFilterFocusChange?: (isFilterActive: boolean) => void;
children?: ReactNode;
style?: NativeStyleObject;
@@ -31,6 +33,7 @@ export const SearchableSheetHeader = ({
children,
onFilterFocusChange = noOp,
style,
+ leftButtonIcon = 'x',
}: SearchableSheetHeaderProps) => {
const { applyStyle } = useNativeStyles();
const { translate } = useTranslate();
@@ -56,7 +59,7 @@ export const SearchableSheetHeader = ({
exiting={FadeOut.duration(FOCUS_ANIMATION_DURATION)}
>
diff --git a/suite-native/module-trading/src/hooks/__tests__/useReceiveAccountsListData.test.tsx b/suite-native/module-trading/src/hooks/__tests__/useReceiveAccountsListData.test.tsx
new file mode 100644
index 00000000000..b7727aa5f65
--- /dev/null
+++ b/suite-native/module-trading/src/hooks/__tests__/useReceiveAccountsListData.test.tsx
@@ -0,0 +1,164 @@
+import { NetworkSymbol } from '@suite-common/wallet-config';
+import { Account } from '@suite-common/wallet-types';
+import { StoreProviderForTests, renderHook, waitFor } from '@suite-native/test-utils';
+
+import { useReceiveAccountsListData } from '../useReceiveAccountsListData';
+
+describe('useReceiveAccountsListData', () => {
+ const preloadedState = {
+ device: {
+ selectedDevice: {
+ state: {
+ staticSessionId: 'staticSessionId',
+ },
+ },
+ },
+ 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' }],
+ },
+ },
+ {
+ symbol: 'btc',
+ accountLabel: 'BTC Account #2',
+ deviceState: 'staticSessionId',
+ key: 'btc2',
+ addresses: {
+ used: [],
+ change: [],
+ unused: [],
+ },
+ },
+ {
+ symbol: 'eth',
+ accountLabel: 'ETH Account #1',
+ deviceState: 'staticSessionId',
+ addresses: undefined,
+ key: 'eth1',
+ },
+ ] as unknown as Account[],
+ },
+ };
+
+ const renderUseReceiveAccountsListDataHook = async (
+ initialSymbol: NetworkSymbol,
+ initialSelectedAccount: undefined | Account,
+ ) => {
+ const ret = renderHook(
+ ({ symbol, selectedAccount }) => useReceiveAccountsListData(symbol, selectedAccount),
+ {
+ wrapper: ({ children }) => (
+
+ {children}
+
+ ),
+ initialProps: {
+ symbol: initialSymbol,
+ selectedAccount: initialSelectedAccount,
+ },
+ },
+ );
+
+ await waitFor(() => {
+ expect(ret.result.current).not.toBeUndefined();
+ });
+
+ return ret;
+ };
+
+ describe('without account selected', () => {
+ it('should display all accounts for given symbol', async () => {
+ const { result } = await renderUseReceiveAccountsListDataHook('btc', undefined);
+
+ expect(result.current).toEqual([
+ {
+ key: '',
+ label: '',
+ data: [
+ { account: expect.objectContaining({ key: 'btc1' }) },
+ { account: expect.objectContaining({ key: 'btc2' }) },
+ ],
+ },
+ ]);
+ });
+
+ it('should react to symbol change', async () => {
+ const { result, rerender } = await renderUseReceiveAccountsListDataHook(
+ 'btc',
+ undefined,
+ );
+
+ rerender({ symbol: 'eth', selectedAccount: undefined });
+
+ expect(result.current).toEqual([
+ {
+ key: '',
+ label: '',
+ data: [{ account: expect.objectContaining({ key: 'eth1' }) }],
+ },
+ ]);
+ });
+ });
+
+ describe('with account selected', () => {
+ it('should be empty array for non BTC like assets', async () => {
+ const { result } = await renderUseReceiveAccountsListDataHook(
+ 'eth',
+ preloadedState.wallet.accounts[2],
+ );
+
+ expect(result.current).toEqual([]);
+ });
+
+ it('should return 1 unused address and all used addresses for BTC like assets', async () => {
+ const { result } = await renderUseReceiveAccountsListDataHook(
+ 'btc',
+ preloadedState.wallet.accounts[0],
+ );
+
+ expect(result.current).toEqual([
+ {
+ key: 'unused',
+ label: 'New address',
+ data: [
+ {
+ account: expect.objectContaining({ key: 'btc1' }),
+ address: { address: 'UNUSED1' },
+ },
+ ],
+ },
+ {
+ key: 'used',
+ label: 'Used addresses',
+ data: [
+ {
+ account: expect.objectContaining({ key: 'btc1' }),
+ address: { address: 'USED1' },
+ },
+ {
+ account: expect.objectContaining({ key: 'btc1' }),
+ address: { address: 'USED2' },
+ },
+ ],
+ },
+ ]);
+ });
+
+ it('should not return empty sections', async () => {
+ const { result } = await renderUseReceiveAccountsListDataHook(
+ 'btc',
+ preloadedState.wallet.accounts[1],
+ );
+
+ expect(result.current).toEqual([]);
+ });
+ });
+});
diff --git a/suite-native/module-trading/src/hooks/__tests__/useSelectedAccount.test.ts b/suite-native/module-trading/src/hooks/__tests__/useSelectedAccount.test.ts
new file mode 100644
index 00000000000..eae5b148555
--- /dev/null
+++ b/suite-native/module-trading/src/hooks/__tests__/useSelectedAccount.test.ts
@@ -0,0 +1,130 @@
+import { Account } from '@suite-common/wallet-types/';
+import { BasicProviderForTests, act, renderHook } from '@suite-native/test-utils';
+
+import { useSelectedAccount } from '../useSelectedAccount';
+
+describe('useSelectedAccount', () => {
+ const onCloseMock = jest.fn();
+ const onAccountSelectMock = jest.fn();
+
+ const renderUseSelectedAccountHook = (isVisible: boolean) =>
+ renderHook(useSelectedAccount, {
+ wrapper: BasicProviderForTests,
+ initialProps: {
+ isVisible,
+ onClose: onCloseMock,
+ onAccountSelect: onAccountSelectMock,
+ },
+ });
+
+ const getBtcAccount = () =>
+ ({
+ symbol: 'btc',
+ accountType: 'normal',
+ accountLabel: 'BTC Account',
+ addresses: {
+ used: [
+ {
+ address: '1BTC',
+ path: 'm/84/0/0',
+ transfers: 0,
+ balance: '0',
+ sent: '0',
+ received: '0',
+ },
+ ],
+ change: [],
+ unused: [],
+ },
+ }) as unknown as Account;
+
+ const getEthAccount = () =>
+ ({
+ symbol: 'eth',
+ accountType: 'normal',
+ accountLabel: 'ETH Account',
+ addresses: undefined,
+ }) as unknown as Account;
+
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
+ describe('selectedAccount', () => {
+ it('should be undefined by default', () => {
+ const { result } = renderUseSelectedAccountHook(false);
+
+ expect(result.current.selectedAccount).toBeUndefined();
+ });
+
+ it('should be set to undefined when isVisible changed to false', () => {
+ const { result, rerender } = renderUseSelectedAccountHook(true);
+ result.current.onItemSelect({ account: getBtcAccount() });
+
+ rerender({
+ isVisible: false,
+ onAccountSelect: onAccountSelectMock,
+ onClose: onCloseMock,
+ });
+
+ expect(result.current.selectedAccount).toBeUndefined();
+ });
+ });
+
+ describe('onItemSelect', () => {
+ it('should set selectedAccount for BTC like accounts', () => {
+ const account = getBtcAccount();
+ const { result } = renderUseSelectedAccountHook(false);
+
+ act(() => {
+ result.current.onItemSelect({ account });
+ });
+
+ expect(result.current.selectedAccount).toBe(account);
+ expect(onAccountSelectMock).not.toHaveBeenCalled();
+ expect(onCloseMock).not.toHaveBeenCalled();
+ });
+
+ it('should call onAccountSelect for ETH like accounts', () => {
+ const account = getEthAccount();
+ const { result } = renderUseSelectedAccountHook(false);
+
+ act(() => {
+ result.current.onItemSelect({ account });
+ });
+
+ expect(result.current.selectedAccount).toBeUndefined();
+ expect(onAccountSelectMock).toHaveBeenCalledWith({ account });
+ expect(onCloseMock).toHaveBeenCalledTimes(1);
+ });
+
+ it('should call onAccountSelect for BTC like accounts when selected address is specified', () => {
+ const account = getBtcAccount();
+ const selectedReceiveAccount = { account, address: account.addresses!.used[0] };
+ const { result } = renderUseSelectedAccountHook(true);
+
+ act(() => {
+ result.current.onItemSelect(selectedReceiveAccount);
+ });
+
+ expect(result.current.selectedAccount).toBeUndefined();
+ expect(onAccountSelectMock).toHaveBeenCalledWith(selectedReceiveAccount);
+ expect(onCloseMock).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('clearSelectedAccount', () => {
+ it('should clear selectedAccount', () => {
+ const { result } = renderUseSelectedAccountHook(true);
+ act(() => {
+ result.current.onItemSelect({ account: getBtcAccount() });
+ });
+
+ act(() => {
+ result.current.clearSelectedAccount();
+ });
+
+ expect(result.current.selectedAccount).toBeUndefined();
+ });
+ });
+});
diff --git a/suite-native/module-trading/src/hooks/__tests__/useTradeableAssetDominantColor.test.tsx b/suite-native/module-trading/src/hooks/__tests__/useTradeableAssetDominantColor.test.ts
similarity index 100%
rename from suite-native/module-trading/src/hooks/__tests__/useTradeableAssetDominantColor.test.tsx
rename to suite-native/module-trading/src/hooks/__tests__/useTradeableAssetDominantColor.test.ts
diff --git a/suite-native/module-trading/src/hooks/useReceiveAccountsListData.ts b/suite-native/module-trading/src/hooks/useReceiveAccountsListData.ts
new file mode 100644
index 00000000000..9c173849e4e
--- /dev/null
+++ b/suite-native/module-trading/src/hooks/useReceiveAccountsListData.ts
@@ -0,0 +1,63 @@
+import { useMemo } from 'react';
+import { useSelector } from 'react-redux';
+
+import { NetworkSymbol } from '@suite-common/wallet-config';
+import {
+ AccountsRootState,
+ DeviceRootState,
+ selectDeviceAccountsByNetworkSymbol,
+} from '@suite-common/wallet-core';
+import { Account } from '@suite-common/wallet-types/';
+import { useTranslate } from '@suite-native/intl';
+
+import { SectionListData } from '../components/general/TradingBottomSheetSectionList';
+import { ReceiveAccount } from '../types';
+
+export const useReceiveAccountsListData = (
+ symbol: NetworkSymbol,
+ selectedAccount: undefined | Account,
+) => {
+ const { translate } = useTranslate();
+
+ const accounts = useSelector((state: AccountsRootState & DeviceRootState) =>
+ selectDeviceAccountsByNetworkSymbol(state, symbol),
+ );
+
+ return useMemo | undefined>(() => {
+ if (!accounts) {
+ return undefined;
+ }
+
+ if (!selectedAccount) {
+ return [
+ {
+ key: '',
+ label: '',
+ data: accounts.map(account => ({ account })),
+ sectionData: undefined,
+ },
+ ];
+ }
+
+ if (!selectedAccount.addresses) {
+ return [];
+ }
+
+ const { used, unused } = selectedAccount.addresses;
+
+ return [
+ {
+ key: 'unused',
+ label: translate('moduleTrading.accountSheet.newAddress'),
+ data: unused.slice(0, 1).map(address => ({ account: selectedAccount, address })),
+ sectionData: undefined,
+ },
+ {
+ key: 'used',
+ label: translate('moduleTrading.accountSheet.usedAddresses'),
+ data: used.map(address => ({ account: selectedAccount, address })),
+ sectionData: undefined,
+ },
+ ].filter(section => section.data.length > 0);
+ }, [accounts, selectedAccount, translate]);
+};
diff --git a/suite-native/module-trading/src/hooks/useSelectedAccount.ts b/suite-native/module-trading/src/hooks/useSelectedAccount.ts
new file mode 100644
index 00000000000..64f89c57a13
--- /dev/null
+++ b/suite-native/module-trading/src/hooks/useSelectedAccount.ts
@@ -0,0 +1,41 @@
+import { useCallback, useEffect, useState } from 'react';
+
+import { Account } from '@suite-common/wallet-types/';
+
+import { ReceiveAccount } from '../types';
+
+type AccountSheetState = {
+ isVisible: boolean;
+ onClose: () => void;
+ onAccountSelect: (account: ReceiveAccount) => void;
+};
+
+export const useSelectedAccount = ({ onAccountSelect, onClose, isVisible }: AccountSheetState) => {
+ const [selectedAccount, setSelectedAccount] = useState();
+
+ const onItemSelect = useCallback(
+ (selectedReceiveAccount: ReceiveAccount) => {
+ const { account, address } = selectedReceiveAccount;
+
+ if (account.addresses && !address) {
+ setSelectedAccount(account);
+
+ return;
+ }
+
+ onAccountSelect(selectedReceiveAccount);
+ onClose();
+ },
+ [onAccountSelect, onClose],
+ );
+
+ const clearSelectedAccount = useCallback(() => setSelectedAccount(undefined), []);
+
+ useEffect(() => {
+ if (!isVisible) {
+ clearSelectedAccount();
+ }
+ }, [isVisible, clearSelectedAccount]);
+
+ return { selectedAccount, onItemSelect, clearSelectedAccount };
+};
diff --git a/suite-native/module-trading/src/types.ts b/suite-native/module-trading/src/types.ts
index 055795012ee..65a262efb0e 100644
--- a/suite-native/module-trading/src/types.ts
+++ b/suite-native/module-trading/src/types.ts
@@ -1,6 +1,7 @@
import { NetworkSymbol } from '@suite-common/wallet-config';
-import { TokenAddress } from '@suite-common/wallet-types';
+import { Account, TokenAddress } from '@suite-common/wallet-types';
import { IconName } from '@suite-native/icons';
+import { Address } from '@trezor/blockchain-link-types';
// NOTE: in production code we probably want to use `TokenInfoBranded` or something similar instead
export type TradeableAsset = {
@@ -14,3 +15,8 @@ export type Country = {
name: string;
flag: IconName;
};
+
+export type ReceiveAccount = {
+ account: Account;
+ address?: Address;
+};
diff --git a/suite-native/module-trading/tsconfig.json b/suite-native/module-trading/tsconfig.json
index ca53899b029..abfdf2e5624 100644
--- a/suite-native/module-trading/tsconfig.json
+++ b/suite-native/module-trading/tsconfig.json
@@ -3,7 +3,10 @@
"compilerOptions": { "outDir": "libDev" },
"references": [
{ "path": "../navigation" },
- { "path": "../test-utils" }
+ { "path": "../test-utils" },
+ {
+ "path": "../../packages/blockchain-link-types"
+ }
],
"include": [".", "**/*.json"]
}
diff --git a/yarn.lock b/yarn.lock
index 69378db16e6..89cd46e4479 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -11438,6 +11438,7 @@ __metadata:
react: "npm:18.2.0"
react-native: "npm:0.76.1"
react-native-reanimated: "npm:3.16.7"
+ react-redux: "npm:8.0.7"
languageName: unknown
linkType: soft