Skip to content

Commit ea9e8ae

Browse files
committed
feat(suite-native): Mobile Trade - Country picker visual stub
1 parent f4f0d5b commit ea9e8ae

18 files changed

+423
-180
lines changed

Diff for: suite-native/intl/src/en.ts

+7
Original file line numberDiff line numberDiff line change
@@ -1296,7 +1296,14 @@ export const en = {
12961296
emptyDescription:
12971297
'We couldn’t find a coin matching your search. Try checking the spelling or exploring the list for the right option.',
12981298
},
1299+
countrySheet: {
1300+
title: 'Country of residence',
1301+
emptyTitle: 'No country found',
1302+
emptyDescription:
1303+
'We couldn’t find a country matching your search. Try checking the spelling or exploring the list for the right option.',
1304+
},
12991305
defaultSearchLabel: 'Search',
1306+
notSelected: 'Not selected',
13001307
},
13011308
};
13021309

Diff for: suite-native/module-trading/src/components/buy/BuyCard.tsx

+7-26
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
1-
import React from 'react';
2-
31
import { useFormatters } from '@suite-common/formatters';
42
import { Card, HStack, Text, VStack } from '@suite-native/atoms';
53
import { Icon } from '@suite-native/icons';
64
import { Translation, useTranslate } from '@suite-native/intl';
75
import { prepareNativeStyle, useNativeStyles } from '@trezor/styles';
86

9-
import { useTradeableAssetsSheetControls } from '../../hooks/useTradeableAssetsSheetControls';
10-
import { SelectTradeableAssetButton } from '../general/SelectTradeableAssetButton';
11-
import { TradeableAssetsSheet } from '../general/TradeableAssetsSheet/TradeableAssetsSheet';
7+
import { TradeableAssetPicker } from './TradeableAssetPicker';
8+
import { useTradeSheetControls } from '../../hooks/useTradeSheetControls';
9+
import { TradeableAsset } from '../../types';
1210
import { TradingOverviewRow } from '../general/TradingOverviewRow';
1311

