Skip to content

Commit

Permalink
feat(suite-native): Mobile Trade - Country picker visual stub
Browse files Browse the repository at this point in the history
  • Loading branch information
jbazant authored and vytick committed Feb 7, 2025
1 parent 7c91d84 commit f0ae958
Show file tree
Hide file tree
Showing 18 changed files with 423 additions and 180 deletions.
7 changes: 7 additions & 0 deletions suite-native/intl/src/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1296,7 +1296,14 @@ export const en = {
emptyDescription:
'We couldn’t find a coin matching your search. Try checking the spelling or exploring the list for the right option.',
},
countrySheet: {
title: 'Country of residence',
emptyTitle: 'No country found',
emptyDescription:
'We couldn’t find a country matching your search. Try checking the spelling or exploring the list for the right option.',
},
defaultSearchLabel: 'Search',
notSelected: 'Not selected',
},
};

Expand Down
33 changes: 7 additions & 26 deletions suite-native/module-trading/src/components/buy/BuyCard.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import React from 'react';

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 { prepareNativeStyle, useNativeStyles } from '@trezor/styles';

import { useTradeableAssetsSheetControls } from '../../hooks/useTradeableAssetsSheetControls';
import { SelectTradeableAssetButton } from '../general/SelectTradeableAssetButton';
import { TradeableAssetsSheet } from '../general/TradeableAssetsSheet/TradeableAssetsSheet';
import { TradeableAssetPicker } from './TradeableAssetPicker';
import { useTradeSheetControls } from '../../hooks/useTradeSheetControls';
import { TradeableAsset } from '../../types';
import { TradingOverviewRow } from '../general/TradingOverviewRow';

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

const {
isTradeableAssetsSheetVisible,
showTradeableAssetsSheet,
hideTradeableAssetsSheet,
selectedTradeableAsset,
setSelectedTradeableAsset,
} = useTradeableAssetsSheetControls();
const { selectedValue, ...restControls } = useTradeSheetControls<TradeableAsset>();

