diff --git a/suite-native/intl/src/en.ts b/suite-native/intl/src/en.ts index 63a5af9ccbe..64f4ec51bed 100644 --- a/suite-native/intl/src/en.ts +++ b/suite-native/intl/src/en.ts @@ -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', }, }; diff --git a/suite-native/module-trading/src/components/buy/BuyCard.tsx b/suite-native/module-trading/src/components/buy/BuyCard.tsx index c01c055db52..e9c91afbf27 100644 --- a/suite-native/module-trading/src/components/buy/BuyCard.tsx +++ b/suite-native/module-trading/src/components/buy/BuyCard.tsx @@ -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 = () => { @@ -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(); return ( @@ -42,21 +34,15 @@ export const BuyCard = () => { - + 0.0 - {selectedTradeableAsset?.symbol ? ( - + {selectedValue?.symbol ? ( + ) : ( '-' )} @@ -83,11 +69,6 @@ export const BuyCard = () => { - ); }; diff --git a/suite-native/module-trading/src/components/buy/CountryOfResidencePicker.tsx b/suite-native/module-trading/src/components/buy/CountryOfResidencePicker.tsx new file mode 100644 index 00000000000..68006e3d292 --- /dev/null +++ b/suite-native/module-trading/src/components/buy/CountryOfResidencePicker.tsx @@ -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(); + + return ( + <> + + {selectedValue ? ( + + + + {selectedValue.name} + + + ) : ( + + {translate('moduleTrading.notSelected')} + + )} + + + + ); +}; diff --git a/suite-native/module-trading/src/components/buy/PaymentCard.tsx b/suite-native/module-trading/src/components/buy/PaymentCard.tsx index 29c4bf67dd2..e76eff36a54 100644 --- a/suite-native/module-trading/src/components/buy/PaymentCard.tsx +++ b/suite-native/module-trading/src/components/buy/PaymentCard.tsx @@ -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 = () => { @@ -21,27 +22,16 @@ export const PaymentCard = () => { Credit card - - - Czech Republic - - + Anycoin - - - ); }; diff --git a/suite-native/module-trading/src/components/buy/TradeableAssetPicker.tsx b/suite-native/module-trading/src/components/buy/TradeableAssetPicker.tsx new file mode 100644 index 00000000000..d6ed29fa4af --- /dev/null +++ b/suite-native/module-trading/src/components/buy/TradeableAssetPicker.tsx @@ -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>; + +export const TradeableAssetPicker = ({ + isSheetVisible, + showSheet, + hideSheet, + selectedValue, + setSelectedValue, +}: TradeableAssetPickerProps) => ( + <> + + + +); diff --git a/suite-native/module-trading/src/components/general/CountrySheet/CountryListEmptyComponent.tsx b/suite-native/module-trading/src/components/general/CountrySheet/CountryListEmptyComponent.tsx new file mode 100644 index 00000000000..eb988dd9ec9 --- /dev/null +++ b/suite-native/module-trading/src/components/general/CountrySheet/CountryListEmptyComponent.tsx @@ -0,0 +1,10 @@ +import { Translation } from '@suite-native/intl'; + +import { TradingEmptyComponent } from '../TradingEmptyComponent'; + +export const CountryListEmptyComponent = () => ( + } + description={} + /> +); diff --git a/suite-native/module-trading/src/components/general/CountrySheet/CountryListItem.tsx b/suite-native/module-trading/src/components/general/CountrySheet/CountryListItem.tsx new file mode 100644 index 00000000000..900848cd0db --- /dev/null +++ b/suite-native/module-trading/src/components/general/CountrySheet/CountryListItem.tsx @@ -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 ( + + + + + + + {name} + + + + + + + ); +}; diff --git a/suite-native/module-trading/src/components/general/CountrySheet/CountrySheet.tsx b/suite-native/module-trading/src/components/general/CountrySheet/CountrySheet.tsx new file mode 100644 index 00000000000..f05cfa32c96 --- /dev/null +++ b/suite-native/module-trading/src/components/general/CountrySheet/CountrySheet.tsx @@ -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 ( + + isVisible={isVisible} + onClose={onClose} + ListEmptyComponent={} + handleComponent={() => ( + } + /> + )} + renderItem={({ item }) => ( + onCountrySelectCallback(item)} + isSelected={item.id === selectedCountryId} + /> + )} + data={data} + estimatedListHeight={getEstimatedListHeight(data.length)} + estimatedItemSize={COUNTRY_LIST_ITEM_HEIGHT} + keyExtractor={keyExtractor} + /> + ); +}; diff --git a/suite-native/module-trading/src/components/general/SearchableSheetHeader.tsx b/suite-native/module-trading/src/components/general/SearchableSheetHeader.tsx new file mode 100644 index 00000000000..7f75f65cd4e --- /dev/null +++ b/suite-native/module-trading/src/components/general/SearchableSheetHeader.tsx @@ -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 ( + + + + {!isFilterActive && ( + + + {title} + + + )} + + + changeFilterFocus(true)} + onBlur={() => changeFilterFocus(false)} + value={filterValue} + /> + + {children} + + ); +}; diff --git a/suite-native/module-trading/src/components/general/TradeableAssetsSheet/TradeAssetsListEmptyComponent.tsx b/suite-native/module-trading/src/components/general/TradeableAssetsSheet/TradeAssetsListEmptyComponent.tsx index 53baca1fa24..f3eeed56089 100644 --- a/suite-native/module-trading/src/components/general/TradeableAssetsSheet/TradeAssetsListEmptyComponent.tsx +++ b/suite-native/module-trading/src/components/general/TradeableAssetsSheet/TradeAssetsListEmptyComponent.tsx @@ -1,25 +1,10 @@ -import { Text, VStack } from '@suite-native/atoms'; import { Translation } from '@suite-native/intl'; -import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; -const emptyComponentStyle = prepareNativeStyle(({ spacings }) => ({ - padding: spacings.sp52, - alignContent: 'center', - justifyContent: 'center', - gap: spacings.sp12, -})); +import { TradingEmptyComponent } from '../TradingEmptyComponent'; -export const TradeAssetsListEmptyComponent = () => { - const { applyStyle } = useNativeStyles(); - - return ( - - - - - - - - - ); -}; +export const TradeAssetsListEmptyComponent = () => ( + } + description={} + /> +); diff --git a/suite-native/module-trading/src/components/general/TradeableAssetsSheet/TradeableAssetsSheetHeader.tsx b/suite-native/module-trading/src/components/general/TradeableAssetsSheet/TradeableAssetsSheetHeader.tsx index 51480839030..6c885ba607a 100644 --- a/suite-native/module-trading/src/components/general/TradeableAssetsSheet/TradeableAssetsSheetHeader.tsx +++ b/suite-native/module-trading/src/components/general/TradeableAssetsSheet/TradeableAssetsSheetHeader.tsx @@ -1,67 +1,40 @@ import { useState } from 'react'; -import Animated, { FadeIn, FadeOut, LinearTransition } from 'react-native-reanimated'; +import Animated, { LinearTransition } from 'react-native-reanimated'; -import { BottomSheetGrabber, VStack } from '@suite-native/atoms'; -import { Translation, useTranslate } from '@suite-native/intl'; +import { Translation } from '@suite-native/intl'; import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; -import { SheetHeaderTitle } from '../SheetHeaderTitle'; +import { FOCUS_ANIMATION_DURATION, SearchableSheetHeader } from '../SearchableSheetHeader'; import { TradeableAssetsFilterTabs } from './TradeableAssetsFilterTabs'; -import { SearchInputWithCancel } from '../SearchInputWithCancel'; type TradeableAssetsSheetHeaderProps = { onClose: () => void; }; const HEADER_HEIGHT = 160; -const FOCUS_ANIMATION_DURATION = 300 as const; -const wrapperStyle = prepareNativeStyle<{}>(({ spacings }) => ({ +const wrapperStyle = prepareNativeStyle<{}>(() => ({ height: HEADER_HEIGHT, - padding: spacings.sp16, - gap: spacings.sp16, })); export const TradeableAssetsSheetHeader = ({ onClose }: TradeableAssetsSheetHeaderProps) => { const { applyStyle } = useNativeStyles(); - const { translate } = useTranslate(); const [isFilterActive, setIsFilterActive] = useState(false); - const [filterValue, setFilterValue] = useState(''); return ( - - - - {!isFilterActive && ( - - - - - - )} - - - setIsFilterActive(true)} - onBlur={() => setIsFilterActive(false)} - value={filterValue} - /> - + } + onFilterFocusChange={setIsFilterActive} + style={applyStyle(wrapperStyle)} + > - + ); }; diff --git a/suite-native/module-trading/src/components/general/TradingEmptyComponent.tsx b/suite-native/module-trading/src/components/general/TradingEmptyComponent.tsx new file mode 100644 index 00000000000..a1b97fbf1ea --- /dev/null +++ b/suite-native/module-trading/src/components/general/TradingEmptyComponent.tsx @@ -0,0 +1,31 @@ +import { ReactNode } from 'react'; + +import { Text, VStack } from '@suite-native/atoms'; +import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; + +export type TradingEmptyComponentProps = { + title: ReactNode; + description: ReactNode; +}; + +const emptyComponentStyle = prepareNativeStyle(({ spacings }) => ({ + padding: spacings.sp52, + alignContent: 'center', + justifyContent: 'center', + gap: spacings.sp12, +})); + +export const TradingEmptyComponent = ({ title, description }: TradingEmptyComponentProps) => { + const { applyStyle } = useNativeStyles(); + + return ( + + + {title} + + + {description} + + + ); +}; diff --git a/suite-native/module-trading/src/hooks/__tests__/useAssetsSheetControls.test.ts b/suite-native/module-trading/src/hooks/__tests__/useAssetsSheetControls.test.ts deleted file mode 100644 index 690ce6e2920..00000000000 --- a/suite-native/module-trading/src/hooks/__tests__/useAssetsSheetControls.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { act, renderHook } from '@suite-native/test-utils'; - -import { useTradeableAssetsSheetControls } from '../useTradeableAssetsSheetControls'; - -describe('useTradeableAssetsSheetControls', () => { - describe('isTradeableAssetsSheetVisible', () => { - it('should be false by default', () => { - const { result } = renderHook(() => useTradeableAssetsSheetControls()); - - expect(result.current.isTradeableAssetsSheetVisible).toBe(false); - }); - - it('should be true after showTradeableAssetsSheet call', () => { - const { result } = renderHook(() => useTradeableAssetsSheetControls()); - - act(() => { - result.current.showTradeableAssetsSheet(); - }); - - expect(result.current.isTradeableAssetsSheetVisible).toBe(true); - }); - - it('should be false after hideTradeableAssetsSheet call', () => { - const { result } = renderHook(() => useTradeableAssetsSheetControls()); - - act(() => { - result.current.showTradeableAssetsSheet(); - result.current.hideTradeableAssetsSheet(); - }); - - expect(result.current.isTradeableAssetsSheetVisible).toBe(false); - }); - }); - - describe('selectedTradeableAsset', () => { - it('should be undefined by default', () => { - const { result } = renderHook(() => useTradeableAssetsSheetControls()); - - expect(result.current.selectedTradeableAsset).toBeUndefined(); - }); - - it('should be set after setSelectedTradeableAsset call', () => { - const { result } = renderHook(() => useTradeableAssetsSheetControls()); - - act(() => { - result.current.setSelectedTradeableAsset({ symbol: 'btc' }); - }); - - expect(result.current.selectedTradeableAsset?.symbol).toBe('btc'); - }); - }); -}); diff --git a/suite-native/module-trading/src/hooks/__tests__/useTradeSheetControls.test.ts b/suite-native/module-trading/src/hooks/__tests__/useTradeSheetControls.test.ts new file mode 100644 index 00000000000..eec84be422e --- /dev/null +++ b/suite-native/module-trading/src/hooks/__tests__/useTradeSheetControls.test.ts @@ -0,0 +1,52 @@ +import { act, renderHook } from '@suite-native/test-utils'; + +import { useTradeSheetControls } from '../useTradeSheetControls'; + +describe('useTradeSheetControls', () => { + describe('isSheetVisible', () => { + it('should be false by default', () => { + const { result } = renderHook(() => useTradeSheetControls()); + + expect(result.current.isSheetVisible).toBe(false); + }); + + it('should be true after showTradeableAssetsSheet call', () => { + const { result } = renderHook(() => useTradeSheetControls()); + + act(() => { + result.current.showSheet(); + }); + + expect(result.current.isSheetVisible).toBe(true); + }); + + it('should be false after hideTradeableAssetsSheet call', () => { + const { result } = renderHook(() => useTradeSheetControls()); + + act(() => { + result.current.showSheet(); + result.current.hideSheet(); + }); + + expect(result.current.isSheetVisible).toBe(false); + }); + }); + + describe('selectedTradeableAsset', () => { + it('should be undefined by default', () => { + const { result } = renderHook(() => useTradeSheetControls()); + + expect(result.current.selectedValue).toBeUndefined(); + }); + + it('should be set after setSelectedTradeableAsset call', () => { + const { result } = renderHook(() => useTradeSheetControls()); + + act(() => { + result.current.setSelectedValue('btc'); + }); + + expect(result.current.selectedValue).toBe('btc'); + }); + }); +}); diff --git a/suite-native/module-trading/src/hooks/useTradeSheetControls.ts b/suite-native/module-trading/src/hooks/useTradeSheetControls.ts new file mode 100644 index 00000000000..e7095af69dc --- /dev/null +++ b/suite-native/module-trading/src/hooks/useTradeSheetControls.ts @@ -0,0 +1,22 @@ +import { useState } from 'react'; + +export const useTradeSheetControls = () => { + const [isSheetVisible, setIsSheetVisible] = useState(false); + const [selectedValue, setSelectedValue] = useState(); + + const showSheet = () => { + setIsSheetVisible(true); + }; + + const hideSheet = () => { + setIsSheetVisible(false); + }; + + return { + isSheetVisible, + showSheet, + hideSheet, + selectedValue, + setSelectedValue, + }; +}; diff --git a/suite-native/module-trading/src/hooks/useTradeableAssetsSheetControls.ts b/suite-native/module-trading/src/hooks/useTradeableAssetsSheetControls.ts deleted file mode 100644 index 9d8006f336c..00000000000 --- a/suite-native/module-trading/src/hooks/useTradeableAssetsSheetControls.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { useState } from 'react'; - -import { TradeableAsset } from '../types'; - -export const useTradeableAssetsSheetControls = () => { - const [isTradeableAssetsSheetVisible, setIsTradeableAssetsSheetVisible] = useState(false); - const [selectedTradeableAsset, setSelectedTradeableAsset] = useState< - undefined | TradeableAsset - >(); - - 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 7d81ff4b2a9..0b7c7076bf7 100644 --- a/suite-native/module-trading/src/screens/TradingScreen.tsx +++ b/suite-native/module-trading/src/screens/TradingScreen.tsx @@ -1,4 +1,4 @@ -import { Text, VStack } from '@suite-native/atoms'; +import { Button, Text, VStack } from '@suite-native/atoms'; import { DeviceManagerScreenHeader } from '@suite-native/device-manager'; import { Translation } from '@suite-native/intl'; import { Screen } from '@suite-native/navigation'; @@ -7,6 +7,11 @@ import { BuyCard } from '../components/buy/BuyCard'; import { PaymentCard } from '../components/buy/PaymentCard'; import { TradingFooter } from '../components/general/TradingFooter'; +const notImplementedCallback = () => { + // eslint-disable-next-line no-console + console.log('Not implemented'); +}; + export const TradingScreen = () => ( }> @@ -15,6 +20,9 @@ export const TradingScreen = () => ( + diff --git a/suite-native/module-trading/src/types.ts b/suite-native/module-trading/src/types.ts index 870227d38b1..055795012ee 100644 --- a/suite-native/module-trading/src/types.ts +++ b/suite-native/module-trading/src/types.ts @@ -1,5 +1,6 @@ import { NetworkSymbol } from '@suite-common/wallet-config'; import { TokenAddress } from '@suite-common/wallet-types'; +import { IconName } from '@suite-native/icons'; // NOTE: in production code we probably want to use `TokenInfoBranded` or something similar instead export type TradeableAsset = { @@ -7,3 +8,9 @@ export type TradeableAsset = { contractAddress?: TokenAddress; name?: string; }; + +export type Country = { + id: string; + name: string; + flag: IconName; +};