1412
const notImplementedCallback = () => {
@@ -27,13 +25,7 @@ export const BuyCard = () => {
2725
const { FiatAmountFormatter, CryptoAmountFormatter } = useFormatters();
2826
const { applyStyle } = useNativeStyles();
2927

30-
const {
31-
isTradeableAssetsSheetVisible,
32-
showTradeableAssetsSheet,
33-
hideTradeableAssetsSheet,
34-
selectedTradeableAsset,
35-
setSelectedTradeableAsset,
36-
} = useTradeableAssetsSheetControls();
28+
const { selectedValue, ...restControls } = useTradeSheetControls<TradeableAsset>();
3729

3830
return (
3931
<Card noPadding>
@@ -42,21 +34,15 @@ export const BuyCard = () => {
4234
<Translation id="moduleTrading.tradingScreen.buyTitle" />
4335
</Text>
4436
<HStack justifyContent="space-between" alignItems="center">
45-
<SelectTradeableAssetButton
46-
onPress={showTradeableAssetsSheet}
47-
selectedAsset={selectedTradeableAsset}
48-
/>
37+
<TradeableAssetPicker selectedValue={selectedValue} {...restControls} />
4938
<Text variant="titleMedium" color="textDisabled">
5039
0.0
5140
</Text>
5241
</HStack>
5342
<HStack justifyContent="space-between" alignItems="center">
5443
<Text variant="body" color="textSubdued">
55-
{selectedTradeableAsset?.symbol ? (
56-
<CryptoAmountFormatter
57-
value="0"
58-
symbol={selectedTradeableAsset.symbol}
59-
/>
44+
{selectedValue?.symbol ? (
45+
<CryptoAmountFormatter value="0" symbol={selectedValue.symbol} />
6046
) : (
6147
'-'
6248
)}
@@ -83,11 +69,6 @@ export const BuyCard = () => {
8369
</Text>
8470
</VStack>
8571
</TradingOverviewRow>
86-
<TradeableAssetsSheet
87-
isVisible={isTradeableAssetsSheetVisible}
88-
onClose={hideTradeableAssetsSheet}
89-
onAssetSelect={setSelectedTradeableAsset}
90-
/>
9172
</Card>
9273
);
9374
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { HStack, Text } from '@suite-native/atoms';
2+
import { Icon } from '@suite-native/icons';
3+
import { useTranslate } from '@suite-native/intl';
4+
5+
import { useTradeSheetControls } from '../../hooks/useTradeSheetControls';
6+
import { Country } from '../../types';
7+
import { CountrySheet } from '../general/CountrySheet/CountrySheet';
8+
import { TradingOverviewRow } from '../general/TradingOverviewRow';
9+
10+
export const CountryOfResidencePicker = () => {
11+
const { translate } = useTranslate();
12+
13+
const { isSheetVisible, hideSheet, showSheet, setSelectedValue, selectedValue } =
14+
useTradeSheetControls<Country>();
15+
16+
return (
17+
<>
18+
<TradingOverviewRow
19+
title={translate('moduleTrading.tradingScreen.countryOfResidence')}
20+
onPress={showSheet}
21+
>
22+
{selectedValue ? (
23+
<HStack>
24+
<Icon name={selectedValue.flag} size="medium" />
25+
<Text color="textSubdued" variant="body">
26+
{selectedValue.name}
27+
</Text>
28+
</HStack>
29+
) : (
30+
<Text color="textDisabled" variant="body">
31+
{translate('moduleTrading.notSelected')}
32+
</Text>
33+
)}
34+
</TradingOverviewRow>
35+
<CountrySheet
36+
isVisible={isSheetVisible}
37+
onClose={hideSheet}
38+
onCountrySelect={setSelectedValue}
39+
selectedCountryId={selectedValue?.id}
40+
/>
41+
</>
42+
);
43+
};
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { Box, Button, Card, Text } from '@suite-native/atoms';
2-
import { Translation, useTranslate } from '@suite-native/intl';
1+
import { Card, Text } from '@suite-native/atoms';
2+
import { useTranslate } from '@suite-native/intl';
33

4+
import { CountryOfResidencePicker } from './CountryOfResidencePicker';
45
import { TradingOverviewRow } from '../general/TradingOverviewRow';
56

67
const notImplementedCallback = () => {
@@ -21,27 +22,16 @@ export const PaymentCard = () => {
2122
Credit card
2223
</Text>
2324
</TradingOverviewRow>
24-
<TradingOverviewRow
25-
title={translate('moduleTrading.tradingScreen.countryOfResidence')}
26-
onPress={notImplementedCallback}
27-
>
28-
<Text color="textSubdued" variant="body">
29-
Czech Republic
30-
</Text>
31-
</TradingOverviewRow>
25+
<CountryOfResidencePicker />
3226
<TradingOverviewRow
3327
title={translate('moduleTrading.tradingScreen.provider')}
3428
onPress={notImplementedCallback}
29+
noBottomBorder
3530
>
3631
<Text color="textSubdued" variant="body">
3732
Anycoin
3833
</Text>
3934
</TradingOverviewRow>
40-
<Box padding="sp20">
41-
<Button onPress={notImplementedCallback}>
42-
<Translation id="moduleTrading.tradingScreen.continueButton" />
43-
</Button>
44-
</Box>
4535
</Card>
4636
);
4737
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { useTradeSheetControls } from '../../hooks/useTradeSheetControls';
2+
import { TradeableAsset } from '../../types';
3+
import { SelectTradeableAssetButton } from '../general/SelectTradeableAssetButton';
4+
import { TradeableAssetsSheet } from '../general/TradeableAssetsSheet/TradeableAssetsSheet';
5+
6+
type TradeableAssetPickerProps = ReturnType<typeof useTradeSheetControls<TradeableAsset>>;
7+
8+
export const TradeableAssetPicker = ({
9+
isSheetVisible,
10+
showSheet,
11+
hideSheet,
12+
selectedValue,
13+
setSelectedValue,
14+
}: TradeableAssetPickerProps) => (
15+
<>
16+
<SelectTradeableAssetButton onPress={showSheet} selectedAsset={selectedValue} />
17+
<TradeableAssetsSheet
18+
isVisible={isSheetVisible}
19+
onClose={hideSheet}
20+
onAssetSelect={setSelectedValue}
21+
/>
22+
</>
23+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Translation } from '@suite-native/intl';
2+
3+
import { TradingEmptyComponent } from '../TradingEmptyComponent';
4+
5+
export const CountryListEmptyComponent = () => (
6+
<TradingEmptyComponent
7+
title={<Translation id="moduleTrading.countrySheet.emptyTitle" />}
8+
description={<Translation id="moduleTrading.countrySheet.emptyDescription" />}
9+
/>
10+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { ReactNode } from 'react';
2+
import { Pressable } from 'react-native';
3+
4+
import { Card, HStack, Radio, Text } from '@suite-native/atoms';
5+
import { Icon, IconName } from '@suite-native/icons';
6+
import { prepareNativeStyle, useNativeStyles } from '@trezor/styles';
7+
8+
export type CountryListItemProps = {
9+
flag: IconName;
10+
id: string;
11+
name: ReactNode;
12+
isSelected: boolean;
13+
onPress: () => void;
14+
};
15+
16+
export const COUNTRY_LIST_ITEM_HEIGHT = 64 as const;
17+
18+
const wrapperStyle = prepareNativeStyle(({ spacings }) => ({
19+
marginVertical: spacings.sp4,
20+
}));
21+
22+
export const CountryListItem = ({ flag, name, onPress, id, isSelected }: CountryListItemProps) => {
23+
const { applyStyle } = useNativeStyles();
24+
25+
return (
26+
<Pressable onPress={onPress} style={applyStyle(wrapperStyle)}>
27+
<Card>
28+
<HStack alignItems="center" justifyContent="space-between">
29+
<HStack>
30+
<Icon name={flag} size="medium" />
31+
<Text variant="body" color="textDefault">
32+
{name}
33+
</Text>
34+
</HStack>
35+
<Radio value={id} onPress={onPress} isChecked={isSelected} />
36+
</HStack>
37+
</Card>
38+
</Pressable>
39+
);
40+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { BottomSheetFlashList } from '@suite-native/atoms';
2+
import { Translation } from '@suite-native/intl';
3+
4+
import { SearchableSheetHeader } from '../SearchableSheetHeader';
5+
import { CountryListEmptyComponent } from './CountryListEmptyComponent';
6+
import { COUNTRY_LIST_ITEM_HEIGHT, CountryListItem } from './CountryListItem';
7+
import { Country } from '../../../types';
8+
9+
export type CountrySheetProps = {
10+
isVisible: boolean;
11+
onClose: () => void;
12+
onCountrySelect: (symbol: Country) => void;
13+
selectedCountryId?: string;
14+
};
15+
16+
const mockCountries: Country[] = [
17+
{ id: 'us', name: 'United States', flag: 'flag' },
18+
{ id: 'cz', name: 'Czech Republic', flag: 'flagCheckered' },
19+
{ id: 'sk', name: 'Slovakia', flag: 'flag' },
20+
{ id: 'de', name: 'Germany', flag: 'flagCheckered' },
21+
{ id: 'fr', name: 'France', flag: 'flag' },
22+
{ id: 'es', name: 'Spain', flag: 'flagCheckered' },
23+
{ id: 'it', name: 'Italy', flag: 'flag' },
24+
{ id: 'pl', name: 'Poland', flag: 'flagCheckered' },
25+
{ id: 'hu', name: 'Hungary', flag: 'flag' },
26+
{ id: 'at', name: 'Austria', flag: 'flagCheckered' },
27+
{ id: 'ch', name: 'Switzerland', flag: 'flag' },
28+
];
29+
30+
const keyExtractor = (item: Country) => item.id;
31+
const getEstimatedListHeight = (itemsCount: number) => itemsCount * COUNTRY_LIST_ITEM_HEIGHT;
32+
33+
export const CountrySheet = ({
34+
isVisible,
35+
onClose,
36+
onCountrySelect,
37+
selectedCountryId,
38+
}: CountrySheetProps) => {
39+
const onCountrySelectCallback = (country: Country) => {
40+
onCountrySelect(country);
41+
onClose();
42+
};
43+
44+
const data: Country[] = mockCountries;
45+
46+
return (
47+
<BottomSheetFlashList<Country>
48+
isVisible={isVisible}
49+
onClose={onClose}
50+
ListEmptyComponent={<CountryListEmptyComponent />}
51+
handleComponent={() => (
52+
<SearchableSheetHeader
53+
onClose={onClose}
54+
title={<Translation id="moduleTrading.countrySheet.title" />}
55+
/>
56+
)}
57+
renderItem={({ item }) => (
58+
<CountryListItem
59+
{...item}
60+
onPress={() => onCountrySelectCallback(item)}
61+
isSelected={item.id === selectedCountryId}
62+
/>
63+
)}
64+
data={data}
65+
estimatedListHeight={getEstimatedListHeight(data.length)}
66+
estimatedItemSize={COUNTRY_LIST_ITEM_HEIGHT}
67+
keyExtractor={keyExtractor}
68+
/>
69+
);
70+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { ReactNode, useCallback, useState } from 'react';
2+
import Animated, { FadeIn, FadeOut, LinearTransition } from 'react-native-reanimated';
3+
4+
import { BottomSheetGrabber, VStack } from '@suite-native/atoms';
5+
import { useTranslate } from '@suite-native/intl';
6+
import { NativeStyleObject, prepareNativeStyle, useNativeStyles } from '@trezor/styles';
7+
8+
import { SearchInputWithCancel } from './SearchInputWithCancel';
9+
import { SheetHeaderTitle } from './SheetHeaderTitle';
10+
11+
export type SearchableSheetHeaderProps = {
12+
onClose: () => void;
13+
title: ReactNode;
14+
onFilterFocusChange?: (isFilterActive: boolean) => void;
15+
children?: ReactNode;
16+
style?: NativeStyleObject;
17+
};
18+
19+
export const FOCUS_ANIMATION_DURATION = 300 as const;
20+
21+
const noOp = () => {};
22+
23+
const wrapperStyle = prepareNativeStyle<{}>(({ spacings }) => ({
24+
padding: spacings.sp16,
25+
gap: spacings.sp16,
26+
}));
27+
28+
export const SearchableSheetHeader = ({
29+
onClose,
30+
title,
31+
children,
32+
onFilterFocusChange = noOp,
33+
style,
34+
}: SearchableSheetHeaderProps) => {
35+
const { applyStyle } = useNativeStyles();
36+
const { translate } = useTranslate();
37+
38+
const [isFilterActive, setIsFilterActive] = useState(false);
39+
const [filterValue, setFilterValue] = useState('');
40+
41+
const changeFilterFocus = useCallback(
42+
(newValue: boolean) => {
43+
setIsFilterActive(newValue);
44+
onFilterFocusChange(newValue);
45+
},
46+
[onFilterFocusChange],
47+
);
48+
49+
return (
50+
<VStack style={[applyStyle(wrapperStyle), style]}>
51+
<BottomSheetGrabber />
52+
<Animated.View layout={LinearTransition.duration(FOCUS_ANIMATION_DURATION)}>
53+
{!isFilterActive && (
54+
<Animated.View
55+
entering={FadeIn.duration(FOCUS_ANIMATION_DURATION)}
56+
exiting={FadeOut.duration(FOCUS_ANIMATION_DURATION)}
57+
>
58+
<SheetHeaderTitle
59+
leftButtonIcon="x"
60+
onLeftButtonPress={onClose}
61+
leftButtonA11yLabel={translate('generic.buttons.close')}
62+
>
63+
{title}
64+
</SheetHeaderTitle>
65+
</Animated.View>
66+
)}
67+
</Animated.View>
68+
<Animated.View layout={LinearTransition.duration(FOCUS_ANIMATION_DURATION)}>
69+
<SearchInputWithCancel
70+
onChange={setFilterValue}
71+
onFocus={() => changeFilterFocus(true)}
72+
onBlur={() => changeFilterFocus(false)}
73+
value={filterValue}
74+
/>
75+
</Animated.View>
76+
{children}
77+
</VStack>
78+
);
79+
};

0 commit comments

Comments
 (0)