return (
<Card noPadding>
Expand All @@ -42,21 +34,15 @@ export const BuyCard = () => {
<Translation id="moduleTrading.tradingScreen.buyTitle" />
</Text>
<HStack justifyContent="space-between" alignItems="center">
<SelectTradeableAssetButton
onPress={showTradeableAssetsSheet}
selectedAsset={selectedTradeableAsset}
/>
<TradeableAssetPicker selectedValue={selectedValue} {...restControls} />
<Text variant="titleMedium" color="textDisabled">
0.0
</Text>
</HStack>
<HStack justifyContent="space-between" alignItems="center">
<Text variant="body" color="textSubdued">
{selectedTradeableAsset?.symbol ? (
<CryptoAmountFormatter
value="0"
symbol={selectedTradeableAsset.symbol}
/>
{selectedValue?.symbol ? (
<CryptoAmountFormatter value="0" symbol={selectedValue.symbol} />
) : (
'-'
)}
Expand All @@ -83,11 +69,6 @@ export const BuyCard = () => {
</Text>
</VStack>
</TradingOverviewRow>
<TradeableAssetsSheet
isVisible={isTradeableAssetsSheetVisible}
onClose={hideTradeableAssetsSheet}
onAssetSelect={setSelectedTradeableAsset}
/>
</Card>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { HStack, Text } from '@suite-native/atoms';
import { Icon } from '@suite-native/icons';
import { useTranslate } from '@suite-native/intl';

import { useTradeSheetControls } from '../../hooks/useTradeSheetControls';
import { Country } from '../../types';
import { CountrySheet } from '../general/CountrySheet/CountrySheet';
import { TradingOverviewRow } from '../general/TradingOverviewRow';

export const CountryOfResidencePicker = () => {
const { translate } = useTranslate();

const { isSheetVisible, hideSheet, showSheet, setSelectedValue, selectedValue } =
useTradeSheetControls<Country>();

return (
<>
<TradingOverviewRow
title={translate('moduleTrading.tradingScreen.countryOfResidence')}
onPress={showSheet}
>
{selectedValue ? (
<HStack>
<Icon name={selectedValue.flag} size="medium" />
<Text color="textSubdued" variant="body">
{selectedValue.name}
</Text>
</HStack>
) : (
<Text color="textDisabled" variant="body">
{translate('moduleTrading.notSelected')}
</Text>
)}
</TradingOverviewRow>
<CountrySheet
isVisible={isSheetVisible}
onClose={hideSheet}
onCountrySelect={setSelectedValue}
selectedCountryId={selectedValue?.id}
/>
</>
);
};
20 changes: 5 additions & 15 deletions suite-native/module-trading/src/components/buy/PaymentCard.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Box, Button, Card, Text } from '@suite-native/atoms';
import { Translation, useTranslate } from '@suite-native/intl';
import { Card, Text } from '@suite-native/atoms';
import { useTranslate } from '@suite-native/intl';

import { CountryOfResidencePicker } from './CountryOfResidencePicker';
import { TradingOverviewRow } from '../general/TradingOverviewRow';

const notImplementedCallback = () => {
Expand All @@ -21,27 +22,16 @@ export const PaymentCard = () => {
Credit card
</Text>
</TradingOverviewRow>
<TradingOverviewRow
title={translate('moduleTrading.tradingScreen.countryOfResidence')}
onPress={notImplementedCallback}
>
<Text color="textSubdued" variant="body">
Czech Republic
</Text>
</TradingOverviewRow>
<CountryOfResidencePicker />
<TradingOverviewRow
title={translate('moduleTrading.tradingScreen.provider')}
onPress={notImplementedCallback}
noBottomBorder
>
<Text color="textSubdued" variant="body">
Anycoin
</Text>
</TradingOverviewRow>
<Box padding="sp20">
<Button onPress={notImplementedCallback}>
<Translation id="moduleTrading.tradingScreen.continueButton" />
</Button>
</Box>
</Card>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useTradeSheetControls } from '../../hooks/useTradeSheetControls';
import { TradeableAsset } from '../../types';
import { SelectTradeableAssetButton } from '../general/SelectTradeableAssetButton';
import { TradeableAssetsSheet } from '../general/TradeableAssetsSheet/TradeableAssetsSheet';

type TradeableAssetPickerProps = ReturnType<typeof useTradeSheetControls<TradeableAsset>>;

export const TradeableAssetPicker = ({
isSheetVisible,
showSheet,
hideSheet,
selectedValue,
setSelectedValue,
}: TradeableAssetPickerProps) => (
<>
<SelectTradeableAssetButton onPress={showSheet} selectedAsset={selectedValue} />
<TradeableAssetsSheet
isVisible={isSheetVisible}
onClose={hideSheet}
onAssetSelect={setSelectedValue}
/>
</>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Translation } from '@suite-native/intl';

import { TradingEmptyComponent } from '../TradingEmptyComponent';

export const CountryListEmptyComponent = () => (
<TradingEmptyComponent
title={<Translation id="moduleTrading.countrySheet.emptyTitle" />}
description={<Translation id="moduleTrading.countrySheet.emptyDescription" />}
/>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { ReactNode } from 'react';
import { Pressable } from 'react-native';

import { Card, HStack, Radio, Text } from '@suite-native/atoms';
import { Icon, IconName } from '@suite-native/icons';
import { prepareNativeStyle, useNativeStyles } from '@trezor/styles';

export type CountryListItemProps = {
flag: IconName;
id: string;
name: ReactNode;
isSelected: boolean;
onPress: () => void;
};

export const COUNTRY_LIST_ITEM_HEIGHT = 64 as const;

const wrapperStyle = prepareNativeStyle(({ spacings }) => ({
marginVertical: spacings.sp4,
}));

export const CountryListItem = ({ flag, name, onPress, id, isSelected }: CountryListItemProps) => {
const { applyStyle } = useNativeStyles();

return (
<Pressable onPress={onPress} style={applyStyle(wrapperStyle)}>
<Card>
<HStack alignItems="center" justifyContent="space-between">
<HStack>
<Icon name={flag} size="medium" />
<Text variant="body" color="textDefault">
{name}
</Text>
</HStack>
<Radio value={id} onPress={onPress} isChecked={isSelected} />
</HStack>
</Card>
</Pressable>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { BottomSheetFlashList } from '@suite-native/atoms';
import { Translation } from '@suite-native/intl';

import { SearchableSheetHeader } from '../SearchableSheetHeader';
import { CountryListEmptyComponent } from './CountryListEmptyComponent';
import { COUNTRY_LIST_ITEM_HEIGHT, CountryListItem } from './CountryListItem';
import { Country } from '../../../types';

export type CountrySheetProps = {
isVisible: boolean;
onClose: () => void;
onCountrySelect: (symbol: Country) => void;
selectedCountryId?: string;
};

const mockCountries: Country[] = [
{ id: 'us', name: 'United States', flag: 'flag' },
{ id: 'cz', name: 'Czech Republic', flag: 'flagCheckered' },
{ id: 'sk', name: 'Slovakia', flag: 'flag' },
{ id: 'de', name: 'Germany', flag: 'flagCheckered' },
{ id: 'fr', name: 'France', flag: 'flag' },
{ id: 'es', name: 'Spain', flag: 'flagCheckered' },
{ id: 'it', name: 'Italy', flag: 'flag' },
{ id: 'pl', name: 'Poland', flag: 'flagCheckered' },
{ id: 'hu', name: 'Hungary', flag: 'flag' },
{ id: 'at', name: 'Austria', flag: 'flagCheckered' },
{ id: 'ch', name: 'Switzerland', flag: 'flag' },
];

const keyExtractor = (item: Country) => item.id;
const getEstimatedListHeight = (itemsCount: number) => itemsCount * COUNTRY_LIST_ITEM_HEIGHT;

export const CountrySheet = ({
isVisible,
onClose,
onCountrySelect,
selectedCountryId,
}: CountrySheetProps) => {
const onCountrySelectCallback = (country: Country) => {
onCountrySelect(country);
onClose();
};

const data: Country[] = mockCountries;

return (
<BottomSheetFlashList<Country>
isVisible={isVisible}
onClose={onClose}
ListEmptyComponent={<CountryListEmptyComponent />}
handleComponent={() => (
<SearchableSheetHeader
onClose={onClose}
title={<Translation id="moduleTrading.countrySheet.title" />}
/>
)}
renderItem={({ item }) => (
<CountryListItem
{...item}
onPress={() => onCountrySelectCallback(item)}
isSelected={item.id === selectedCountryId}
/>
)}
data={data}
estimatedListHeight={getEstimatedListHeight(data.length)}
estimatedItemSize={COUNTRY_LIST_ITEM_HEIGHT}
keyExtractor={keyExtractor}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { ReactNode, useCallback, useState } from 'react';
import Animated, { FadeIn, FadeOut, LinearTransition } from 'react-native-reanimated';

import { BottomSheetGrabber, VStack } from '@suite-native/atoms';
import { useTranslate } from '@suite-native/intl';
import { NativeStyleObject, prepareNativeStyle, useNativeStyles } from '@trezor/styles';

import { SearchInputWithCancel } from './SearchInputWithCancel';
import { SheetHeaderTitle } from './SheetHeaderTitle';

export type SearchableSheetHeaderProps = {
onClose: () => void;
title: ReactNode;
onFilterFocusChange?: (isFilterActive: boolean) => void;
children?: ReactNode;
style?: NativeStyleObject;
};

export const FOCUS_ANIMATION_DURATION = 300 as const;

const noOp = () => {};

const wrapperStyle = prepareNativeStyle<{}>(({ spacings }) => ({
padding: spacings.sp16,
gap: spacings.sp16,
}));

export const SearchableSheetHeader = ({
onClose,
title,
children,
onFilterFocusChange = noOp,
style,
}: SearchableSheetHeaderProps) => {
const { applyStyle } = useNativeStyles();
const { translate } = useTranslate();

const [isFilterActive, setIsFilterActive] = useState(false);
const [filterValue, setFilterValue] = useState('');

const changeFilterFocus = useCallback(
(newValue: boolean) => {
setIsFilterActive(newValue);
onFilterFocusChange(newValue);
},
[onFilterFocusChange],
);

return (
<VStack style={[applyStyle(wrapperStyle), style]}>
<BottomSheetGrabber />
<Animated.View layout={LinearTransition.duration(FOCUS_ANIMATION_DURATION)}>
{!isFilterActive && (
<Animated.View
entering={FadeIn.duration(FOCUS_ANIMATION_DURATION)}
exiting={FadeOut.duration(FOCUS_ANIMATION_DURATION)}
>
<SheetHeaderTitle
leftButtonIcon="x"
onLeftButtonPress={onClose}
leftButtonA11yLabel={translate('generic.buttons.close')}
>
{title}
</SheetHeaderTitle>
</Animated.View>
)}
</Animated.View>
<Animated.View layout={LinearTransition.duration(FOCUS_ANIMATION_DURATION)}>
<SearchInputWithCancel
onChange={setFilterValue}
onFocus={() => changeFilterFocus(true)}
onBlur={() => changeFilterFocus(false)}
value={filterValue}
/>
</Animated.View>
{children}
</VStack>
);
};
Loading

0 comments on commit f0ae958

Please sign in to comment.