From 819d98e768f242618d5b0e51500a6751f9f01f75 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] feat(suite-native): Mobile Trade: Token picker modal visual stub --- suite-common/icons/generateIconFont.ts | 2 + .../iconFontsMobile/TrezorSuiteIcons.json | 162 +++--- .../iconFontsMobile/TrezorSuiteIcons.ttf | Bin 22196 -> 22572 bytes suite-native/atoms/src/Input/SearchInput.tsx | 122 +++-- suite-native/intl/src/en.ts | 16 + suite-native/module-trading/jest.config.js | 5 + suite-native/module-trading/package.json | 7 +- .../src/components/buy/AmountCard.tsx | 33 ++ .../components/general/PickerCloseButton.tsx | 49 +- .../src/components/general/PickerHeader.tsx | 51 -- .../general/SearchInputWithCancel.tsx | 49 ++ .../general/SelectTradeableAssetButton.tsx | 37 ++ .../general/TradeableAssetButton.tsx | 78 +++ .../TradeableAssetsSheet/FavouriteIcon.tsx | 25 + .../TradeAssetsListEmptyComponent.tsx | 30 ++ .../TradeableAssetListItem.tsx | 88 ++++ .../TradeableAssetsFilterTabs.tsx | 63 +++ .../TradeableAssetsSheet.tsx | 201 ++++++++ .../TradeableAssetsSheetHeader.tsx | 57 +++ .../__tests__/FavouriteIcon.comp.test.tsx | 25 + .../TradeableAssetListItem.comp.test.tsx | 79 +++ .../TradeableAssetsSheetHeader.comp.test.tsx | 37 ++ .../__tests__/PickerHeader.comp.test.tsx | 38 -- .../SelectTradeableAssetButton.comp.test.tsx | 22 + .../TradeableAssetButton.comp.test.tsx | 36 ++ .../__tests__/useAssetsSheetControls.test.ts | 52 ++ .../useTradeableAssetDominantColor.test.tsx | 92 ++++ .../hooks/useTradeableAssetDominantColor.ts | 43 ++ .../hooks/useTradeableAssetsSheetControls.ts | 26 + .../src/screens/TradingScreen.tsx | 9 +- suite-native/module-trading/src/types.ts | 9 + suite-native/test-utils/src/expoMock.js | 11 + yarn.lock | 466 +++++++++++++++++- 33 files changed, 1785 insertions(+), 235 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/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/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/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-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/intl/src/en.ts b/suite-native/intl/src/en.ts index 8e110e5b1fd..7c668595f49 100644 --- a/suite-native/intl/src/en.ts +++ b/suite-native/intl/src/en.ts @@ -1245,6 +1245,22 @@ export const en = { description: 'We currently support staking as view-only in Trezor Suite Lite.', }, }, + moduleTrading: { + 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', + }, }; export type Translations = typeof en; 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..c776dcaead6 100644 --- a/suite-native/module-trading/package.json +++ b/suite-native/module-trading/package.json @@ -8,14 +8,17 @@ "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-image-colors": "^2.4.0", + "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 index 20e9c48bb42..648da059ded 100644 --- a/suite-native/module-trading/src/components/general/PickerCloseButton.tsx +++ b/suite-native/module-trading/src/components/general/PickerCloseButton.tsx @@ -1,13 +1,50 @@ -import { Button, ButtonProps } from '@suite-native/atoms'; +import { useMemo } from 'react'; + +import { LinearGradient } from 'expo-linear-gradient'; + +import { hexToRgba } from '@suite-common/suite-utils'; +import { Box, Button, ButtonProps, VStack } from '@suite-native/atoms'; import { Translation } from '@suite-native/intl'; +import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; export type PickerCloseButtonProps = Omit< ButtonProps, 'children' | 'colorScheme' | 'viewLeft' | 'viewRight' >; -export const PickerCloseButton = (props: PickerCloseButtonProps) => ( - -); +const spacerStyle = prepareNativeStyle(({ colors, spacings }) => ({ + backgroundColor: colors.backgroundSurfaceElevation0, + height: spacings.sp32, +})); + +export const PICKER_BUTTON_HEIGHT = 52 as const; + +const GRADIENT_START = { x: 0.5, y: 0.5 } as const; +const GRADIENT_END = { x: 0.5, y: 0 } as const; + +export const PickerCloseButton = (props: PickerCloseButtonProps) => { + const { + utils: { + colors: { backgroundSurfaceElevation0 }, + }, + applyStyle, + } = useNativeStyles(); + + const gradientColors = useMemo<[string, string]>( + () => [backgroundSurfaceElevation0, hexToRgba(backgroundSurfaceElevation0, 0.1)], + [backgroundSurfaceElevation0], + ); + + return ( + + + + + + + + + ); +}; 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..1b55c13ac6f --- /dev/null +++ b/suite-native/module-trading/src/components/general/SearchInputWithCancel.tsx @@ -0,0 +1,49 @@ +import { useRef, useState } from 'react'; +import { TextInput } from 'react-native'; + +import { Box, HStack, SearchInput, SearchInputProps, TextButton } from '@suite-native/atoms'; +import { Translation, useTranslate } from '@suite-native/intl'; + +export type SearchInputWithCancelProps = Omit; + +const noOp = () => {}; + +export const SearchInputWithCancel = ({ + onFocus = noOp, + onBlur = noOp, + ...props +}: SearchInputWithCancelProps) => { + 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..2d186482636 --- /dev/null +++ b/suite-native/module-trading/src/components/general/SelectTradeableAssetButton.tsx @@ -0,0 +1,37 @@ +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/TradeableAssetButton.tsx b/suite-native/module-trading/src/components/general/TradeableAssetButton.tsx new file mode 100644 index 00000000000..f112c0d65d4 --- /dev/null +++ b/suite-native/module-trading/src/components/general/TradeableAssetButton.tsx @@ -0,0 +1,78 @@ +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 buttonStyle = prepareNativeStyle(({ spacings }) => ({ + height: 36, + padding: spacings.sp4, + paddingRight: spacings.sp12, + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + gap: spacings.sp8, +})); + +const gradientBackgroundStyle = prepareNativeStyle(({ borders }) => ({ + borderRadius: borders.radii.round, + borderWidth: borders.widths.small, + borderColor: 'rgba(0, 0, 0, 0.06)', +})); + +const GRADIEN_START = { x: 0, y: 0.5 } as const; +const GRADIEN_END = { x: 1, y: 0.5 } as const; + +export const TradeableAssetButton = ({ + asset: { symbol, contractAddress }, + 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], + ); + + return ( + + + + + + + {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..228f338aef8 --- /dev/null +++ b/suite-native/module-trading/src/components/general/TradeableAssetsSheet/FavouriteIcon.tsx @@ -0,0 +1,25 @@ +import { Pressable } from 'react-native'; + +import { Icon, IconColor, IconName } from '@suite-native/icons'; +import { useTranslate } from '@suite-native/intl'; + +export type FavouriteIconProps = { + isFavourite: boolean; + onPress: () => void; +}; + +export const FavouriteIcon = ({ isFavourite, onPress }: FavouriteIconProps) => { + const { translate } = useTranslate(); + + const hint: string = isFavourite + ? translate('moduleTrading.tradeableAssetsSheet.favouritesRemove') + : translate('moduleTrading.tradeableAssetsSheet.favouritesAdd'); + const iconName: IconName = isFavourite ? 'starFilled' : 'star'; + const iconColor: IconColor = isFavourite ? 'backgroundAlertYellowBold' : 'textSubdued'; + + 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..12df3893a95 --- /dev/null +++ b/suite-native/module-trading/src/components/general/TradeableAssetsSheet/TradeAssetsListEmptyComponent.tsx @@ -0,0 +1,30 @@ +import { Text, VStack } from '@suite-native/atoms'; +import { Translation } from '@suite-native/intl'; +import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; + +import { SHEET_HEADER_HEIGHT } from './TradeableAssetsSheetHeader'; +import { PICKER_BUTTON_HEIGHT } from '../PickerCloseButton'; + +const emptyComponentStyle = prepareNativeStyle(({ spacings }) => ({ + paddingTop: SHEET_HEADER_HEIGHT + spacings.sp12, + paddingBottom: PICKER_BUTTON_HEIGHT + spacings.sp12, + paddingHorizontal: 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..74166df1877 --- /dev/null +++ b/suite-native/module-trading/src/components/general/TradeableAssetsSheet/TradeableAssetListItem.tsx @@ -0,0 +1,88 @@ +import { Pressable } from 'react-native'; + +import { useFormatters } from '@suite-common/formatters'; +import { 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; +}; + +export const ASSET_ITEM_HEIGHT = 68; + +const itemStyle = prepareNativeStyle(({ colors, spacings, borders }, { isFirst, isLast }) => ({ + backgroundColor: colors.backgroundSurfaceElevation1, + paddingHorizontal: spacings.sp12, + borderTopRightRadius: isFirst ? borders.radii.r20 : 0, + borderTopLeftRadius: isFirst ? borders.radii.r20 : 0, + borderBottomRightRadius: isLast ? borders.radii.r20 : 0, + borderBottomLeftRadius: isLast ? borders.radii.r20 : 0, +})); + +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..c38e46f4f9b --- /dev/null +++ b/suite-native/module-trading/src/components/general/TradeableAssetsSheet/TradeableAssetsFilterTabs.tsx @@ -0,0 +1,63 @@ +import { useState } from 'react'; +import Animated, { FadeIn } 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 ( + ( + 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..4f4f4ec1261 --- /dev/null +++ b/suite-native/module-trading/src/components/general/TradeableAssetsSheet/TradeableAssetsSheet.tsx @@ -0,0 +1,201 @@ +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 { SHEET_HEADER_HEIGHT, TradeableAssetsSheetHeader } from './TradeableAssetsSheetHeader'; +import { TradeableAsset } from '../../../types'; +import { PICKER_BUTTON_HEIGHT, PickerCloseButton } from '../PickerCloseButton'; + +export type TradeableAssetsSheetProps = { + isVisible: boolean; + onClose: () => void; + onAssetSelect: (symbol: TradeableAsset) => void; +}; + +type ListInnerItemShape = + // [type, height, key] + | ['spacer', number, string] + // [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 + + SHEET_HEADER_HEIGHT + + PICKER_BUTTON_HEIGHT + + 2 * SECTION_HEADER_HEIGHT; + +const transformToInnerFlatListData = ( + favourites: TradeableAsset[], + assetsData: TradeableAsset[], +): ListInnerItemShape[] => [ + ['spacer', SHEET_HEADER_HEIGHT, 'spacer_top'], + [ + '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, + ), + ['spacer', PICKER_BUTTON_HEIGHT, 'spacer_bottom'], +]; + +const keyExtractor = (item: ListInnerItemShape) => { + switch (item[0]) { + case 'spacer': + 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 'spacer': { + const height = data[1]; + + return ; + } + + 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} + isCloseDisplayed={false} + ListEmptyComponent={} + StickyListHeaderComponent={} + StickyListFooterComponent={} + data={data} + keyExtractor={keyExtractor} + estimatedListHeight={estimatedListHeight} + 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..abd0970f088 --- /dev/null +++ b/suite-native/module-trading/src/components/general/TradeableAssetsSheet/TradeableAssetsSheetHeader.tsx @@ -0,0 +1,57 @@ +import { useMemo, useState } from 'react'; +import Animated, { FadeIn } from 'react-native-reanimated'; + +import { LinearGradient } from 'expo-linear-gradient'; + +import { hexToRgba } from '@suite-common/suite-utils'; +import { Text, VStack } from '@suite-native/atoms'; +import { Translation } from '@suite-native/intl'; +import { useNativeStyles } from '@trezor/styles'; + +import { TradeableAssetsFilterTabs } from './TradeableAssetsFilterTabs'; +import { SearchInputWithCancel } from '../SearchInputWithCancel'; + +export const SHEET_HEADER_HEIGHT = 110 as const; +const FOCUS_ANIMATION_DURATION = 500 as const; + +const GRADIENT_START = { x: 0.5, y: 0.9 } as const; +const GRADIENT_END = { x: 0.5, y: 1 } as const; + +export const TradeableAssetsSheetHeader = () => { + const { + utils: { + colors: { backgroundSurfaceElevation0 }, + }, + } = useNativeStyles(); + const [isFilterActive, setIsFilterActive] = useState(false); + const [filterValue, setFilterValue] = useState(''); + + const gradientColors = useMemo<[string, string]>( + () => [backgroundSurfaceElevation0, hexToRgba(backgroundSurfaceElevation0, 0.1)], + [backgroundSurfaceElevation0], + ); + + 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..793d05be410 --- /dev/null +++ b/suite-native/module-trading/src/components/general/TradeableAssetsSheet/__tests__/TradeableAssetsSheetHeader.comp.test.tsx @@ -0,0 +1,37 @@ +import { fireEvent, render } from '@suite-native/test-utils'; + +import { TradeableAssetsSheetHeader } from '../TradeableAssetsSheetHeader'; + +describe('TradeableAssetsSheetHeader', () => { + it('should display "Coins" and do not display tabs by default', () => { + const { getByText, queryByText } = render(); + + expect(getByText('Coins')).toBeDefined(); + expect(queryByText('All')).toBeNull(); + }); + + it('should display tabs after focusing search input', () => { + const { getByPlaceholderText, getByText, queryByText } = render( + , + ); + + fireEvent(getByPlaceholderText('Search'), 'focus'); + + expect(getByText('All')).toBeDefined(); + expect(queryByText('Coins')).toBeNull(); + }); + + it('should not display cancel button by default', () => { + const { queryByText } = render(); + + expect(queryByText('Cancel')).toBeNull(); + }); + + it('should display cancel button after focusing search input', () => { + const { getByPlaceholderText, getByText } = render(); + + fireEvent(getByPlaceholderText('Search'), 'focus'); + + expect(getByText('Cancel')).toBeDefined(); + }); +}); 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..c50ffc75abb --- /dev/null +++ b/suite-native/module-trading/src/components/general/__tests__/SelectTradeableAssetButton.comp.test.tsx @@ -0,0 +1,22 @@ +import { render } from '@suite-native/test-utils'; + +import { SelectTradeableAssetButton } from '../SelectTradeableAssetButton'; + +describe('SelectTradeableAssetButton', () => { + it('should render "Select coin" when no network is selected', () => { + const { getByText } = render( + , + ); + + expect(getByText('Select coin')).toBeDefined(); + }); + + it('should render TradeableAssetButton when network is selected', () => { + const { queryByText } = render( + , + ); + + expect(queryByText('Select coin')).toBeNull(); + expect(queryByText('ADA')).toBeDefined(); + }); +}); 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..d16c51fefb7 --- /dev/null +++ b/suite-native/module-trading/src/hooks/__tests__/useTradeableAssetDominantColor.test.tsx @@ -0,0 +1,92 @@ +import ImageColors from 'react-native-image-colors'; + +import { NetworkSymbol } from '@suite-common/wallet-config'; +import { TokenAddress } from '@suite-common/wallet-types'; +import { BasicProviderForTests, renderHook, waitFor } from '@suite-native/test-utils'; +import { useNativeStyles } from '@trezor/styles'; +import { CoinsColors, Colors } from '@trezor/theme'; + +import { TradeableAsset } from '../../types'; +import { useTradeableAssetDominantColor } from '../useTradeableAssetDominantColor'; + +const MOCK_CONTRACT_ADDR = 'mockContractAddress' as TokenAddress; + +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); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('with contract address undefined', () => { + 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); + }); + }); + + describe('with contract address defined', () => { + it('should return network color until resolved', () => { + const { result } = renderTradeableAssetDominantColorHook('btc', MOCK_CONTRACT_ADDR); + + expect(result.current).toBe(coinsColors.btc); + }); + + it('should return resolved color on iOS', async () => { + const { result } = renderTradeableAssetDominantColorHook('btc', MOCK_CONTRACT_ADDR); + + await waitFor(() => expect(result.current).toBe('#333333')); + }); + + it('should return resolved color on Android', async () => { + ImageColors.getColors = jest + .fn() + .mockResolvedValue({ platform: 'android', dominant: '#444444' }); + const { result } = renderTradeableAssetDominantColorHook('btc', MOCK_CONTRACT_ADDR); + + await waitFor(() => expect(result.current).toBe('#444444')); + }); + + it('should fallback to network color on error', async () => { + ImageColors.getColors = jest + .fn() + .mockResolvedValueOnce({ platform: 'android', dominant: '#111111' }) + .mockRejectedValue(new Error('Test error')); + const { result, rerender } = renderTradeableAssetDominantColorHook( + 'btc', + MOCK_CONTRACT_ADDR, + ); + await waitFor(() => expect(result.current).toBe('#111111')); + + rerender({ symbol: 'eth', contractAddress: MOCK_CONTRACT_ADDR }); + + await waitFor(() => expect(result.current).toBe(coinsColors.eth)); + }); + }); +}); 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..2ee7ac2c157 --- /dev/null +++ b/suite-native/module-trading/src/hooks/useTradeableAssetDominantColor.ts @@ -0,0 +1,43 @@ +import { useEffect, useState } from 'react'; +import { getColors } from 'react-native-image-colors'; + +import { NetworkSymbol } from '@suite-common/wallet-config'; +import { TokenAddress } from '@suite-common/wallet-types'; +import { getCryptoIconUrl } from '@suite-native/icons'; +import { useNativeStyles } from '@trezor/styles'; + +export const useTradeableAssetDominantColor = ( + symbol: NetworkSymbol, + contractAddress?: TokenAddress, +) => { + const { utils } = useNativeStyles(); + const defaultColor = utils.colors.backgroundNeutralBold; + const { coinsColors } = utils; + const [color, setColor] = useState(defaultColor); + + useEffect(() => { + (async () => { + const networkColor = coinsColors[symbol]; + setColor(networkColor ?? defaultColor); + + if (!contractAddress) { + return; + } + + try { + const coinUrl = getCryptoIconUrl(symbol, contractAddress); + const coinColors = await getColors(coinUrl, { + fallback: defaultColor, + cache: true, + }); + setColor(coinColors.platform === 'ios' ? coinColors.primary : coinColors.dominant); + } catch (error) { + console.error('Error getting dominant color', error); + } + })(); + + return () => {}; + }, [symbol, contractAddress, defaultColor, coinsColors]); + + return color; +}; 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/screens/TradingScreen.tsx b/suite-native/module-trading/src/screens/TradingScreen.tsx index 1d4842c8b93..ce79ae9a4a6 100644 --- a/suite-native/module-trading/src/screens/TradingScreen.tsx +++ b/suite-native/module-trading/src/screens/TradingScreen.tsx @@ -1,13 +1,14 @@ import React from 'react'; -import { Card, Text } from '@suite-native/atoms'; +import { Text } from '@suite-native/atoms'; import { DeviceManagerScreenHeader } from '@suite-native/device-manager'; import { Screen } from '@suite-native/navigation'; +import { AmountCard } from '../components/buy/AmountCard'; + export const TradingScreen = () => ( }> - - Trading placeholder - + Trading placeholder + ); diff --git a/suite-native/module-trading/src/types.ts b/suite-native/module-trading/src/types.ts new file mode 100644 index 00000000000..870227d38b1 --- /dev/null +++ b/suite-native/module-trading/src/types.ts @@ -0,0 +1,9 @@ +import { NetworkSymbol } from '@suite-common/wallet-config'; +import { TokenAddress } from '@suite-common/wallet-types'; + +// NOTE: in production code we probably want to use `TokenInfoBranded` or something similar instead +export type TradeableAsset = { + symbol: NetworkSymbol; + contractAddress?: TokenAddress; + name?: string; +}; diff --git a/suite-native/test-utils/src/expoMock.js b/suite-native/test-utils/src/expoMock.js index 00b5785d979..6d1b81b8790 100644 --- a/suite-native/test-utils/src/expoMock.js +++ b/suite-native/test-utils/src/expoMock.js @@ -407,3 +407,14 @@ jest.mock('expo-constants', () => { jest.mock('redux-devtools-expo-dev-plugin', () => () => next => next); jest.mock('react-native-safe-area-context', () => mockSafeAreaContext); + +jest.mock('react-native-image-colors', () => ({ + getColors: () => + Promise.resolve({ + background: '#111111', + primary: '#333333', + secondary: '#555555', + detail: '#777777', + platform: 'ios', + }), +})); diff --git a/yarn.lock b/yarn.lock index bb21ffbb11a..468e77d9c20 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1768,6 +1768,15 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.7.2": + version: 7.26.7 + resolution: "@babel/runtime@npm:7.26.7" + dependencies: + regenerator-runtime: "npm:^0.14.0" + checksum: 10/c7a661a6836b332d9d2e047cba77ba1862c1e4f78cec7146db45808182ef7636d8a7170be9797e5d8fd513180bffb9fa16f6ca1c69341891efec56113cf22bfc + languageName: node + linkType: hard + "@babel/template@npm:^7.0.0, @babel/template@npm:^7.25.0, @babel/template@npm:^7.25.9, @babel/template@npm:^7.3.3": version: 7.25.9 resolution: "@babel/template@npm:7.25.9" @@ -4319,6 +4328,139 @@ __metadata: languageName: node linkType: hard +"@jimp/bmp@npm:^0.16.13": + version: 0.16.13 + resolution: "@jimp/bmp@npm:0.16.13" + dependencies: + "@babel/runtime": "npm:^7.7.2" + "@jimp/utils": "npm:^0.16.13" + bmp-js: "npm:^0.1.0" + peerDependencies: + "@jimp/custom": ">=0.3.5" + checksum: 10/e3dfdde928da73819264eaa00d973718d4d228de16f52be6e56f126e35fd24aaa1e0372f8450a7453fea19805e3a7c4e8fd15f0735ada01c591ed082e05cd1e4 + languageName: node + linkType: hard + +"@jimp/core@npm:^0.16.13": + version: 0.16.13 + resolution: "@jimp/core@npm:0.16.13" + dependencies: + "@babel/runtime": "npm:^7.7.2" + "@jimp/utils": "npm:^0.16.13" + any-base: "npm:^1.1.0" + buffer: "npm:^5.2.0" + exif-parser: "npm:^0.1.12" + file-type: "npm:^16.5.4" + load-bmfont: "npm:^1.3.1" + mkdirp: "npm:^0.5.1" + phin: "npm:^2.9.1" + pixelmatch: "npm:^4.0.2" + tinycolor2: "npm:^1.4.1" + checksum: 10/3c619bddebd2da54d175449bf91ae70794833b116dc4670523456f3033cf682a959a50a9f7444106fcf5167d3edb1804adb54b85dc40ae75f7c09d24142203f5 + languageName: node + linkType: hard + +"@jimp/custom@npm:^0.16.1": + version: 0.16.13 + resolution: "@jimp/custom@npm:0.16.13" + dependencies: + "@babel/runtime": "npm:^7.7.2" + "@jimp/core": "npm:^0.16.13" + checksum: 10/7ce0b8b54d26983b1b390d52096e448fa58f67f28a85ae4ba3fc32416bf5e85c4bd12aab2175b8f763c03a396eee8be9575575f73fbbcaca7204d32a4e3a95f9 + languageName: node + linkType: hard + +"@jimp/gif@npm:^0.16.13": + version: 0.16.13 + resolution: "@jimp/gif@npm:0.16.13" + dependencies: + "@babel/runtime": "npm:^7.7.2" + "@jimp/utils": "npm:^0.16.13" + gifwrap: "npm:^0.9.2" + omggif: "npm:^1.0.9" + peerDependencies: + "@jimp/custom": ">=0.3.5" + checksum: 10/e28e30af4cb5f2e9a95b6e8bf2ff203d226ea779b5ade290bae40e205359307d9cdd35c981070482c56efca575a80e27ab5211e2756b51e2571715934b193140 + languageName: node + linkType: hard + +"@jimp/jpeg@npm:^0.16.13": + version: 0.16.13 + resolution: "@jimp/jpeg@npm:0.16.13" + dependencies: + "@babel/runtime": "npm:^7.7.2" + "@jimp/utils": "npm:^0.16.13" + jpeg-js: "npm:^0.4.2" + peerDependencies: + "@jimp/custom": ">=0.3.5" + checksum: 10/d2564dc9ff481d6335b8f2bf234846d95fcb061c4bd4cb4c4385a24f7eeeab14c101e1bebdd5579a11d313dfd9cdcb035fbf8aeb8398b83bc369a56e51a2c190 + languageName: node + linkType: hard + +"@jimp/plugin-resize@npm:^0.16.1": + version: 0.16.13 + resolution: "@jimp/plugin-resize@npm:0.16.13" + dependencies: + "@babel/runtime": "npm:^7.7.2" + "@jimp/utils": "npm:^0.16.13" + peerDependencies: + "@jimp/custom": ">=0.3.5" + checksum: 10/37baee3cad5c8d2e481b498192233a7a692d3443a348d9d8c76ae109528291991fade79afe502833d37053015262ecf4f75ccc6b5ba3948ba9c9b33d98fd26d7 + languageName: node + linkType: hard + +"@jimp/png@npm:^0.16.13": + version: 0.16.13 + resolution: "@jimp/png@npm:0.16.13" + dependencies: + "@babel/runtime": "npm:^7.7.2" + "@jimp/utils": "npm:^0.16.13" + pngjs: "npm:^3.3.3" + peerDependencies: + "@jimp/custom": ">=0.3.5" + checksum: 10/0688403fa4bfd53f0e63fa8a8ee256741823367855f1f9ab8c1b582e48ba1c8ff7f1fe333dbc8149e80bee45c000607e374261b4e9fb70bb5bf0daeb8ccbbbd4 + languageName: node + linkType: hard + +"@jimp/tiff@npm:^0.16.13": + version: 0.16.13 + resolution: "@jimp/tiff@npm:0.16.13" + dependencies: + "@babel/runtime": "npm:^7.7.2" + utif: "npm:^2.0.1" + peerDependencies: + "@jimp/custom": ">=0.3.5" + checksum: 10/c34d3b24bfaabeb7a718cfcb41b5e88c43a08380128ce11594448c09145e2c49cbc1ab5e7f6bd95dcc71b01a6339ff1f67cf667dcee7fcdba4c836fc58ca16e4 + languageName: node + linkType: hard + +"@jimp/types@npm:^0.16.1": + version: 0.16.13 + resolution: "@jimp/types@npm:0.16.13" + dependencies: + "@babel/runtime": "npm:^7.7.2" + "@jimp/bmp": "npm:^0.16.13" + "@jimp/gif": "npm:^0.16.13" + "@jimp/jpeg": "npm:^0.16.13" + "@jimp/png": "npm:^0.16.13" + "@jimp/tiff": "npm:^0.16.13" + timm: "npm:^1.6.1" + peerDependencies: + "@jimp/custom": ">=0.3.5" + checksum: 10/bab48dd06fd418db2429c6aed60cac772b8503127f87eded95b2fa811457d55a53380fec52f9c1500998875391bb3de22dd7c1b10da483e7b557e14dcc368488 + languageName: node + linkType: hard + +"@jimp/utils@npm:^0.16.13": + version: 0.16.13 + resolution: "@jimp/utils@npm:0.16.13" + dependencies: + "@babel/runtime": "npm:^7.7.2" + regenerator-runtime: "npm:^0.13.3" + checksum: 10/1a8eb0657c645dd0a1e0acb2123a93072e6210eb640fecbf0a60b281809d68712850aa0a037e135bc1529c5837b878a536ad547abf54487630067036b7674869 + languageName: node + linkType: hard + "@jridgewell/gen-mapping@npm:^0.3.0, @jridgewell/gen-mapping@npm:^0.3.2, @jridgewell/gen-mapping@npm:^0.3.5": version: 0.3.5 resolution: "@jridgewell/gen-mapping@npm:0.3.5" @@ -10950,8 +11092,11 @@ __metadata: "@reduxjs/toolkit": "npm:1.9.5" "@suite-native/navigation": "workspace:*" "@suite-native/test-utils": "workspace:*" + expo-linear-gradient: "npm:^14.0.1" react: "npm:18.2.0" react-native: "npm:0.76.1" + react-native-image-colors: "npm:^2.4.0" + react-native-reanimated: "npm:3.16.7" languageName: unknown linkType: soft @@ -11586,6 +11731,13 @@ __metadata: languageName: node linkType: hard +"@tokenizer/token@npm:^0.3.0": + version: 0.3.0 + resolution: "@tokenizer/token@npm:0.3.0" + checksum: 10/889c1f1e63ac7c92c0ea22d4a2861142f1b43c3d92eb70ec42aa9e9851fab2e9952211d50f541b287781280df2f979bf5600a9c1f91fbc61b7fcf9994e9376a5 + languageName: node + linkType: hard + "@tootallnate/once@npm:2": version: 2.0.0 resolution: "@tootallnate/once@npm:2.0.0" @@ -13918,6 +14070,13 @@ __metadata: languageName: node linkType: hard +"@types/lodash@npm:^4.14.53": + version: 4.17.15 + resolution: "@types/lodash@npm:4.17.15" + checksum: 10/27b348b5971b9c670215331b52448a13d7d65bf1fbd320a7049c9c153c1186ff5d116ba75f05f07d32d7ece8a992b26a30c7bdc9be22a3d1e4e3e6068aa04603 + languageName: node + linkType: hard + "@types/long@npm:^3.0.0": version: 3.0.32 resolution: "@types/long@npm:3.0.32" @@ -15635,6 +15794,13 @@ __metadata: languageName: node linkType: hard +"any-base@npm:^1.1.0": + version: 1.1.0 + resolution: "any-base@npm:1.1.0" + checksum: 10/c1fd040de52e710e2de7d9ae4df52bac589f35622adb24686c98ce21c7b824859a8db9614bc119ed8614f42fd08918b2612e6a6c385480462b3100a1af59289d + languageName: node + linkType: hard + "any-promise@npm:^1.0.0": version: 1.3.0 resolution: "any-promise@npm:1.3.0" @@ -16857,6 +17023,13 @@ __metadata: languageName: node linkType: hard +"bmp-js@npm:^0.1.0": + version: 0.1.0 + resolution: "bmp-js@npm:0.1.0" + checksum: 10/9597f41038f4a326bc465d009e2e170203fc296219a743efbcf531289913680761f155be8a2e586c0b48c59644e46449be556a5ec5b09c413b7e84a05db25fd4 + languageName: node + linkType: hard + "bn.js@npm:5.2.1": version: 5.2.1 resolution: "bn.js@npm:5.2.1" @@ -17337,7 +17510,7 @@ __metadata: languageName: node linkType: hard -"buffer@npm:^5.1.0, buffer@npm:^5.4.3, buffer@npm:^5.5.0, buffer@npm:^5.6.0": +"buffer@npm:^5.1.0, buffer@npm:^5.2.0, buffer@npm:^5.4.3, buffer@npm:^5.5.0, buffer@npm:^5.6.0": version: 5.7.1 resolution: "buffer@npm:5.7.1" dependencies: @@ -17794,6 +17967,15 @@ __metadata: languageName: node linkType: hard +"centra@npm:^2.7.0": + version: 2.7.0 + resolution: "centra@npm:2.7.0" + dependencies: + follow-redirects: "npm:^1.15.6" + checksum: 10/59ec76d9ba7086b76e9594129b9843856fe7293400b89cb8b133f444a62ca5d4c536df0d4722374b0c16d86dd4e0baba1fc9722640b7d3b532865bebdec2b1a2 + languageName: node + linkType: hard + "chai@npm:^4.3.10": version: 4.4.1 resolution: "chai@npm:4.4.1" @@ -20902,6 +21084,13 @@ __metadata: languageName: node linkType: hard +"dom-walk@npm:^0.1.0": + version: 0.1.2 + resolution: "dom-walk@npm:0.1.2" + checksum: 10/19eb0ce9c6de39d5e231530685248545d9cd2bd97b2cb3486e0bfc0f2a393a9addddfd5557463a932b52fdfcf68ad2a619020cd2c74a5fe46fbecaa8e80872f3 + languageName: node + linkType: hard + "domain-browser@npm:^1.1.1": version: 1.2.0 resolution: "domain-browser@npm:1.2.0" @@ -22836,6 +23025,13 @@ __metadata: languageName: node linkType: hard +"exif-parser@npm:^0.1.12": + version: 0.1.12 + resolution: "exif-parser@npm:0.1.12" + checksum: 10/72bffba154fd33b270908ea1f9f63a6c5dffadf4eb427c85ab82d6006204ed762dfeb76969e1577614b8d18dadd411b11583569e54ed2beea0af8b61c8f4de29 + languageName: node + linkType: hard + "exit@npm:^0.1.2": version: 0.1.2 resolution: "exit@npm:0.1.2" @@ -23958,6 +24154,17 @@ __metadata: languageName: node linkType: hard +"file-type@npm:^16.5.4": + version: 16.5.4 + resolution: "file-type@npm:16.5.4" + dependencies: + readable-web-to-node-stream: "npm:^3.0.0" + strtok3: "npm:^6.2.4" + token-types: "npm:^4.1.1" + checksum: 10/46ced46bb925ab547e0a6d43108a26d043619d234cb0588d7abce7b578dafac142bcfd2e23a6adb0a4faa4b951bd1b14b355134a193362e07cd352f9bf0dc349 + languageName: node + linkType: hard + "file-uri-to-path@npm:1.0.0": version: 1.0.0 resolution: "file-uri-to-path@npm:1.0.0" @@ -24779,6 +24986,16 @@ __metadata: languageName: node linkType: hard +"gifwrap@npm:^0.9.2": + version: 0.9.4 + resolution: "gifwrap@npm:0.9.4" + dependencies: + image-q: "npm:^4.0.0" + omggif: "npm:^1.0.10" + checksum: 10/fe9675460371136a78c121c13fa417d57e8eb700af261356ba0c0b16661a8a68a0b0779e7b5a341dc6c02c6e12b80e269442b9896dda0576c1743116946b225b + languageName: node + linkType: hard + "giget@npm:^1.0.0": version: 1.1.2 resolution: "giget@npm:1.1.2" @@ -25032,6 +25249,16 @@ __metadata: languageName: node linkType: hard +"global@npm:~4.4.0": + version: 4.4.0 + resolution: "global@npm:4.4.0" + dependencies: + min-document: "npm:^2.19.0" + process: "npm:^0.11.10" + checksum: 10/9c057557c8f5a5bcfbeb9378ba4fe2255d04679452be504608dd5f13b54edf79f7be1db1031ea06a4ec6edd3b9f5f17d2d172fb47e6c69dae57fd84b7e72b77f + languageName: node + linkType: hard + "globals@npm:^11.1.0": version: 11.12.0 resolution: "globals@npm:11.12.0" @@ -26276,6 +26503,15 @@ __metadata: languageName: node linkType: hard +"image-q@npm:^4.0.0": + version: 4.0.0 + resolution: "image-q@npm:4.0.0" + dependencies: + "@types/node": "npm:16.9.1" + checksum: 10/1f7c6c84d0b4d83d50603a38443a51bd45d0aee38ff4284135b30744df9481fa18ae818f906a648d0a6a9a78c884147b7632424199d107998427bb98e96f2584 + languageName: node + linkType: hard + "image-size@npm:^1.0.2": version: 1.1.1 resolution: "image-size@npm:1.1.1" @@ -26881,6 +27117,13 @@ __metadata: languageName: node linkType: hard +"is-function@npm:^1.0.1": + version: 1.0.2 + resolution: "is-function@npm:1.0.2" + checksum: 10/7d564562e07b4b51359547d3ccc10fb93bb392fd1b8177ae2601ee4982a0ece86d952323fc172a9000743a3971f09689495ab78a1d49a9b14fc97a7e28521dc0 + languageName: node + linkType: hard + "is-generator-fn@npm:^2.0.0": version: 2.1.0 resolution: "is-generator-fn@npm:2.1.0" @@ -28230,6 +28473,13 @@ __metadata: languageName: node linkType: hard +"jpeg-js@npm:^0.4.2": + version: 0.4.4 + resolution: "jpeg-js@npm:0.4.4" + checksum: 10/30bb6e16e79db4bd6edbecf1140039bbab326de876f2d20685f04a38d03f70ef9fada1886f0bf58832a4dba4aa179311fc4be7df3d756cab8f1c74d745542f38 + languageName: node + linkType: hard + "js-cookie@npm:^2.2.1": version: 2.2.1 resolution: "js-cookie@npm:2.2.1" @@ -29408,6 +29658,22 @@ __metadata: languageName: node linkType: hard +"load-bmfont@npm:^1.3.1": + version: 1.4.2 + resolution: "load-bmfont@npm:1.4.2" + dependencies: + buffer-equal: "npm:0.0.1" + mime: "npm:^1.3.4" + parse-bmfont-ascii: "npm:^1.0.3" + parse-bmfont-binary: "npm:^1.0.5" + parse-bmfont-xml: "npm:^1.1.4" + phin: "npm:^3.7.1" + xhr: "npm:^2.0.1" + xtend: "npm:^4.0.0" + checksum: 10/73d80e9d5bd3ba12ba1174a33a6dfdc90a635106bb9a040b375060f24a9e15f757f06f3adfbcaa1f6effd93e380ef8c51f2b946dc6d976037f7119f0dd5266bf + languageName: node + linkType: hard + "load-plugin@npm:^6.0.0": version: 6.0.2 resolution: "load-plugin@npm:6.0.2" @@ -31942,7 +32208,7 @@ __metadata: languageName: node linkType: hard -"mime@npm:1.6.0": +"mime@npm:1.6.0, mime@npm:^1.3.4": version: 1.6.0 resolution: "mime@npm:1.6.0" bin: @@ -32002,6 +32268,15 @@ __metadata: languageName: node linkType: hard +"min-document@npm:^2.19.0": + version: 2.19.0 + resolution: "min-document@npm:2.19.0" + dependencies: + dom-walk: "npm:^0.1.0" + checksum: 10/4e45a0686c81cc04509989235dc6107e2678a59bb48ce017d3c546d7d9a18d782e341103e66c78081dd04544704e2196e529905c41c2550bca069b69f95f07c8 + languageName: node + linkType: hard + "min-indent@npm:^1.0.0, min-indent@npm:^1.0.1": version: 1.0.1 resolution: "min-indent@npm:1.0.1" @@ -32948,6 +33223,21 @@ __metadata: languageName: node linkType: hard +"node-vibrant@npm:3.1.6": + version: 3.1.6 + resolution: "node-vibrant@npm:3.1.6" + dependencies: + "@jimp/custom": "npm:^0.16.1" + "@jimp/plugin-resize": "npm:^0.16.1" + "@jimp/types": "npm:^0.16.1" + "@types/lodash": "npm:^4.14.53" + "@types/node": "npm:^10.11.7" + lodash: "npm:^4.17.20" + url: "npm:^0.11.0" + checksum: 10/b70f3ed1028097b367043afbdce26be44fca571762f2b51d18431c2e2ee7914dcd354761382666a3de5ca194a00c82bc21a651589bb088ddb418a7bbb3197810 + languageName: node + linkType: hard + "non-layered-tidy-tree-layout@npm:^2.0.2": version: 2.0.2 resolution: "non-layered-tidy-tree-layout@npm:2.0.2" @@ -33339,6 +33629,13 @@ __metadata: languageName: node linkType: hard +"omggif@npm:^1.0.10, omggif@npm:^1.0.9": + version: 1.0.10 + resolution: "omggif@npm:1.0.10" + checksum: 10/a7b063d702969a911a8a337a4e2b17a370bfb66f0615344f8d7a7cfff5ee6e8c201a6a4ab41895fa9adfb51cb653894c52a306cf07bd7ceca355f240fea93261 + languageName: node + linkType: hard + "on-exit-leak-free@npm:^2.1.0": version: 2.1.2 resolution: "on-exit-leak-free@npm:2.1.2" @@ -33754,7 +34051,7 @@ __metadata: languageName: node linkType: hard -"pako@npm:^1.0.0, pako@npm:~1.0.5": +"pako@npm:^1.0.0, pako@npm:^1.0.5, pako@npm:~1.0.5": version: 1.0.11 resolution: "pako@npm:1.0.11" checksum: 10/1ad07210e894472685564c4d39a08717e84c2a68a70d3c1d9e657d32394ef1670e22972a433cbfe48976cb98b154ba06855dcd3fcfba77f60f1777634bec48c0 @@ -33800,6 +34097,30 @@ __metadata: languageName: node linkType: hard +"parse-bmfont-ascii@npm:^1.0.3": + version: 1.0.6 + resolution: "parse-bmfont-ascii@npm:1.0.6" + checksum: 10/9dd46f8ad8db8e067904c97a21546a1e338eaabb909abe070c643e4e06dbf76fa685277114ca22a05a4a35d38197512b2826d5de46a03b10e9bf49119ced2e39 + languageName: node + linkType: hard + +"parse-bmfont-binary@npm:^1.0.5": + version: 1.0.6 + resolution: "parse-bmfont-binary@npm:1.0.6" + checksum: 10/728fbc05876c3f0ab116ea238be99f8c1188551e54997965038db558aab08c71f0ae1fee64c2a18c8d629c6b2aaea43e84a91783ec4f114ac400faf0b5170b86 + languageName: node + linkType: hard + +"parse-bmfont-xml@npm:^1.1.4": + version: 1.1.6 + resolution: "parse-bmfont-xml@npm:1.1.6" + dependencies: + xml-parse-from-string: "npm:^1.0.0" + xml2js: "npm:^0.5.0" + checksum: 10/71a202da289a124db7bb7bee1b2a01b8a38b5ba36f93d6a98cea6fc1d140c16c8bc7bcccff48864ec886da035944d337b04cf70723393c411991af952fc6086b + languageName: node + linkType: hard + "parse-entities@npm:^2.0.0": version: 2.0.0 resolution: "parse-entities@npm:2.0.0" @@ -33830,6 +34151,13 @@ __metadata: languageName: node linkType: hard +"parse-headers@npm:^2.0.0": + version: 2.0.5 + resolution: "parse-headers@npm:2.0.5" + checksum: 10/210b13bc0f99cf6f1183896f01de164797ac35b2720c9f1c82a3e2ceab256f87b9048e8e16a14cfd1b75448771f8379cd564bd1674a179ab0168c90005d4981b + languageName: node + linkType: hard + "parse-json@npm:^4.0.0": version: 4.0.0 resolution: "parse-json@npm:4.0.0" @@ -34154,6 +34482,13 @@ __metadata: languageName: node linkType: hard +"peek-readable@npm:^4.1.0": + version: 4.1.0 + resolution: "peek-readable@npm:4.1.0" + checksum: 10/97373215dcf382748645c3d22ac5e8dbd31759f7bd0c539d9fdbaaa7d22021838be3e55110ad0ed8f241c489342304b14a50dfee7ef3bcee2987d003b24ecc41 + languageName: node + linkType: hard + "peek-stream@npm:^1.1.0": version: 1.1.3 resolution: "peek-stream@npm:1.1.3" @@ -34239,6 +34574,22 @@ __metadata: languageName: node linkType: hard +"phin@npm:^2.9.1": + version: 2.9.3 + resolution: "phin@npm:2.9.3" + checksum: 10/7e2abd7be74a54eb7be92dccb1d7a019725c8adaa79ac22a38f25220f9a859393e654ea753a559d326aed7bbc966fadac88270cc8c39d78896f7784219560c47 + languageName: node + linkType: hard + +"phin@npm:^3.7.1": + version: 3.7.1 + resolution: "phin@npm:3.7.1" + dependencies: + centra: "npm:^2.7.0" + checksum: 10/eebbfb0ab63d90f1513a2da05ef5ccc4bfb17216567fe62e9f0b8a4da27ff301b6409da8dcada6a66711c040b318ffb456e1adf24e8d261e24a916d30d91aadf + languageName: node + linkType: hard + "picocolors@npm:^1.0.0, picocolors@npm:^1.0.1, picocolors@npm:^1.1.0": version: 1.1.1 resolution: "picocolors@npm:1.1.1" @@ -34319,6 +34670,17 @@ __metadata: languageName: node linkType: hard +"pixelmatch@npm:^4.0.2": + version: 4.0.2 + resolution: "pixelmatch@npm:4.0.2" + dependencies: + pngjs: "npm:^3.0.0" + bin: + pixelmatch: bin/pixelmatch + checksum: 10/3dfb1c0bc6d333a5ad34e78737c3ea33ac3743b52db73b5e8bebbbfd87376afacfec5d3c268d9fdb6e77b07c5ecd6b01f98657087457107f9e03ad1a872545e1 + languageName: node + linkType: hard + "pixelmatch@npm:^5.1.0": version: 5.3.0 resolution: "pixelmatch@npm:5.3.0" @@ -34491,7 +34853,7 @@ __metadata: languageName: node linkType: hard -"pngjs@npm:^3.3.0, pngjs@npm:^3.4.0": +"pngjs@npm:^3.0.0, pngjs@npm:^3.3.0, pngjs@npm:^3.3.3, pngjs@npm:^3.4.0": version: 3.4.0 resolution: "pngjs@npm:3.4.0" checksum: 10/0e9227a413ce4b4f5ebae4465b366efc9ca545c74304f3cc30ba2075159eb12f01a6a821c4f61f2b048bd85356abbe6d2109df7052a9030ef4d7a42d99760af6 @@ -36130,6 +36492,19 @@ __metadata: languageName: node linkType: hard +"react-native-image-colors@npm:^2.4.0": + version: 2.4.0 + resolution: "react-native-image-colors@npm:2.4.0" + dependencies: + node-vibrant: "npm:3.1.6" + peerDependencies: + expo: "*" + react: "*" + react-native: "*" + checksum: 10/534c92a606010b5ae02958edce643bd9686fb23a62a6a7126378088b396462f55583e9351a0373e5d9e64f3f7fa7c703fdb179089dcac8c7291d21bbfd488257 + languageName: node + linkType: hard + "react-native-iphone-x-helper@npm:^1.0.3": version: 1.3.1 resolution: "react-native-iphone-x-helper@npm:1.3.1" @@ -36756,6 +37131,15 @@ __metadata: languageName: node linkType: hard +"readable-web-to-node-stream@npm:^3.0.0": + version: 3.0.2 + resolution: "readable-web-to-node-stream@npm:3.0.2" + dependencies: + readable-stream: "npm:^3.6.0" + checksum: 10/d3a5bf9d707c01183d546a64864aa63df4d9cb835dfd2bf89ac8305e17389feef2170c4c14415a10d38f9b9bfddf829a57aaef7c53c8b40f11d499844bf8f1a4 + languageName: node + linkType: hard + "readdir-glob@npm:^1.1.2": version: 1.1.3 resolution: "readdir-glob@npm:1.1.3" @@ -37026,7 +37410,7 @@ __metadata: languageName: node linkType: hard -"regenerator-runtime@npm:^0.13.2": +"regenerator-runtime@npm:^0.13.2, regenerator-runtime@npm:^0.13.3": version: 0.13.11 resolution: "regenerator-runtime@npm:0.13.11" checksum: 10/d493e9e118abef5b099c78170834f18540c4933cedf9bfabc32d3af94abfb59a7907bd7950259cbab0a929ebca7db77301e8024e5121e6482a82f78283dfd20c @@ -39732,6 +40116,16 @@ __metadata: languageName: node linkType: hard +"strtok3@npm:^6.2.4": + version: 6.3.0 + resolution: "strtok3@npm:6.3.0" + dependencies: + "@tokenizer/token": "npm:^0.3.0" + peek-readable: "npm:^4.1.0" + checksum: 10/98fba564d3830202aa3a6bcd5ccaf2cbd849bd87ae79ece91d337e1913916705a8e633c9577138d030a984f8ec987dea51807e01252f995cf5e183fdea35eb2b + languageName: node + linkType: hard + "structured-headers@npm:^0.4.1": version: 0.4.1 resolution: "structured-headers@npm:0.4.1" @@ -40575,6 +40969,13 @@ __metadata: languageName: node linkType: hard +"timm@npm:^1.6.1": + version: 1.7.1 + resolution: "timm@npm:1.7.1" + checksum: 10/7ff241bdd48c3d67f2c501e8bc6b11aee595889cb60d53d32baad77a0840de8f393c55830718275f38bf808410247fff53ffd9c4bb1bfa637febde63ea343095 + languageName: node + linkType: hard + "tiny-case@npm:^1.0.3": version: 1.0.3 resolution: "tiny-case@npm:1.0.3" @@ -40640,6 +41041,13 @@ __metadata: languageName: node linkType: hard +"tinycolor2@npm:^1.4.1": + version: 1.6.0 + resolution: "tinycolor2@npm:1.6.0" + checksum: 10/066c3acf4f82b81c58a0d3ab85f49407efe95ba87afc3c7a16b1d77625193dfbe10dd46c26d0a263c1137361dd5a6a68bff2fb71def5fb9b9aec940fb030bcd4 + languageName: node + linkType: hard + "tinypool@npm:^0.7.0": version: 0.7.0 resolution: "tinypool@npm:0.7.0" @@ -40751,6 +41159,16 @@ __metadata: languageName: node linkType: hard +"token-types@npm:^4.1.1": + version: 4.2.1 + resolution: "token-types@npm:4.2.1" + dependencies: + "@tokenizer/token": "npm:^0.3.0" + ieee754: "npm:^1.2.1" + checksum: 10/2995257d246387e773758c3c92a3cc99d0c0bf13cbafe0de5d712e4c35ed298da6704e21545cb123fa1f1b42ad62936c35bbd0611018b735e78c30b8b22b42d9 + languageName: node + linkType: hard + "toml@npm:^3.0.0": version: 3.0.0 resolution: "toml@npm:3.0.0" @@ -42093,6 +42511,15 @@ __metadata: languageName: node linkType: hard +"utif@npm:^2.0.1": + version: 2.0.1 + resolution: "utif@npm:2.0.1" + dependencies: + pako: "npm:^1.0.5" + checksum: 10/c3c2e924b20c6b177391b8212bb79fdb65d6ae01efdffe63323ae14dca1ef9301272a380a23497d2e7a14f2c09507f0780e9b53814c50cf8f328760dc2912645 + languageName: node + linkType: hard + "util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2, util-deprecate@npm:~1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" @@ -43815,6 +44242,18 @@ __metadata: languageName: node linkType: hard +"xhr@npm:^2.0.1": + version: 2.6.0 + resolution: "xhr@npm:2.6.0" + dependencies: + global: "npm:~4.4.0" + is-function: "npm:^1.0.1" + parse-headers: "npm:^2.0.0" + xtend: "npm:^4.0.0" + checksum: 10/31f34aba708955008c87bcd21482be6afc7ff8adc28090e633b1d3f8d3e8e93150bac47b262738b046d7729023a884b655d55cf34e9d14d5850a1275ab49fb37 + languageName: node + linkType: hard + "xml-name-validator@npm:^4.0.0": version: 4.0.0 resolution: "xml-name-validator@npm:4.0.0" @@ -43829,6 +44268,13 @@ __metadata: languageName: node linkType: hard +"xml-parse-from-string@npm:^1.0.0": + version: 1.0.1 + resolution: "xml-parse-from-string@npm:1.0.1" + checksum: 10/628eda047d93bed85165b2605d68bd86a18cab2d362ed29553ee0d4124cec348ffa6dfb0f73361f46329ce9ee1079bb152af49caf1b5f694232c554a8d5daaa4 + languageName: node + linkType: hard + "xml2js@npm:0.6.0": version: 0.6.0 resolution: "xml2js@npm:0.6.0" @@ -43839,6 +44285,16 @@ __metadata: languageName: node linkType: hard +"xml2js@npm:^0.5.0": + version: 0.5.0 + resolution: "xml2js@npm:0.5.0" + dependencies: + sax: "npm:>=0.6.0" + xmlbuilder: "npm:~11.0.0" + checksum: 10/27c4d759214e99be5ec87ee5cb1290add427fa43df509d3b92d10152b3806fd2f7c9609697a18b158ccf2caa01e96af067cdba93196f69ca10c90e4f79a08896 + languageName: node + linkType: hard + "xml2js@npm:^0.6.2": version: 0.6.2 resolution: "xml2js@npm:0.6.2"