Skip to content

Commit

Permalink
feat(suite-native): select coin modal UI
Browse files Browse the repository at this point in the history
  • Loading branch information
jbazant committed Jan 28, 2025
1 parent 4ee0ed9 commit 4bc8227
Show file tree
Hide file tree
Showing 20 changed files with 574 additions and 14 deletions.
10 changes: 10 additions & 0 deletions suite-native/intl/src/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
1 change: 1 addition & 0 deletions suite-native/module-trading/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
33 changes: 33 additions & 0 deletions suite-native/module-trading/src/components/buy/AmountCard.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Card>
<HStack>
<SelectTradeableAssetButton
onPress={showTradeableAssetsSheet}
selectedAsset={selectedTradeableAsset}
/>
</HStack>
<TradeableAssetsSheet
isVisible={isTradeableAssetsSheetVisible}
onClose={hideTradeableAssetsSheet}
onAssetSelect={setSelectedTradeableAsset}
/>
</Card>
);
};
Original file line number Diff line number Diff line change
@@ -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(<AmountCard />);

expect(getByText('Select coin')).toBeDefined();
expect(queryByText('Tokens')).toBeNull();
});

it('should display AssetsSheet after button click', () => {
const { getByText } = render(<AmountCard />);

fireEvent.press(getByText('Select coin'));

expect(getByText('Tokens')).toBeDefined();
});

it('should display selected network from AssetsSheet', () => {
const { getByText, queryByText } = render(<AmountCard />);

fireEvent.press(getByText('Select coin'));
fireEvent.press(getByText('BTC'));

expect(queryByText('Tokens')).toBeNull();
expect(getByText('BTC')).toBeDefined();
});
});
Original file line number Diff line number Diff line change
@@ -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 (
<VStack>
<Text>
<Translation id="moduleTrading.networksSheet.popularTitle" />
</Text>
<HStack justifyContent="space-between">
{limitedSymbols.map(symbol => (
<TradeableNetworkButton
key={symbol}
symbol={symbol}
onPress={() => onNetworkSelect(symbol)}
/>
))}
</HStack>
</VStack>
);
};
Original file line number Diff line number Diff line change
@@ -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 <TradeableNetworkButton symbol={selectedAsset} onPress={onPress} caret />;
}

return (
<Button
onPress={onPress}
viewRight={<Icon name="caretDown" color={iconColor} size="medium" />}
size="small"
>
<Translation id="moduleTrading.selectCoin.buttonTitle" />
</Button>
);
};
Original file line number Diff line number Diff line change
@@ -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 (
<LinearGradient
colors={[hexToRgba(bgBaseColor, 0.3), hexToRgba(bgBaseColor, 0.01)]}
style={applyStyle(gradientBackgroundStyle)}
start={{ x: 0, y: 0.5 }}
end={{ x: 1, y: 0.5 }}
>
<Pressable onPress={onPress} style={styles.button}>
{icon}
<Text color="textSubdued" variant="callout">
{children}
</Text>
{caret && <Icon name="caretDown" color="textSubdued" size="medium" />}
</Pressable>
</LinearGradient>
);
};
Original file line number Diff line number Diff line change
@@ -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 (
<Pressable onPress={onPress}>
<HStack alignItems="center" paddingLeft="sp16" spacing="sp12">
{icon}
<VStack style={applyStyle(vStackStyle)} spacing={0}>
<Text variant="body" color="textDefault">
{title}
</Text>
<HStack alignItems="center">
<Text variant="hint" color="textSubdued">
{subtitle}
</Text>
{!!badge && <Badge label={badge} />}
</HStack>
</VStack>
</HStack>
</Pressable>
);
};
Original file line number Diff line number Diff line change
@@ -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 (
<BottomSheet isVisible={isVisible} onClose={onClose} isCloseDisplayed={false}>
<VStack spacing="sp16">
<PickerHeader title={<Translation id="moduleTrading.networksSheet.title" />} />
<PopularTradeableNetworks
symbols={['btc', 'eth', 'sol', 'base']}
onNetworkSelect={onAssetSelectCallback}
/>
<PickerCloseButton onPress={onClose} />
</VStack>
</BottomSheet>
);
};
Original file line number Diff line number Diff line change
@@ -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 (
<TradeableAssetButton
bgBaseColor={baseSymbolColor}
caret={caret}
onPress={onPress}
icon={<CryptoIcon symbol={symbol} size="small" />}
>
<DisplaySymbolFormatter value={symbol} areAmountUnitsEnabled={false} />
</TradeableAssetButton>
);
};
Original file line number Diff line number Diff line change
@@ -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 (
<TradeableAssetListItem
title={<NetworkNameFormatter value={symbol} />}
subtitle={<DisplaySymbolFormatter value={symbol} areAmountUnitsEnabled={false} />}
badge={<NetworkNameFormatter value={symbol} />}
icon={<CryptoIcon symbol={symbol} size="small" />}
onPress={onPress}
/>
);
};
Loading

0 comments on commit 4bc8227

Please sign in to comment.