diff --git a/suite-native/intl/src/en.ts b/suite-native/intl/src/en.ts
index 825192413a85..209b8bbc5c94 100644
--- a/suite-native/intl/src/en.ts
+++ b/suite-native/intl/src/en.ts
@@ -1195,6 +1195,16 @@ export const en = {
description: 'We currently support staking as view-only in Trezor Suite Lite.',
},
},
+ moduleTrading: {
+ selectCoin: {
+ buttonTitle: 'Select coin',
+ },
+ networksSheet: {
+ title: 'Tokens',
+ popularTitle: 'Popular',
+ listTitle: 'Tokens',
+ },
+ },
};
export type Translations = typeof en;
diff --git a/suite-native/module-trading/package.json b/suite-native/module-trading/package.json
index 25182f5e1e3e..b13b41622917 100644
--- a/suite-native/module-trading/package.json
+++ b/suite-native/module-trading/package.json
@@ -15,6 +15,7 @@
"@reduxjs/toolkit": "1.9.5",
"@suite-native/navigation": "workspace:*",
"@suite-native/test-utils": "workspace:*",
+ "expo-linear-gradient": "^14.0.1",
"react": "18.2.0",
"react-native": "0.76.1"
}
diff --git a/suite-native/module-trading/src/components/buy/AmountCard.tsx b/suite-native/module-trading/src/components/buy/AmountCard.tsx
new file mode 100644
index 000000000000..f33495a871f2
--- /dev/null
+++ b/suite-native/module-trading/src/components/buy/AmountCard.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+
+import { Card, HStack } from '@suite-native/atoms';
+
+import { SelectTradeableAssetButton } from '../general/SelectTradeableAssetButton';
+import { TradeableAssetsSheet } from '../general/TradeableAssetsSheet';
+import { useTradeableAssetsSheetControls } from '../../hooks/useTradeableAssetsSheetControls';
+
+export const AmountCard = () => {
+ const {
+ isTradeableAssetsSheetVisible,
+ showTradeableAssetsSheet,
+ hideTradeableAssetsSheet,
+ selectedTradeableAsset,
+ setSelectedTradeableAsset,
+ } = useTradeableAssetsSheetControls();
+
+ return (
+
+
+
+
+
+
+ );
+};
diff --git a/suite-native/module-trading/src/components/buy/__tests__/AmountCard.comp.test.tsx b/suite-native/module-trading/src/components/buy/__tests__/AmountCard.comp.test.tsx
new file mode 100644
index 000000000000..bdce10c0c928
--- /dev/null
+++ b/suite-native/module-trading/src/components/buy/__tests__/AmountCard.comp.test.tsx
@@ -0,0 +1,30 @@
+import { render, fireEvent } from '@suite-native/test-utils';
+
+import { AmountCard } from '../AmountCard';
+
+describe('AmountCard', () => {
+ it('should display Select coin button', () => {
+ const { getByText, queryByText } = render();
+
+ expect(getByText('Select coin')).toBeDefined();
+ expect(queryByText('Tokens')).toBeNull();
+ });
+
+ it('should display AssetsSheet after button click', () => {
+ const { getByText } = render();
+
+ fireEvent.press(getByText('Select coin'));
+
+ expect(getByText('Tokens')).toBeDefined();
+ });
+
+ it('should display selected network from AssetsSheet', () => {
+ const { getByText, queryByText } = render();
+
+ fireEvent.press(getByText('Select coin'));
+ fireEvent.press(getByText('BTC'));
+
+ expect(queryByText('Tokens')).toBeNull();
+ expect(getByText('BTC')).toBeDefined();
+ });
+});
diff --git a/suite-native/module-trading/src/components/general/PopularTradeableNetworks.tsx b/suite-native/module-trading/src/components/general/PopularTradeableNetworks.tsx
new file mode 100644
index 000000000000..2e9be2e5407c
--- /dev/null
+++ b/suite-native/module-trading/src/components/general/PopularTradeableNetworks.tsx
@@ -0,0 +1,38 @@
+import { NetworkSymbol } from '@suite-common/wallet-config';
+import { HStack, Text, VStack } from '@suite-native/atoms';
+import { Translation } from '@suite-native/intl';
+
+import { TradeableNetworkButton } from './TradeableNetworkButton';
+
+export type PopularTradeableNetworksProps = {
+ symbols: NetworkSymbol[];
+ onNetworkSelect: (symbol: NetworkSymbol) => void;
+ maxNetworkSymbols?: number;
+};
+
+const DEFAULT_MAX_NETWORK_SYMBOLS = 4;
+
+export const PopularTradeableNetworks = ({
+ symbols,
+ onNetworkSelect,
+ maxNetworkSymbols = DEFAULT_MAX_NETWORK_SYMBOLS,
+}: PopularTradeableNetworksProps) => {
+ const limitedSymbols = symbols.slice(0, maxNetworkSymbols);
+
+ return (
+
+
+
+
+
+ {limitedSymbols.map(symbol => (
+ onNetworkSelect(symbol)}
+ />
+ ))}
+
+
+ );
+};
diff --git a/suite-native/module-trading/src/components/general/SelectTradeableAssetButton.tsx b/suite-native/module-trading/src/components/general/SelectTradeableAssetButton.tsx
new file mode 100644
index 000000000000..4fb85935a7ec
--- /dev/null
+++ b/suite-native/module-trading/src/components/general/SelectTradeableAssetButton.tsx
@@ -0,0 +1,32 @@
+import { NetworkSymbol } from '@suite-common/wallet-config';
+import { Button, buttonSchemeToColorsMap } from '@suite-native/atoms';
+import { Translation } from '@suite-native/intl';
+import { Icon } from '@suite-native/icons';
+
+import { TradeableNetworkButton } from './TradeableNetworkButton';
+
+export type SelectTradeableAssetButtonProps = {
+ onPress: () => void;
+ selectedAsset: NetworkSymbol | undefined;
+};
+
+export const SelectTradeableAssetButton = ({
+ onPress,
+ selectedAsset,
+}: SelectTradeableAssetButtonProps) => {
+ const { iconColor } = buttonSchemeToColorsMap.primary;
+
+ if (selectedAsset) {
+ return ;
+ }
+
+ return (
+ }
+ size="small"
+ >
+
+
+ );
+};
diff --git a/suite-native/module-trading/src/components/general/TradeableAssetButton.tsx b/suite-native/module-trading/src/components/general/TradeableAssetButton.tsx
new file mode 100644
index 000000000000..d5af09acdd28
--- /dev/null
+++ b/suite-native/module-trading/src/components/general/TradeableAssetButton.tsx
@@ -0,0 +1,63 @@
+import { ReactNode } from 'react';
+import { Pressable, StyleSheet } from 'react-native';
+
+import { LinearGradient } from 'expo-linear-gradient';
+
+import { hexToRgba } from '@suite-common/suite-utils';
+import { Text } from '@suite-native/atoms';
+import { Icon } from '@suite-native/icons';
+import { prepareNativeStyle, useNativeStyles } from '@trezor/styles';
+import { nativeSpacings } from '@trezor/theme';
+
+export type TradeableAssetButtonProps = {
+ icon: ReactNode;
+ children: ReactNode;
+ bgBaseColor: string;
+ caret?: boolean;
+ onPress: () => void;
+};
+
+const styles = StyleSheet.create({
+ button: {
+ height: 36,
+ padding: nativeSpacings.sp4,
+ paddingRight: nativeSpacings.sp12,
+ flexDirection: 'row',
+ justifyContent: 'center',
+ alignItems: 'center',
+ gap: nativeSpacings.sp8,
+ },
+});
+
+const gradientBackgroundStyle = prepareNativeStyle(({ borders }) => ({
+ borderRadius: borders.radii.round,
+ borderWidth: borders.widths.small,
+ borderColor: 'rgba(0, 0, 0, 0.06)',
+}));
+
+export const TradeableAssetButton = ({
+ bgBaseColor,
+ caret,
+ icon,
+ children,
+ onPress,
+}: TradeableAssetButtonProps) => {
+ const { applyStyle } = useNativeStyles();
+
+ return (
+
+
+ {icon}
+
+ {children}
+
+ {caret && }
+
+
+ );
+};
diff --git a/suite-native/module-trading/src/components/general/TradeableAssetListItem.tsx b/suite-native/module-trading/src/components/general/TradeableAssetListItem.tsx
new file mode 100644
index 000000000000..4fa46fb02d7f
--- /dev/null
+++ b/suite-native/module-trading/src/components/general/TradeableAssetListItem.tsx
@@ -0,0 +1,52 @@
+import { ReactNode } from 'react';
+import { Pressable } from 'react-native';
+
+import { HStack, VStack, Text, Badge } from '@suite-native/atoms';
+import { prepareNativeStyle, useNativeStyles } from '@trezor/styles';
+import { nativeSpacings } from '@trezor/theme';
+
+export type AssetListItemProps = {
+ title: ReactNode;
+ subtitle: ReactNode;
+ icon: ReactNode;
+ badge?: ReactNode;
+ onPress: () => void;
+};
+
+const vStackStyle = prepareNativeStyle(({ borders, colors }) => ({
+ height: 68,
+ paddingVertical: nativeSpacings.sp8,
+ borderBottomWidth: borders.widths.small,
+ borderBottomColor: colors.borderElevation1,
+ flex: 1,
+ spacing: 0,
+}));
+
+export const TradeableAssetListItem = ({
+ title,
+ icon,
+ subtitle,
+ badge,
+ onPress,
+}: AssetListItemProps) => {
+ const { applyStyle } = useNativeStyles();
+
+ return (
+
+
+ {icon}
+
+
+ {title}
+
+
+
+ {subtitle}
+
+ {!!badge && }
+
+
+
+
+ );
+};
diff --git a/suite-native/module-trading/src/components/general/TradeableAssetsSheet.tsx b/suite-native/module-trading/src/components/general/TradeableAssetsSheet.tsx
new file mode 100644
index 000000000000..2ae3a23ab69a
--- /dev/null
+++ b/suite-native/module-trading/src/components/general/TradeableAssetsSheet.tsx
@@ -0,0 +1,37 @@
+import { NetworkSymbol } from '@suite-common/wallet-config';
+import { BottomSheet, VStack } from '@suite-native/atoms';
+import { Translation } from '@suite-native/intl';
+
+import { PickerCloseButton } from './PickerCloseButton';
+import { PickerHeader } from './PickerHeader';
+import { PopularTradeableNetworks } from './PopularTradeableNetworks';
+
+export type TradeableAssetsSheetProps = {
+ isVisible: boolean;
+ onClose: () => void;
+ onAssetSelect: (symbol: NetworkSymbol) => void;
+};
+
+export const TradeableAssetsSheet = ({
+ isVisible,
+ onClose,
+ onAssetSelect,
+}: TradeableAssetsSheetProps) => {
+ const onAssetSelectCallback = (symbol: NetworkSymbol) => {
+ onAssetSelect(symbol);
+ onClose();
+ };
+
+ return (
+
+
+ } />
+
+
+
+
+ );
+};
diff --git a/suite-native/module-trading/src/components/general/TradeableNetworkButton.tsx b/suite-native/module-trading/src/components/general/TradeableNetworkButton.tsx
new file mode 100644
index 000000000000..f95142e812e7
--- /dev/null
+++ b/suite-native/module-trading/src/components/general/TradeableNetworkButton.tsx
@@ -0,0 +1,29 @@
+import { useFormatters } from '@suite-common/formatters';
+import { NetworkSymbol } from '@suite-common/wallet-config';
+import { CryptoIcon } from '@suite-native/icons';
+import { useNativeStyles } from '@trezor/styles';
+
+import { TradeableAssetButton } from './TradeableAssetButton';
+
+export type TradeableNetworkButtonProps = {
+ symbol: NetworkSymbol;
+ onPress: () => void;
+ caret?: boolean;
+};
+
+export const TradeableNetworkButton = ({ symbol, onPress, caret }: TradeableNetworkButtonProps) => {
+ const { DisplaySymbolFormatter } = useFormatters();
+ const { utils } = useNativeStyles();
+ const baseSymbolColor = utils.coinsColors[symbol];
+
+ return (
+ }
+ >
+
+
+ );
+};
diff --git a/suite-native/module-trading/src/components/general/TradeableNetworkListItem.tsx b/suite-native/module-trading/src/components/general/TradeableNetworkListItem.tsx
new file mode 100644
index 000000000000..49f6afd075e2
--- /dev/null
+++ b/suite-native/module-trading/src/components/general/TradeableNetworkListItem.tsx
@@ -0,0 +1,24 @@
+import { useFormatters } from '@suite-common/formatters';
+import { NetworkSymbol } from '@suite-common/wallet-config';
+import { CryptoIcon } from '@suite-native/icons';
+
+import { TradeableAssetListItem } from './TradeableAssetListItem';
+
+export type TradeableNetworkListItemProps = {
+ symbol: NetworkSymbol;
+ onPress: () => void;
+};
+
+export const TradeableNetworkListItem = ({ symbol, onPress }: TradeableNetworkListItemProps) => {
+ const { DisplaySymbolFormatter, NetworkNameFormatter } = useFormatters();
+
+ return (
+ }
+ subtitle={}
+ badge={}
+ icon={}
+ onPress={onPress}
+ />
+ );
+};
diff --git a/suite-native/module-trading/src/components/general/__tests__/PopularNetworks.comp.test.tsx b/suite-native/module-trading/src/components/general/__tests__/PopularNetworks.comp.test.tsx
new file mode 100644
index 000000000000..feb8581fcc03
--- /dev/null
+++ b/suite-native/module-trading/src/components/general/__tests__/PopularNetworks.comp.test.tsx
@@ -0,0 +1,48 @@
+import { render, fireEvent } from '@suite-native/test-utils';
+
+import { PopularTradeableNetworks } from '../PopularTradeableNetworks';
+
+describe('PopularNetworks', () => {
+ it('should render up to 4 networks by default', () => {
+ const { queryByText } = render(
+ ,
+ );
+
+ expect(queryByText('BTC')).toBeDefined();
+ expect(queryByText('ETH')).toBeDefined();
+ expect(queryByText('ADA')).toBeDefined();
+ expect(queryByText('SOL')).toBeDefined();
+ expect(queryByText('DOGE')).toBeNull();
+ });
+
+ it('should render up to maxNetworkSymbols networks', () => {
+ const { queryByText } = render(
+ ,
+ );
+
+ expect(queryByText('BTC')).toBeDefined();
+ expect(queryByText('ETH')).toBeDefined();
+ expect(queryByText('ADA')).toBeDefined();
+ expect(queryByText('SOL')).toBeNull();
+ expect(queryByText('DOGE')).toBeNull();
+ });
+
+ it('should call onNetworkSelect callback', () => {
+ const selectSpy = jest.fn();
+ const { getByText } = render(
+ ,
+ );
+
+ const button = getByText('BTC');
+ fireEvent.press(button);
+
+ expect(selectSpy).toHaveBeenCalledWith('btc');
+ });
+});
diff --git a/suite-native/module-trading/src/components/general/__tests__/SelectTradeableAssetButton.comp.test.tsx b/suite-native/module-trading/src/components/general/__tests__/SelectTradeableAssetButton.comp.test.tsx
new file mode 100644
index 000000000000..d9c1787f5561
--- /dev/null
+++ b/suite-native/module-trading/src/components/general/__tests__/SelectTradeableAssetButton.comp.test.tsx
@@ -0,0 +1,22 @@
+import { render } from '@suite-native/test-utils';
+
+import { SelectTradeableAssetButton } from '../SelectTradeableAssetButton';
+
+describe('SelectTradeableAssetButton', () => {
+ it('should render "Select coin" when no network is selected', () => {
+ const { getByText } = render(
+ ,
+ );
+
+ expect(getByText('Select coin')).toBeDefined();
+ });
+
+ it('should render TradeableAssetButton when network is selected', () => {
+ const { queryByText } = render(
+ ,
+ );
+
+ expect(queryByText('Select coin')).toBeNull();
+ expect(queryByText('ADA')).toBeDefined();
+ });
+});
diff --git a/suite-native/module-trading/src/components/general/__tests__/TradeableNetworkButton.comp.test.tsx b/suite-native/module-trading/src/components/general/__tests__/TradeableNetworkButton.comp.test.tsx
new file mode 100644
index 000000000000..18b4b21f34a6
--- /dev/null
+++ b/suite-native/module-trading/src/components/general/__tests__/TradeableNetworkButton.comp.test.tsx
@@ -0,0 +1,21 @@
+import { render, fireEvent } from '@suite-native/test-utils';
+
+import { TradeableNetworkButton } from '../TradeableNetworkButton';
+
+describe('TradeableNetworkButton', () => {
+ it('should render display name of given symbol', () => {
+ const { getByText } = render();
+
+ expect(getByText('BTC')).toBeDefined();
+ });
+
+ it('should call onPress callback', () => {
+ const pressSpy = jest.fn();
+ const { getByText } = render();
+
+ const button = getByText('BTC');
+ fireEvent.press(button);
+
+ expect(pressSpy).toHaveBeenCalledWith();
+ });
+});
diff --git a/suite-native/module-trading/src/components/general/__tests__/TradeableNetworkListItem.comp.test.tsx b/suite-native/module-trading/src/components/general/__tests__/TradeableNetworkListItem.comp.test.tsx
new file mode 100644
index 000000000000..51d596c7ae4f
--- /dev/null
+++ b/suite-native/module-trading/src/components/general/__tests__/TradeableNetworkListItem.comp.test.tsx
@@ -0,0 +1,24 @@
+import { render, fireEvent } from '@suite-native/test-utils';
+
+import { TradeableNetworkListItem } from '../TradeableNetworkListItem';
+
+describe('TradeableNetworkListItem', () => {
+ it('should render with correct labels', () => {
+ const { getByText, getAllByText } = render(
+ ,
+ );
+
+ // title and badge
+ expect(getAllByText('Bitcoin').length).toBe(2);
+ expect(getByText('BTC')).toBeDefined();
+ });
+
+ it('should call onPress callback when clicked', () => {
+ const onPress = jest.fn();
+ const { getByText } = render();
+
+ fireEvent.press(getByText('BTC'));
+
+ expect(onPress).toHaveBeenCalledWith();
+ });
+});
diff --git a/suite-native/module-trading/src/hooks/__tests__/useAssetsSheetControls.test.ts b/suite-native/module-trading/src/hooks/__tests__/useAssetsSheetControls.test.ts
new file mode 100644
index 000000000000..b3959e53ea5b
--- /dev/null
+++ b/suite-native/module-trading/src/hooks/__tests__/useAssetsSheetControls.test.ts
@@ -0,0 +1,52 @@
+import { renderHook, act } from '@suite-native/test-utils';
+
+import { useTradeableAssetsSheetControls } from '../useTradeableAssetsSheetControls';
+
+describe('useTradeableAssetsSheetControls', () => {
+ describe('isTokensSheetVisible', () => {
+ it('should be false by default', () => {
+ const { result } = renderHook(() => useTradeableAssetsSheetControls());
+
+ expect(result.current.isTradeableAssetsSheetVisible).toBe(false);
+ });
+
+ it('should be true after showTokensSheet call', () => {
+ const { result } = renderHook(() => useTradeableAssetsSheetControls());
+
+ act(() => {
+ result.current.showTradeableAssetsSheet();
+ });
+
+ expect(result.current.isTradeableAssetsSheetVisible).toBe(true);
+ });
+
+ it('should be false after hideTokensSheet call', () => {
+ const { result } = renderHook(() => useTradeableAssetsSheetControls());
+
+ act(() => {
+ result.current.showTradeableAssetsSheet();
+ result.current.hideTradeableAssetsSheet();
+ });
+
+ expect(result.current.isTradeableAssetsSheetVisible).toBe(false);
+ });
+ });
+
+ describe('selectedAsset', () => {
+ it('should be undefined by default', () => {
+ const { result } = renderHook(() => useTradeableAssetsSheetControls());
+
+ expect(result.current.selectedTradeableAsset).toBeUndefined();
+ });
+
+ it('should be set after setSelectedNetwork call', () => {
+ const { result } = renderHook(() => useTradeableAssetsSheetControls());
+
+ act(() => {
+ result.current.setSelectedTradeableAsset('btc');
+ });
+
+ expect(result.current.selectedTradeableAsset).toBe('btc');
+ });
+ });
+});
diff --git a/suite-native/module-trading/src/hooks/useTradeableAssetsSheetControls.ts b/suite-native/module-trading/src/hooks/useTradeableAssetsSheetControls.ts
new file mode 100644
index 000000000000..be659d5b326b
--- /dev/null
+++ b/suite-native/module-trading/src/hooks/useTradeableAssetsSheetControls.ts
@@ -0,0 +1,26 @@
+import { useState } from 'react';
+
+import { NetworkSymbol } from '@suite-common/wallet-config';
+
+export const useTradeableAssetsSheetControls = () => {
+ const [isTradeableAssetsSheetVisible, setIsTradeableAssetsSheetVisible] = useState(false);
+ const [selectedTradeableAsset, setSelectedTradeableAsset] = useState<
+ undefined | NetworkSymbol
+ >();
+
+ const showTradeableAssetsSheet = () => {
+ setIsTradeableAssetsSheetVisible(true);
+ };
+
+ const hideTradeableAssetsSheet = () => {
+ setIsTradeableAssetsSheetVisible(false);
+ };
+
+ return {
+ isTradeableAssetsSheetVisible,
+ showTradeableAssetsSheet,
+ hideTradeableAssetsSheet,
+ selectedTradeableAsset,
+ setSelectedTradeableAsset,
+ };
+};
diff --git a/suite-native/module-trading/src/screens/TradingScreen.tsx b/suite-native/module-trading/src/screens/TradingScreen.tsx
index 7c6c8996fecb..de0b73a79144 100644
--- a/suite-native/module-trading/src/screens/TradingScreen.tsx
+++ b/suite-native/module-trading/src/screens/TradingScreen.tsx
@@ -1,13 +1,14 @@
import React from 'react';
import { Screen } from '@suite-native/navigation';
-import { Card, Text } from '@suite-native/atoms';
+import { Text } from '@suite-native/atoms';
import { DeviceManagerScreenHeader } from '@suite-native/device-manager';
+import { AmountCard } from '../components/buy/AmountCard';
+
export const TradingScreen = () => (
}>
-
- Trading placeholder
-
+ Trading placeholder
+
);
diff --git a/suite-native/test-utils/src/BasicProvider.tsx b/suite-native/test-utils/src/BasicProvider.tsx
index 981f6dd96430..c664608fa5ca 100644
--- a/suite-native/test-utils/src/BasicProvider.tsx
+++ b/suite-native/test-utils/src/BasicProvider.tsx
@@ -1,8 +1,9 @@
-import { ReactNode } from 'react';
+import { ReactNode, useMemo } from 'react';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { NavigationContainer } from '@react-navigation/native';
+import { FormatterProvider } from '@suite-common/formatters';
import { createRenderer, StylesProvider } from '@trezor/styles';
import { prepareNativeTheme } from '@trezor/theme';
import { IntlProvider } from '@suite-native/intl';
@@ -14,12 +15,26 @@ type ProviderProps = {
const renderer = createRenderer();
const theme = prepareNativeTheme({ colorVariant: 'standard' });
-export const BasicProvider = ({ children }: ProviderProps) => (
-
-
-
- {children}
-
-
-
-);
+export const BasicProvider = ({ children }: ProviderProps) => {
+ const formattersConfig = useMemo(
+ () => ({
+ locale: 'en' as const,
+ fiatCurrency: 'usd' as const,
+ bitcoinAmountUnit: 0,
+ is24HourFormat: true,
+ }),
+ [],
+ );
+
+ return (
+
+
+
+
+ {children}
+
+
+
+
+ );
+};
diff --git a/yarn.lock b/yarn.lock
index f25cccbb642e..f5b89a71c695 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -10941,8 +10941,10 @@ __metadata:
"@reduxjs/toolkit": "npm:1.9.5"
"@suite-native/navigation": "workspace:*"
"@suite-native/test-utils": "workspace:*"
+ expo-linear-gradient: "npm:^14.0.1"
react: "npm:18.2.0"
react-native: "npm:0.76.1"
+ react-native-reanimated: "npm:^3.16.7"
languageName: unknown
linkType: soft