From 0f52e922a63c591e2b6853c16267a6665a9e9e60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jirka=20Ba=C5=BEant?= Date: Fri, 7 Feb 2025 19:10:33 +0100 Subject: [PATCH] feat(suite-native): Mobile Trade: Account and address picker stub --- suite-native/intl/src/en.ts | 8 + suite-native/module-trading/package.json | 4 +- .../src/components/buy/BuyCard.tsx | 25 +-- .../components/buy/ReceiveAccountPicker.tsx | 74 +++++++++ .../general/AccountSheet/AccountListItem.tsx | 103 ++++++++++++ .../general/AccountSheet/AccountSheet.tsx | 54 ++++++ .../AccountSheet/AccountSheetHeader.tsx | 52 ++++++ .../AddressListEmptyComponent.tsx | 10 ++ .../general/SearchableSheetHeader.tsx | 5 +- .../useReceiveAccountsListData.test.tsx | 155 ++++++++++++++++++ .../__tests__/useSelectedAccount.test.ts | 130 +++++++++++++++ .../src/hooks/useReceiveAccountsListData.ts | 63 +++++++ .../src/hooks/useSelectedAccount.ts | 41 +++++ suite-native/module-trading/src/types.ts | 8 +- suite-native/module-trading/tsconfig.json | 5 +- yarn.lock | 1 + 16 files changed, 712 insertions(+), 26 deletions(-) create mode 100644 suite-native/module-trading/src/components/buy/ReceiveAccountPicker.tsx create mode 100644 suite-native/module-trading/src/components/general/AccountSheet/AccountListItem.tsx create mode 100644 suite-native/module-trading/src/components/general/AccountSheet/AccountSheet.tsx create mode 100644 suite-native/module-trading/src/components/general/AccountSheet/AccountSheetHeader.tsx create mode 100644 suite-native/module-trading/src/components/general/AccountSheet/AddressListEmptyComponent.tsx create mode 100644 suite-native/module-trading/src/hooks/__tests__/useReceiveAccountsListData.test.tsx create mode 100644 suite-native/module-trading/src/hooks/__tests__/useSelectedAccount.test.ts create mode 100644 suite-native/module-trading/src/hooks/useReceiveAccountsListData.ts create mode 100644 suite-native/module-trading/src/hooks/useSelectedAccount.ts diff --git a/suite-native/intl/src/en.ts b/suite-native/intl/src/en.ts index 64f4ec51bed..b3fcae915e8 100644 --- a/suite-native/intl/src/en.ts +++ b/suite-native/intl/src/en.ts @@ -1302,6 +1302,14 @@ 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', + }, 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..dd49a0a5d27 --- /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..f167fc202ca --- /dev/null +++ b/suite-native/module-trading/src/components/general/AccountSheet/AccountListItem.tsx @@ -0,0 +1,103 @@ +import { Pressable } from 'react-native'; + +import { useFormatters } from '@suite-common/formatters'; +import { Box, HStack, RoundedIcon, Text, VStack } from '@suite-native/atoms'; +import { Icon } from '@suite-native/icons'; +import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; + +import { ReceiveAccount, TradeableAsset } from '../../../types'; + +export type AccountListItemProps = { + asset: TradeableAsset; + 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 = ({ + asset: { symbol }, + receiveAccount: { account, address }, + onPress, +}: AccountListItemProps) => { + const { applyStyle } = useNativeStyles(); + const { DisplaySymbolFormatter, FiatAmountFormatter, CryptoAmountFormatter } = useFormatters(); + + // TODO probably should be part of props + 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..1b5cd63e0f9 --- /dev/null +++ b/suite-native/module-trading/src/components/general/AccountSheet/AccountSheet.tsx @@ -0,0 +1,54 @@ +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, TradeableAsset } from '../../../types'; + +export type AccountSheetProps = { + isVisible: boolean; + onClose: () => void; + onAccountSelect: (account: ReceiveAccount) => void; + asset: TradeableAsset; +}; + +const keyExtractor = (item: ReceiveAccount) => `${item.account}_${item.address?.address}`; + +export const AccountSheet = ({ isVisible, onClose, onAccountSelect, asset }: AccountSheetProps) => { + const { selectedAccount, clearSelectedAccount, onItemSelect } = useSelectedAccount({ + onAccountSelect, + onClose, + isVisible, + }); + + // TODO 16638 should we display loading state instead of empty? + const data = useReceiveAccountsListData(asset.symbol, selectedAccount) ?? []; + + // TODO 16638 step 2 + return ( + + isVisible={isVisible} + onClose={onClose} + handleComponent={() => ( + + )} + ListEmptyComponent={} + renderItem={item => ( + onItemSelect(item)} + asset={asset} + /> + )} + 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..459b0b306d0 --- /dev/null +++ b/suite-native/module-trading/src/components/general/AccountSheet/AccountSheetHeader.tsx @@ -0,0 +1,52 @@ +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(); + + // TODO 16638 animations + + 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..d6134637394 --- /dev/null +++ b/suite-native/module-trading/src/components/general/AccountSheet/AddressListEmptyComponent.tsx @@ -0,0 +1,10 @@ +import { Translation } from '@suite-native/intl'; + +import { TradingEmptyComponent } from '../TradingEmptyComponent'; + +export const AddressListEmptyComponent = () => ( + } + description={} + /> +); 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..926fd6c4a5a --- /dev/null +++ b/suite-native/module-trading/src/hooks/__tests__/useReceiveAccountsListData.test.tsx @@ -0,0 +1,155 @@ +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' }, + }, + ], + }, + ]); + }); + }); +}); 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/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