From e55d6005b090851c93578468539bc39458e507d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jirka=20Ba=C5=BEant?= Date: Fri, 31 Jan 2025 11:15:11 +0100 Subject: [PATCH 1/6] feat(suite-native): introduce new ButtonColorScheme --- suite-native/atoms/src/Button/Button.tsx | 10 +++++++++- .../module-dev-utils/src/screens/DemoScreen.tsx | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/suite-native/atoms/src/Button/Button.tsx b/suite-native/atoms/src/Button/Button.tsx index 8f35d09c058..9a9ee14bb8a 100644 --- a/suite-native/atoms/src/Button/Button.tsx +++ b/suite-native/atoms/src/Button/Button.tsx @@ -32,7 +32,8 @@ export type ButtonColorScheme = | 'yellowElevation1' | 'blueBold' | 'blueElevation0' - | 'blueElevation1'; + | 'blueElevation1' + | 'backgroundSurfaceElevation0'; export type ButtonProps = Omit & { children: ReactNode; @@ -181,6 +182,13 @@ export const buttonSchemeToColorsMap = { iconColor: 'iconAlertBlue', disabledColors: baseDisabledScheme, }, + backgroundSurfaceElevation0: { + backgroundColor: 'backgroundSurfaceElevation0', + onPressColor: 'backgroundTertiaryPressedOnElevation0', + textColor: 'textSubdued', + iconColor: 'textSubdued', + disabledColors: baseDisabledScheme, + }, } as const satisfies Record; const sizeToDimensionsMap = { diff --git a/suite-native/module-dev-utils/src/screens/DemoScreen.tsx b/suite-native/module-dev-utils/src/screens/DemoScreen.tsx index 2c70d605eb8..35beec48e7d 100644 --- a/suite-native/module-dev-utils/src/screens/DemoScreen.tsx +++ b/suite-native/module-dev-utils/src/screens/DemoScreen.tsx @@ -79,6 +79,7 @@ export const DemoScreen = () => { 'yellowElevation0', 'blueBold', 'blueElevation0', + 'backgroundSurfaceElevation0', ] satisfies ButtonColorScheme[]; const textButtonVariants = ['primary', 'tertiary'] satisfies TextButtonVariant[]; From ef571c2ec411744cf542657d7b59639e31189736 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jirka=20Ba=C5=BEant?= Date: Fri, 31 Jan 2025 11:19:30 +0100 Subject: [PATCH 2/6] feat(suite-native): Extend BasicProvider by another providers --- .../__tests__/PriceChangeBadge.comp.test.tsx | 4 +- suite-native/test-utils/package.json | 1 + suite-native/test-utils/src/BasicProvider.tsx | 25 ----------- .../test-utils/src/BasicProviderForTests.tsx | 43 +++++++++++++++++++ .../test-utils/src/StoreProviderForTests.tsx | 4 +- suite-native/test-utils/src/index.tsx | 6 +-- yarn.lock | 1 + 7 files changed, 52 insertions(+), 32 deletions(-) delete mode 100644 suite-native/test-utils/src/BasicProvider.tsx create mode 100644 suite-native/test-utils/src/BasicProviderForTests.tsx diff --git a/suite-native/atoms/src/__tests__/PriceChangeBadge.comp.test.tsx b/suite-native/atoms/src/__tests__/PriceChangeBadge.comp.test.tsx index 4df5f3cba77..5a3193d375e 100644 --- a/suite-native/atoms/src/__tests__/PriceChangeBadge.comp.test.tsx +++ b/suite-native/atoms/src/__tests__/PriceChangeBadge.comp.test.tsx @@ -1,4 +1,4 @@ -import { BasicProvider, render, renderHook } from '@suite-native/test-utils'; +import { BasicProviderForTests, render, renderHook } from '@suite-native/test-utils'; import { NativeStyleUtils, useNativeStyles } from '@trezor/styles'; import { Box as MockBox } from '../Box'; @@ -12,7 +12,7 @@ describe('PriceChangeBadge', () => { let colors: NativeStyleUtils['colors']; beforeAll(() => { - const { result } = renderHook(() => useNativeStyles(), { wrapper: BasicProvider }); + const { result } = renderHook(() => useNativeStyles(), { wrapper: BasicProviderForTests }); ({ colors } = result.current.utils); }); diff --git a/suite-native/test-utils/package.json b/suite-native/test-utils/package.json index 5f07746a6eb..c8086b6df3a 100644 --- a/suite-native/test-utils/package.json +++ b/suite-native/test-utils/package.json @@ -10,6 +10,7 @@ "type-check": "yarn g:tsc --build" }, "dependencies": { + "@gorhom/bottom-sheet": "5.0.5", "@react-navigation/native": "6.1.18", "@reduxjs/toolkit": "1.9.5", "@testing-library/react-native": "13.0.0", diff --git a/suite-native/test-utils/src/BasicProvider.tsx b/suite-native/test-utils/src/BasicProvider.tsx deleted file mode 100644 index 605f1172d46..00000000000 --- a/suite-native/test-utils/src/BasicProvider.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { ReactNode } from 'react'; -import { SafeAreaProvider } from 'react-native-safe-area-context'; - -import { NavigationContainer } from '@react-navigation/native'; - -import { IntlProvider } from '@suite-native/intl'; -import { StylesProvider, createRenderer } from '@trezor/styles'; -import { prepareNativeTheme } from '@trezor/theme'; - -type ProviderProps = { - children: ReactNode; -}; - -const renderer = createRenderer(); -const theme = prepareNativeTheme({ colorVariant: 'standard' }); - -export const BasicProvider = ({ children }: ProviderProps) => ( - - - - {children} - - - -); diff --git a/suite-native/test-utils/src/BasicProviderForTests.tsx b/suite-native/test-utils/src/BasicProviderForTests.tsx new file mode 100644 index 00000000000..44973d90c03 --- /dev/null +++ b/suite-native/test-utils/src/BasicProviderForTests.tsx @@ -0,0 +1,43 @@ +import { ReactNode, useMemo } from 'react'; +import { SafeAreaProvider } from 'react-native-safe-area-context'; + +import { BottomSheetModalProvider } from '@gorhom/bottom-sheet'; +import { NavigationContainer } from '@react-navigation/native'; + +import { FormatterProvider } from '@suite-common/formatters'; +import { IntlProvider } from '@suite-native/intl'; +import { StylesProvider, createRenderer } from '@trezor/styles'; +import { prepareNativeTheme } from '@trezor/theme'; + +type ProviderProps = { + children: ReactNode; +}; + +const renderer = createRenderer(); +const theme = prepareNativeTheme({ colorVariant: 'standard' }); + +export const BasicProviderForTests = ({ children }: ProviderProps) => { + const formattersConfig = useMemo( + () => ({ + locale: 'en' as const, + fiatCurrency: 'usd' as const, + bitcoinAmountUnit: 0, + is24HourFormat: true, + }), + [], + ); + + return ( + + + + + + {children} + + + + + + ); +}; diff --git a/suite-native/test-utils/src/StoreProviderForTests.tsx b/suite-native/test-utils/src/StoreProviderForTests.tsx index 88de05f51d0..38aeab4379f 100644 --- a/suite-native/test-utils/src/StoreProviderForTests.tsx +++ b/suite-native/test-utils/src/StoreProviderForTests.tsx @@ -5,7 +5,7 @@ import { EnhancedStore } from '@reduxjs/toolkit'; import { PreloadedState, initStore } from '@suite-native/state'; -import { BasicProvider } from './BasicProvider'; +import { BasicProviderForTests } from './BasicProviderForTests'; type ReduxProviderProps = { children: ReactNode; @@ -32,7 +32,7 @@ export const StoreProviderForTests = ({ children, preloadedState }: ReduxProvide return ( - {children} + {children} ); }; diff --git a/suite-native/test-utils/src/index.tsx b/suite-native/test-utils/src/index.tsx index bd779009014..cb4cda0d7f7 100644 --- a/suite-native/test-utils/src/index.tsx +++ b/suite-native/test-utils/src/index.tsx @@ -1,4 +1,4 @@ -import { BasicProvider } from './BasicProvider'; +import { BasicProviderForTests } from './BasicProviderForTests'; import { StoreProviderForTests } from './StoreProviderForTests'; import { createRender } from './createRender'; @@ -14,8 +14,8 @@ export { waitForElementToBeRemoved, } from '@testing-library/react-native'; -export const render = createRender(BasicProvider); +export const render = createRender(BasicProviderForTests); export const renderWithStore = createRender(StoreProviderForTests); -export { BasicProvider, StoreProviderForTests }; +export { BasicProviderForTests, StoreProviderForTests }; diff --git a/yarn.lock b/yarn.lock index cdf9f116354..5627c91a20a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11196,6 +11196,7 @@ __metadata: version: 0.0.0-use.local resolution: "@suite-native/test-utils@workspace:suite-native/test-utils" dependencies: + "@gorhom/bottom-sheet": "npm:5.0.5" "@react-navigation/native": "npm:6.1.18" "@reduxjs/toolkit": "npm:1.9.5" "@testing-library/react-native": "npm:13.0.0" From 495f89cba0c2eb02bed7ab83bf4603bef57b5121 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jirka=20Ba=C5=BEant?= Date: Mon, 3 Feb 2025 11:28:30 +0100 Subject: [PATCH 3/6] feat(suite-native): Propagade handleComponent from BottomSheetFlashList, refactoring --- .../components/AccountSelectBottomSheet.tsx | 1 - .../atoms/src/Sheet/BottomSheetFlashList.tsx | 7 +++++-- .../atoms/src/Sheet/BottomSheetGrabber.tsx | 20 +++++++++++++++++++ .../atoms/src/Sheet/BottomSheetHeader.tsx | 20 ++----------------- suite-native/atoms/src/index.ts | 1 + 5 files changed, 28 insertions(+), 21 deletions(-) create mode 100644 suite-native/atoms/src/Sheet/BottomSheetGrabber.tsx diff --git a/suite-native/accounts/src/components/AccountSelectBottomSheet.tsx b/suite-native/accounts/src/components/AccountSelectBottomSheet.tsx index 60cfb8a8dd0..dbe26df582e 100644 --- a/suite-native/accounts/src/components/AccountSelectBottomSheet.tsx +++ b/suite-native/accounts/src/components/AccountSelectBottomSheet.tsx @@ -93,7 +93,6 @@ export const AccountSelectBottomSheet = React.memo( return ( isVisible - isCloseDisplayed={false} onClose={onClose} data={data} renderItem={renderItem} diff --git a/suite-native/atoms/src/Sheet/BottomSheetFlashList.tsx b/suite-native/atoms/src/Sheet/BottomSheetFlashList.tsx index 4d132190202..7927d85ccad 100644 --- a/suite-native/atoms/src/Sheet/BottomSheetFlashList.tsx +++ b/suite-native/atoms/src/Sheet/BottomSheetFlashList.tsx @@ -5,6 +5,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { BottomSheetBackdrop, BottomSheetModal, + BottomSheetProps, BottomSheetFlashList as FlashList, } from '@gorhom/bottom-sheet'; import { FlashListProps } from '@shopify/flash-list'; @@ -13,11 +14,12 @@ import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; export type BottomSheetFlashListProps = { isVisible: boolean; - isCloseDisplayed?: boolean; onClose: (isVisible: boolean) => void; title?: ReactNode; subtitle?: ReactNode; estimatedListHeight?: number; + handleComponent?: BottomSheetProps['handleComponent']; + stickyListFooterComponent?: ReactNode; } & FlashListProps; const DEFAULT_INSET_BOTTOM = 25; @@ -46,11 +48,11 @@ const WindowOverlay = ({ children }: { children: ReactNode }) => ( export const BottomSheetFlashList = ({ isVisible, - isCloseDisplayed = true, onClose, title, subtitle, estimatedListHeight = 0, + handleComponent, ...flashListProps }: BottomSheetFlashListProps) => { const { applyStyle } = useNativeStyles(); @@ -106,6 +108,7 @@ export const BottomSheetFlashList = ({ handleIndicatorStyle={applyStyle(handleStyle)} // @ts-expect-error wrong type, doesn't expect children containerComponent={WindowOverlay} + handleComponent={handleComponent} > ({ + width: 32, + height: 4, + borderRadius: utils.borders.radii.round, + backgroundColor: utils.colors.borderDashed, +})); + +export const BottomSheetGrabber = () => { + const { applyStyle } = useNativeStyles(); + + return ( + + + + ); +}; diff --git a/suite-native/atoms/src/Sheet/BottomSheetHeader.tsx b/suite-native/atoms/src/Sheet/BottomSheetHeader.tsx index 519e26cd7a3..d68e19eb6cd 100644 --- a/suite-native/atoms/src/Sheet/BottomSheetHeader.tsx +++ b/suite-native/atoms/src/Sheet/BottomSheetHeader.tsx @@ -3,9 +3,10 @@ import { View } from 'react-native'; import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; -import { Box } from '../Box'; import { IconButton } from '../Button/IconButton'; import { Text } from '../Text'; +import { BottomSheetGrabber } from './BottomSheetGrabber'; +import { Box } from '../Box'; type BottomSheetHeaderProps = { title: ReactNode; @@ -45,23 +46,6 @@ const titlesContainer = prepareNativeStyle<{ isCloseDisplayed: boolean }>( }), ); -const bottomSheetGrabberStyle = prepareNativeStyle(utils => ({ - width: 32, - height: 4, - borderRadius: utils.borders.radii.round, - backgroundColor: utils.colors.borderDashed, -})); - -const BottomSheetGrabber = () => { - const { applyStyle } = useNativeStyles(); - - return ( - - - - ); -}; - export const BottomSheetHeader = ({ title, subtitle, diff --git a/suite-native/atoms/src/index.ts b/suite-native/atoms/src/index.ts index 12eeedbdfd0..02eaf8b7e47 100644 --- a/suite-native/atoms/src/index.ts +++ b/suite-native/atoms/src/index.ts @@ -19,6 +19,7 @@ export * from './BottomSheetListItem'; export * from './OrderedListIcon'; export * from './Sheet/BottomSheet'; export * from './Sheet/BottomSheetFlashList'; +export * from './Sheet/BottomSheetGrabber'; export * from './Sheet/useBottomSheetAnimation'; export * from './Button/Button'; export * from './Button/IconButton'; From 66d8a50e27115281b6a8458100dd60558a4d328e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jirka=20Ba=C5=BEant?= Date: Fri, 31 Jan 2025 11:19:48 +0100 Subject: [PATCH 4/6] feat(suite-native): Mobile Trade: Token picker modal visual stub --- packages/theme/src/spacings.ts | 3 +- suite-common/icons/generateIconFont.ts | 2 + .../iconFontsMobile/TrezorSuiteIcons.json | 162 +++++++-------- .../iconFontsMobile/TrezorSuiteIcons.ttf | Bin 22196 -> 22572 bytes suite-common/suite-utils/src/hexToRgba.ts | 4 +- suite-native/app/app.config.ts | 2 +- suite-native/atoms/src/Input/SearchInput.tsx | 122 +++++++----- .../atoms/src/Sheet/BottomSheetFlashList.tsx | 1 - suite-native/intl/src/en.ts | 14 ++ suite-native/module-trading/jest.config.js | 5 + suite-native/module-trading/package.json | 6 +- .../src/components/buy/AmountCard.tsx | 33 ++++ .../components/general/PickerCloseButton.tsx | 13 -- .../src/components/general/PickerHeader.tsx | 51 ----- .../general/SearchInputWithCancel.tsx | 68 +++++++ .../general/SelectTradeableAssetButton.tsx | 38 ++++ .../components/general/SheetHeaderTitle.tsx | 42 ++++ .../general/TradeableAssetButton.tsx | 82 ++++++++ .../TradeableAssetsSheet/FavouriteIcon.tsx | 37 ++++ .../TradeAssetsListEmptyComponent.tsx | 25 +++ .../TradeableAssetListItem.tsx | 107 ++++++++++ .../TradeableAssetsFilterTabs.tsx | 73 +++++++ .../TradeableAssetsSheet.tsx | 185 ++++++++++++++++++ .../TradeableAssetsSheetHeader.tsx | 67 +++++++ .../__tests__/FavouriteIcon.comp.test.tsx | 25 +++ .../TradeableAssetListItem.comp.test.tsx | 79 ++++++++ .../TradeableAssetsSheetHeader.comp.test.tsx | 47 +++++ .../__tests__/PickerHeader.comp.test.tsx | 38 ---- .../SelectTradeableAssetButton.comp.test.tsx | 34 ++++ .../TradeableAssetButton.comp.test.tsx | 36 ++++ .../__tests__/useAssetsSheetControls.test.ts | 52 +++++ .../useTradeableAssetDominantColor.test.tsx | 43 ++++ .../hooks/useTradeableAssetDominantColor.ts | 17 ++ .../hooks/useTradeableAssetsSheetControls.ts | 26 +++ .../TradingStackNavigator.comp.test.tsx | 4 +- .../src/screens/TradingScreen.tsx | 6 +- suite-native/module-trading/src/types.ts | 9 + yarn.lock | 14 +- 38 files changed, 1323 insertions(+), 249 deletions(-) create mode 100644 suite-native/module-trading/jest.config.js create mode 100644 suite-native/module-trading/src/components/buy/AmountCard.tsx delete mode 100644 suite-native/module-trading/src/components/general/PickerCloseButton.tsx delete mode 100644 suite-native/module-trading/src/components/general/PickerHeader.tsx create mode 100644 suite-native/module-trading/src/components/general/SearchInputWithCancel.tsx create mode 100644 suite-native/module-trading/src/components/general/SelectTradeableAssetButton.tsx create mode 100644 suite-native/module-trading/src/components/general/SheetHeaderTitle.tsx create mode 100644 suite-native/module-trading/src/components/general/TradeableAssetButton.tsx create mode 100644 suite-native/module-trading/src/components/general/TradeableAssetsSheet/FavouriteIcon.tsx create mode 100644 suite-native/module-trading/src/components/general/TradeableAssetsSheet/TradeAssetsListEmptyComponent.tsx create mode 100644 suite-native/module-trading/src/components/general/TradeableAssetsSheet/TradeableAssetListItem.tsx create mode 100644 suite-native/module-trading/src/components/general/TradeableAssetsSheet/TradeableAssetsFilterTabs.tsx create mode 100644 suite-native/module-trading/src/components/general/TradeableAssetsSheet/TradeableAssetsSheet.tsx create mode 100644 suite-native/module-trading/src/components/general/TradeableAssetsSheet/TradeableAssetsSheetHeader.tsx create mode 100644 suite-native/module-trading/src/components/general/TradeableAssetsSheet/__tests__/FavouriteIcon.comp.test.tsx create mode 100644 suite-native/module-trading/src/components/general/TradeableAssetsSheet/__tests__/TradeableAssetListItem.comp.test.tsx create mode 100644 suite-native/module-trading/src/components/general/TradeableAssetsSheet/__tests__/TradeableAssetsSheetHeader.comp.test.tsx delete mode 100644 suite-native/module-trading/src/components/general/__tests__/PickerHeader.comp.test.tsx create mode 100644 suite-native/module-trading/src/components/general/__tests__/SelectTradeableAssetButton.comp.test.tsx create mode 100644 suite-native/module-trading/src/components/general/__tests__/TradeableAssetButton.comp.test.tsx create mode 100644 suite-native/module-trading/src/hooks/__tests__/useAssetsSheetControls.test.ts create mode 100644 suite-native/module-trading/src/hooks/__tests__/useTradeableAssetDominantColor.test.tsx create mode 100644 suite-native/module-trading/src/hooks/useTradeableAssetDominantColor.ts create mode 100644 suite-native/module-trading/src/hooks/useTradeableAssetsSheetControls.ts create mode 100644 suite-native/module-trading/src/types.ts diff --git a/packages/theme/src/spacings.ts b/packages/theme/src/spacings.ts index 5483d2f1ce8..2b677997cc7 100644 --- a/packages/theme/src/spacings.ts +++ b/packages/theme/src/spacings.ts @@ -43,7 +43,7 @@ export const spacingsPx = (Object.keys(spacings) as Array).reduce((resu return result; }, {} as SpacingPx); -type NativeSpacingValue = 1 | 2 | 4 | 8 | 12 | 16 | 20 | 24 | 32 | 40 | 52 | 64; +type NativeSpacingValue = 1 | 2 | 4 | 8 | 12 | 16 | 20 | 24 | 32 | 36 | 40 | 52 | 64; export const nativeSpacings = { sp1: 1, @@ -55,6 +55,7 @@ export const nativeSpacings = { sp20: 20, sp24: 24, sp32: 32, + sp36: 36, sp40: 40, sp52: 52, sp64: 64, diff --git a/suite-common/icons/generateIconFont.ts b/suite-common/icons/generateIconFont.ts index da79eab2b9d..613062fac5f 100644 --- a/suite-common/icons/generateIconFont.ts +++ b/suite-common/icons/generateIconFont.ts @@ -93,6 +93,8 @@ const usedIcons = [ 'shieldWarning', 'shuffle', 'stack', + 'star', + 'starFilled', 'swap', 'trashSimple', 'treeStructure', diff --git a/suite-common/icons/iconFontsMobile/TrezorSuiteIcons.json b/suite-common/icons/iconFontsMobile/TrezorSuiteIcons.json index b4b2da1b616..ae44b085364 100644 --- a/suite-common/icons/iconFontsMobile/TrezorSuiteIcons.json +++ b/suite-common/icons/iconFontsMobile/TrezorSuiteIcons.json @@ -20,84 +20,86 @@ "treeStructure": 61715, "trashSimple": 61716, "swap": 61717, - "stack": 61718, - "shuffle": 61719, - "shieldWarning": 61720, - "shieldCheck": 61721, - "shareNetwork": 61722, - "question": 61723, - "qrCode": 61724, - "prohibit": 61725, - "plusCircle": 61726, - "plus": 61727, - "plugs": 61728, - "piggyBankFilled": 61729, - "piggyBank": 61730, - "pictureFrame": 61731, - "pencilSimpleLine": 61732, - "pencilSimple": 61733, - "pencil": 61734, - "password": 61735, - "palette": 61736, - "magnifyingGlass": 61737, - "lock": 61738, - "link": 61739, - "lightbulb": 61740, - "lifebuoy": 61741, - "info": 61742, - "image": 61743, - "houseFilled": 61744, - "house": 61745, - "handPalm": 61746, - "githubLogo": 61747, - "gearFilled": 61748, - "gear": 61749, - "flagCheckered": 61750, - "flag": 61751, - "fingerprintSimple": 61752, - "fingerprint": 61753, - "filePdf": 61754, - "facebookLogo": 61755, - "eyeSlash": 61756, - "eye": 61757, - "discoverFilled": 61758, - "discover": 61759, - "detective": 61760, - "database": 61761, - "cpu": 61762, - "copy": 61763, - "coins": 61764, - "coinVerticalCheck": 61765, - "code": 61766, - "clockClockwise": 61767, - "circleDashed": 61768, - "checks": 61769, - "checkCircleFilled": 61770, - "checkCircle": 61771, - "check": 61772, - "chatCircle": 61773, - "change": 61774, - "caretUpFilled": 61775, - "caretUpDown": 61776, - "caretUp": 61777, - "caretRight": 61778, - "caretLeft": 61779, - "caretDownFilled": 61780, - "caretDown": 61781, - "caretCircleRight": 61782, - "calendar": 61783, - "bugBeetle": 61784, - "bookmarkSimple": 61785, - "backspace": 61786, - "arrowsLeftRight": 61787, - "arrowsCounterClockwise": 61788, - "arrowUpRight": 61789, - "arrowUp": 61790, - "arrowURightDown": 61791, - "arrowSquareOut": 61792, - "arrowRight": 61793, - "arrowLineUpRight": 61794, - "arrowLineUp": 61795, - "arrowLineDown": 61796, - "arrowDown": 61797 + "starFilled": 61718, + "star": 61719, + "stack": 61720, + "shuffle": 61721, + "shieldWarning": 61722, + "shieldCheck": 61723, + "shareNetwork": 61724, + "question": 61725, + "qrCode": 61726, + "prohibit": 61727, + "plusCircle": 61728, + "plus": 61729, + "plugs": 61730, + "piggyBankFilled": 61731, + "piggyBank": 61732, + "pictureFrame": 61733, + "pencilSimpleLine": 61734, + "pencilSimple": 61735, + "pencil": 61736, + "password": 61737, + "palette": 61738, + "magnifyingGlass": 61739, + "lock": 61740, + "link": 61741, + "lightbulb": 61742, + "lifebuoy": 61743, + "info": 61744, + "image": 61745, + "houseFilled": 61746, + "house": 61747, + "handPalm": 61748, + "githubLogo": 61749, + "gearFilled": 61750, + "gear": 61751, + "flagCheckered": 61752, + "flag": 61753, + "fingerprintSimple": 61754, + "fingerprint": 61755, + "filePdf": 61756, + "facebookLogo": 61757, + "eyeSlash": 61758, + "eye": 61759, + "discoverFilled": 61760, + "discover": 61761, + "detective": 61762, + "database": 61763, + "cpu": 61764, + "copy": 61765, + "coins": 61766, + "coinVerticalCheck": 61767, + "code": 61768, + "clockClockwise": 61769, + "circleDashed": 61770, + "checks": 61771, + "checkCircleFilled": 61772, + "checkCircle": 61773, + "check": 61774, + "chatCircle": 61775, + "change": 61776, + "caretUpFilled": 61777, + "caretUpDown": 61778, + "caretUp": 61779, + "caretRight": 61780, + "caretLeft": 61781, + "caretDownFilled": 61782, + "caretDown": 61783, + "caretCircleRight": 61784, + "calendar": 61785, + "bugBeetle": 61786, + "bookmarkSimple": 61787, + "backspace": 61788, + "arrowsLeftRight": 61789, + "arrowsCounterClockwise": 61790, + "arrowUpRight": 61791, + "arrowUp": 61792, + "arrowURightDown": 61793, + "arrowSquareOut": 61794, + "arrowRight": 61795, + "arrowLineUpRight": 61796, + "arrowLineUp": 61797, + "arrowLineDown": 61798, + "arrowDown": 61799 } diff --git a/suite-common/icons/iconFontsMobile/TrezorSuiteIcons.ttf b/suite-common/icons/iconFontsMobile/TrezorSuiteIcons.ttf index a5518161b6d8f9feb8488ca4786fdef6945ee3f9..1de8a6ca8bd58ed98ab29ea5db31f0969db3eeba 100644 GIT binary patch delta 847 zcmZWnO=uHQ5T1Ga-fnj{*~A+2pUozX7DYB1suiuNg2|~uRV<~57;T#fiB=6-f3TiB zh}5=GK@d@ts-RNZUMxj=5WVyuB0Z>xf{2tN_;c`})JeSQTfUunGaui43~%xwJ{ZHH zhJi}}co6{h9Vq4cexGa_1d!;i@O<%P@1CiZLjZa=z_y=-o?Lh5<(u1y`<}4AKn2!` zS16w*BnqW+|A{+`i=?+exLG`WAouwD{UU%y8t}4|>+i$U@Q8YTx?6{GrJlPVg3-4C z?Rx;&@xH^y%H!{^k=V{x0Aly>bc3@1NL)fFKQceOuoepy6OUkHWe8g_UwMJ|Mp)fM zKa1i@t40M3f>g^^Q`AV;0uEnDgwsbtU-Ok2>5bA0dD4|{-{M$LO$Pz^$LgO>equ~K z;oh&kL?=Ks61@t@SBhG4z*NpDuj}^LO{s=@T%FT;wd>lv?$Pf!9F9@Pg41-iInNtl zoHZtmAFeLftb3LFlzYbgZHc{Pw7#i+Z0QY8!gFES8ZUSUz3+Tw-<%)(+x+7JPvB}` zHrNsT97=@-!=7+k_-y!T_+!M16eH&&Pa^Lkzsz95OquQG<)|6G5q)OWTb$oJTayG1pDexm$!LHn|w*O&OuGP>5|-MRrR327#N)Q zWTYmhL`+%W3sn06h|My90!$H%lY#6#K&+CHTT)T#^Z!54yf;8RBPTyOapvL9c?=9( zIY9Yyxrr48jFk-QfN~N*zCvDNZtC)fQu22g82o-PFff%BYX$W5}2`S&p%gQEl=BMtjE8$rl(`Y;ItZXVjDdt7BjkV_*ie zbbvG$&`=o$7KT?q6Ig%{Xv)Xb$tRd^1C_8%-pKN2@&OihejcDK1Irl@2?VK|_pt6` znLLS0x88>R2!{+u0mlta3(gaq|G0d(%DCCM^LRLT{CFC8*73aHP2zpPC&m}Xw})Sk zzlZ;hfRaFnzyv`K!D&K5!VJP~!f!+-i6)7D5OWZ_CT<~~B)&{SM50XMhoqk54k<6G zDN-M#%cR#yKaf$Aag%A1IVbZ%RzcQ5Hcqxic8csK1=(+M4e|`~8uBIbyA+fZJQNBP sRw%qv6jSt3Y*O5zcyDtO_i^6MsZtXxfPwNY=*;9}!GAVi3W;I{0Ok#Yp#T5? diff --git a/suite-common/suite-utils/src/hexToRgba.ts b/suite-common/suite-utils/src/hexToRgba.ts index 6da917e07ad..d1f02016c17 100644 --- a/suite-common/suite-utils/src/hexToRgba.ts +++ b/suite-common/suite-utils/src/hexToRgba.ts @@ -4,8 +4,8 @@ export function hexToRgba(hex: string, alpha: number) { const b = parseInt(hex.slice(5, 7), 16); if (alpha > 0) { - return `rgba(${r}, ${g}, ${b}, ${alpha})`; + return `rgba(${r}, ${g}, ${b}, ${alpha})` as `rgba(${number}, ${number}, ${number}, ${number})`; } - return `rgb(${r}, ${g}, ${b})`; + return `rgb(${r}, ${g}, ${b})` as `rgb(${number}, ${number}, ${number})`; } diff --git a/suite-native/app/app.config.ts b/suite-native/app/app.config.ts index 9d08145f22b..7ee8f1b490a 100644 --- a/suite-native/app/app.config.ts +++ b/suite-native/app/app.config.ts @@ -168,7 +168,7 @@ export default ({ config }: ConfigContext): ExpoConfig => { slug: appSlugs[buildType], owner: appOwners[buildType], version: suiteNativeVersion, - runtimeVersion: '24', + runtimeVersion: '26', ...(buildType === 'production' ? {} : { diff --git a/suite-native/atoms/src/Input/SearchInput.tsx b/suite-native/atoms/src/Input/SearchInput.tsx index 5c2b4ae7dc9..f35e6e57a5f 100644 --- a/suite-native/atoms/src/Input/SearchInput.tsx +++ b/suite-native/atoms/src/Input/SearchInput.tsx @@ -1,4 +1,4 @@ -import { useRef, useState } from 'react'; +import { forwardRef, useImperativeHandle, useRef, useState } from 'react'; import { Pressable, TextInput, TouchableOpacity } from 'react-native'; import { Icon } from '@suite-native/icons'; @@ -7,12 +7,15 @@ import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; import { Box } from '../Box'; import { SurfaceElevation } from '../types'; -type InputProps = { +export type SearchInputProps = { onChange: (value: string) => void; placeholder?: string; isDisabled?: boolean; maxLength?: number; elevation?: SurfaceElevation; + onFocus?: () => void; + onBlur?: () => void; + value?: string; }; const inputStyle = prepareNativeStyle(utils => ({ @@ -27,6 +30,7 @@ type InputStyleProps = { isFocused: boolean; elevation: SurfaceElevation; }; + const inputWrapperStyle = prepareNativeStyle( (utils, { isFocused, elevation }) => ({ flexDirection: 'row', @@ -57,53 +61,73 @@ const inputWrapperStyle = prepareNativeStyle( }), ); -export const SearchInput = ({ - onChange, - placeholder, - maxLength, - isDisabled = false, - elevation = '0', -}: InputProps) => { - const { applyStyle, utils } = useNativeStyles(); - const [isFocused, setIsFocused] = useState(false); - const [isClearButtonVisible, setIsClearButtonVisible] = useState(false); - const searchInputRef = useRef(null); - const handleClear = () => { - setIsClearButtonVisible(false); - searchInputRef.current?.clear(); - onChange(''); - }; +const noOp = () => {}; - const handleInputFocus = () => { - searchInputRef?.current?.focus(); - }; +export const SearchInput = forwardRef( + ( + { + onChange, + placeholder, + maxLength, + isDisabled = false, + elevation = '0', + onFocus = noOp, + onBlur = noOp, + value, + }, + ref, + ) => { + const { applyStyle, utils } = useNativeStyles(); + const [isFocused, setIsFocused] = useState(false); + const [isClearButtonVisible, setIsClearButtonVisible] = useState(false); + const searchInputRef = useRef(null); - const handleOnChangeText = (value: string) => { - setIsClearButtonVisible(!!value.length); - onChange(value); - }; + useImperativeHandle(ref, () => searchInputRef.current!, [searchInputRef]); - return ( - - - - setIsFocused(true)} - onBlur={() => setIsFocused(false)} - style={applyStyle(inputStyle)} - maxLength={maxLength} - /> - {isClearButtonVisible && ( - - - - )} - - - ); -}; + const handleClear = () => { + setIsClearButtonVisible(false); + searchInputRef.current?.clear(); + onChange(''); + }; + + const handleInputFocus = () => { + searchInputRef.current?.focus(); + }; + + const handleOnChangeText = (inputValue: string) => { + setIsClearButtonVisible(!!inputValue.length); + onChange(inputValue); + }; + + return ( + + + + { + setIsFocused(true); + onFocus(); + }} + onBlur={() => { + setIsFocused(false); + onBlur(); + }} + style={applyStyle(inputStyle)} + maxLength={maxLength} + value={value} + /> + {isClearButtonVisible && ( + + + + )} + + + ); + }, +); diff --git a/suite-native/atoms/src/Sheet/BottomSheetFlashList.tsx b/suite-native/atoms/src/Sheet/BottomSheetFlashList.tsx index 7927d85ccad..abec704a822 100644 --- a/suite-native/atoms/src/Sheet/BottomSheetFlashList.tsx +++ b/suite-native/atoms/src/Sheet/BottomSheetFlashList.tsx @@ -19,7 +19,6 @@ export type BottomSheetFlashListProps = { subtitle?: ReactNode; estimatedListHeight?: number; handleComponent?: BottomSheetProps['handleComponent']; - stickyListFooterComponent?: ReactNode; } & FlashListProps; const DEFAULT_INSET_BOTTOM = 25; diff --git a/suite-native/intl/src/en.ts b/suite-native/intl/src/en.ts index a386799a44e..d3c335c50dc 100644 --- a/suite-native/intl/src/en.ts +++ b/suite-native/intl/src/en.ts @@ -1278,6 +1278,20 @@ export const en = { provider: 'Provider', continueButton: 'Continue', }, + selectCoin: { + buttonTitle: 'Select coin', + }, + tradeableAssetsSheet: { + title: 'Coins', + favouritesTitle: 'Favourites', + allTitle: 'All coins', + favouritesAdd: 'Add to favourites', + favouritesRemove: 'Remove from favourites', + emptyTitle: 'No coin found', + emptyDescription: + 'We couldn’t find a coin matching your search. Try checking the spelling or exploring the list for the right option.', + }, + defaultSearchLabel: 'Search', }, }; diff --git a/suite-native/module-trading/jest.config.js b/suite-native/module-trading/jest.config.js new file mode 100644 index 00000000000..4299995bffa --- /dev/null +++ b/suite-native/module-trading/jest.config.js @@ -0,0 +1,5 @@ +const { ...baseConfig } = require('../../jest.config.native'); + +module.exports = { + ...baseConfig, +}; diff --git a/suite-native/module-trading/package.json b/suite-native/module-trading/package.json index 25182f5e1e3..bfaca191ab6 100644 --- a/suite-native/module-trading/package.json +++ b/suite-native/module-trading/package.json @@ -8,14 +8,16 @@ "scripts": { "depcheck": "yarn g:depcheck", "type-check": "yarn g:tsc --build", - "test:unit": "yarn g:jest -c ../../jest.config.native.js" + "test:unit": "yarn g:jest" }, "dependencies": { "@react-navigation/native-stack": "6.11.0", "@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" + "react-native": "0.76.1", + "react-native-reanimated": "3.16.7" } } 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 00000000000..03cd3010000 --- /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 { useTradeableAssetsSheetControls } from '../../hooks/useTradeableAssetsSheetControls'; +import { SelectTradeableAssetButton } from '../general/SelectTradeableAssetButton'; +import { TradeableAssetsSheet } from '../general/TradeableAssetsSheet/TradeableAssetsSheet'; + +export const AmountCard = () => { + const { + isTradeableAssetsSheetVisible, + showTradeableAssetsSheet, + hideTradeableAssetsSheet, + selectedTradeableAsset, + setSelectedTradeableAsset, + } = useTradeableAssetsSheetControls(); + + return ( + + + + + + + ); +}; diff --git a/suite-native/module-trading/src/components/general/PickerCloseButton.tsx b/suite-native/module-trading/src/components/general/PickerCloseButton.tsx deleted file mode 100644 index 20e9c48bb42..00000000000 --- a/suite-native/module-trading/src/components/general/PickerCloseButton.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Button, ButtonProps } from '@suite-native/atoms'; -import { Translation } from '@suite-native/intl'; - -export type PickerCloseButtonProps = Omit< - ButtonProps, - 'children' | 'colorScheme' | 'viewLeft' | 'viewRight' ->; - -export const PickerCloseButton = (props: PickerCloseButtonProps) => ( - -); diff --git a/suite-native/module-trading/src/components/general/PickerHeader.tsx b/suite-native/module-trading/src/components/general/PickerHeader.tsx deleted file mode 100644 index bfd2ca6ef14..00000000000 --- a/suite-native/module-trading/src/components/general/PickerHeader.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { ReactNode } from 'react'; - -import { HStack, SearchInput, Text, VStack } from '@suite-native/atoms'; - -type PickerHeaderSearchInputProps = { - onSearchInputChange: (value: string) => void; - searchInputPlaceholder?: string; - isSearchInputDisabled?: boolean; - maxSearchInputLength?: number; -}; - -export type PickerHeaderProps = { - title: ReactNode; - children?: ReactNode; -} & ( - | { isSearchInputVisible?: false } - | ({ isSearchInputVisible: true } & PickerHeaderSearchInputProps) -); - -const PickerHeaderSearchInput = ({ - onSearchInputChange, - searchInputPlaceholder, - isSearchInputDisabled, - maxSearchInputLength, -}: PickerHeaderSearchInputProps) => ( - -); - -export const PickerHeader = ({ - title, - isSearchInputVisible, - children, - ...searchInputProps -}: PickerHeaderProps) => ( - - - - {title} - - {children} - - {isSearchInputVisible && ( - - )} - -); diff --git a/suite-native/module-trading/src/components/general/SearchInputWithCancel.tsx b/suite-native/module-trading/src/components/general/SearchInputWithCancel.tsx new file mode 100644 index 00000000000..a69de8a87c5 --- /dev/null +++ b/suite-native/module-trading/src/components/general/SearchInputWithCancel.tsx @@ -0,0 +1,68 @@ +import { useRef, useState } from 'react'; +import { TextInput } from 'react-native'; +import Animated, { FadeIn, FadeOut, LinearTransition } from 'react-native-reanimated'; + +import { HStack, SearchInput, SearchInputProps, TextButton } from '@suite-native/atoms'; +import { Translation, useTranslate } from '@suite-native/intl'; +import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; + +export type SearchInputWithCancelProps = Omit; + +const noOp = () => {}; + +const inputWrapperStyle = prepareNativeStyle(_ => ({ + flex: 1, +})); + +const buttonWrapperStyle = prepareNativeStyle(({ spacings }) => ({ + height: spacings.sp24, +})); + +export const SearchInputWithCancel = ({ + onFocus = noOp, + onBlur = noOp, + ...props +}: SearchInputWithCancelProps) => { + const { applyStyle } = useNativeStyles(); + const { translate } = useTranslate(); + const [isInputActive, setIsInputActive] = useState(false); + const inputRef = useRef(null); + + const handleCancel = () => { + inputRef.current?.clear(); + inputRef.current?.blur(); + }; + + return ( + + + { + setIsInputActive(true); + onFocus(); + }} + onBlur={() => { + setIsInputActive(false); + onBlur(); + }} + {...props} + /> + + + {isInputActive && ( + + + + + + )} + + + ); +}; 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 00000000000..249fbc33ab2 --- /dev/null +++ b/suite-native/module-trading/src/components/general/SelectTradeableAssetButton.tsx @@ -0,0 +1,38 @@ +import { Button, buttonSchemeToColorsMap } from '@suite-native/atoms'; +import { Icon } from '@suite-native/icons'; +import { Translation, useTranslate } from '@suite-native/intl'; + +import { TradeableAssetButton } from './TradeableAssetButton'; +import { TradeableAsset } from '../../types'; + +export type SelectAssetButtonProps = { + onPress: () => void; + selectedAsset: TradeableAsset | undefined; +}; + +export const SelectTradeableAssetButton = ({ onPress, selectedAsset }: SelectAssetButtonProps) => { + const { translate } = useTranslate(); + const { iconColor } = buttonSchemeToColorsMap.primary; + + if (selectedAsset) { + return ( + + ); + } + + return ( + + ); +}; diff --git a/suite-native/module-trading/src/components/general/SheetHeaderTitle.tsx b/suite-native/module-trading/src/components/general/SheetHeaderTitle.tsx new file mode 100644 index 00000000000..276952aee2b --- /dev/null +++ b/suite-native/module-trading/src/components/general/SheetHeaderTitle.tsx @@ -0,0 +1,42 @@ +import { ReactNode } from 'react'; + +import { RequireAllOrNone } from 'type-fest'; + +import { Box, HStack, IconButton, Text } from '@suite-native/atoms'; +import { IconName } from '@suite-native/icons'; + +export type SheetHeaderTitleProps = { + children: ReactNode; +} & RequireAllOrNone<{ + onLeftButtonPress: () => void; + leftButtonIcon: IconName; + leftButtonA11yLabel: string; +}>; + +export const SheetHeaderTitle = ({ + onLeftButtonPress, + leftButtonIcon, + leftButtonA11yLabel, + children, +}: SheetHeaderTitleProps) => ( + + {leftButtonIcon && ( + + + + )} + + + {children} + + + {leftButtonIcon && } + +); 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 00000000000..079c0f15004 --- /dev/null +++ b/suite-native/module-trading/src/components/general/TradeableAssetButton.tsx @@ -0,0 +1,82 @@ +import { useMemo } from 'react'; +import { Pressable } from 'react-native'; + +import { LinearGradient } from 'expo-linear-gradient'; + +import { useFormatters } from '@suite-common/formatters'; +import { hexToRgba } from '@suite-common/suite-utils'; +import { Text } from '@suite-native/atoms'; +import { CryptoIcon, Icon } from '@suite-native/icons'; +import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; + +import { useTradeableAssetDominantColor } from '../../hooks/useTradeableAssetDominantColor'; +import { TradeableAsset } from '../../types'; + +export type TradeableAssetButtonProps = { + asset: TradeableAsset; + caret?: boolean; + onPress: () => void; + accessibilityLabel: string; +}; + +const GRADIENT_START = { x: 0, y: 0.5 } as const; +const GRADIENT_END = { x: 1, y: 0.5 } as const; + +const buttonStyle = prepareNativeStyle(({ spacings }) => ({ + height: spacings.sp36, + padding: spacings.sp4, + paddingRight: spacings.sp12, + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + gap: spacings.sp8, +})); + +const gradientBackgroundStyle = prepareNativeStyle(({ colors, borders }) => ({ + borderRadius: borders.radii.round, + borderWidth: borders.widths.small, + borderColor: hexToRgba(colors.iconDefault, 0.06), +})); + +export const TradeableAssetButton = ({ + asset: { symbol, contractAddress, name }, + caret, + onPress, + accessibilityLabel, +}: TradeableAssetButtonProps) => { + const { applyStyle } = useNativeStyles(); + const { DisplaySymbolFormatter } = useFormatters(); + + const dominantAssetColor = useTradeableAssetDominantColor(symbol, contractAddress); + const gradientColors = useMemo<[string, string]>( + () => [hexToRgba(dominantAssetColor, 0.3), hexToRgba(dominantAssetColor, 0.01)], + [dominantAssetColor], + ); + + const displayName = name ?? ( + + ); + + return ( + + + + + {displayName} + + {caret && } + + + ); +}; diff --git a/suite-native/module-trading/src/components/general/TradeableAssetsSheet/FavouriteIcon.tsx b/suite-native/module-trading/src/components/general/TradeableAssetsSheet/FavouriteIcon.tsx new file mode 100644 index 00000000000..b605f7bee61 --- /dev/null +++ b/suite-native/module-trading/src/components/general/TradeableAssetsSheet/FavouriteIcon.tsx @@ -0,0 +1,37 @@ +import { Pressable } from 'react-native'; + +import { Box } from '@suite-native/atoms'; +import { Icon, IconProps } from '@suite-native/icons'; +import { useTranslate } from '@suite-native/intl'; + +export type FavouriteIconProps = { + isFavourite: boolean; + onPress: () => void; +}; + +const getIconProps = (isFavourite: boolean): IconProps => + isFavourite + ? { name: 'starFilled', color: 'backgroundAlertYellowBold' } + : { name: 'star', color: 'textSubdued' }; + +const useA11yButtonHint = (isFavourite: boolean): string => { + const { translate } = useTranslate(); + + return isFavourite + ? translate('moduleTrading.tradeableAssetsSheet.favouritesRemove') + : translate('moduleTrading.tradeableAssetsSheet.favouritesAdd'); +}; + +export const FavouriteIcon = ({ isFavourite, onPress }: FavouriteIconProps) => { + const buttonA11yHint = useA11yButtonHint(isFavourite); + + const iconProps = getIconProps(isFavourite); + + return ( + + + + + + ); +}; diff --git a/suite-native/module-trading/src/components/general/TradeableAssetsSheet/TradeAssetsListEmptyComponent.tsx b/suite-native/module-trading/src/components/general/TradeableAssetsSheet/TradeAssetsListEmptyComponent.tsx new file mode 100644 index 00000000000..53baca1fa24 --- /dev/null +++ b/suite-native/module-trading/src/components/general/TradeableAssetsSheet/TradeAssetsListEmptyComponent.tsx @@ -0,0 +1,25 @@ +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, +})); + +export const TradeAssetsListEmptyComponent = () => { + const { applyStyle } = useNativeStyles(); + + return ( + + + + + + + + + ); +}; diff --git a/suite-native/module-trading/src/components/general/TradeableAssetsSheet/TradeableAssetListItem.tsx b/suite-native/module-trading/src/components/general/TradeableAssetsSheet/TradeableAssetListItem.tsx new file mode 100644 index 00000000000..e380214162c --- /dev/null +++ b/suite-native/module-trading/src/components/general/TradeableAssetsSheet/TradeableAssetListItem.tsx @@ -0,0 +1,107 @@ +import { Pressable } from 'react-native'; + +import { useFormatters } from '@suite-common/formatters'; +import { Box, HStack, PriceChangeBadge, RoundedIcon, Text, VStack } from '@suite-native/atoms'; +import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; + +import { FavouriteIcon } from './FavouriteIcon'; +import { TradeableAsset } from '../../../types'; + +export type AssetListItemProps = { + asset: TradeableAsset; + fiatRate: number; + priceChange: number; + onPress: () => void; + onFavouritePress: () => void; + isFavourite?: boolean; + isFirst?: boolean; + isLast?: boolean; +}; + +type AssetItemStyleProps = { + isFirst: boolean; + isLast: boolean; +}; + +export const ASSET_ITEM_HEIGHT = 68; + +const itemStyle = prepareNativeStyle( + ({ colors, spacings, borders }, { isFirst, isLast }) => ({ + backgroundColor: colors.backgroundSurfaceElevation1, + paddingHorizontal: spacings.sp12, + extend: [ + { + condition: isFirst, + style: { + borderTopLeftRadius: borders.radii.r20, + borderTopRightRadius: borders.radii.r20, + }, + }, + { + condition: isLast, + style: { + borderBottomLeftRadius: borders.radii.r20, + borderBottomRightRadius: borders.radii.r20, + }, + }, + ], + }), +); + +const vStackStyle = prepareNativeStyle(({ spacings }) => ({ + height: ASSET_ITEM_HEIGHT, + justifyContent: 'center', + flex: 1, + gap: 0, + paddingVertical: spacings.sp12, +})); + +export const TradeableAssetListItem = ({ + asset: { symbol, contractAddress, name }, + fiatRate, + priceChange, + onPress, + onFavouritePress, + isFavourite = false, + isFirst = false, + isLast = false, +}: AssetListItemProps) => { + const { applyStyle } = useNativeStyles(); + const { DisplaySymbolFormatter, FiatAmountFormatter, NetworkNameFormatter } = useFormatters(); + + const assetName = name ?? ; + + return ( + + + + + + + + + {assetName} + + + + + + + + + + + + + + + + + + ); +}; diff --git a/suite-native/module-trading/src/components/general/TradeableAssetsSheet/TradeableAssetsFilterTabs.tsx b/suite-native/module-trading/src/components/general/TradeableAssetsSheet/TradeableAssetsFilterTabs.tsx new file mode 100644 index 00000000000..82c7969a8dc --- /dev/null +++ b/suite-native/module-trading/src/components/general/TradeableAssetsSheet/TradeableAssetsFilterTabs.tsx @@ -0,0 +1,73 @@ +import { useState } from 'react'; +import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'; + +import { Button } from '@suite-native/atoms'; + +type FilterTabProps = { + children: React.ReactNode; + active: boolean; + onPress: () => void; +}; + +type TradeableAssetsFilterTabsProps = { + visible: boolean; + animationDuration: number; +}; + +const mockTabNames = [ + 'All', + 'Ethereum', + 'Solana', + 'Base', + 'Ethereum 2', + 'Solana 2', + 'Base 2', + 'Ethereum 3', + 'Solana 3', + 'Base 3', +]; + +const FilterTab = ({ active, onPress, children }: FilterTabProps) => { + const colorScheme = active ? 'tertiaryElevation0' : 'backgroundSurfaceElevation0'; + + return ( + + ); +}; + +export const TradeableAssetsFilterTabs = ({ + visible, + animationDuration, +}: TradeableAssetsFilterTabsProps) => { + const [activeTab, setActiveTab] = useState(mockTabNames[0]); + + if (!visible) { + return null; + } + + return ( + item} + accessible={true} + accessibilityRole="tablist" + renderItem={({ item }) => ( + setActiveTab(item)}> + {item} + + )} + /> + ); +}; diff --git a/suite-native/module-trading/src/components/general/TradeableAssetsSheet/TradeableAssetsSheet.tsx b/suite-native/module-trading/src/components/general/TradeableAssetsSheet/TradeableAssetsSheet.tsx new file mode 100644 index 00000000000..1df690e695e --- /dev/null +++ b/suite-native/module-trading/src/components/general/TradeableAssetsSheet/TradeableAssetsSheet.tsx @@ -0,0 +1,185 @@ +import { ReactNode, useMemo } from 'react'; + +import { UnreachableCaseError } from '@suite-common/suite-utils'; +import { TokenAddress } from '@suite-common/wallet-types'; +import { BottomSheetFlashList, Box, Text } from '@suite-native/atoms'; +import { Translation } from '@suite-native/intl'; + +import { TradeAssetsListEmptyComponent } from './TradeAssetsListEmptyComponent'; +import { ASSET_ITEM_HEIGHT, TradeableAssetListItem } from './TradeableAssetListItem'; +import { TradeableAssetsSheetHeader } from './TradeableAssetsSheetHeader'; +import { TradeableAsset } from '../../../types'; + +export type TradeableAssetsSheetProps = { + isVisible: boolean; + onClose: () => void; + onAssetSelect: (symbol: TradeableAsset) => void; +}; + +type ListInnerItemShape = + // [type, text, key] + | ['sectionHeader', ReactNode, string] + // [type, data, isFavourite] + | ['asset', TradeableAsset, { isFavourite?: boolean; isFirst?: boolean; isLast?: boolean }]; + +const SECTION_HEADER_HEIGHT = 48 as const; + +const mockFavourites: TradeableAsset[] = [ + { symbol: 'btc' }, + { symbol: 'eth' }, + { + symbol: 'eth', + contractAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' as TokenAddress, + name: 'USDC', + }, +]; +const mockAssets: TradeableAsset[] = [ + { symbol: 'doge' }, + { symbol: 'sol' }, + { symbol: 'dsol' }, + { symbol: 'etc' }, + { symbol: 'xrp' }, + { symbol: 'ltc' }, + { symbol: 'arb' }, + { symbol: 'base' }, + { symbol: 'btc' }, + { symbol: 'eth' }, + { symbol: 'ada' }, + { symbol: 'bsc' }, +]; +const getMockFiatRate = () => Math.random() * 1000; +const getMockPriceChange = () => Math.random() * 3 - 1; + +const getEstimatedListHeight = (itemsCount: number) => + itemsCount * ASSET_ITEM_HEIGHT + 2 * SECTION_HEADER_HEIGHT; + +const transformToInnerFlatListData = ( + favourites: TradeableAsset[], + assetsData: TradeableAsset[], +): ListInnerItemShape[] => [ + [ + 'sectionHeader', + , + 'section_favourites', + ], + ...favourites.map( + (asset, index) => + [ + 'asset', + asset, + { + isFavourite: true, + isFirst: index === 0, + isLast: index === favourites.length - 1, + }, + ] as ListInnerItemShape, + ), + [ + 'sectionHeader', + , + 'section_all', + ], + ...assetsData.map( + (asset, index) => + [ + 'asset', + asset, + { + isFavourite: false, + isFirst: index === 0, + isLast: index === assetsData.length - 1, + }, + ] as ListInnerItemShape, + ), +]; + +const keyExtractor = (item: ListInnerItemShape) => { + switch (item[0]) { + case 'sectionHeader': + return item[2]; + + case 'asset': { + const [_, { symbol, contractAddress }, { isFavourite }] = item; + + return `asset_${symbol}_${contractAddress ?? ''}_${isFavourite ? 'favourite' : ''}`; + } + + default: + throw new UnreachableCaseError(item[0]); + } +}; + +const renderItem = (data: ListInnerItemShape, onAssetSelect: (asset: TradeableAsset) => void) => { + switch (data[0]) { + case 'sectionHeader': { + const text = data[1]; + + return ( + + + {text} + + + ); + } + + case 'asset': { + const [_, asset, { isFavourite, isFirst, isLast }] = data; + const toggleFavourite = () => { + // TODO: Implement + // eslint-disable-next-line no-console + console.log('Not implemented!'); + }; + + return ( + onAssetSelect(asset)} + onFavouritePress={toggleFavourite} + priceChange={getMockPriceChange()} + fiatRate={getMockFiatRate()} + isFavourite={isFavourite} + isFirst={isFirst} + isLast={isLast} + /> + ); + } + + default: + throw new UnreachableCaseError(data[0]); + } +}; + +export const TradeableAssetsSheet = ({ + isVisible, + onClose, + onAssetSelect, +}: TradeableAssetsSheetProps) => { + const onAssetSelectCallback = (asset: TradeableAsset) => { + onAssetSelect(asset); + onClose(); + }; + + const favourites = mockFavourites; + const assetsData = mockAssets; + const estimatedListHeight = getEstimatedListHeight(favourites.length + assetsData.length); + + const data: ListInnerItemShape[] = useMemo( + () => transformToInnerFlatListData(favourites, assetsData), + [favourites, assetsData], + ); + + return ( + + isVisible={isVisible} + onClose={onClose} + ListEmptyComponent={} + handleComponent={() => } + data={data} + keyExtractor={keyExtractor} + estimatedListHeight={estimatedListHeight} + estimatedItemSize={ASSET_ITEM_HEIGHT} + renderItem={({ item }) => renderItem(item, onAssetSelectCallback)} + /> + ); +}; diff --git a/suite-native/module-trading/src/components/general/TradeableAssetsSheet/TradeableAssetsSheetHeader.tsx b/suite-native/module-trading/src/components/general/TradeableAssetsSheet/TradeableAssetsSheetHeader.tsx new file mode 100644 index 00000000000..51480839030 --- /dev/null +++ b/suite-native/module-trading/src/components/general/TradeableAssetsSheet/TradeableAssetsSheetHeader.tsx @@ -0,0 +1,67 @@ +import { useState } from 'react'; +import Animated, { FadeIn, FadeOut, LinearTransition } from 'react-native-reanimated'; + +import { BottomSheetGrabber, VStack } from '@suite-native/atoms'; +import { Translation, useTranslate } from '@suite-native/intl'; +import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; + +import { SheetHeaderTitle } from '../SheetHeaderTitle'; +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 }) => ({ + 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} + /> + + + + + + ); +}; diff --git a/suite-native/module-trading/src/components/general/TradeableAssetsSheet/__tests__/FavouriteIcon.comp.test.tsx b/suite-native/module-trading/src/components/general/TradeableAssetsSheet/__tests__/FavouriteIcon.comp.test.tsx new file mode 100644 index 00000000000..f9d96cd125d --- /dev/null +++ b/suite-native/module-trading/src/components/general/TradeableAssetsSheet/__tests__/FavouriteIcon.comp.test.tsx @@ -0,0 +1,25 @@ +import { fireEvent, render } from '@suite-native/test-utils'; + +import { FavouriteIcon } from '../FavouriteIcon'; + +describe('FavouriteIcon', () => { + it('should have correct hint when marked as favourite', () => { + const { getByA11yHint } = render(); + expect(getByA11yHint('Remove from favourites')).toBeDefined(); + }); + + it('should have correct hint when not marked as favourite', () => { + const { getByA11yHint } = render(); + expect(getByA11yHint('Add to favourites')).toBeDefined(); + }); + + it('should call onPress callback', () => { + const pressSpy = jest.fn(); + const { getByA11yHint } = render(); + + const button = getByA11yHint('Add to favourites'); + fireEvent.press(button); + + expect(pressSpy).toHaveBeenCalledWith(); + }); +}); diff --git a/suite-native/module-trading/src/components/general/TradeableAssetsSheet/__tests__/TradeableAssetListItem.comp.test.tsx b/suite-native/module-trading/src/components/general/TradeableAssetsSheet/__tests__/TradeableAssetListItem.comp.test.tsx new file mode 100644 index 00000000000..773f604a5d0 --- /dev/null +++ b/suite-native/module-trading/src/components/general/TradeableAssetsSheet/__tests__/TradeableAssetListItem.comp.test.tsx @@ -0,0 +1,79 @@ +import { fireEvent, render } from '@suite-native/test-utils'; + +import { TradeableAsset } from '../../../../types'; +import { TradeableAssetListItem } from '../TradeableAssetListItem'; + +describe('TradeableAssetListItem', () => { + const btcAsset: TradeableAsset = { symbol: 'btc' }; + const usdcAsset = { + symbol: 'eth', + name: 'USDC', + contractAddress: 'contractAddress', + } as TradeableAsset; + + it('should render with correct labels when no name is specified', () => { + const { getByText } = render( + , + ); + + expect(getByText('Bitcoin')).toBeDefined(); + expect(getByText('BTC')).toBeDefined(); + expect(getByText('$100.00')).toBeDefined(); + expect(getByText('90.0%')).toBeDefined(); + }); + + it('should render with correct labels when name is specified', () => { + const { getByText } = render( + , + ); + + expect(getByText('USDC')).toBeDefined(); + expect(getByText('ETH')).toBeDefined(); + }); + + it('should call onPress callback when clicked', () => { + const onPress = jest.fn(); + const { getByText } = render( + , + ); + + fireEvent.press(getByText('BTC')); + + expect(onPress).toHaveBeenCalledWith(); + }); + + it('should call onFavouritePress when star is clicked', () => { + const onFavouritePress = jest.fn(); + const { getByAccessibilityHint } = render( + , + ); + + fireEvent.press(getByAccessibilityHint('Add to favourites')); + + expect(onFavouritePress).toHaveBeenCalledWith(); + }); +}); diff --git a/suite-native/module-trading/src/components/general/TradeableAssetsSheet/__tests__/TradeableAssetsSheetHeader.comp.test.tsx b/suite-native/module-trading/src/components/general/TradeableAssetsSheet/__tests__/TradeableAssetsSheetHeader.comp.test.tsx new file mode 100644 index 00000000000..b7657f0b86a --- /dev/null +++ b/suite-native/module-trading/src/components/general/TradeableAssetsSheet/__tests__/TradeableAssetsSheetHeader.comp.test.tsx @@ -0,0 +1,47 @@ +import { fireEvent, render } from '@suite-native/test-utils'; + +import { TradeableAssetsSheetHeader } from '../TradeableAssetsSheetHeader'; + +describe('TradeableAssetsSheetHeader', () => { + const renderComponent = (onClose = jest.fn()) => + render(); + + it('should display "Coins" and do not display tabs by default', () => { + const { getByText, queryByText } = renderComponent(); + + expect(getByText('Coins')).toBeDefined(); + expect(queryByText('All')).toBeNull(); + }); + + it('should display tabs after focusing search input', () => { + const { getByPlaceholderText, getByText, queryByText } = renderComponent(); + + fireEvent(getByPlaceholderText('Search'), 'focus'); + + expect(getByText('All')).toBeDefined(); + expect(queryByText('Coins')).toBeNull(); + }); + + it('should not display cancel button by default', () => { + const { queryByText } = renderComponent(); + + expect(queryByText('Cancel')).toBeNull(); + }); + + it('should display cancel button after focusing search input', () => { + const { getByPlaceholderText, getByText } = renderComponent(); + + fireEvent(getByPlaceholderText('Search'), 'focus'); + + expect(getByText('Cancel')).toBeDefined(); + }); + + it('should call onClose when close button is pressed ', () => { + const onClose = jest.fn(); + const { getByLabelText } = renderComponent(onClose); + + fireEvent.press(getByLabelText('Close')); + + expect(onClose).toHaveBeenCalled(); + }); +}); diff --git a/suite-native/module-trading/src/components/general/__tests__/PickerHeader.comp.test.tsx b/suite-native/module-trading/src/components/general/__tests__/PickerHeader.comp.test.tsx deleted file mode 100644 index 76f1783459e..00000000000 --- a/suite-native/module-trading/src/components/general/__tests__/PickerHeader.comp.test.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { Text } from '@suite-native/atoms'; -import { fireEvent, render } from '@suite-native/test-utils'; - -import { PickerHeader } from '../PickerHeader'; - -describe('PickerHeader', () => { - it('should render without children', () => { - const { getByText } = render(); - expect(getByText('Title')).toBeDefined(); - }); - - it('should render with children', () => { - const { getByText } = render( - - Child - , - ); - expect(getByText('Title')).toBeDefined(); - expect(getByText('Child')).toBeDefined(); - }); - - it('should render search input when `isSearchInputVisible`', () => { - const searchInputSpy = jest.fn(); - const { getByPlaceholderText } = render( - , - ); - const searchInput = getByPlaceholderText('Search placeholder'); - - fireEvent.changeText(searchInput, 'search'); - - expect(searchInputSpy).toHaveBeenCalledWith('search'); - }); -}); 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 00000000000..3ad9b5fd45c --- /dev/null +++ b/suite-native/module-trading/src/components/general/__tests__/SelectTradeableAssetButton.comp.test.tsx @@ -0,0 +1,34 @@ +import { render } from '@suite-native/test-utils'; + +import { SelectTradeableAssetButton } from '../SelectTradeableAssetButton'; + +describe('SelectTradeableAssetButton', () => { + it('should render "Select coin" when no network is selected', () => { + const { getByLabelText } = render( + , + ); + + const button = getByLabelText('Select coin'); + expect(button).toHaveTextContent(/^Select coin.$/); + }); + + it('should render TradeableAssetButton when network is selected', () => { + const { getByLabelText } = render( + , + ); + const button = getByLabelText('Select coin'); + expect(button).toHaveTextContent(/^ADA.$/); + }); + + it('should render asset name when specified in selectedAsset', () => { + const { getByLabelText } = render( + , + ); + + const button = getByLabelText('Select coin'); + expect(button).toHaveTextContent(/^USDC.$/); + }); +}); diff --git a/suite-native/module-trading/src/components/general/__tests__/TradeableAssetButton.comp.test.tsx b/suite-native/module-trading/src/components/general/__tests__/TradeableAssetButton.comp.test.tsx new file mode 100644 index 00000000000..d8e28d7bce0 --- /dev/null +++ b/suite-native/module-trading/src/components/general/__tests__/TradeableAssetButton.comp.test.tsx @@ -0,0 +1,36 @@ +import { fireEvent, render } from '@suite-native/test-utils'; + +import { TradeableAsset } from '../../../types'; +import { TradeableAssetButton } from '../TradeableAssetButton'; + +describe('TradeableAssetButton', () => { + const btcAsset: TradeableAsset = { symbol: 'btc' }; + + 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/hooks/__tests__/useAssetsSheetControls.test.ts b/suite-native/module-trading/src/hooks/__tests__/useAssetsSheetControls.test.ts new file mode 100644 index 00000000000..690ce6e2920 --- /dev/null +++ b/suite-native/module-trading/src/hooks/__tests__/useAssetsSheetControls.test.ts @@ -0,0 +1,52 @@ +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__/useTradeableAssetDominantColor.test.tsx b/suite-native/module-trading/src/hooks/__tests__/useTradeableAssetDominantColor.test.tsx new file mode 100644 index 00000000000..4b677dcf2d4 --- /dev/null +++ b/suite-native/module-trading/src/hooks/__tests__/useTradeableAssetDominantColor.test.tsx @@ -0,0 +1,43 @@ +import { NetworkSymbol } from '@suite-common/wallet-config'; +import { TokenAddress } from '@suite-common/wallet-types'; +import { BasicProviderForTests, renderHook } from '@suite-native/test-utils'; +import { useNativeStyles } from '@trezor/styles'; +import { CoinsColors, Colors } from '@trezor/theme'; + +import { TradeableAsset } from '../../types'; +import { useTradeableAssetDominantColor } from '../useTradeableAssetDominantColor'; + +describe('useTradeableAssetDominantColor', () => { + let colors: Colors; + let coinsColors: CoinsColors; + + const renderTradeableAssetDominantColorHook = ( + givenSymbol: NetworkSymbol, + givenContractAddress?: TokenAddress, + ) => + renderHook( + ({ symbol, contractAddress }: TradeableAsset) => + useTradeableAssetDominantColor(symbol, contractAddress), + { + initialProps: { symbol: givenSymbol, contractAddress: givenContractAddress }, + wrapper: BasicProviderForTests, + }, + ); + + beforeAll(() => { + const { result } = renderHook(useNativeStyles, { wrapper: BasicProviderForTests }); + ({ coinsColors, colors } = result.current.utils); + }); + + it('should return network color', () => { + const { result } = renderTradeableAssetDominantColorHook('btc'); + + expect(result.current).toBe(coinsColors.btc); + }); + + it('should fallback to backgroundNeutralBold for undefined networks', () => { + const { result } = renderTradeableAssetDominantColorHook('und' as NetworkSymbol); + + expect(result.current).toBe(colors.backgroundNeutralBold); + }); +}); diff --git a/suite-native/module-trading/src/hooks/useTradeableAssetDominantColor.ts b/suite-native/module-trading/src/hooks/useTradeableAssetDominantColor.ts new file mode 100644 index 00000000000..103be78ebcf --- /dev/null +++ b/suite-native/module-trading/src/hooks/useTradeableAssetDominantColor.ts @@ -0,0 +1,17 @@ +import { NetworkSymbol } from '@suite-common/wallet-config'; +import { TokenAddress } from '@suite-common/wallet-types'; +import { useNativeStyles } from '@trezor/styles'; + +export const useTradeableAssetDominantColor = ( + symbol: NetworkSymbol, + _contractAddress?: TokenAddress, +) => { + const { + utils: { colors, coinsColors }, + } = useNativeStyles(); + + const defaultColor = colors.backgroundNeutralBold; + const networkColor = coinsColors[symbol]; + + return networkColor ?? defaultColor; +}; 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 00000000000..9d8006f336c --- /dev/null +++ b/suite-native/module-trading/src/hooks/useTradeableAssetsSheetControls.ts @@ -0,0 +1,26 @@ +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/navigation/__tests__/TradingStackNavigator.comp.test.tsx b/suite-native/module-trading/src/navigation/__tests__/TradingStackNavigator.comp.test.tsx index 7f300814a22..36dc030b690 100644 --- a/suite-native/module-trading/src/navigation/__tests__/TradingStackNavigator.comp.test.tsx +++ b/suite-native/module-trading/src/navigation/__tests__/TradingStackNavigator.comp.test.tsx @@ -4,7 +4,7 @@ import { TradingStackNavigator } from '../TradingStackNavigator'; describe('TradingStackNavigator', () => { it('should render', async () => { - const { getByText } = renderWithStore(); - await waitFor(() => expect(getByText('Trading placeholder')).toBeDefined()); + const { getAllByText } = renderWithStore(); + await waitFor(() => expect(getAllByText('Buy').length).toBe(2)); }); }); diff --git a/suite-native/module-trading/src/screens/TradingScreen.tsx b/suite-native/module-trading/src/screens/TradingScreen.tsx index ca91d9e0e37..df0c7af79b6 100644 --- a/suite-native/module-trading/src/screens/TradingScreen.tsx +++ b/suite-native/module-trading/src/screens/TradingScreen.tsx @@ -5,6 +5,7 @@ import { DeviceManagerScreenHeader } from '@suite-native/device-manager'; import { Translation, useTranslate } from '@suite-native/intl'; import { Screen } from '@suite-native/navigation'; +import { AmountCard } from '../components/buy/AmountCard'; import { TradingOverviewRow } from '../components/general/TradingOverviewRow'; import { TradingRowDivider } from '../components/general/TradingRowDivider'; @@ -19,10 +20,9 @@ export const TradingScreen = () => { return ( }> + Trading placeholder + - - Trading placeholder - Date: Wed, 5 Feb 2025 17:45:01 +0100 Subject: [PATCH 5/6] feat(suite-native): Create buy screen visual stub --- suite-common/icons/generateIconFont.ts | 1 + .../iconFontsMobile/TrezorSuiteIcons.json | 21 ++-- .../iconFontsMobile/TrezorSuiteIcons.ttf | Bin 22572 -> 22748 bytes suite-native/intl/src/en.ts | 5 + .../module-trading/assets/InvityLogo.png | Bin 0 -> 1175 bytes .../src/components/buy/AmountCard.tsx | 33 ------ .../src/components/buy/BuyCard.tsx | 93 +++++++++++++++++ .../src/components/buy/PaymentCard.tsx | 47 +++++++++ .../src/components/general/TradingFooter.tsx | 34 +++++++ .../components/general/TradingRowDivider.tsx | 13 --- .../src/screens/TradingScreen.tsx | 94 ++++-------------- 11 files changed, 208 insertions(+), 133 deletions(-) create mode 100644 suite-native/module-trading/assets/InvityLogo.png delete mode 100644 suite-native/module-trading/src/components/buy/AmountCard.tsx create mode 100644 suite-native/module-trading/src/components/buy/BuyCard.tsx create mode 100644 suite-native/module-trading/src/components/buy/PaymentCard.tsx create mode 100644 suite-native/module-trading/src/components/general/TradingFooter.tsx delete mode 100644 suite-native/module-trading/src/components/general/TradingRowDivider.tsx diff --git a/suite-common/icons/generateIconFont.ts b/suite-common/icons/generateIconFont.ts index 613062fac5f..52569a72d8e 100644 --- a/suite-common/icons/generateIconFont.ts +++ b/suite-common/icons/generateIconFont.ts @@ -17,6 +17,7 @@ const usedIcons = [ 'arrowLineUpRight', 'arrowRight', 'arrowsCounterClockwise', + 'arrowsDownUp', 'arrowsLeftRight', 'arrowSquareOut', 'arrowUp', diff --git a/suite-common/icons/iconFontsMobile/TrezorSuiteIcons.json b/suite-common/icons/iconFontsMobile/TrezorSuiteIcons.json index ae44b085364..dfae97ca1a0 100644 --- a/suite-common/icons/iconFontsMobile/TrezorSuiteIcons.json +++ b/suite-common/icons/iconFontsMobile/TrezorSuiteIcons.json @@ -92,14 +92,15 @@ "bookmarkSimple": 61787, "backspace": 61788, "arrowsLeftRight": 61789, - "arrowsCounterClockwise": 61790, - "arrowUpRight": 61791, - "arrowUp": 61792, - "arrowURightDown": 61793, - "arrowSquareOut": 61794, - "arrowRight": 61795, - "arrowLineUpRight": 61796, - "arrowLineUp": 61797, - "arrowLineDown": 61798, - "arrowDown": 61799 + "arrowsDownUp": 61790, + "arrowsCounterClockwise": 61791, + "arrowUpRight": 61792, + "arrowUp": 61793, + "arrowURightDown": 61794, + "arrowSquareOut": 61795, + "arrowRight": 61796, + "arrowLineUpRight": 61797, + "arrowLineUp": 61798, + "arrowLineDown": 61799, + "arrowDown": 61800 } diff --git a/suite-common/icons/iconFontsMobile/TrezorSuiteIcons.ttf b/suite-common/icons/iconFontsMobile/TrezorSuiteIcons.ttf index 1de8a6ca8bd58ed98ab29ea5db31f0969db3eeba..96d6f872fcbe48345fa23b3e880f536e3de0be1d 100644 GIT binary patch delta 505 zcmXv~O-LJ25T1EI>m!LMHoHo(wY!=~q4X!%O* z6;C1_3|^!(Ec8$UEmWk39&!-rLD1Y4^imL{ItluQZ{E!JeKQZ<&R0AvVWBf$0Ki26 z7#(x%O!I|c0GzmnmdTlgiR$3TQ-EL&VBmu@ZYM`pH(!zWm=Je}U?aFjb(zrRxO2H& zh;?B=2a~K%7 zfMzgU%1x{&V60|X2b7Zl@)hzDb5oZ;l#;*0zz`6_z`#^ikY8M~>A_5(VL?|I7+6Cm z?osB=U|2~Zcz|*YEKfir5TtKD#CnZo@-nWyq6ZY7 zDe@`0DfTFCQGBJOpcJN5!@#h)f_t08=6yc5*?=yX6?|cHPRKRp$^H?&%s|ZmF%Vn0 diff --git a/suite-native/intl/src/en.ts b/suite-native/intl/src/en.ts index d3c335c50dc..63a5af9ccbe 100644 --- a/suite-native/intl/src/en.ts +++ b/suite-native/intl/src/en.ts @@ -1272,11 +1272,16 @@ export const en = { }, moduleTrading: { tradingScreen: { + buyTitle: 'Buy', receiveAccount: 'Receive account', paymentMethod: 'Payment method', countryOfResidence: 'Country of residence', provider: 'Provider', continueButton: 'Continue', + footer: { + poweredBy: 'Powered by', + learnMore: 'Learn more', + }, }, selectCoin: { buttonTitle: 'Select coin', diff --git a/suite-native/module-trading/assets/InvityLogo.png b/suite-native/module-trading/assets/InvityLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..a1b259c29d42e9edb0d1dd7dc2617368e12d9bec GIT binary patch literal 1175 zcmV;I1Zew-P)#GD}V2{0$H&1W6w1c4J6b}RtfCkUJ%$_XGRKsSil1I~NSjA;*J zNtSI@3hPy=H2-h9?oiIgZ&qC|-j?*J9qK(yQK`qtJ~o#{%YGP=6Dx{)=YDjNt0 zwmBY;zcY<@jMrL(yCoY42(%^zU?YWF6bg3>@OQ!X_IAMZ#l=M|?-;o@v`(k<2d`je zph{(MeSO`r`GbRl6YJLqq<7G!k7QljJB9=Mho<}<<+~XAY?sa0}A> z7=(By|HuYTzc%Z*gR8bf)g(k+%{M0cwp2EzU?l-)A4o=k!>!8J!6BT}bkE^1uolJCYU_V9cpTLAwUJ61#g_-g2zTt7cJfr;-AUOTH1F*r{S! z1P+nIVY)ulHO0dL^EOqRXok0VfJSFDtEZ=@JD=9T{P&HiCTfa<5zQ0*Dtu_dGP}r{ z)ehq#B3psBU);eMPB}%9qyXdIkhfzQbOq$q&~6QFDOkO}mWM;A^Y|dcnR+P*5YRXJ zJ)5Tq(?97pHn1lgvkbM{FyraPM%rhu3XU}du50kPrAXHo8H&Bpa+i!K*WsF_)>LCn zQ(2cR`!ZP58`)bnU&@~)F#es<2juXwTR;zT(ZTrY;7{o<2Ro$Kx2x#xQ%1(W)@r{?j!5%)|AjvIx zFK$3Og6fY=dTpCURxsuTkI*SzCbpef6QYtH);qBRx=8VJ7jw z<3ba)hcT})ORcrJH!^oTjMc@7v}?G0oYUt9eR?u`>2#hn&nn%*qvS)XWq6LF#3He6 z?2gA1qbvZ!VdjHU;7NU!y3ge-`}h-cP=54pSbDSa`)bL9!lXRz+BGs-E#hx%G6& { - const { - isTradeableAssetsSheetVisible, - showTradeableAssetsSheet, - hideTradeableAssetsSheet, - selectedTradeableAsset, - setSelectedTradeableAsset, - } = useTradeableAssetsSheetControls(); - - return ( - - - - - - - ); -}; diff --git a/suite-native/module-trading/src/components/buy/BuyCard.tsx b/suite-native/module-trading/src/components/buy/BuyCard.tsx new file mode 100644 index 00000000000..c01c055db52 --- /dev/null +++ b/suite-native/module-trading/src/components/buy/BuyCard.tsx @@ -0,0 +1,93 @@ +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 { TradingOverviewRow } from '../general/TradingOverviewRow'; + +const notImplementedCallback = () => { + // eslint-disable-next-line no-console + console.log('Not implemented'); +}; + +const buySectionStyle = prepareNativeStyle(({ borders, colors, spacings }) => ({ + borderBottomWidth: borders.widths.small, + borderBottomColor: colors.backgroundSurfaceElevation0, + padding: spacings.sp20, +})); + +export const BuyCard = () => { + const { translate } = useTranslate(); + const { FiatAmountFormatter, CryptoAmountFormatter } = useFormatters(); + const { applyStyle } = useNativeStyles(); + + const { + isTradeableAssetsSheetVisible, + showTradeableAssetsSheet, + hideTradeableAssetsSheet, + selectedTradeableAsset, + setSelectedTradeableAsset, + } = useTradeableAssetsSheetControls(); + + return ( + + + + + + + + + 0.0 + + + + + {selectedTradeableAsset?.symbol ? ( + + ) : ( + '-' + )} + + + + + + + + + + + + + Bitcoin Vault + + + 3QJmV3qfvL9SuYo34YihAf3sRCW3qSinyC + + + + + + ); +}; diff --git a/suite-native/module-trading/src/components/buy/PaymentCard.tsx b/suite-native/module-trading/src/components/buy/PaymentCard.tsx new file mode 100644 index 00000000000..29c4bf67dd2 --- /dev/null +++ b/suite-native/module-trading/src/components/buy/PaymentCard.tsx @@ -0,0 +1,47 @@ +import { Box, Button, Card, Text } from '@suite-native/atoms'; +import { Translation, useTranslate } from '@suite-native/intl'; + +import { TradingOverviewRow } from '../general/TradingOverviewRow'; + +const notImplementedCallback = () => { + // eslint-disable-next-line no-console + console.log('Not implemented'); +}; + +export const PaymentCard = () => { + const { translate } = useTranslate(); + + return ( + + + + Credit card + + + + + Czech Republic + + + + + Anycoin + + + + + + + ); +}; diff --git a/suite-native/module-trading/src/components/general/TradingFooter.tsx b/suite-native/module-trading/src/components/general/TradingFooter.tsx new file mode 100644 index 00000000000..8e6d24b6b32 --- /dev/null +++ b/suite-native/module-trading/src/components/general/TradingFooter.tsx @@ -0,0 +1,34 @@ +import { useMemo } from 'react'; +import { TouchableOpacity } from 'react-native'; + +import { Button, HStack, Image, Text } from '@suite-native/atoms'; +import { Translation } from '@suite-native/intl'; +import { useOpenLink } from '@suite-native/link'; + +export const TradingFooter = () => { + const openLink = useOpenLink(); + + const imageSource = useMemo(() => require('../../../assets/InvityLogo.png'), []); + const openLinkToInvity = () => openLink('https://invity.io'); + + return ( + + + + + + + + + + + + ); +}; diff --git a/suite-native/module-trading/src/components/general/TradingRowDivider.tsx b/suite-native/module-trading/src/components/general/TradingRowDivider.tsx deleted file mode 100644 index f5b60b90f78..00000000000 --- a/suite-native/module-trading/src/components/general/TradingRowDivider.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Box } from '@suite-native/atoms'; -import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; - -const dividerStyle = prepareNativeStyle(({ borders, colors }) => ({ - height: borders.widths.small, - backgroundColor: colors.backgroundSurfaceElevation0, -})); - -export const TradingRowDivider = () => { - const { applyStyle } = useNativeStyles(); - - return ; -}; diff --git a/suite-native/module-trading/src/screens/TradingScreen.tsx b/suite-native/module-trading/src/screens/TradingScreen.tsx index df0c7af79b6..7d81ff4b2a9 100644 --- a/suite-native/module-trading/src/screens/TradingScreen.tsx +++ b/suite-native/module-trading/src/screens/TradingScreen.tsx @@ -1,81 +1,21 @@ -import React from 'react'; - -import { Box, Button, Card, Text, VStack } from '@suite-native/atoms'; +import { Text, VStack } from '@suite-native/atoms'; import { DeviceManagerScreenHeader } from '@suite-native/device-manager'; -import { Translation, useTranslate } from '@suite-native/intl'; +import { Translation } from '@suite-native/intl'; import { Screen } from '@suite-native/navigation'; -import { AmountCard } from '../components/buy/AmountCard'; -import { TradingOverviewRow } from '../components/general/TradingOverviewRow'; -import { TradingRowDivider } from '../components/general/TradingRowDivider'; - -const notImplementedCallback = () => { - // eslint-disable-next-line no-console - console.log('Not implemented'); -}; - -export const TradingScreen = () => { - const { translate } = useTranslate(); +import { BuyCard } from '../components/buy/BuyCard'; +import { PaymentCard } from '../components/buy/PaymentCard'; +import { TradingFooter } from '../components/general/TradingFooter'; - return ( - }> - - Trading placeholder - - - - - - - Bitcoin Vault - - - 3QJmV3qfvL9SuYo34YihAf3sRCW3qSinyC - - - - - - - - Credit card - - - - - Czech Republic - - - - - Anycoin - - - - - - - - - ); -}; +export const TradingScreen = () => ( + }> + + + + + + + + + +); From ea9e8ae6fea8255645f735778856b52fb841626a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jirka=20Ba=C5=BEant?= Date: Thu, 6 Feb 2025 19:36:17 +0100 Subject: [PATCH 6/6] feat(suite-native): Mobile Trade - Country picker visual stub --- suite-native/intl/src/en.ts | 7 ++ .../src/components/buy/BuyCard.tsx | 33 ++------ .../buy/CountryOfResidencePicker.tsx | 43 ++++++++++ .../src/components/buy/PaymentCard.tsx | 20 ++--- .../components/buy/TradeableAssetPicker.tsx | 23 ++++++ .../CountryListEmptyComponent.tsx | 10 +++ .../general/CountrySheet/CountryListItem.tsx | 40 ++++++++++ .../general/CountrySheet/CountrySheet.tsx | 70 ++++++++++++++++ .../general/SearchableSheetHeader.tsx | 79 +++++++++++++++++++ .../TradeAssetsListEmptyComponent.tsx | 29 ++----- .../TradeableAssetsSheetHeader.tsx | 49 +++--------- .../general/TradingEmptyComponent.tsx | 31 ++++++++ .../__tests__/useAssetsSheetControls.test.ts | 52 ------------ .../__tests__/useTradeSheetControls.test.ts | 52 ++++++++++++ .../src/hooks/useTradeSheetControls.ts | 22 ++++++ .../hooks/useTradeableAssetsSheetControls.ts | 26 ------ .../src/screens/TradingScreen.tsx | 10 ++- suite-native/module-trading/src/types.ts | 7 ++ 18 files changed, 423 insertions(+), 180 deletions(-) create mode 100644 suite-native/module-trading/src/components/buy/CountryOfResidencePicker.tsx create mode 100644 suite-native/module-trading/src/components/buy/TradeableAssetPicker.tsx create mode 100644 suite-native/module-trading/src/components/general/CountrySheet/CountryListEmptyComponent.tsx create mode 100644 suite-native/module-trading/src/components/general/CountrySheet/CountryListItem.tsx create mode 100644 suite-native/module-trading/src/components/general/CountrySheet/CountrySheet.tsx create mode 100644 suite-native/module-trading/src/components/general/SearchableSheetHeader.tsx create mode 100644 suite-native/module-trading/src/components/general/TradingEmptyComponent.tsx delete mode 100644 suite-native/module-trading/src/hooks/__tests__/useAssetsSheetControls.test.ts create mode 100644 suite-native/module-trading/src/hooks/__tests__/useTradeSheetControls.test.ts create mode 100644 suite-native/module-trading/src/hooks/useTradeSheetControls.ts delete mode 100644 suite-native/module-trading/src/hooks/useTradeableAssetsSheetControls.ts 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; +};