From 2b56497b3143623442b75baf3b715c754bac77e2 Mon Sep 17 00:00:00 2001 From: nachosan Date: Thu, 4 Aug 2022 16:46:38 -0300 Subject: [PATCH 01/93] wip: Implementing first step of custom tokens --- declarations.d.ts | 1 + package.json | 3 +- src/components/common/SearchBar/index.tsx | 24 +++++++ src/components/common/SearchBar/styles.ts | 9 +++ src/components/common/Text/index.tsx | 3 +- src/components/common/Text/styles.ts | 1 + src/components/common/TextInput/styles.js | 11 ++-- src/components/common/Touchable/index.tsx | 10 ++- src/interfaces/dab.ts | 17 +++++ src/interfaces/general.ts | 7 --- .../components/AddToken/hooks/useSteps.tsx | 55 ++++++++++++++++ .../tabs/Tokens/components/AddToken/index.tsx | 36 +++++++++++ .../AddToken/steps/ReviewToken/index.tsx | 16 +++++ .../AddToken/steps/TokenList/index.tsx | 63 +++++++++++++++++++ .../AddToken/steps/TokenList/styles.ts | 36 +++++++++++ .../tabs/Tokens/components/AddToken/styles.ts | 0 src/screens/tabs/Tokens/index.js | 46 +++++++------- src/services/DAB.ts | 48 ++++++++++++++ src/translations/en/index.js | 5 ++ src/utils/walletConnect.js | 23 +------ yarn.lock | 14 +++++ 21 files changed, 368 insertions(+), 60 deletions(-) create mode 100644 src/components/common/SearchBar/index.tsx create mode 100644 src/components/common/SearchBar/styles.ts create mode 100644 src/interfaces/dab.ts create mode 100644 src/screens/tabs/Tokens/components/AddToken/hooks/useSteps.tsx create mode 100644 src/screens/tabs/Tokens/components/AddToken/index.tsx create mode 100644 src/screens/tabs/Tokens/components/AddToken/steps/ReviewToken/index.tsx create mode 100644 src/screens/tabs/Tokens/components/AddToken/steps/TokenList/index.tsx create mode 100644 src/screens/tabs/Tokens/components/AddToken/steps/TokenList/styles.ts create mode 100644 src/screens/tabs/Tokens/components/AddToken/styles.ts create mode 100644 src/services/DAB.ts diff --git a/declarations.d.ts b/declarations.d.ts index fed8d166..c9c94f09 100644 --- a/declarations.d.ts +++ b/declarations.d.ts @@ -4,3 +4,4 @@ declare module '*.svg' { const content: React.FC; export default content; } +declare module 'react-native-fetch-api'; diff --git a/package.json b/package.json index 4aff6467..4f19af5e 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@dfinity/candid": "0.9.3", "@dfinity/principal": "^0.9.3", "@hookform/error-message": "^2.0.0", + "@psychedelic/dab-js": "^1.4.4", "@psychedelic/plug-controller": "0.19.2", "@react-native-async-storage/async-storage": "^1.15.11", "@react-native-community/blur": "^3.6.0", @@ -44,10 +45,10 @@ "@react-navigation/native": "^6.0.10", "@react-navigation/stack": "^6.2.1", "@reduxjs/toolkit": "^1.6.1", + "@sentry/react-native": "^4.1.2", "@tradle/react-native-http": "^2.0.1", "@walletconnect/client": "^1.7.5", "assert": "^2.0.0", - "@sentry/react-native": "^4.1.2", "assert-browserify": "^2.0.0", "axios": "0.24.0", "babel-plugin-transform-exponentiation-operator": "^6.24.1", diff --git a/src/components/common/SearchBar/index.tsx b/src/components/common/SearchBar/index.tsx new file mode 100644 index 00000000..7c0f3314 --- /dev/null +++ b/src/components/common/SearchBar/index.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { StyleProp, View, ViewStyle } from 'react-native'; + +import TextInput from '../TextInput'; +import styles from './styles'; + +interface Props { + placeholder: string; + style?: StyleProp; +} + +function SearchBar({ placeholder, style }: Props) { + return ( + + + + ); +} + +export default SearchBar; diff --git a/src/components/common/SearchBar/styles.ts b/src/components/common/SearchBar/styles.ts new file mode 100644 index 00000000..abefc6b1 --- /dev/null +++ b/src/components/common/SearchBar/styles.ts @@ -0,0 +1,9 @@ +import { StyleSheet } from 'react-native'; + +import { Colors } from '@/constants/theme'; + +export default StyleSheet.create({ + input: { + backgroundColor: Colors.Black.Pure, + }, +}); diff --git a/src/components/common/Text/index.tsx b/src/components/common/Text/index.tsx index 1cca6972..0429971d 100644 --- a/src/components/common/Text/index.tsx +++ b/src/components/common/Text/index.tsx @@ -11,7 +11,8 @@ type Type = | 'body2' | 'button' | 'caption' - | 'overline'; + | 'overline' + | 'normal'; interface Props extends TextProps { type?: Type; diff --git a/src/components/common/Text/styles.ts b/src/components/common/Text/styles.ts index 65ed1ad1..a82dbcf8 100644 --- a/src/components/common/Text/styles.ts +++ b/src/components/common/Text/styles.ts @@ -14,4 +14,5 @@ export default StyleSheet.create({ button: FontStyles.Button, caption: FontStyles.Caption, overline: FontStyles.Overline, + normal: FontStyles.Normal, }); diff --git a/src/components/common/TextInput/styles.js b/src/components/common/TextInput/styles.js index c47a8093..cba05308 100644 --- a/src/components/common/TextInput/styles.js +++ b/src/components/common/TextInput/styles.js @@ -4,15 +4,14 @@ import { SEMIBOLD } from '@/constants/fonts'; import { Colors } from '@/constants/theme'; import { fontMaker } from '@/utils/fonts'; -const commonBorderRadius = 15; -const inputHeight = 60; +const commonBorderRadius = 16; export default StyleSheet.create({ inputStyle: { ...fontMaker({ size: 18, color: Colors.White.Pure, weight: SEMIBOLD }), paddingHorizontal: 20, paddingVertical: 13, - borderRadius: 15, + borderRadius: commonBorderRadius, width: '100%', height: 56, }, @@ -33,13 +32,13 @@ export default StyleSheet.create({ backgroundColor: Colors.Black.Primary, alignItems: 'center', flexDirection: 'row', - borderRadius: 15, + borderRadius: commonBorderRadius, height: 56, flexGrow: 0, }, viewInnerStyle: { backgroundColor: Colors.Black.Primary, - borderRadius: 15, + borderRadius: commonBorderRadius, height: 56, flexGrow: 0, }, @@ -57,7 +56,7 @@ export default StyleSheet.create({ focusedGradient: { borderRadius: commonBorderRadius, position: 'absolute', - height: inputHeight, + height: 60, width: '101%', left: -2, top: -2, diff --git a/src/components/common/Touchable/index.tsx b/src/components/common/Touchable/index.tsx index 16b6046f..1edb5d36 100644 --- a/src/components/common/Touchable/index.tsx +++ b/src/components/common/Touchable/index.tsx @@ -1,12 +1,16 @@ import React from 'react'; -import { StyleProp, TouchableWithoutFeedback, ViewStyle } from 'react-native'; +import { + Insets, + StyleProp, + TouchableWithoutFeedback, + ViewStyle, +} from 'react-native'; import Animated, { useAnimatedStyle, useSharedValue, withTiming, } from 'react-native-reanimated'; -import { ReactObjectType } from '@/interfaces/general'; import scales from '@/utils/animationScales'; import haptics, { HapticFeedbackTypes } from '@/utils/haptics'; @@ -17,7 +21,7 @@ interface Props { hapticType?: string; scale?: number; disabled?: boolean; - hitSlop?: ReactObjectType; + hitSlop?: Insets; style?: StyleProp; } diff --git a/src/interfaces/dab.ts b/src/interfaces/dab.ts new file mode 100644 index 00000000..e00d4701 --- /dev/null +++ b/src/interfaces/dab.ts @@ -0,0 +1,17 @@ +import { Principal } from '@dfinity/principal'; +import { Token } from '@psychedelic/dab-js/dist/interfaces/token'; + +import { Standard } from './keyring'; + +export interface DABToken extends Token { + canisterId: Principal; + thumbnail?: string; + details?: { + symbol: string; + standard: Standard; + total_supply: number; + verified: boolean; + decimals: number; + fee: number; + }; +} diff --git a/src/interfaces/general.ts b/src/interfaces/general.ts index 3f536cf3..18049880 100644 --- a/src/interfaces/general.ts +++ b/src/interfaces/general.ts @@ -20,10 +20,3 @@ declare global { tron: any; } } - -export interface ReactObjectType { - bottom?: number | null; - left?: number | null; - right?: number | null; - top?: number | null; -} diff --git a/src/screens/tabs/Tokens/components/AddToken/hooks/useSteps.tsx b/src/screens/tabs/Tokens/components/AddToken/hooks/useSteps.tsx new file mode 100644 index 00000000..f312e71f --- /dev/null +++ b/src/screens/tabs/Tokens/components/AddToken/hooks/useSteps.tsx @@ -0,0 +1,55 @@ +import { t } from 'i18next'; +import React, { useMemo, useState } from 'react'; + +import { DABToken } from '@/interfaces/dab'; + +import { ReviewToken } from '../steps/ReviewToken'; +import { TokenList } from '../steps/TokenList'; + +const useSteps = () => { + const [step, setStep] = useState(0); + + const [selectedToken, setSelectedToken] = useState(); + + const handleSelectedToken = (token: DABToken) => () => { + setSelectedToken(token); + setStep(1); + }; + + // const handleChangeStep = (index: number) => setStep(index); + // const handleClose = () => {}; + + // const leftButton = onClick => ( + // + // ); + // const rightButton = ( + // handleClose()} /> + // ); + + const currentStep = useMemo( + () => + [ + { + component: , + // left: leftButton(() => handleClose()), + // right: rightButton, + center: t('addToken.title'), + }, + { + component: , + // left: leftButton(() => handleChangeStep(0)), + // right: rightButton, + center: t('addToken.title'), + }, + ][step], + [step] + ); + + return currentStep; +}; + +export default useSteps; diff --git a/src/screens/tabs/Tokens/components/AddToken/index.tsx b/src/screens/tabs/Tokens/components/AddToken/index.tsx new file mode 100644 index 00000000..5b46da06 --- /dev/null +++ b/src/screens/tabs/Tokens/components/AddToken/index.tsx @@ -0,0 +1,36 @@ +import React, { useRef } from 'react'; + +import Header from '@/components/common/Header'; +import Modal from '@/components/common/Modal'; +import Text from '@/components/common/Text'; +import Touchable from '@/components/common/Touchable'; +import animationScales from '@/utils/animationScales'; + +import useSteps from './hooks/useSteps'; + +export function AddToken() { + const modalRef = useRef(null); + const { center, component } = useSteps(); + + return ( + <> + modalRef?.current?.open()} + scale={animationScales.medium} + style={{ + height: 48, + width: 48, + backgroundColor: 'blue', + position: 'absolute', + bottom: 24, + right: 20, + borderRadius: 100, + }} + /> + +
{center}} /> + {component} + + + ); +} diff --git a/src/screens/tabs/Tokens/components/AddToken/steps/ReviewToken/index.tsx b/src/screens/tabs/Tokens/components/AddToken/steps/ReviewToken/index.tsx new file mode 100644 index 00000000..5e0de1dd --- /dev/null +++ b/src/screens/tabs/Tokens/components/AddToken/steps/ReviewToken/index.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { View } from 'react-native'; + +import { DABToken } from '@/interfaces/dab'; + +interface Props { + token?: DABToken; +} + +export function ReviewToken({ token }: Props) { + return ( + + ); +} diff --git a/src/screens/tabs/Tokens/components/AddToken/steps/TokenList/index.tsx b/src/screens/tabs/Tokens/components/AddToken/steps/TokenList/index.tsx new file mode 100644 index 00000000..096193f1 --- /dev/null +++ b/src/screens/tabs/Tokens/components/AddToken/steps/TokenList/index.tsx @@ -0,0 +1,63 @@ +import { t } from 'i18next'; +import React, { useEffect, useState } from 'react'; +import { ActivityIndicator, View } from 'react-native'; + +import Image from '@/components/common/Image'; +import SearchBar from '@/components/common/SearchBar'; +import Text from '@/components/common/Text'; +import Touchable from '@/components/common/Touchable'; +import { DABToken } from '@/interfaces/dab'; +import { getDabTokens } from '@/services/DAB'; +import animationScales from '@/utils/animationScales'; + +import styles from './styles'; + +interface Props { + onSelectedToken: (token: any) => void; //TODO: DEFINE TOKEN TYPE +} + +export function TokenList({ onSelectedToken }: Props) { + const [tokens, setTokens] = useState([]); + const [loading, setLoading] = useState(true); + + function renderToken(token: DABToken) { + return ( + onSelectedToken(token)}> + + {token.name} + + ); + } + + useEffect(() => { + const getTokens = async () => { + const tempTokens = await getDabTokens(); + setTokens(tempTokens); + setLoading(false); + }; + getTokens(); + }, []); + + console.tron.log(tokens); + + return ( + <> + + + {t('addToken.availableTokens')} + {loading ? ( + + ) : ( + tokens.map((token: DABToken) => renderToken(token)) + )} + + + ); +} diff --git a/src/screens/tabs/Tokens/components/AddToken/steps/TokenList/styles.ts b/src/screens/tabs/Tokens/components/AddToken/steps/TokenList/styles.ts new file mode 100644 index 00000000..6412ec02 --- /dev/null +++ b/src/screens/tabs/Tokens/components/AddToken/steps/TokenList/styles.ts @@ -0,0 +1,36 @@ +import { StyleSheet } from 'react-native'; + +import { SEMIBOLD } from '@/constants/fonts'; +import { WINDOW_HEIGHT } from '@/constants/platform'; +import { Colors } from '@/constants/theme'; +import { fontMaker } from '@/utils/fonts'; + +const ITEM_HEIGHT = 40; + +export default StyleSheet.create({ + container: { + paddingHorizontal: 16, + }, + item: { + height: ITEM_HEIGHT, + flexDirection: 'row', + alignItems: 'center', + marginVertical: 12, + }, + logo: { + height: ITEM_HEIGHT, + width: ITEM_HEIGHT, + borderRadius: 100, + marginRight: 12, + }, + loader: { + flex: 1, + height: WINDOW_HEIGHT / 2, + }, + listTitle: { + ...fontMaker({ size: 16, weight: SEMIBOLD, color: Colors.White.Secondary }), + }, + searchBar: { + marginBottom: 24, + }, +}); diff --git a/src/screens/tabs/Tokens/components/AddToken/styles.ts b/src/screens/tabs/Tokens/components/AddToken/styles.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/screens/tabs/Tokens/index.js b/src/screens/tabs/Tokens/index.js index fb40effa..f2b4c2fa 100644 --- a/src/screens/tabs/Tokens/index.js +++ b/src/screens/tabs/Tokens/index.js @@ -1,6 +1,6 @@ import React, { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { RefreshControl, ScrollView } from 'react-native'; +import { Pressable, RefreshControl, ScrollView } from 'react-native'; import { useDispatch, useSelector } from 'react-redux'; import ErrorState from '@/commonComponents/ErrorState'; @@ -13,6 +13,7 @@ import { getBalance, setAssetsLoading } from '@/redux/slices/user'; import Send from '@/screens/flows/Send'; import WalletHeader from '../components/WalletHeader'; +import { AddToken } from './components/AddToken'; import styles from './styles'; function Tokens() { @@ -57,26 +58,29 @@ function Tokens() { {!assetsError ? ( - - }> - {assets?.map(token => ( - - ))} - + <> + + }> + {assets?.map(token => ( + + ))} + + + ) : ( => { + const agent = new HttpAgent({ fetch, host: IC_URL_HOST }); + const tokens = await getTokens({ agent }); + const parsedTokens = (tokens || []).map(token => + recursiveParseBigint(recursiveParsePrincipal(token)) + ); + return parsedTokens.map((token: Token) => ({ + ...token, + canisterId: token?.principal_id, + })); +}; + +export const getDabNfts = async () => { + const agent = new HttpAgent({ fetch, host: IC_URL_HOST }); + return getAllNFTS({ agent }); +}; + +export const getTokenBalance = async ( + token: Token, + user: Principal | string +) => { + const agent = new HttpAgent({ + host: 'https://mainnet.dfinity.network', + }); + const tokenActor = await getTokenActor({ + canisterId: token.principal_id.toString(), + agent, + standard: token.standard, + }); + const amount = await tokenActor.getBalance( + user instanceof Principal ? user : Principal.fromText(user) + ); + return amount; +}; diff --git a/src/translations/en/index.js b/src/translations/en/index.js index ae495e64..4d5a953b 100644 --- a/src/translations/en/index.js +++ b/src/translations/en/index.js @@ -210,6 +210,11 @@ const translations = { buttonImage: 'discord', }, }, + addToken: { + title: 'Add Token', + availableTokens: 'Available Tokens', + search: 'Search Tokens', + }, walletConnect: { changeWallet: 'Change Wallet', decline: 'Decline', diff --git a/src/utils/walletConnect.js b/src/utils/walletConnect.js index 80a30b5c..a4418802 100644 --- a/src/utils/walletConnect.js +++ b/src/utils/walletConnect.js @@ -1,13 +1,10 @@ -import { HttpAgent } from '@dfinity/agent'; import { blobFromBuffer } from '@dfinity/candid'; -import { getAllNFTS, getTokens } from '@psychedelic/dab-js'; import PlugController from '@psychedelic/plug-controller'; import { fetch } from 'react-native-fetch-api'; import { XTC_FEE } from '@/constants/addresses'; import { CYCLES_PER_TC } from '@/constants/assets'; import { ASSET_CANISTER_IDS } from '@/constants/canister'; -import { IC_URL_HOST } from '@/constants/general'; import { ERRORS } from '@/constants/walletconnect'; import { ConnectionModule, @@ -23,6 +20,7 @@ import { walletConnectApproveSession, walletConnectRejectSession, } from '@/redux/slices/walletconnect'; +import { getDabNfts, getDabTokens } from '@/services/DAB'; import { validateAccountId, validatePrincipalId } from '@/utils/ids'; import { validateCanisterId } from '@/utils/ids'; import Navigation from '@/utils/navigation'; @@ -31,7 +29,7 @@ import { validateAmount, validateFloatStrAmount, } from '@/utils/number'; -import { recursiveParseBigint, recursiveParsePrincipal } from '@/utils/objects'; +import { recursiveParseBigint } from '@/utils/objects'; import { base64ToBuffer } from './utilities'; @@ -133,23 +131,6 @@ export const initializeProtectedIds = async () => { await setProtectedIds(PROTECTED_IDS); }; -export const getDabTokens = async () => { - const agent = new HttpAgent({ fetch, host: IC_URL_HOST }); - const tokens = await getTokens({ agent }); - const parsedTokens = (tokens || []).map(token => - recursiveParseBigint(recursiveParsePrincipal(token)) - ); - return parsedTokens.map(token => ({ - ...token, - canisterId: token?.principal_id, - })); -}; - -export const getDabNfts = async () => { - const agent = new HttpAgent({ fetch, host: IC_URL_HOST }); - return getAllNFTS({ agent }); -}; - export const fetchCanistersInfo = async whitelist => { if (whitelist && whitelist.length > 0) { const canistersInfo = await Promise.all( diff --git a/yarn.lock b/yarn.lock index c41e7a34..8e365c30 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1640,6 +1640,20 @@ cross-fetch "^3.1.4" crypto-js "^4.1.1" +"@psychedelic/dab-js@^1.4.4": + version "1.4.4" + resolved "https://npm.pkg.github.com/download/@Psychedelic/dab-js/1.4.4/5816040f8f36c7db5bce36ae1153a828b8f78dbf#5816040f8f36c7db5bce36ae1153a828b8f78dbf" + integrity sha512-FuRfyj6nttdN3Jrdo/Rg+76Lnb856WhRR6GKwQ9MidNQCbPfb/WUkPODAdH/+oS0uLNefoJDa/tz8O/FvW4tZA== + dependencies: + "@dfinity/agent" "0.9.3" + "@dfinity/candid" "0.9.3" + "@dfinity/identity" "0.9.3" + "@dfinity/principal" "0.9.3" + axios "^0.24.0" + buffer-crc32 "^0.2.13" + cross-fetch "^3.1.4" + crypto-js "^4.1.1" + "@psychedelic/plug-controller@0.19.2": version "0.19.2" resolved "https://npm.pkg.github.com/download/@Psychedelic/plug-controller/0.19.2/663c67f8ccc9f6eaecca299fcded39ee7efd9feb#663c67f8ccc9f6eaecca299fcded39ee7efd9feb" From f5138bf9fd3396a8e60dd497bc8f66e96c878744 Mon Sep 17 00:00:00 2001 From: nachosan Date: Thu, 4 Aug 2022 17:02:09 -0300 Subject: [PATCH 02/93] Migrated text input to TS --- src/components/common/TextInput/constants.js | 39 ----------------- .../common/TextInput/{index.js => index.tsx} | 43 +++++++++++++------ .../common/TextInput/{styles.js => styles.ts} | 35 ++++++++++++++- .../Send/components/SaveContact/index.js | 2 +- .../components/AddEditContact/index.js | 2 +- .../screens/CreateEditAccount/index.js | 2 +- 6 files changed, 68 insertions(+), 55 deletions(-) delete mode 100644 src/components/common/TextInput/constants.js rename src/components/common/TextInput/{index.js => index.tsx} (72%) rename src/components/common/TextInput/{styles.js => styles.ts} (59%) diff --git a/src/components/common/TextInput/constants.js b/src/components/common/TextInput/constants.js deleted file mode 100644 index 9004b332..00000000 --- a/src/components/common/TextInput/constants.js +++ /dev/null @@ -1,39 +0,0 @@ -import { Colors } from '@/constants/theme'; - -import styles from './styles'; - -export const variants = { - text: { - viewStyle: { ...styles.viewStyle, ...styles.textStyle }, - inputStyle: styles.inputStyle, - placeholderTextColor: Colors.White.Secondary, - autoCorrect: false, - autoCapitalize: 'none', - secureTextEntry: false, - }, - multi: { - viewStyle: { ...styles.viewStyle, ...styles.multiStyle }, - inputStyle: { ...styles.inputStyle, ...styles.multiStyle }, - placeholderTextColor: Colors.White.Secondary, - autoCorrect: false, - autoCapitalize: 'none', - secureTextEntry: false, - }, - password: { - viewStyle: styles.viewStyle, - inputStyle: styles.inputStyle, - placeholderTextColor: Colors.White.Secondary, - autoCorrect: false, - autoCapitalize: 'none', - secureTextEntry: true, - }, - innerLabel: { - viewStyle: { ...styles.labledViewStyle }, - inputStyle: { ...styles.inputStyle, ...styles.labledInputStyle }, - innerLabelStyle: styles.innerLabelStyle, - placeholderTextColor: Colors.White.Secondary, - autoCorrect: false, - autoCapitalize: 'none', - secureTextEntry: false, - }, -}; diff --git a/src/components/common/TextInput/index.js b/src/components/common/TextInput/index.tsx similarity index 72% rename from src/components/common/TextInput/index.js rename to src/components/common/TextInput/index.tsx index a10f8a66..fdcab90a 100644 --- a/src/components/common/TextInput/index.js +++ b/src/components/common/TextInput/index.tsx @@ -1,5 +1,12 @@ import React, { useState } from 'react'; -import { TextInput as Input, View } from 'react-native'; +import { + StyleProp, + TextInput as Input, + TextInputProps, + TextStyle, + View, + ViewStyle, +} from 'react-native'; import LinearGradient from 'react-native-linear-gradient'; import Touchable from '@/commonComponents/Touchable'; @@ -8,14 +15,29 @@ import Icon from '@/icons'; import animationScales from '@/utils/animationScales'; import Text from '../Text'; -import { variants } from './constants'; -import styles from './styles'; +import styles, { variants } from './styles'; + +interface Props extends TextInputProps { + label: string; + ref?: React.RefObject; + value?: string; + variant?: 'text' | 'password' | 'multi' | 'innerLabel'; + onChangeText?: (text: string) => void; + hideGradient?: boolean; + placeholder?: string; + onSubmitEditing?: () => void; + customStyle?: StyleProp; + textStyle?: StyleProp; + disabled?: boolean; + maxLength?: number; + saveContactRef?: React.RefObject; +} const TextInput = ({ label, ref, value, - variant, + variant = 'text', onChangeText, hideGradient, placeholder, @@ -23,18 +45,16 @@ const TextInput = ({ customStyle, textStyle, disabled, - maxLenght, + maxLength, saveContactRef, testID, ...props -}) => { +}: Props) => { const { viewStyle, inputStyle, innerLabelStyle, placeholderTextColor, - autoCorrect, - autoCapitalize, secureTextEntry, } = variants[variant]; const [isFocused, setIsFocused] = useState(false); @@ -53,7 +73,7 @@ const TextInput = ({ }; return ( - + {}}> {isFocused && !hideGradient && ( { autoFocus value={name} variant="text" - maxLenght={22} + maxLength={22} placeholder={t('contacts.namePlaceholder')} onChangeText={handleOnChangeName} /> diff --git a/src/screens/tabs/Profile/screens/CreateEditAccount/index.js b/src/screens/tabs/Profile/screens/CreateEditAccount/index.js index 9717411a..11f77ba3 100644 --- a/src/screens/tabs/Profile/screens/CreateEditAccount/index.js +++ b/src/screens/tabs/Profile/screens/CreateEditAccount/index.js @@ -117,7 +117,7 @@ const CreateEditAccount = ({ modalRef, account, accountsModalRef }) => { Date: Thu, 4 Aug 2022 17:18:00 -0300 Subject: [PATCH 03/93] Removed contacts logic from text input --- src/components/common/TextInput/index.tsx | 22 ++++------------------ src/screens/flows/Send/index.js | 18 +++++++++--------- 2 files changed, 13 insertions(+), 27 deletions(-) diff --git a/src/components/common/TextInput/index.tsx b/src/components/common/TextInput/index.tsx index fdcab90a..03b268be 100644 --- a/src/components/common/TextInput/index.tsx +++ b/src/components/common/TextInput/index.tsx @@ -11,26 +11,20 @@ import LinearGradient from 'react-native-linear-gradient'; import Touchable from '@/commonComponents/Touchable'; import { Rainbow } from '@/constants/theme'; -import Icon from '@/icons'; import animationScales from '@/utils/animationScales'; import Text from '../Text'; import styles, { variants } from './styles'; interface Props extends TextInputProps { - label: string; + label?: string; ref?: React.RefObject; - value?: string; variant?: 'text' | 'password' | 'multi' | 'innerLabel'; - onChangeText?: (text: string) => void; hideGradient?: boolean; - placeholder?: string; - onSubmitEditing?: () => void; customStyle?: StyleProp; textStyle?: StyleProp; disabled?: boolean; - maxLength?: number; - saveContactRef?: React.RefObject; + right?: () => React.ReactNode; } const TextInput = ({ @@ -46,7 +40,7 @@ const TextInput = ({ textStyle, disabled, maxLength, - saveContactRef, + right, testID, ...props }: Props) => { @@ -68,10 +62,6 @@ const TextInput = ({ setIsFocused(false); }; - const handleAddContact = () => { - saveContactRef?.current.open(); - }; - return ( {}}> {isFocused && !hideGradient && ( @@ -107,11 +97,7 @@ const TextInput = ({ testID={testID} {...props} /> - {saveContactRef && ( - - - - )} + {right} ); diff --git a/src/screens/flows/Send/index.js b/src/screens/flows/Send/index.js index 4fbd7daa..3b372b2b 100644 --- a/src/screens/flows/Send/index.js +++ b/src/screens/flows/Send/index.js @@ -8,6 +8,8 @@ import Modal, { modalOffset } from '@/commonComponents/Modal'; import PasswordModal from '@/commonComponents/PasswordModal'; import TextInput from '@/commonComponents/TextInput'; import Text from '@/components/common/Text'; +import Touchable from '@/components/common/Touchable'; +import Icon from '@/components/icons'; import { ADDRESS_TYPES } from '@/constants/addresses'; import { TOKENS, USD_PER_TC } from '@/constants/assets'; import { isAndroid } from '@/constants/platform'; @@ -276,14 +278,6 @@ function Send({ modalRef, nft, token, onSuccess }) { [availableAmount, selectedTokenPrice] ); - const getSaveContactRef = () => { - if (selectedContact || !isValidAddress) { - return null; - } else { - return saveContactRef; - } - }; - const handleBack = () => { setAddress(null); setSelectedContact(null); @@ -343,7 +337,13 @@ function Send({ modalRef, nft, token, onSuccess }) { onChangeText={onChangeText} textStyle={isValidAddress ? styles.valid : null} autoFocus - saveContactRef={getSaveContactRef()} + right={ + !selectedContact && isValidAddress ? ( + + + + ) : null + } /> {!isValidAddress && ( From 11704ecbfa8f32401fccf7b5a84b55e76d8b8039 Mon Sep 17 00:00:00 2001 From: nachosan Date: Fri, 5 Aug 2022 15:34:05 -0300 Subject: [PATCH 04/93] wip: token list ui, improved text input component --- src/components/common/SearchBar/index.tsx | 10 ++++++++-- src/components/common/SearchBar/styles.ts | 15 ++++++++++++++ src/components/common/TextInput/index.tsx | 20 ++++++------------- src/components/common/TextInput/styles.ts | 11 +--------- src/components/icons/svg/Search.svg | 3 +++ src/constants/theme.js | 3 +++ src/screens/flows/Send/index.js | 3 +-- src/screens/flows/Send/styles.js | 6 ++++++ .../tabs/Tokens/components/AddToken/index.tsx | 3 ++- .../AddToken/steps/TokenList/index.tsx | 2 -- .../AddToken/steps/TokenList/styles.ts | 2 ++ 11 files changed, 47 insertions(+), 31 deletions(-) create mode 100644 src/components/icons/svg/Search.svg diff --git a/src/components/common/SearchBar/index.tsx b/src/components/common/SearchBar/index.tsx index 7c0f3314..d10d4d54 100644 --- a/src/components/common/SearchBar/index.tsx +++ b/src/components/common/SearchBar/index.tsx @@ -1,8 +1,12 @@ import React from 'react'; import { StyleProp, View, ViewStyle } from 'react-native'; +import Search from '@/components/icons/svg/Search.svg'; +import animationScales from '@/utils/animationScales'; + import TextInput from '../TextInput'; -import styles from './styles'; +import Touchable from '../Touchable'; +import styles, { searchColor } from './styles'; interface Props { placeholder: string; @@ -11,12 +15,14 @@ interface Props { function SearchBar({ placeholder, style }: Props) { return ( - + } /> + ); } diff --git a/src/components/common/SearchBar/styles.ts b/src/components/common/SearchBar/styles.ts index abefc6b1..3a665f85 100644 --- a/src/components/common/SearchBar/styles.ts +++ b/src/components/common/SearchBar/styles.ts @@ -2,8 +2,23 @@ import { StyleSheet } from 'react-native'; import { Colors } from '@/constants/theme'; +export const searchColor = Colors.White.Secondary; + export default StyleSheet.create({ + container: { + flexDirection: 'row', + }, input: { backgroundColor: Colors.Black.Pure, + borderColor: Colors.Gray.Primary, + borderWidth: 1, + width: '90%', + }, + icon: { + marginRight: 4, + }, + addButton: { + backgroundColor: Colors.Black.Primary, + width: '10%', }, }); diff --git a/src/components/common/TextInput/index.tsx b/src/components/common/TextInput/index.tsx index 03b268be..6cd24fcc 100644 --- a/src/components/common/TextInput/index.tsx +++ b/src/components/common/TextInput/index.tsx @@ -13,22 +13,20 @@ import Touchable from '@/commonComponents/Touchable'; import { Rainbow } from '@/constants/theme'; import animationScales from '@/utils/animationScales'; -import Text from '../Text'; import styles, { variants } from './styles'; interface Props extends TextInputProps { - label?: string; ref?: React.RefObject; variant?: 'text' | 'password' | 'multi' | 'innerLabel'; hideGradient?: boolean; customStyle?: StyleProp; textStyle?: StyleProp; disabled?: boolean; - right?: () => React.ReactNode; + left?: React.ReactNode; + right?: React.ReactNode; } const TextInput = ({ - label, ref, value, variant = 'text', @@ -40,17 +38,13 @@ const TextInput = ({ textStyle, disabled, maxLength, + left, right, testID, ...props }: Props) => { - const { - viewStyle, - inputStyle, - innerLabelStyle, - placeholderTextColor, - secureTextEntry, - } = variants[variant]; + const { viewStyle, inputStyle, placeholderTextColor, secureTextEntry } = + variants[variant]; const [isFocused, setIsFocused] = useState(false); const isMultiline = variant === 'multi'; @@ -75,9 +69,7 @@ const TextInput = ({ /> )} - {variant === 'innerLabel' && ( - {label} - )} + {left} + + diff --git a/src/constants/theme.js b/src/constants/theme.js index 777c6378..afeecb66 100644 --- a/src/constants/theme.js +++ b/src/constants/theme.js @@ -41,6 +41,9 @@ export const Colors = { }, Red: '#FF453A', Transparent: '#ffffff00', + Divider: { + 16: '#3D3D3D', + }, }; export const Rainbow = { diff --git a/src/screens/flows/Send/index.js b/src/screens/flows/Send/index.js index 3b372b2b..45af1534 100644 --- a/src/screens/flows/Send/index.js +++ b/src/screens/flows/Send/index.js @@ -329,14 +329,13 @@ function Send({ modalRef, nft, token, onSuccess }) { center={{t('send.title')}} /> {t('send.inputLabel')}} right={ !selectedContact && isValidAddress ? ( diff --git a/src/screens/flows/Send/styles.js b/src/screens/flows/Send/styles.js index fdbe325e..9a4f161d 100644 --- a/src/screens/flows/Send/styles.js +++ b/src/screens/flows/Send/styles.js @@ -1,6 +1,8 @@ import { StyleSheet } from 'react-native'; +import { SEMIBOLD } from '@/constants/fonts'; import { Colors, FontStyles } from '@/constants/theme'; +import { fontMaker } from '@/utils/fonts'; export default StyleSheet.create({ valid: { @@ -14,4 +16,8 @@ export default StyleSheet.create({ paddingBottom: 10, }, centerText: FontStyles.Subtitle2, + inputLabel: { + ...fontMaker({ size: 18, color: Colors.White.Pure, weight: SEMIBOLD }), + width: 32, + }, }); diff --git a/src/screens/tabs/Tokens/components/AddToken/index.tsx b/src/screens/tabs/Tokens/components/AddToken/index.tsx index 5b46da06..82bf62d4 100644 --- a/src/screens/tabs/Tokens/components/AddToken/index.tsx +++ b/src/screens/tabs/Tokens/components/AddToken/index.tsx @@ -1,4 +1,5 @@ import React, { useRef } from 'react'; +import { Modalize } from 'react-native-modalize'; import Header from '@/components/common/Header'; import Modal from '@/components/common/Modal'; @@ -9,7 +10,7 @@ import animationScales from '@/utils/animationScales'; import useSteps from './hooks/useSteps'; export function AddToken() { - const modalRef = useRef(null); + const modalRef = useRef(null); const { center, component } = useSteps(); return ( diff --git a/src/screens/tabs/Tokens/components/AddToken/steps/TokenList/index.tsx b/src/screens/tabs/Tokens/components/AddToken/steps/TokenList/index.tsx index 096193f1..f571bd71 100644 --- a/src/screens/tabs/Tokens/components/AddToken/steps/TokenList/index.tsx +++ b/src/screens/tabs/Tokens/components/AddToken/steps/TokenList/index.tsx @@ -42,8 +42,6 @@ export function TokenList({ onSelectedToken }: Props) { getTokens(); }, []); - console.tron.log(tokens); - return ( <> diff --git a/src/screens/tabs/Tokens/components/AddToken/steps/TokenList/styles.ts b/src/screens/tabs/Tokens/components/AddToken/steps/TokenList/styles.ts index 6412ec02..a9629a51 100644 --- a/src/screens/tabs/Tokens/components/AddToken/steps/TokenList/styles.ts +++ b/src/screens/tabs/Tokens/components/AddToken/steps/TokenList/styles.ts @@ -22,6 +22,8 @@ export default StyleSheet.create({ width: ITEM_HEIGHT, borderRadius: 100, marginRight: 12, + borderWidth: 1, + borderColor: Colors.Divider[16], }, loader: { flex: 1, From 9423a7f3bdad82cf210a75eb864e7dff8f4134ba Mon Sep 17 00:00:00 2001 From: nachosan Date: Fri, 5 Aug 2022 17:56:02 -0300 Subject: [PATCH 05/93] wip: Improved text input component --- src/components/common/PasswordInput/index.js | 2 +- src/components/common/SearchBar/index.tsx | 1 - src/components/common/SearchBar/styles.ts | 8 +-- src/components/common/TextInput/index.tsx | 16 +++--- src/components/common/TextInput/styles.ts | 49 +------------------ src/screens/auth/ImportSeedPhrase/index.js | 1 - .../Send/components/SaveContact/index.js | 1 - src/screens/flows/Send/index.js | 14 ++++-- src/screens/flows/Send/styles.js | 19 +++++-- .../components/AddEditContact/index.js | 2 - .../screens/CreateEditAccount/index.js | 1 - 11 files changed, 40 insertions(+), 74 deletions(-) diff --git a/src/components/common/PasswordInput/index.js b/src/components/common/PasswordInput/index.js index ec08492d..05065858 100644 --- a/src/components/common/PasswordInput/index.js +++ b/src/components/common/PasswordInput/index.js @@ -79,7 +79,7 @@ function PasswordInput({ disabled={disabled} customStyle={[styles.input, inputStyle]} textStyle={styles.text} - variant={`${showPassword ? 'text' : 'password'}`} + secureTextEntry={!showPassword} onFocus={handleOnFocus} onBlur={handleOnBlur} onSubmitEditing={handleOnSubmit} diff --git a/src/components/common/SearchBar/index.tsx b/src/components/common/SearchBar/index.tsx index d10d4d54..75748d98 100644 --- a/src/components/common/SearchBar/index.tsx +++ b/src/components/common/SearchBar/index.tsx @@ -18,7 +18,6 @@ function SearchBar({ placeholder, style }: Props) { } /> diff --git a/src/components/common/SearchBar/styles.ts b/src/components/common/SearchBar/styles.ts index 3a665f85..9de57787 100644 --- a/src/components/common/SearchBar/styles.ts +++ b/src/components/common/SearchBar/styles.ts @@ -6,13 +6,13 @@ export const searchColor = Colors.White.Secondary; export default StyleSheet.create({ container: { - flexDirection: 'row', + // flexDirection: 'row', }, input: { backgroundColor: Colors.Black.Pure, - borderColor: Colors.Gray.Primary, - borderWidth: 1, - width: '90%', + // borderColor: Colors.Gray.Primary, + // borderWidth: 1, + // width: '90%', }, icon: { marginRight: 4, diff --git a/src/components/common/TextInput/index.tsx b/src/components/common/TextInput/index.tsx index 6cd24fcc..d4023034 100644 --- a/src/components/common/TextInput/index.tsx +++ b/src/components/common/TextInput/index.tsx @@ -13,11 +13,10 @@ import Touchable from '@/commonComponents/Touchable'; import { Rainbow } from '@/constants/theme'; import animationScales from '@/utils/animationScales'; -import styles, { variants } from './styles'; +import styles, { defaultPlaceholderTextColor } from './styles'; interface Props extends TextInputProps { ref?: React.RefObject; - variant?: 'text' | 'password' | 'multi' | 'innerLabel'; hideGradient?: boolean; customStyle?: StyleProp; textStyle?: StyleProp; @@ -29,7 +28,6 @@ interface Props extends TextInputProps { const TextInput = ({ ref, value, - variant = 'text', onChangeText, hideGradient, placeholder, @@ -41,12 +39,14 @@ const TextInput = ({ left, right, testID, + placeholderTextColor = defaultPlaceholderTextColor, + secureTextEntry, + multiline, ...props }: Props) => { - const { viewStyle, inputStyle, placeholderTextColor, secureTextEntry } = - variants[variant]; + const viewStyle = [styles.viewStyle, multiline && styles.multiStyle]; + const baseInputStyle = [styles.inputStyle, multiline && styles.multiStyle]; const [isFocused, setIsFocused] = useState(false); - const isMultiline = variant === 'multi'; const handleOnFocus = () => { setIsFocused(true); @@ -62,7 +62,7 @@ const TextInput = ({ {left} { {t('send.inputLabel')}} + customStyle={styles.input} + left={ + {t('send.inputLabel')} + } right={ !selectedContact && isValidAddress ? ( - + ) : null diff --git a/src/screens/flows/Send/styles.js b/src/screens/flows/Send/styles.js index 9a4f161d..d5f122d9 100644 --- a/src/screens/flows/Send/styles.js +++ b/src/screens/flows/Send/styles.js @@ -5,9 +5,6 @@ import { Colors, FontStyles } from '@/constants/theme'; import { fontMaker } from '@/utils/fonts'; export default StyleSheet.create({ - valid: { - color: Colors.ActionBlue, - }, contactItem: { marginTop: 15, }, @@ -16,8 +13,22 @@ export default StyleSheet.create({ paddingBottom: 10, }, centerText: FontStyles.Subtitle2, - inputLabel: { + input: { + backgroundColor: Colors.Black.Pure, + paddingHorizontal: 0, + marginVertical: 6, + }, + inputLeftLabel: { ...fontMaker({ size: 18, color: Colors.White.Pure, weight: SEMIBOLD }), width: 32, }, + inputText: { + flex: 1, + }, + inputTextValid: { + color: Colors.ActionBlue, + }, + addIcon: { + marginLeft: 4, + }, }); diff --git a/src/screens/tabs/Profile/screens/Contacts/components/AddEditContact/index.js b/src/screens/tabs/Profile/screens/Contacts/components/AddEditContact/index.js index 0bfe8c70..702c8543 100644 --- a/src/screens/tabs/Profile/screens/Contacts/components/AddEditContact/index.js +++ b/src/screens/tabs/Profile/screens/Contacts/components/AddEditContact/index.js @@ -145,14 +145,12 @@ const AddEditContact = ({ modalRef, contact, onClose, contactsRef }) => { { Date: Mon, 8 Aug 2022 11:05:05 -0300 Subject: [PATCH 06/93] wip: last details of add token screen --- src/components/common/SearchBar/index.tsx | 6 ++- src/components/common/SearchBar/styles.ts | 6 --- src/components/common/TextInput/index.tsx | 1 + .../AddToken/steps/TokenList/index.tsx | 40 +++++++++++++++++-- .../AddToken/steps/TokenList/styles.ts | 19 ++++++++- src/translations/en/index.js | 2 + 6 files changed, 61 insertions(+), 13 deletions(-) diff --git a/src/components/common/SearchBar/index.tsx b/src/components/common/SearchBar/index.tsx index 75748d98..67be343e 100644 --- a/src/components/common/SearchBar/index.tsx +++ b/src/components/common/SearchBar/index.tsx @@ -9,16 +9,18 @@ import Touchable from '../Touchable'; import styles, { searchColor } from './styles'; interface Props { + onChangeText: (text: string) => void; placeholder: string; style?: StyleProp; } -function SearchBar({ placeholder, style }: Props) { +function SearchBar({ placeholder, style, onChangeText }: Props) { return ( - + } /> diff --git a/src/components/common/SearchBar/styles.ts b/src/components/common/SearchBar/styles.ts index 9de57787..dae9edfb 100644 --- a/src/components/common/SearchBar/styles.ts +++ b/src/components/common/SearchBar/styles.ts @@ -5,14 +5,8 @@ import { Colors } from '@/constants/theme'; export const searchColor = Colors.White.Secondary; export default StyleSheet.create({ - container: { - // flexDirection: 'row', - }, input: { backgroundColor: Colors.Black.Pure, - // borderColor: Colors.Gray.Primary, - // borderWidth: 1, - // width: '90%', }, icon: { marginRight: 4, diff --git a/src/components/common/TextInput/index.tsx b/src/components/common/TextInput/index.tsx index d4023034..4db678b8 100644 --- a/src/components/common/TextInput/index.tsx +++ b/src/components/common/TextInput/index.tsx @@ -87,6 +87,7 @@ const TextInput = ({ onBlur={handleOnBlur} keyboardAppearance="dark" testID={testID} + multiline={multiline} {...props} /> {right} diff --git a/src/screens/tabs/Tokens/components/AddToken/steps/TokenList/index.tsx b/src/screens/tabs/Tokens/components/AddToken/steps/TokenList/index.tsx index f571bd71..9461b4b8 100644 --- a/src/screens/tabs/Tokens/components/AddToken/steps/TokenList/index.tsx +++ b/src/screens/tabs/Tokens/components/AddToken/steps/TokenList/index.tsx @@ -18,7 +18,9 @@ interface Props { export function TokenList({ onSelectedToken }: Props) { const [tokens, setTokens] = useState([]); + const [filteredTokens, setFilteredTokens] = useState([]); const [loading, setLoading] = useState(true); + const [search, setSearch] = useState(''); function renderToken(token: DABToken) { return ( @@ -33,27 +35,59 @@ export function TokenList({ onSelectedToken }: Props) { ); } + function renderEmptyState() { + return ( + + 🤔 + + {t('addToken.noResults')} + + + {t('addToken.addToken')} + + + ); + } + useEffect(() => { const getTokens = async () => { const tempTokens = await getDabTokens(); setTokens(tempTokens); + setFilteredTokens(tempTokens); setLoading(false); }; getTokens(); }, []); + useEffect(() => { + if (search === '') { + setFilteredTokens(tokens); + } else { + const filtered = tokens.filter(token => + token.name.toLowerCase().includes(search.toLowerCase()) + ); + setFilteredTokens(filtered); + } + }, [search]); + return ( <> - {t('addToken.availableTokens')} {loading ? ( + ) : filteredTokens.length ? ( + <> + + {t('addToken.availableTokens')} + + {filteredTokens.map((token: DABToken) => renderToken(token))} + ) : ( - tokens.map((token: DABToken) => renderToken(token)) + renderEmptyState() )} diff --git a/src/screens/tabs/Tokens/components/AddToken/steps/TokenList/styles.ts b/src/screens/tabs/Tokens/components/AddToken/steps/TokenList/styles.ts index a9629a51..51e791c1 100644 --- a/src/screens/tabs/Tokens/components/AddToken/steps/TokenList/styles.ts +++ b/src/screens/tabs/Tokens/components/AddToken/steps/TokenList/styles.ts @@ -31,8 +31,23 @@ export default StyleSheet.create({ }, listTitle: { ...fontMaker({ size: 16, weight: SEMIBOLD, color: Colors.White.Secondary }), + marginTop: 24, + marginBottom: 12, }, - searchBar: { - marginBottom: 24, + emptyContainer: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + marginTop: 50, + }, + emoji: { + fontSize: 34, + marginBottom: 8, + }, + emptyText: { + color: Colors.White.Secondary, + }, + emptyLink: { + color: Colors.ActionBlue, }, }); diff --git a/src/translations/en/index.js b/src/translations/en/index.js index 4d5a953b..06baf11f 100644 --- a/src/translations/en/index.js +++ b/src/translations/en/index.js @@ -214,6 +214,8 @@ const translations = { title: 'Add Token', availableTokens: 'Available Tokens', search: 'Search Tokens', + noResults: 'No search results found.', + addToken: 'Add custom token', }, walletConnect: { changeWallet: 'Change Wallet', From 8659028a4dad0eb0d544552cb748f4905aa30b0e Mon Sep 17 00:00:00 2001 From: nachosan Date: Mon, 8 Aug 2022 11:15:56 -0300 Subject: [PATCH 07/93] Added button asset, moved components to TS --- .../common/Header/{index.js => index.tsx} | 8 +++++++- .../common/Header/{styles.js => styles.ts} | 0 .../common/Modal/{index.js => index.tsx} | 13 ++++++++++++- .../common/Modal/{styles.js => styles.ts} | 0 src/components/common/Touchable/index.tsx | 2 +- .../Tokens/components/AddToken/assets/Add.png | Bin 0 -> 4639 bytes .../components/AddToken/assets/Add@2x.png | Bin 0 -> 14103 bytes .../components/AddToken/assets/Add@3x.png | Bin 0 -> 27003 bytes .../tabs/Tokens/components/AddToken/index.tsx | 16 ++++++---------- .../tabs/Tokens/components/AddToken/styles.ts | 10 ++++++++++ 10 files changed, 36 insertions(+), 13 deletions(-) rename src/components/common/Header/{index.js => index.tsx} (68%) rename src/components/common/Header/{styles.js => styles.ts} (100%) rename src/components/common/Modal/{index.js => index.tsx} (83%) rename src/components/common/Modal/{styles.js => styles.ts} (100%) create mode 100644 src/screens/tabs/Tokens/components/AddToken/assets/Add.png create mode 100644 src/screens/tabs/Tokens/components/AddToken/assets/Add@2x.png create mode 100644 src/screens/tabs/Tokens/components/AddToken/assets/Add@3x.png diff --git a/src/components/common/Header/index.js b/src/components/common/Header/index.tsx similarity index 68% rename from src/components/common/Header/index.js rename to src/components/common/Header/index.tsx index fab00709..6ed6f20d 100644 --- a/src/components/common/Header/index.js +++ b/src/components/common/Header/index.tsx @@ -3,7 +3,13 @@ import { View } from 'react-native'; import styles from './styles'; -const Header = ({ left, center, right }) => ( +interface Props { + left?: React.ReactNode; + center?: React.ReactNode; + right?: React.ReactNode; +} + +const Header = ({ left, center, right }: Props) => ( {left && {left}} {center && {center}} diff --git a/src/components/common/Header/styles.js b/src/components/common/Header/styles.ts similarity index 100% rename from src/components/common/Header/styles.js rename to src/components/common/Header/styles.ts diff --git a/src/components/common/Modal/index.js b/src/components/common/Modal/index.tsx similarity index 83% rename from src/components/common/Modal/index.js rename to src/components/common/Modal/index.tsx index dd3376f1..b4da5f60 100644 --- a/src/components/common/Modal/index.js +++ b/src/components/common/Modal/index.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { StyleProp, ViewStyle } from 'react-native'; import { Modalize } from 'react-native-modalize'; import { Portal } from 'react-native-portalize'; @@ -8,6 +9,16 @@ import styles from './styles'; export const modalOffset = withNotch ? undefined : isIos ? 10 : 35; +interface Props { + children: React.ReactNode; + modalRef: React.RefObject; + onClose?: () => void; + fullHeight?: boolean; + adjustToContentHeight?: boolean; + scrollViewProps?: any; + modalStyle?: StyleProp; +} + function Modal({ children, modalRef, @@ -17,7 +28,7 @@ function Modal({ scrollViewProps, modalStyle, ...props -}) { +}: Props) { return ( void; onLongPress?: () => void; hapticType?: string; diff --git a/src/screens/tabs/Tokens/components/AddToken/assets/Add.png b/src/screens/tabs/Tokens/components/AddToken/assets/Add.png new file mode 100644 index 0000000000000000000000000000000000000000..06af876eff17e0b510d2bab376d20df035c22524 GIT binary patch literal 4639 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 VD>d-+XE6W(002ovPDHLkV1iTlxPkxx literal 0 HcmV?d00001 diff --git a/src/screens/tabs/Tokens/components/AddToken/assets/Add@2x.png b/src/screens/tabs/Tokens/components/AddToken/assets/Add@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..fea7a50615d5ec8b38a74d5d323fe81a22a15216 GIT binary patch literal 14103 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 eBO1{&qyHa8SRio3cz>J#0000 literal 0 HcmV?d00001 diff --git a/src/screens/tabs/Tokens/components/AddToken/index.tsx b/src/screens/tabs/Tokens/components/AddToken/index.tsx index 82bf62d4..6cbc565b 100644 --- a/src/screens/tabs/Tokens/components/AddToken/index.tsx +++ b/src/screens/tabs/Tokens/components/AddToken/index.tsx @@ -1,4 +1,5 @@ import React, { useRef } from 'react'; +import { Image } from 'react-native'; import { Modalize } from 'react-native-modalize'; import Header from '@/components/common/Header'; @@ -7,7 +8,9 @@ import Text from '@/components/common/Text'; import Touchable from '@/components/common/Touchable'; import animationScales from '@/utils/animationScales'; +import Add from './assets/Add.png'; import useSteps from './hooks/useSteps'; +import styles from './styles'; export function AddToken() { const modalRef = useRef(null); @@ -18,16 +21,9 @@ export function AddToken() { modalRef?.current?.open()} scale={animationScales.medium} - style={{ - height: 48, - width: 48, - backgroundColor: 'blue', - position: 'absolute', - bottom: 24, - right: 20, - borderRadius: 100, - }} - /> + style={styles.buttonContainer}> + +
{center}} /> {component} diff --git a/src/screens/tabs/Tokens/components/AddToken/styles.ts b/src/screens/tabs/Tokens/components/AddToken/styles.ts index e69de29b..928f781a 100644 --- a/src/screens/tabs/Tokens/components/AddToken/styles.ts +++ b/src/screens/tabs/Tokens/components/AddToken/styles.ts @@ -0,0 +1,10 @@ +import { StyleSheet } from 'react-native'; + +export default StyleSheet.create({ + buttonContainer: { + position: 'absolute', + bottom: 16, + right: 20, + borderRadius: 100, + }, +}); From aa44a6a235083ee82b59f4a07ff11bda1d352ca8 Mon Sep 17 00:00:00 2001 From: nachosan Date: Tue, 9 Aug 2022 12:48:58 -0300 Subject: [PATCH 08/93] wip: implementing token review screen, migrated Back component to TS --- package.json | 2 +- src/components/common/ActionButton/index.tsx | 25 ++++++ .../styles.js => ActionButton/styles.ts} | 4 +- src/components/common/Back/index.js | 19 ----- src/components/common/Modal/index.tsx | 3 + .../formatters/TokenFormat/index.tsx | 6 +- src/components/formatters/UsdFormat/index.tsx | 6 +- src/constants/theme.js | 2 +- src/interfaces/dab.ts | 2 + src/redux/utils.js | 3 +- src/screens/auth/BackupSeedPhrase/index.js | 4 +- src/screens/auth/CreatePassword/index.js | 6 +- src/screens/auth/ImportSeedPhrase/index.js | 4 +- src/screens/flows/Send/index.js | 8 +- .../components/AddToken/hooks/useSteps.tsx | 82 +++++++++++++------ .../tabs/Tokens/components/AddToken/index.tsx | 26 ++++-- .../AddToken/steps/ReviewToken/index.tsx | 77 +++++++++++++++-- .../AddToken/steps/ReviewToken/styles.ts | 51 ++++++++++++ .../AddToken/steps/TokenList/index.tsx | 33 +++----- .../tabs/Tokens/components/AddToken/styles.ts | 5 ++ .../tabs/Tokens/components/AddToken/utils.ts | 11 +++ src/services/DAB.ts | 1 + src/translations/en/index.js | 7 +- src/utils/currencies.ts | 2 +- src/utils/number.ts | 26 ------ yarn.lock | 2 +- 26 files changed, 294 insertions(+), 123 deletions(-) create mode 100644 src/components/common/ActionButton/index.tsx rename src/components/common/{Back/styles.js => ActionButton/styles.ts} (72%) delete mode 100644 src/components/common/Back/index.js create mode 100644 src/screens/tabs/Tokens/components/AddToken/steps/ReviewToken/styles.ts create mode 100644 src/screens/tabs/Tokens/components/AddToken/utils.ts diff --git a/package.json b/package.json index 4f19af5e..e9459910 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "@dfinity/candid": "0.9.3", "@dfinity/principal": "^0.9.3", "@hookform/error-message": "^2.0.0", - "@psychedelic/dab-js": "^1.4.4", + "@psychedelic/dab-js": "1.4.4", "@psychedelic/plug-controller": "0.19.2", "@react-native-async-storage/async-storage": "^1.15.11", "@react-native-community/blur": "^3.6.0", diff --git a/src/components/common/ActionButton/index.tsx b/src/components/common/ActionButton/index.tsx new file mode 100644 index 00000000..af359a40 --- /dev/null +++ b/src/components/common/ActionButton/index.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { TouchableOpacity } from 'react-native'; + +import Text from '../Text'; +import styles from './styles'; + +interface Props { + onPress: () => void; + label: string; +} + +function ActionButton({ onPress, label }: Props) { + return ( + + + {label} + + + ); +} + +export default ActionButton; diff --git a/src/components/common/Back/styles.js b/src/components/common/ActionButton/styles.ts similarity index 72% rename from src/components/common/Back/styles.js rename to src/components/common/ActionButton/styles.ts index 179b7db3..ccda6885 100644 --- a/src/components/common/Back/styles.js +++ b/src/components/common/ActionButton/styles.ts @@ -1,6 +1,7 @@ import { StyleSheet } from 'react-native'; import { Colors } from '@/constants/theme'; +import { fontMaker } from '@/utils/fonts'; export default StyleSheet.create({ container: { @@ -8,8 +9,7 @@ export default StyleSheet.create({ alignItems: 'center', }, text: { - color: Colors.ActionBlue, - fontSize: 17, + ...fontMaker({ color: Colors.ActionBlue }), marginLeft: 5, }, }); diff --git a/src/components/common/Back/index.js b/src/components/common/Back/index.js deleted file mode 100644 index ed57f713..00000000 --- a/src/components/common/Back/index.js +++ /dev/null @@ -1,19 +0,0 @@ -import { t } from 'i18next'; -import React from 'react'; -import { TouchableOpacity } from 'react-native'; - -import Icon from '@/icons'; - -import Text from '../Text'; -import styles from './styles'; - -function Back({ onPress }) { - return ( - - - {t('common.back')} - - ); -} - -export default Back; diff --git a/src/components/common/Modal/index.tsx b/src/components/common/Modal/index.tsx index b4da5f60..b16ae023 100644 --- a/src/components/common/Modal/index.tsx +++ b/src/components/common/Modal/index.tsx @@ -13,6 +13,7 @@ interface Props { children: React.ReactNode; modalRef: React.RefObject; onClose?: () => void; + onClosed?: () => void; fullHeight?: boolean; adjustToContentHeight?: boolean; scrollViewProps?: any; @@ -23,6 +24,7 @@ function Modal({ children, modalRef, onClose, + onClosed, fullHeight, adjustToContentHeight, scrollViewProps, @@ -58,6 +60,7 @@ function Modal({ modalTopOffset={modalOffset} onOverlayPress={onClose} onClose={onClose} + onClosed={onClosed} adjustToContentHeight={adjustToContentHeight} threshold={15}> {children} diff --git a/src/components/formatters/TokenFormat/index.tsx b/src/components/formatters/TokenFormat/index.tsx index 5fe22c9c..ce695be8 100644 --- a/src/components/formatters/TokenFormat/index.tsx +++ b/src/components/formatters/TokenFormat/index.tsx @@ -21,7 +21,11 @@ function TokenFormat({ value, token, style, decimalScale }: Props) { fixedDecimalScale decimalScale={decimalScale || (value && value > 0) ? VISIBLE_DECIMALS : 2} suffix={` ${token}`} - renderText={textValue => {textValue}} + renderText={textValue => ( + + {textValue} + + )} /> ); } diff --git a/src/components/formatters/UsdFormat/index.tsx b/src/components/formatters/UsdFormat/index.tsx index f5ff66f6..51bd9362 100644 --- a/src/components/formatters/UsdFormat/index.tsx +++ b/src/components/formatters/UsdFormat/index.tsx @@ -19,7 +19,11 @@ function UsdFormat({ value, style, decimalScale = 2 }: Props) { fixedDecimalScale decimalScale={decimalScale} prefix="$" - renderText={textValue => {textValue}} + renderText={textValue => ( + + {textValue} + + )} /> ); } diff --git a/src/constants/theme.js b/src/constants/theme.js index afeecb66..23e9205a 100644 --- a/src/constants/theme.js +++ b/src/constants/theme.js @@ -147,5 +147,5 @@ export const FontStyles = StyleSheet.create({ Body2: fontMaker({ size: 17, weight: MEDIUM }), Button: fontMaker({ size: 20, weight: SEMIBOLD, color: Colors.White.Pure }), Caption: fontMaker({ size: 14, weight: MEDIUM }), - Overline: fontMaker({ size: 12, weight: REGULAR }), + Overline: fontMaker({ size: 10, weight: REGULAR }), }); diff --git a/src/interfaces/dab.ts b/src/interfaces/dab.ts index e00d4701..cbfcde98 100644 --- a/src/interfaces/dab.ts +++ b/src/interfaces/dab.ts @@ -3,6 +3,8 @@ import { Token } from '@psychedelic/dab-js/dist/interfaces/token'; import { Standard } from './keyring'; +export type { BalanceResponse } from '@psychedelic/dab-js/dist/interfaces/token'; + export interface DABToken extends Token { canisterId: Principal; thumbnail?: string; diff --git a/src/redux/utils.js b/src/redux/utils.js index 2f592d56..a38ea637 100644 --- a/src/redux/utils.js +++ b/src/redux/utils.js @@ -3,8 +3,7 @@ import { t } from 'i18next'; import { TOKEN_IMAGES, TOKENS } from '@/constants/assets'; import { ACTIVITY_STATUS } from '@/constants/business'; -import { formatAssetBySymbol } from '@/utils/currencies'; -import { parseToFloatAmount } from '@/utils/number'; +import { formatAssetBySymbol, parseToFloatAmount } from '@/utils/currencies'; import { recursiveParseBigint } from '@/utils/objects'; import { reset } from './slices/keyring'; diff --git a/src/screens/auth/BackupSeedPhrase/index.js b/src/screens/auth/BackupSeedPhrase/index.js index 5cb61fbc..c6bfc17f 100644 --- a/src/screens/auth/BackupSeedPhrase/index.js +++ b/src/screens/auth/BackupSeedPhrase/index.js @@ -3,11 +3,11 @@ import { useTranslation } from 'react-i18next'; import { Image, ScrollView, View } from 'react-native'; import PlugLogo from '@/assets/icons/plug-logo-full.png'; -import Back from '@/commonComponents/Back'; import Copy from '@/commonComponents/Copy'; import Header from '@/commonComponents/Header'; import SeedPhrase from '@/commonComponents/SeedPhrase'; import RainbowButton from '@/components/buttons/RainbowButton'; +import ActionButton from '@/components/common/ActionButton'; import Text from '@/components/common/Text'; import { Container } from '@/layout'; import Routes from '@/navigation/Routes'; @@ -26,7 +26,7 @@ const BackupSeedPhrase = ({ route, navigation }) => { return (
} + left={} center={ { return (
goBack()} />} + left={ + goBack()} label={t('common.back')} /> + } center={ diff --git a/src/screens/auth/ImportSeedPhrase/index.js b/src/screens/auth/ImportSeedPhrase/index.js index fb8d5844..aed9fb0d 100644 --- a/src/screens/auth/ImportSeedPhrase/index.js +++ b/src/screens/auth/ImportSeedPhrase/index.js @@ -4,10 +4,10 @@ import { Image, View } from 'react-native'; import { useDispatch, useSelector } from 'react-redux'; import PlugLogo from '@/assets/icons/plug-logo-full.png'; -import Back from '@/commonComponents/Back'; import Header from '@/commonComponents/Header'; import TextInput from '@/commonComponents/TextInput'; import RainbowButton from '@/components/buttons/RainbowButton'; +import ActionButton from '@/components/common/ActionButton'; import KeyboardScrollView from '@/components/common/KeyboardScrollView'; import Text from '@/components/common/Text'; import { TestIds } from '@/constants/testIds'; @@ -83,7 +83,7 @@ const ImportSeedPhrase = ({ navigation, route }) => { return (
} + left={} center={ diff --git a/src/screens/flows/Send/index.js b/src/screens/flows/Send/index.js index ae79a5fa..c9f9ea43 100644 --- a/src/screens/flows/Send/index.js +++ b/src/screens/flows/Send/index.js @@ -7,13 +7,13 @@ import Header from '@/commonComponents/Header'; import Modal, { modalOffset } from '@/commonComponents/Modal'; import PasswordModal from '@/commonComponents/PasswordModal'; import TextInput from '@/commonComponents/TextInput'; +import ActionButton from '@/components/common/ActionButton'; import Text from '@/components/common/Text'; import Touchable from '@/components/common/Touchable'; import Icon from '@/components/icons'; import { ADDRESS_TYPES } from '@/constants/addresses'; import { TOKENS, USD_PER_TC } from '@/constants/assets'; import { isAndroid } from '@/constants/platform'; -import { FontStyles } from '@/constants/theme'; import XTC_OPTIONS from '@/constants/xtc'; import useKeychain from '@/hooks/useKeychain'; import { getICPPrice } from '@/redux/slices/icp'; @@ -319,11 +319,7 @@ function Send({ modalRef, nft, token, onSuccess }) {
- {t('common.back')} - + ) } center={{t('send.title')}} diff --git a/src/screens/tabs/Tokens/components/AddToken/hooks/useSteps.tsx b/src/screens/tabs/Tokens/components/AddToken/hooks/useSteps.tsx index f312e71f..69ec7f04 100644 --- a/src/screens/tabs/Tokens/components/AddToken/hooks/useSteps.tsx +++ b/src/screens/tabs/Tokens/components/AddToken/hooks/useSteps.tsx @@ -1,55 +1,87 @@ import { t } from 'i18next'; -import React, { useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import ActionButton from '@/components/common/ActionButton'; import { DABToken } from '@/interfaces/dab'; +import { getDabTokens } from '@/services/DAB'; import { ReviewToken } from '../steps/ReviewToken'; import { TokenList } from '../steps/TokenList'; -const useSteps = () => { - const [step, setStep] = useState(0); +interface Return { + currentStep: { + component: React.ReactNode; + center?: string; + left?: React.ReactNode; + right?: React.ReactNode; + adjustModalContent?: boolean; + }; + setStep: (step: number) => void; +} +interface Props { + handleModalClose?: () => void; +} + +const useSteps = ({ handleModalClose }: Props): Return => { + const [step, setStep] = useState(0); + const [tokens, setTokens] = useState([]); + const [tokensLoading, setTokensLoading] = useState(true); const [selectedToken, setSelectedToken] = useState(); - const handleSelectedToken = (token: DABToken) => () => { + const handleSelectedToken = useCallback((token: DABToken) => { setSelectedToken(token); setStep(1); - }; + }, []); + + const handleChangeStep = (index: number) => setStep(index); - // const handleChangeStep = (index: number) => setStep(index); - // const handleClose = () => {}; + const handleClose = () => { + handleModalClose?.(); + }; - // const leftButton = onClick => ( - // - // ); - // const rightButton = ( - // handleClose()} /> - // ); + useEffect(() => { + const getTokens = async () => { + const tempTokens = await getDabTokens(); + setTokens(tempTokens); + setTokensLoading(false); + }; + getTokens(); + }, []); const currentStep = useMemo( () => [ { - component: , - // left: leftButton(() => handleClose()), - // right: rightButton, + component: ( + + ), center: t('addToken.title'), + adjustModalContent: false, }, { component: , - // left: leftButton(() => handleChangeStep(0)), - // right: rightButton, - center: t('addToken.title'), + left: ( + handleChangeStep(0)} + label={t('common.back')} + /> + ), + right: ( + + ), + center: t('addToken.reviewTitle'), + adjustModalContent: true, }, ][step], - [step] + [step, tokens, selectedToken, tokensLoading] ); - return currentStep; + return { currentStep, setStep }; }; export default useSteps; diff --git a/src/screens/tabs/Tokens/components/AddToken/index.tsx b/src/screens/tabs/Tokens/components/AddToken/index.tsx index 6cbc565b..5bcbff8f 100644 --- a/src/screens/tabs/Tokens/components/AddToken/index.tsx +++ b/src/screens/tabs/Tokens/components/AddToken/index.tsx @@ -1,4 +1,4 @@ -import React, { useRef } from 'react'; +import React, { useCallback, useRef } from 'react'; import { Image } from 'react-native'; import { Modalize } from 'react-native-modalize'; @@ -14,7 +14,12 @@ import styles from './styles'; export function AddToken() { const modalRef = useRef(null); - const { center, component } = useSteps(); + + const handleModalClose = useCallback(() => { + modalRef.current?.close(); + }, [modalRef]); + + const { currentStep, setStep } = useSteps({ handleModalClose }); return ( <> @@ -24,9 +29,20 @@ export function AddToken() { style={styles.buttonContainer}> - -
{center}} /> - {component} + setStep(0)}> +
+ {currentStep?.center} + + } + /> + {currentStep.component} ); diff --git a/src/screens/tabs/Tokens/components/AddToken/steps/ReviewToken/index.tsx b/src/screens/tabs/Tokens/components/AddToken/steps/ReviewToken/index.tsx index 5e0de1dd..86defe96 100644 --- a/src/screens/tabs/Tokens/components/AddToken/steps/ReviewToken/index.tsx +++ b/src/screens/tabs/Tokens/components/AddToken/steps/ReviewToken/index.tsx @@ -1,16 +1,83 @@ -import React from 'react'; -import { View } from 'react-native'; +import { t } from 'i18next'; +import React, { useEffect, useState } from 'react'; +import { ActivityIndicator, View } from 'react-native'; +import { useSelector } from 'react-redux'; +import RainbowButton from '@/components/buttons/RainbowButton'; +import Image from '@/components/common/Image'; +import Text from '@/components/common/Text'; +import TokenFormat from '@/components/formatters/TokenFormat'; +import UsdFormat from '@/components/formatters/UsdFormat'; import { DABToken } from '@/interfaces/dab'; +import { getTokenBalance } from '@/services/DAB'; + +import { parseToken } from '../../utils'; +import styles, { loaderColor } from './styles'; interface Props { token?: DABToken; } export function ReviewToken({ token }: Props) { + const { principal } = useSelector(state => state.keyring?.currentWallet); + const { icpPrice } = useSelector(state => state.icp); + const [balance, setBalance] = useState<{ amount: number; value?: number }>(); + const [loading, setLoading] = useState(true); + + function renderToken() { + return ( + + {loading ? ( + + ) : ( + <> + + + + + {token?.name} + + {balance?.value && ( + + )} + + {token && balance && ( + + )} + + + )} + + ); + } + + useEffect(() => { + const getBalance = async () => { + setLoading(true); + const res = await getTokenBalance(token!, principal); + setBalance(parseToken(token!, res, icpPrice)); + setLoading(false); + }; + getBalance(); + }, [token]); + + console.log(balance); + return ( - + + + {t('addToken.safetyAlert')} + + {renderToken()} + {}} + disabled={loading} + /> + ); } diff --git a/src/screens/tabs/Tokens/components/AddToken/steps/ReviewToken/styles.ts b/src/screens/tabs/Tokens/components/AddToken/steps/ReviewToken/styles.ts new file mode 100644 index 00000000..3c361d41 --- /dev/null +++ b/src/screens/tabs/Tokens/components/AddToken/steps/ReviewToken/styles.ts @@ -0,0 +1,51 @@ +import { StyleSheet } from 'react-native'; + +import { Colors } from '@/constants/theme'; + +export const loaderColor = Colors.White.Primary; + +export default StyleSheet.create({ + container: { + flex: 1, + paddingHorizontal: 20, + paddingBottom: 20, + }, + alert: { + color: Colors.White.Secondary, + textAlign: 'center', + }, + tokenContainer: { + marginVertical: 36, + height: 70, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + borderWidth: 1, + borderTopColor: Colors.Divider[16], + borderBottomColor: Colors.Divider[16], + padding: 14, + }, + logo: { + height: 40, + width: 40, + borderColor: Colors.Divider[16], + borderWidth: 1, + borderRadius: 100, + marginRight: 12, + }, + textContainer: { + flex: 1, + height: 50, + justifyContent: 'center', + }, + topRow: { + flexDirection: 'row', + justifyContent: 'space-between', + }, + textWhite: { + color: Colors.White.Primary, + }, + amount: { + color: Colors.White.Secondary, + }, +}); diff --git a/src/screens/tabs/Tokens/components/AddToken/steps/TokenList/index.tsx b/src/screens/tabs/Tokens/components/AddToken/steps/TokenList/index.tsx index 9461b4b8..fe8572cf 100644 --- a/src/screens/tabs/Tokens/components/AddToken/steps/TokenList/index.tsx +++ b/src/screens/tabs/Tokens/components/AddToken/steps/TokenList/index.tsx @@ -7,19 +7,18 @@ import SearchBar from '@/components/common/SearchBar'; import Text from '@/components/common/Text'; import Touchable from '@/components/common/Touchable'; import { DABToken } from '@/interfaces/dab'; -import { getDabTokens } from '@/services/DAB'; import animationScales from '@/utils/animationScales'; import styles from './styles'; interface Props { - onSelectedToken: (token: any) => void; //TODO: DEFINE TOKEN TYPE + onSelectedToken: (token: DABToken) => void; + tokens: DABToken[]; + loading: boolean; } -export function TokenList({ onSelectedToken }: Props) { - const [tokens, setTokens] = useState([]); - const [filteredTokens, setFilteredTokens] = useState([]); - const [loading, setLoading] = useState(true); +export function TokenList({ onSelectedToken, tokens, loading }: Props) { + const [filteredTokens, setFilteredTokens] = useState(tokens); const [search, setSearch] = useState(''); function renderToken(token: DABToken) { @@ -43,28 +42,20 @@ export function TokenList({ onSelectedToken }: Props) { {t('addToken.noResults')} - {t('addToken.addToken')} + {t('addToken.addCustomToken')} ); } - useEffect(() => { - const getTokens = async () => { - const tempTokens = await getDabTokens(); - setTokens(tempTokens); - setFilteredTokens(tempTokens); - setLoading(false); - }; - getTokens(); - }, []); - useEffect(() => { if (search === '') { setFilteredTokens(tokens); } else { - const filtered = tokens.filter(token => - token.name.toLowerCase().includes(search.toLowerCase()) + const filtered = tokens.filter( + token => + token.name.toLowerCase().includes(search.toLowerCase()) || + token.symbol.toLowerCase().includes(search.toLowerCase()) ); setFilteredTokens(filtered); } @@ -82,7 +73,9 @@ export function TokenList({ onSelectedToken }: Props) { ) : filteredTokens.length ? ( <> - {t('addToken.availableTokens')} + {search.length + ? t('addToken.searchResults') + : t('addToken.availableTokens')} {filteredTokens.map((token: DABToken) => renderToken(token))} diff --git a/src/screens/tabs/Tokens/components/AddToken/styles.ts b/src/screens/tabs/Tokens/components/AddToken/styles.ts index 928f781a..37112e64 100644 --- a/src/screens/tabs/Tokens/components/AddToken/styles.ts +++ b/src/screens/tabs/Tokens/components/AddToken/styles.ts @@ -1,5 +1,7 @@ import { StyleSheet } from 'react-native'; +import { Colors } from '@/constants/theme'; + export default StyleSheet.create({ buttonContainer: { position: 'absolute', @@ -7,4 +9,7 @@ export default StyleSheet.create({ right: 20, borderRadius: 100, }, + title: { + color: Colors.White.Primary, + }, }); diff --git a/src/screens/tabs/Tokens/components/AddToken/utils.ts b/src/screens/tabs/Tokens/components/AddToken/utils.ts new file mode 100644 index 00000000..73bdf6fb --- /dev/null +++ b/src/screens/tabs/Tokens/components/AddToken/utils.ts @@ -0,0 +1,11 @@ +import { BalanceResponse, DABToken } from '@/interfaces/dab'; +import { formatAssetBySymbol, parseToFloatAmount } from '@/utils/currencies'; + +export const parseToken = ( + token: DABToken, + amount: BalanceResponse, + icpPrice: number +) => { + const parsedAmount = parseToFloatAmount(amount.value, amount.decimals); + return formatAssetBySymbol(parsedAmount, token.symbol, icpPrice); +}; diff --git a/src/services/DAB.ts b/src/services/DAB.ts index 388e77ff..9fd35aac 100644 --- a/src/services/DAB.ts +++ b/src/services/DAB.ts @@ -34,6 +34,7 @@ export const getTokenBalance = async ( user: Principal | string ) => { const agent = new HttpAgent({ + fetch, host: 'https://mainnet.dfinity.network', }); const tokenActor = await getTokenActor({ diff --git a/src/translations/en/index.js b/src/translations/en/index.js index 06baf11f..c2244207 100644 --- a/src/translations/en/index.js +++ b/src/translations/en/index.js @@ -212,10 +212,15 @@ const translations = { }, addToken: { title: 'Add Token', + reviewTitle: 'Review Token', availableTokens: 'Available Tokens', search: 'Search Tokens', + searchResults: 'Search Results', noResults: 'No search results found.', - addToken: 'Add custom token', + addCustomToken: 'Add custom token', + addButton: 'Add Token', + safetyAlert: + 'Token Safety Alert: For your security, make sure to do proper research before interacting with any token.', }, walletConnect: { changeWallet: 'Change Wallet', diff --git a/src/utils/currencies.ts b/src/utils/currencies.ts index b19eb387..7c6ddae2 100644 --- a/src/utils/currencies.ts +++ b/src/utils/currencies.ts @@ -36,7 +36,7 @@ export const formatAssetBySymbol = ( value: icpValue, icon: TOKEN_IMAGES.WICP, }, - }[symbol] || { amount, value: 0, icon: undefined } // What should we do if we don't know the value? + }[symbol] || { amount, value: undefined, icon: undefined } // What should we do if we don't know the value? ); }; diff --git a/src/utils/number.ts b/src/utils/number.ts index 90dff545..6bab0482 100644 --- a/src/utils/number.ts +++ b/src/utils/number.ts @@ -35,32 +35,6 @@ export const formatToMaxDecimals = (number: number, maxDecimals: number) => { return number; }; -// TODO: check if this goes here -export const parseToFloatAmount = (amount: number, decimals: number) => { - let amountString = `${amount}`; - let prefix = ''; - - if (amountString[0] === '-') { - prefix = '-'; - amountString = amountString.slice(1, amountString.length); - } - - const difference = decimals - amountString.length; - - if (decimals >= amountString.length) { - const formatedString = '0'.repeat(difference + 1) + amountString; - - return `${prefix + formatedString[0]}.${formatedString.slice( - 1, - formatedString.length - )}`; - } - - return `${ - prefix + amountString.slice(0, Math.abs(difference)) - }.${amountString.slice(Math.abs(difference))}`; -}; - export function parseLocaleNumber(stringNumber: string) { return Number( stringNumber diff --git a/yarn.lock b/yarn.lock index 8e365c30..7936c0c6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1640,7 +1640,7 @@ cross-fetch "^3.1.4" crypto-js "^4.1.1" -"@psychedelic/dab-js@^1.4.4": +"@psychedelic/dab-js@1.4.4": version "1.4.4" resolved "https://npm.pkg.github.com/download/@Psychedelic/dab-js/1.4.4/5816040f8f36c7db5bce36ae1153a828b8f78dbf#5816040f8f36c7db5bce36ae1153a828b8f78dbf" integrity sha512-FuRfyj6nttdN3Jrdo/Rg+76Lnb856WhRR6GKwQ9MidNQCbPfb/WUkPODAdH/+oS0uLNefoJDa/tz8O/FvW4tZA== From f8fc8e685215d0eeafc2f15a7b4720d7f941ce15 Mon Sep 17 00:00:00 2001 From: nachosan Date: Tue, 9 Aug 2022 16:32:42 -0300 Subject: [PATCH 09/93] wip: added action to register token --- package.json | 2 +- src/redux/slices/user.js | 22 +++++++++++++++ .../components/AddToken/hooks/useSteps.tsx | 4 ++- .../AddToken/steps/ReviewToken/index.tsx | 27 ++++++++++++++----- src/screens/tabs/Tokens/index.js | 5 +++- src/utils/currencies.ts | 7 ++--- yarn.lock | 16 +---------- 7 files changed, 53 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index e9459910..5d0509a8 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "@dfinity/candid": "0.9.3", "@dfinity/principal": "^0.9.3", "@hookform/error-message": "^2.0.0", - "@psychedelic/dab-js": "1.4.4", + "@psychedelic/dab-js": "1.4.3", "@psychedelic/plug-controller": "0.19.2", "@react-native-async-storage/async-storage": "^1.15.11", "@react-native-community/blur": "^3.6.0", diff --git a/src/redux/slices/user.js b/src/redux/slices/user.js index f5b89e61..22ea9910 100644 --- a/src/redux/slices/user.js +++ b/src/redux/slices/user.js @@ -151,6 +151,7 @@ export const asyncGetBalance = async (params, state, dispatch) => { } else { instance?.getBalances(subaccount); } + const icpPrice = await dispatch(getICPPrice()).unwrap(); return formatAssets(assets, icpPrice); } catch (e) { @@ -356,6 +357,7 @@ export const editContact = createAsyncThunk( } } ); + export const getICNSData = createAsyncThunk( 'keyring/getICNSData', async ({ refresh }, { getState, dispatch }) => { @@ -371,6 +373,26 @@ export const getICNSData = createAsyncThunk( } ); +export const addCustomToken = createAsyncThunk( + 'keyring/addCustomToken', + /** + * @param {{token: DABToken, callback: () => void}} param + */ + async ({ token, callback }, { getState, dispatch }) => { + const { keyring } = getState(); + const response = await keyring?.instance?.getState(); + const { currentWalletId } = response || {}; + const { canisterId, standard, logo } = token; + const tokenList = await keyring?.instance?.registerToken({ + canisterId, + standard, + subaccount: currentWalletId, + logo, + }); + callback?.(tokenList); + } +); + export const userSlice = createSlice({ name: 'user', initialState: DEFAULT_STATE, diff --git a/src/screens/tabs/Tokens/components/AddToken/hooks/useSteps.tsx b/src/screens/tabs/Tokens/components/AddToken/hooks/useSteps.tsx index 69ec7f04..b0636ea1 100644 --- a/src/screens/tabs/Tokens/components/AddToken/hooks/useSteps.tsx +++ b/src/screens/tabs/Tokens/components/AddToken/hooks/useSteps.tsx @@ -64,7 +64,9 @@ const useSteps = ({ handleModalClose }: Props): Return => { adjustModalContent: false, }, { - component: , + component: ( + + ), left: ( handleChangeStep(0)} diff --git a/src/screens/tabs/Tokens/components/AddToken/steps/ReviewToken/index.tsx b/src/screens/tabs/Tokens/components/AddToken/steps/ReviewToken/index.tsx index 86defe96..a3b757c1 100644 --- a/src/screens/tabs/Tokens/components/AddToken/steps/ReviewToken/index.tsx +++ b/src/screens/tabs/Tokens/components/AddToken/steps/ReviewToken/index.tsx @@ -1,7 +1,7 @@ import { t } from 'i18next'; import React, { useEffect, useState } from 'react'; import { ActivityIndicator, View } from 'react-native'; -import { useSelector } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import RainbowButton from '@/components/buttons/RainbowButton'; import Image from '@/components/common/Image'; @@ -9,6 +9,7 @@ import Text from '@/components/common/Text'; import TokenFormat from '@/components/formatters/TokenFormat'; import UsdFormat from '@/components/formatters/UsdFormat'; import { DABToken } from '@/interfaces/dab'; +import { addCustomToken, getBalance } from '@/redux/slices/user'; import { getTokenBalance } from '@/services/DAB'; import { parseToken } from '../../utils'; @@ -16,13 +17,15 @@ import styles, { loaderColor } from './styles'; interface Props { token?: DABToken; + onClose: () => void; } -export function ReviewToken({ token }: Props) { +export function ReviewToken({ token, onClose }: Props) { const { principal } = useSelector(state => state.keyring?.currentWallet); const { icpPrice } = useSelector(state => state.icp); const [balance, setBalance] = useState<{ amount: number; value?: number }>(); const [loading, setLoading] = useState(true); + const dispatch = useDispatch(); function renderToken() { return ( @@ -55,18 +58,28 @@ export function ReviewToken({ token }: Props) { ); } + const handleAddToken = () => { + dispatch( + addCustomToken({ + token, + callback: () => { + dispatch(getBalance()); + onClose?.(); + }, + }) + ); + }; + useEffect(() => { - const getBalance = async () => { + const _getBalance = async () => { setLoading(true); const res = await getTokenBalance(token!, principal); setBalance(parseToken(token!, res, icpPrice)); setLoading(false); }; - getBalance(); + _getBalance(); }, [token]); - console.log(balance); - return ( @@ -75,7 +88,7 @@ export function ReviewToken({ token }: Props) { {renderToken()} {}} + onPress={handleAddToken} disabled={loading} /> diff --git a/src/screens/tabs/Tokens/index.js b/src/screens/tabs/Tokens/index.js index f2b4c2fa..db8d9f4b 100644 --- a/src/screens/tabs/Tokens/index.js +++ b/src/screens/tabs/Tokens/index.js @@ -27,7 +27,10 @@ function Tokens() { const [refreshing, setRefresing] = useState(assetsLoading); const usdSum = Number( - assets.reduce((total, token) => total + Number(token?.value), 0) + assets.reduce( + (total, token) => (token?.value ? total + Number(token?.value) : total), + 0 + ) ).toFixed(2); useEffect(() => { diff --git a/src/utils/currencies.ts b/src/utils/currencies.ts index 7c6ddae2..4a206f7c 100644 --- a/src/utils/currencies.ts +++ b/src/utils/currencies.ts @@ -91,7 +91,7 @@ export const formatAssets = ( ): Asset[] => assets.map(currentAsset => { const { amount, token } = currentAsset; - const { name, canisterId, symbol, decimals } = token; + const { symbol, decimals } = token; const parsedAmount = parseToFloatAmount( amount, @@ -99,10 +99,7 @@ export const formatAssets = ( ); const formattedAsset = { - name, - symbol, - canisterId, - decimals, + ...token, ...formatAssetBySymbol(parsedAmount, symbol, icpPrice), }; diff --git a/yarn.lock b/yarn.lock index 7936c0c6..475bd7d2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1628,7 +1628,7 @@ "@psychedelic/dab-js@1.4.3": version "1.4.3" - resolved "https://npm.pkg.github.com/download/@psychedelic/dab-js/1.4.3/9265ba76868a9e51b1903c8adb3d1c64142b52afaa9899e994f6328638d27d93#4a233113ba319e1bd27e459ca198ccca1143b3f8" + resolved "https://npm.pkg.github.com/download/@Psychedelic/dab-js/1.4.3/4a233113ba319e1bd27e459ca198ccca1143b3f8#4a233113ba319e1bd27e459ca198ccca1143b3f8" integrity sha512-UEwLg21h9yiXupoXn1j8MT3D0VKgJ4bIBG7/ENeqIeDxehXvhoGdMsSuslPa5W9FYKQHT8Ta6b9nBopRS6v7Nw== dependencies: "@dfinity/agent" "0.9.3" @@ -1640,20 +1640,6 @@ cross-fetch "^3.1.4" crypto-js "^4.1.1" -"@psychedelic/dab-js@1.4.4": - version "1.4.4" - resolved "https://npm.pkg.github.com/download/@Psychedelic/dab-js/1.4.4/5816040f8f36c7db5bce36ae1153a828b8f78dbf#5816040f8f36c7db5bce36ae1153a828b8f78dbf" - integrity sha512-FuRfyj6nttdN3Jrdo/Rg+76Lnb856WhRR6GKwQ9MidNQCbPfb/WUkPODAdH/+oS0uLNefoJDa/tz8O/FvW4tZA== - dependencies: - "@dfinity/agent" "0.9.3" - "@dfinity/candid" "0.9.3" - "@dfinity/identity" "0.9.3" - "@dfinity/principal" "0.9.3" - axios "^0.24.0" - buffer-crc32 "^0.2.13" - cross-fetch "^3.1.4" - crypto-js "^4.1.1" - "@psychedelic/plug-controller@0.19.2": version "0.19.2" resolved "https://npm.pkg.github.com/download/@Psychedelic/plug-controller/0.19.2/663c67f8ccc9f6eaecca299fcded39ee7efd9feb#663c67f8ccc9f6eaecca299fcded39ee7efd9feb" From d9c6412353b6a0c800442be905bd9f5b191b2dcb Mon Sep 17 00:00:00 2001 From: nachosan Date: Wed, 10 Aug 2022 16:22:58 -0300 Subject: [PATCH 10/93] wip: updated controller, added spinner while registering token --- package.json | 4 +-- src/components/tokens/TokenIcon/index.tsx | 13 ++++++++- src/components/tokens/TokenIcon/styles.ts | 3 ++ src/components/tokens/TokenItem/index.tsx | 17 ++++------- src/components/tokens/TokenSelector/index.tsx | 20 +++++++------ src/interfaces/redux.ts | 1 + src/redux/slices/user.js | 29 +++++++++++-------- .../Send/components/AmountSection/index.tsx | 9 +++--- .../Send/components/TokenSection/index.js | 2 +- src/screens/flows/Send/interfaces.ts | 5 ---- .../AddToken/steps/ReviewToken/index.tsx | 20 ++++++++----- src/screens/tabs/Tokens/index.js | 3 +- src/screens/tabs/Tokens/styles.js | 3 ++ yarn.lock | 18 ++++++------ 14 files changed, 84 insertions(+), 63 deletions(-) diff --git a/package.json b/package.json index 5d0509a8..c4fa07a3 100644 --- a/package.json +++ b/package.json @@ -33,8 +33,8 @@ "@dfinity/candid": "0.9.3", "@dfinity/principal": "^0.9.3", "@hookform/error-message": "^2.0.0", - "@psychedelic/dab-js": "1.4.3", - "@psychedelic/plug-controller": "0.19.2", + "@psychedelic/dab-js": "1.4.6", + "@psychedelic/plug-controller": "0.19.7", "@react-native-async-storage/async-storage": "^1.15.11", "@react-native-community/blur": "^3.6.0", "@react-native-community/clipboard": "^1.5.1", diff --git a/src/components/tokens/TokenIcon/index.tsx b/src/components/tokens/TokenIcon/index.tsx index 0ec711f5..105f3a58 100644 --- a/src/components/tokens/TokenIcon/index.tsx +++ b/src/components/tokens/TokenIcon/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { StyleProp, View, ViewStyle } from 'react-native'; +import Image from '@/components/common/Image'; import Text from '@/components/common/Text'; import Icon from '@/icons'; @@ -11,11 +12,21 @@ interface Props { icon?: string; customStyle?: StyleProp; color?: string; + logo?: string; } -const TokenIcon = ({ icon, symbol, color, customStyle, ...props }: Props) => { +const TokenIcon = ({ + icon, + symbol, + color, + customStyle, + logo, + ...props +}: Props) => { return icon ? ( + ) : logo ? ( + ) : ( ; onPress?: () => void; + token: Asset; } -function TokenItem({ - icon, - name, - amount, - value, - symbol, - color, - style, - onPress, -}: Props) { +function TokenItem({ color, style, onPress, token }: Props) { + const { amount, symbol, icon, logo, name, value } = token; return ( onPress?.()}> - + {name} void; style?: StyleProp; availableAmount: number; @@ -26,10 +25,8 @@ interface Props { } const TokenSelector = ({ - icon, - name, - symbol, onPress, + token, style, availableAmount, availableUsdAmount, @@ -38,9 +35,14 @@ const TokenSelector = ({ }: Props) => ( - + - {name} + {token.name} {selectedInput === 'USD' ? ( @@ -52,7 +54,7 @@ const TokenSelector = ({ ) : ( diff --git a/src/interfaces/redux.ts b/src/interfaces/redux.ts index 2ea0065a..3d0d7804 100644 --- a/src/interfaces/redux.ts +++ b/src/interfaces/redux.ts @@ -89,6 +89,7 @@ export interface Asset { decimals: number; name: string; canisterId: string; + logo?: string; } export interface IcpState { diff --git a/src/redux/slices/user.js b/src/redux/slices/user.js index 22ea9910..653ff475 100644 --- a/src/redux/slices/user.js +++ b/src/redux/slices/user.js @@ -376,20 +376,25 @@ export const getICNSData = createAsyncThunk( export const addCustomToken = createAsyncThunk( 'keyring/addCustomToken', /** - * @param {{token: DABToken, callback: () => void}} param + * @param {{token: DABToken, onSuccess: () => void}} param */ - async ({ token, callback }, { getState, dispatch }) => { - const { keyring } = getState(); - const response = await keyring?.instance?.getState(); - const { currentWalletId } = response || {}; + async ({ token, onSuccess }, { getState, dispatch }) => { + const { keyring, icp } = getState(); + const currentWalletId = keyring?.instance?.currentWalletId; const { canisterId, standard, logo } = token; - const tokenList = await keyring?.instance?.registerToken({ - canisterId, - standard, - subaccount: currentWalletId, - logo, - }); - callback?.(tokenList); + try { + const tokenList = await keyring?.instance?.registerToken({ + canisterId, + standard, + subaccount: currentWalletId, + logo, + }); + dispatch(setAssets(formatAssets(tokenList, icp.icpPrice))); + onSuccess?.(); + } catch (error) { + // TODO handle error + console.log(error); + } } ); diff --git a/src/screens/flows/Send/components/AmountSection/index.tsx b/src/screens/flows/Send/components/AmountSection/index.tsx index 5921cdc6..a02f5b7e 100644 --- a/src/screens/flows/Send/components/AmountSection/index.tsx +++ b/src/screens/flows/Send/components/AmountSection/index.tsx @@ -5,13 +5,14 @@ import RainbowButton from '@/components/buttons/RainbowButton'; import AmountInput from '@/components/common/AmountInput'; import TokenSelector from '@/components/tokens/TokenSelector'; import { VISIBLE_DECIMALS } from '@/constants/business'; +import { Asset } from '@/interfaces/redux'; import { parseLocaleNumber, toFixedLocale } from '@/utils/number'; -import { Amount, SelectedToken } from '../../interfaces'; +import { Amount } from '../../interfaces'; import styles from './styles'; interface Props { - selectedToken: SelectedToken; + selectedToken: Asset; tokenAmount: Amount; usdAmount: Amount; tokenPrice: number; @@ -19,7 +20,7 @@ interface Props { availableUsdAmount: number; setUsdAmount: (value: Amount | null) => void; setTokenAmount: (value: Amount | null) => void; - setSelectedToken: (token: SelectedToken | null) => void; + setSelectedToken: (token: Asset | null) => void; onReview: () => void; } @@ -129,7 +130,7 @@ const AmountSection = ({ return ( <> { {t('common.tokens')} {tokens.map(token => ( onTokenPress(token)} color={Colors.Gray.Tertiary} diff --git a/src/screens/flows/Send/interfaces.ts b/src/screens/flows/Send/interfaces.ts index ba3d2401..d587aebe 100644 --- a/src/screens/flows/Send/interfaces.ts +++ b/src/screens/flows/Send/interfaces.ts @@ -1,8 +1,3 @@ -export interface SelectedToken { - symbol: string; - amount: number; -} - export interface Amount { value: number; display: string; diff --git a/src/screens/tabs/Tokens/components/AddToken/steps/ReviewToken/index.tsx b/src/screens/tabs/Tokens/components/AddToken/steps/ReviewToken/index.tsx index a3b757c1..ee76b054 100644 --- a/src/screens/tabs/Tokens/components/AddToken/steps/ReviewToken/index.tsx +++ b/src/screens/tabs/Tokens/components/AddToken/steps/ReviewToken/index.tsx @@ -9,7 +9,7 @@ import Text from '@/components/common/Text'; import TokenFormat from '@/components/formatters/TokenFormat'; import UsdFormat from '@/components/formatters/UsdFormat'; import { DABToken } from '@/interfaces/dab'; -import { addCustomToken, getBalance } from '@/redux/slices/user'; +import { addCustomToken } from '@/redux/slices/user'; import { getTokenBalance } from '@/services/DAB'; import { parseToken } from '../../utils'; @@ -24,13 +24,15 @@ export function ReviewToken({ token, onClose }: Props) { const { principal } = useSelector(state => state.keyring?.currentWallet); const { icpPrice } = useSelector(state => state.icp); const [balance, setBalance] = useState<{ amount: number; value?: number }>(); - const [loading, setLoading] = useState(true); + const [loadingBalance, setLoadingBalance] = useState(true); + const [loadingRegister, setLoadingRegister] = useState(false); + const dispatch = useDispatch(); function renderToken() { return ( - {loading ? ( + {loadingBalance ? ( ) : ( <> @@ -59,12 +61,13 @@ export function ReviewToken({ token, onClose }: Props) { } const handleAddToken = () => { + setLoadingRegister(true); dispatch( addCustomToken({ token, - callback: () => { - dispatch(getBalance()); + onSuccess: () => { onClose?.(); + setLoadingRegister(false); }, }) ); @@ -72,10 +75,10 @@ export function ReviewToken({ token, onClose }: Props) { useEffect(() => { const _getBalance = async () => { - setLoading(true); + setLoadingBalance(true); const res = await getTokenBalance(token!, principal); setBalance(parseToken(token!, res, icpPrice)); - setLoading(false); + setLoadingBalance(false); }; _getBalance(); }, [token]); @@ -89,7 +92,8 @@ export function ReviewToken({ token, onClose }: Props) { ); diff --git a/src/screens/tabs/Tokens/index.js b/src/screens/tabs/Tokens/index.js index db8d9f4b..649bc318 100644 --- a/src/screens/tabs/Tokens/index.js +++ b/src/screens/tabs/Tokens/index.js @@ -65,6 +65,7 @@ function Tokens() { {assets?.map(token => ( Date: Thu, 11 Aug 2022 16:24:10 -0300 Subject: [PATCH 11/93] wip: adding custom token button --- package.json | 2 +- src/components/common/SearchBar/index.tsx | 9 ++- src/components/common/SearchBar/styles.ts | 16 ++++- src/components/icons/svg/AddNote.svg | 10 ++++ yarn.lock | 72 ++++++++++++++++------- 5 files changed, 82 insertions(+), 27 deletions(-) create mode 100644 src/components/icons/svg/AddNote.svg diff --git a/package.json b/package.json index c4fa07a3..1b77b299 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "react-native-safe-area-context": "^4.2.5", "react-native-screens": "^3.13.1", "react-native-sensitive-info": "^6.0.0-alpha.9", - "react-native-svg": "^12.1.1", + "react-native-svg": "^12.4.4", "react-native-tab-view": "^3.1.1", "react-native-tcp-socket": "^5.6.2", "react-native-udp": "^4.1.5", diff --git a/src/components/common/SearchBar/index.tsx b/src/components/common/SearchBar/index.tsx index 67be343e..e514207f 100644 --- a/src/components/common/SearchBar/index.tsx +++ b/src/components/common/SearchBar/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { StyleProp, View, ViewStyle } from 'react-native'; +import Add from '@/components/icons/svg/AddNote.svg'; import Search from '@/components/icons/svg/Search.svg'; import animationScales from '@/utils/animationScales'; @@ -16,14 +17,16 @@ interface Props { function SearchBar({ placeholder, style, onChangeText }: Props) { return ( - + } + left={} /> - + + + ); } diff --git a/src/components/common/SearchBar/styles.ts b/src/components/common/SearchBar/styles.ts index dae9edfb..9cac169f 100644 --- a/src/components/common/SearchBar/styles.ts +++ b/src/components/common/SearchBar/styles.ts @@ -5,14 +5,26 @@ import { Colors } from '@/constants/theme'; export const searchColor = Colors.White.Secondary; export default StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, input: { backgroundColor: Colors.Black.Pure, + flexGrow: 1, + // height: 48, }, - icon: { + searchIcon: { marginRight: 4, }, addButton: { backgroundColor: Colors.Black.Primary, - width: '10%', + height: 48, + width: 48, + alignItems: 'center', + justifyContent: 'center', + borderRadius: 16, + marginLeft: 8, }, }); diff --git a/src/components/icons/svg/AddNote.svg b/src/components/icons/svg/AddNote.svg new file mode 100644 index 00000000..037ade02 --- /dev/null +++ b/src/components/icons/svg/AddNote.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/yarn.lock b/yarn.lock index 7cfc9ff6..87bd7877 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4238,18 +4238,18 @@ css-select@^4.1.3: domutils "^2.8.0" nth-check "^2.0.1" -css-select@^4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.2.1.tgz#9e665d6ae4c7f9d65dbe69d0316e3221fb274cdd" - integrity sha512-/aUslKhzkTNCQUB2qTX84lVmfia9NyjP3WpDGtj/WxhwBzWBYUV3DgUpurHTme8UTPcPlAD1DJ+b0nN/t50zDQ== +css-select@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" + integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== dependencies: boolbase "^1.0.0" - css-what "^5.1.0" - domhandler "^4.3.0" - domutils "^2.8.0" + css-what "^6.1.0" + domhandler "^5.0.2" + domutils "^3.0.1" nth-check "^2.0.1" -css-tree@^1.0.0-alpha.39, css-tree@^1.1.2, css-tree@^1.1.3: +css-tree@^1.1.2, css-tree@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== @@ -4257,12 +4257,7 @@ css-tree@^1.0.0-alpha.39, css-tree@^1.1.2, css-tree@^1.1.3: mdn-data "2.0.14" source-map "^0.6.1" -css-what@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.1.0.tgz#3f7b707aadf633baf62c2ceb8579b545bb40f7fe" - integrity sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw== - -css-what@^6.0.1: +css-what@^6.0.1, css-what@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== @@ -4534,6 +4529,15 @@ dom-serializer@^1.0.1: domhandler "^4.2.0" entities "^2.0.0" +dom-serializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" + integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.2" + entities "^4.2.0" + dom-walk@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.2.tgz#0c548bef048f4d1f2a97249002236060daa3fd84" @@ -4549,6 +4553,11 @@ domelementtype@^2.0.1, domelementtype@^2.2.0: resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57" integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A== +domelementtype@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + domexception@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/domexception/-/domexception-2.0.1.tgz#fb44aefba793e1574b0af6aed2801d057529f304" @@ -4556,7 +4565,7 @@ domexception@^2.0.1: dependencies: webidl-conversions "^5.0.0" -domhandler@^4.2.0, domhandler@^4.3.0: +domhandler@^4.2.0: version "4.3.0" resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.0.tgz#16c658c626cf966967e306f966b431f77d4a5626" integrity sha512-fC0aXNQXqKSFTr2wDNZDhsEYjCiYsDWl3D01kwt25hm1YIPyDGHvvi3rw+PLqHAl/m71MaiF7d5zvBr0p5UB2g== @@ -4570,6 +4579,13 @@ domhandler@^4.3.1: dependencies: domelementtype "^2.2.0" +domhandler@^5.0.1, domhandler@^5.0.2: + version "5.0.3" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" + integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== + dependencies: + domelementtype "^2.3.0" + domutils@^2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" @@ -4579,6 +4595,15 @@ domutils@^2.8.0: domelementtype "^2.2.0" domhandler "^4.2.0" +domutils@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.0.1.tgz#696b3875238338cb186b6c0612bd4901c89a4f1c" + integrity sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q== + dependencies: + dom-serializer "^2.0.0" + domelementtype "^2.3.0" + domhandler "^5.0.1" + ed25519-hd-key@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/ed25519-hd-key/-/ed25519-hd-key-1.2.0.tgz#819d43c6a96477c9385bd121dccc94dbc6c6598c" @@ -4670,6 +4695,11 @@ entities@^3.0.1: resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4" integrity sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q== +entities@^4.2.0: + version "4.3.1" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.3.1.tgz#c34062a94c865c322f9d67b4384e4169bcede6a4" + integrity sha512-o4q/dYJlmyjP2zfnaWDUC6A3BQFmVTX+tZPezK7k0GLSU9QYCauscf5Y+qcEPzKL+EixVouYDgLQK5H9GrLpkg== + envinfo@^7.7.2: version "7.8.1" resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475" @@ -9037,13 +9067,13 @@ react-native-svg-transformer@^1.0.0: "@svgr/plugin-svgo" "^6.1.2" path-dirname "^1.0.2" -react-native-svg@^12.1.1: - version "12.3.0" - resolved "https://registry.yarnpkg.com/react-native-svg/-/react-native-svg-12.3.0.tgz#40f657c5d1ee366df23f3ec8dae76fd276b86248" - integrity sha512-ESG1g1j7/WLD7X3XRFTQHVv0r6DpbHNNcdusngAODIxG88wpTWUZkhcM3A2HJTb+BbXTFDamHv7FwtRKWQ/ALg== +react-native-svg@^12.4.4: + version "12.4.4" + resolved "https://registry.yarnpkg.com/react-native-svg/-/react-native-svg-12.4.4.tgz#2ba684eaea9a7402fbbe0ed9737e77284631d00e" + integrity sha512-LpcNlEVCURexqPAvQ9ne8KrPVfYz0wIDygwud8VMRmXLezysXzyQN/DTsjm1BO9lIfYp55WQsr3u3yW/vk6iiA== dependencies: - css-select "^4.2.1" - css-tree "^1.0.0-alpha.39" + css-select "^5.1.0" + css-tree "^1.1.3" react-native-tab-view@^3.1.1: version "3.1.1" From 8abcc857f49cdbb7d273d2e077e942d8de5df858 Mon Sep 17 00:00:00 2001 From: nachosan Date: Fri, 12 Aug 2022 12:27:49 -0300 Subject: [PATCH 12/93] Using JSON.parse instead of flatted to set items in storage --- src/redux/store.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/redux/store.js b/src/redux/store.js index ce5cbcf4..08386070 100644 --- a/src/redux/store.js +++ b/src/redux/store.js @@ -78,7 +78,7 @@ export const keyringStorage = { allKeys.map(async k => { if (!k.includes('@REACTOTRON') && !k.includes('WALLETCONNECT')) { const val = await AsyncStorage.getItem(k); - state[k] = JSON.parse(val)[0]; + state[k] = JSON.parse(val); } }) ); @@ -88,7 +88,7 @@ export const keyringStorage = { set: async values => Promise.all( Object.entries(values).map(async ([key, val]) => { - await AsyncStorage.setItem(key, Flatted.stringify(val)); + await AsyncStorage.setItem(key, JSON.stringify(val)); }) ), clear: AsyncStorage.clear, From 7aefd1fef0501f44944e45f06e4b2c441ca268bd Mon Sep 17 00:00:00 2001 From: nachosan Date: Fri, 12 Aug 2022 12:55:09 -0300 Subject: [PATCH 13/93] Refactored TextInput to make it more generic, improved gradient logic --- src/components/common/PasswordInput/index.js | 95 +++++++------------ src/components/common/PasswordInput/styles.js | 28 +----- src/components/common/SearchBar/index.tsx | 3 +- src/components/common/SearchBar/styles.ts | 11 ++- src/components/common/TextInput/index.tsx | 58 ++++++----- src/components/common/TextInput/styles.ts | 32 +++---- src/constants/theme.js | 12 +++ src/screens/auth/CreatePassword/index.js | 2 +- src/screens/auth/ImportSeedPhrase/index.js | 3 +- src/screens/auth/ImportSeedPhrase/styles.js | 5 +- src/screens/auth/Login/index.js | 1 - src/screens/auth/Login/styles.js | 3 - .../Send/components/SaveContact/index.js | 2 +- src/screens/flows/Send/index.js | 5 +- src/screens/flows/Send/styles.js | 4 +- .../components/AddEditContact/index.js | 2 +- .../screens/CreateEditAccount/index.js | 2 +- src/translations/en/index.js | 1 + 18 files changed, 122 insertions(+), 147 deletions(-) diff --git a/src/components/common/PasswordInput/index.js b/src/components/common/PasswordInput/index.js index 05065858..fe30188d 100644 --- a/src/components/common/PasswordInput/index.js +++ b/src/components/common/PasswordInput/index.js @@ -1,12 +1,10 @@ import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { View } from 'react-native'; -import LinearGradient from 'react-native-linear-gradient'; import TextInput from '@/commonComponents/TextInput'; import Touchable from '@/commonComponents/Touchable'; import { TestIds } from '@/constants/testIds'; -import { Rainbow } from '@/constants/theme'; import Icon from '@/icons/index'; import animationScales from '@/utils/animationScales'; @@ -16,10 +14,10 @@ import styles from './styles'; function PasswordInput({ onChange, password, - customStyle, + style, inputStyle, error, - placeholder = 'Enter Password', + placeholder, autoFocus, disabled, maxLength, @@ -29,26 +27,11 @@ function PasswordInput({ }) { const { t } = useTranslation(); const [showPassword, setShowPassword] = useState(false); - const [isFocused, setIsFocused] = useState(false); - - const handleOnFocus = () => { - setIsFocused(true); - }; - - const handleOnBlur = () => { - setIsFocused(false); - onBlur?.(); - }; const toggleShowPassowrd = () => { setShowPassword(!showPassword); }; - const handleOnSubmit = () => { - setIsFocused(false); - onSubmit?.(); - }; - useEffect(() => { if (disabled) { setShowPassword(false); @@ -56,53 +39,39 @@ function PasswordInput({ }, [disabled]); return ( - - {isFocused && ( - - )} - - - - - - + + + + + } + {...inputProps} + /> {error && ( {t('validations.passIncorrect')} )} - + ); } diff --git a/src/components/common/PasswordInput/styles.js b/src/components/common/PasswordInput/styles.js index 7d4f3ac0..5015b0ed 100644 --- a/src/components/common/PasswordInput/styles.js +++ b/src/components/common/PasswordInput/styles.js @@ -3,39 +3,21 @@ import { StyleSheet } from 'react-native'; import { Colors } from '@/constants/theme'; import { fontMaker } from '@/utils/fonts'; -const inputHeight = 60; -const commonBorderRadius = 15; - export default StyleSheet.create({ + container: { + width: '100%', + }, errorText: { marginTop: 5, color: 'red', }, - input: { + inputContainer: { backgroundColor: Colors.Gray.Secondary, - paddingRight: 30, - }, - eyeContainer: { - position: 'absolute', - right: 20, - top: 17, }, disabledContainer: { opacity: 0.3, }, - focusedGradient: { - borderRadius: commonBorderRadius, - height: inputHeight, - width: '100%', - top: -2, - position: 'absolute', - }, - container: { - backgroundColor: Colors.Gray.Secondary, - borderRadius: commonBorderRadius, - marginHorizontal: 2, - }, - text: { + textInput: { ...fontMaker({ size: 18, color: Colors.White.Pure }), fontFamily: null, // Disabling custom font because of problem with Inter and secure text entry fontWeight: 'bold', diff --git a/src/components/common/SearchBar/index.tsx b/src/components/common/SearchBar/index.tsx index e514207f..a86bdbda 100644 --- a/src/components/common/SearchBar/index.tsx +++ b/src/components/common/SearchBar/index.tsx @@ -20,7 +20,8 @@ function SearchBar({ placeholder, style, onChangeText }: Props) { } /> diff --git a/src/components/common/SearchBar/styles.ts b/src/components/common/SearchBar/styles.ts index 9cac169f..916d8fe4 100644 --- a/src/components/common/SearchBar/styles.ts +++ b/src/components/common/SearchBar/styles.ts @@ -4,6 +4,8 @@ import { Colors } from '@/constants/theme'; export const searchColor = Colors.White.Secondary; +const BAR_HEIGHT = 48; + export default StyleSheet.create({ container: { flexDirection: 'row', @@ -11,17 +13,18 @@ export default StyleSheet.create({ justifyContent: 'space-between', }, input: { + height: BAR_HEIGHT, + }, + inputContent: { backgroundColor: Colors.Black.Pure, - flexGrow: 1, - // height: 48, }, searchIcon: { marginRight: 4, }, addButton: { backgroundColor: Colors.Black.Primary, - height: 48, - width: 48, + height: BAR_HEIGHT, + width: BAR_HEIGHT, alignItems: 'center', justifyContent: 'center', borderRadius: 16, diff --git a/src/components/common/TextInput/index.tsx b/src/components/common/TextInput/index.tsx index 4db678b8..a4580ddc 100644 --- a/src/components/common/TextInput/index.tsx +++ b/src/components/common/TextInput/index.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { StyleProp, TextInput as Input, @@ -9,9 +9,7 @@ import { } from 'react-native'; import LinearGradient from 'react-native-linear-gradient'; -import Touchable from '@/commonComponents/Touchable'; -import { Rainbow } from '@/constants/theme'; -import animationScales from '@/utils/animationScales'; +import { Rainbow, TransparentGradient } from '@/constants/theme'; import styles, { defaultPlaceholderTextColor } from './styles'; @@ -19,7 +17,8 @@ interface Props extends TextInputProps { ref?: React.RefObject; hideGradient?: boolean; customStyle?: StyleProp; - textStyle?: StyleProp; + contentContainerStyle?: StyleProp; + inputStyle?: StyleProp; disabled?: boolean; left?: React.ReactNode; right?: React.ReactNode; @@ -33,7 +32,7 @@ const TextInput = ({ placeholder, onSubmitEditing, customStyle, - textStyle, + inputStyle, disabled, maxLength, left, @@ -42,36 +41,47 @@ const TextInput = ({ placeholderTextColor = defaultPlaceholderTextColor, secureTextEntry, multiline, + style, + contentContainerStyle, + onBlur, + onFocus, ...props }: Props) => { - const viewStyle = [styles.viewStyle, multiline && styles.multiStyle]; - const baseInputStyle = [styles.inputStyle, multiline && styles.multiStyle]; + const adjustInnerHeight = useMemo( + () => (style ? Object.keys(style).includes('height') : false), + [style] + ); + + const innerContainerStyle = [ + styles.innerContainer, + adjustInnerHeight && styles.grow, + multiline && styles.multilineContainer, + ]; + const [isFocused, setIsFocused] = useState(false); + const gradient = useMemo( + () => (isFocused && !hideGradient ? Rainbow : TransparentGradient), + [isFocused, hideGradient] + ); - const handleOnFocus = () => { + const handleOnFocus = (event: any) => { setIsFocused(true); + onFocus?.(event); }; - const handleOnBlur = () => { + const handleOnBlur = (event: any) => { setIsFocused(false); + onBlur?.(event); }; return ( - {}}> - {isFocused && !hideGradient && ( - - )} - + + {left} {right} - + ); }; diff --git a/src/components/common/TextInput/styles.ts b/src/components/common/TextInput/styles.ts index b4d89c72..9df31dc3 100644 --- a/src/components/common/TextInput/styles.ts +++ b/src/components/common/TextInput/styles.ts @@ -8,36 +8,30 @@ const commonBorderRadius = 16; export const defaultPlaceholderTextColor = Colors.White.Secondary; const styles = StyleSheet.create({ - inputStyle: { + textInput: { ...fontMaker({ size: 18, color: Colors.White.Pure, weight: SEMIBOLD }), - paddingVertical: 13, borderRadius: commonBorderRadius, - width: '100%', - height: 56, + flexGrow: 1, }, - multiStyle: { - alignItems: 'flex-start', - height: 90, - }, - viewStyle: { + innerContainer: { backgroundColor: Colors.Black.Primary, alignItems: 'center', flexDirection: 'row', borderRadius: commonBorderRadius, - height: 56, - flexGrow: 0, paddingHorizontal: 16, }, - focusedGradient: { + multilineContainer: { + alignItems: 'flex-start', + // height: 90, + }, + gradientContainer: { borderRadius: commonBorderRadius, - position: 'absolute', - height: 60, - width: '101%', - left: -2, - top: -2, + alignItems: 'center', + justifyContent: 'center', + padding: 1, }, - multiLineGradient: { - height: 94, + grow: { + flexGrow: 1, }, }); diff --git a/src/constants/theme.js b/src/constants/theme.js index 23e9205a..ee449374 100644 --- a/src/constants/theme.js +++ b/src/constants/theme.js @@ -63,6 +63,18 @@ export const Rainbow = { ], }; +export const TransparentGradient = { + start: { + x: 0, + y: 0, + }, + end: { + x: 1, + y: 1, + }, + colors: ['transparent', 'transparent'], +}; + export const DisabledRainbow = { start: { x: 0, diff --git a/src/screens/auth/CreatePassword/index.js b/src/screens/auth/CreatePassword/index.js index b4ecc10c..750974ed 100644 --- a/src/screens/auth/CreatePassword/index.js +++ b/src/screens/auth/CreatePassword/index.js @@ -112,7 +112,7 @@ const CreatePassword = ({ route, navigation }) => { onBlur={onBlur} password={value} onChange={onChange} - customStyle={styles.passwordInput} + style={styles.passwordInput} onSubmit={Keyboard.dismiss} /> )} diff --git a/src/screens/auth/ImportSeedPhrase/index.js b/src/screens/auth/ImportSeedPhrase/index.js index aed9fb0d..4ed045a6 100644 --- a/src/screens/auth/ImportSeedPhrase/index.js +++ b/src/screens/auth/ImportSeedPhrase/index.js @@ -101,7 +101,8 @@ const ImportSeedPhrase = ({ navigation, route }) => { value={seedPhrase} onChangeText={onChangeText} placeholder={t('importSeedPhrase.secretPhrase')} - customStyle={styles.input} + style={styles.input} + contentContainerStyle={styles.inputContainer} testID={TestIds.IMPORT_SEED_PHRASE.PHRASE_INPUT} /> {error && ( diff --git a/src/screens/auth/ImportSeedPhrase/styles.js b/src/screens/auth/ImportSeedPhrase/styles.js index 11f70541..80befe82 100644 --- a/src/screens/auth/ImportSeedPhrase/styles.js +++ b/src/screens/auth/ImportSeedPhrase/styles.js @@ -22,8 +22,11 @@ export default StyleSheet.create({ paddingHorizontal: 30, }, input: { - backgroundColor: Colors.Gray.Secondary, marginTop: 30, + height: 100, + }, + inputContainer: { + backgroundColor: Colors.Gray.Secondary, }, button: { marginTop: 30, diff --git a/src/screens/auth/Login/index.js b/src/screens/auth/Login/index.js index cd7d097a..0ff43db7 100644 --- a/src/screens/auth/Login/index.js +++ b/src/screens/auth/Login/index.js @@ -104,7 +104,6 @@ function Login({ route, navigation }) { disabled={disableInput} password={password} onChange={setPassword} - inputStyle={styles.input} onSubmit={() => handleSubmit(password)} /> {t('send.inputLabel')} } diff --git a/src/screens/flows/Send/styles.js b/src/screens/flows/Send/styles.js index d5f122d9..6f929fda 100644 --- a/src/screens/flows/Send/styles.js +++ b/src/screens/flows/Send/styles.js @@ -14,9 +14,11 @@ export default StyleSheet.create({ }, centerText: FontStyles.Subtitle2, input: { + marginVertical: 6, + }, + inputContent: { backgroundColor: Colors.Black.Pure, paddingHorizontal: 0, - marginVertical: 6, }, inputLeftLabel: { ...fontMaker({ size: 18, color: Colors.White.Pure, weight: SEMIBOLD }), diff --git a/src/screens/tabs/Profile/screens/Contacts/components/AddEditContact/index.js b/src/screens/tabs/Profile/screens/Contacts/components/AddEditContact/index.js index 702c8543..8d1a17fa 100644 --- a/src/screens/tabs/Profile/screens/Contacts/components/AddEditContact/index.js +++ b/src/screens/tabs/Profile/screens/Contacts/components/AddEditContact/index.js @@ -153,7 +153,7 @@ const AddEditContact = ({ modalRef, contact, onClose, contactsRef }) => { value={id} onChangeText={handleOnChangeId} placeholder={t('contacts.idPlaceholder')} - customStyle={styles.marginedContainer} + style={styles.marginedContainer} /> {error && ( diff --git a/src/screens/tabs/Profile/screens/CreateEditAccount/index.js b/src/screens/tabs/Profile/screens/CreateEditAccount/index.js index 8e6f65b6..8704cad7 100644 --- a/src/screens/tabs/Profile/screens/CreateEditAccount/index.js +++ b/src/screens/tabs/Profile/screens/CreateEditAccount/index.js @@ -119,7 +119,7 @@ const CreateEditAccount = ({ modalRef, account, accountsModalRef }) => { maxLength={22} value={accountName} placeholder={t('accounts.accountNamePlaceholder')} - customStyle={styles.input} + style={styles.input} onChangeText={setAccountName} /> Date: Fri, 12 Aug 2022 13:03:00 -0300 Subject: [PATCH 14/93] Migrated PasswordInput to TS --- .../PasswordInput/{index.js => index.tsx} | 20 +++++++++++++------ .../PasswordInput/{styles.js => styles.ts} | 2 +- src/components/common/PasswordModal/index.js | 4 ++-- src/screens/auth/CreatePassword/index.js | 4 ++-- src/screens/auth/Login/index.js | 4 ++-- .../Profile/screens/RevealSeedPhrase/index.js | 4 ++-- .../components/BiometricUnlock/index.js | 4 ++-- 7 files changed, 25 insertions(+), 17 deletions(-) rename src/components/common/PasswordInput/{index.js => index.tsx} (84%) rename src/components/common/PasswordInput/{styles.js => styles.ts} (82%) diff --git a/src/components/common/PasswordInput/index.js b/src/components/common/PasswordInput/index.tsx similarity index 84% rename from src/components/common/PasswordInput/index.js rename to src/components/common/PasswordInput/index.tsx index fe30188d..209f4fd6 100644 --- a/src/components/common/PasswordInput/index.js +++ b/src/components/common/PasswordInput/index.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { View } from 'react-native'; +import { StyleProp, TextInputProps, TextStyle, View } from 'react-native'; import TextInput from '@/commonComponents/TextInput'; import Touchable from '@/commonComponents/Touchable'; @@ -11,9 +11,17 @@ import animationScales from '@/utils/animationScales'; import Text from '../Text'; import styles from './styles'; +interface Props extends TextInputProps { + inputStyle?: StyleProp; + error?: string; + disabled?: boolean; + inputProps?: TextInputProps; + onSubmit?: () => void; +} + function PasswordInput({ - onChange, - password, + onChangeText, + value, style, inputStyle, error, @@ -24,7 +32,7 @@ function PasswordInput({ inputProps, onBlur, onSubmit, -}) { +}: Props) { const { t } = useTranslation(); const [showPassword, setShowPassword] = useState(false); @@ -42,9 +50,9 @@ function PasswordInput({ { diff --git a/src/screens/auth/Login/index.js b/src/screens/auth/Login/index.js index 0ff43db7..7c805e79 100644 --- a/src/screens/auth/Login/index.js +++ b/src/screens/auth/Login/index.js @@ -102,8 +102,8 @@ function Login({ route, navigation }) { autoFocus={!isManualLock} error={error} disabled={disableInput} - password={password} - onChange={setPassword} + value={password} + onChangeText={setPassword} onSubmit={() => handleSubmit(password)} /> Date: Fri, 12 Aug 2022 13:11:30 -0300 Subject: [PATCH 15/93] removed comment --- src/components/common/TextInput/styles.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/common/TextInput/styles.ts b/src/components/common/TextInput/styles.ts index 9df31dc3..0f5d311d 100644 --- a/src/components/common/TextInput/styles.ts +++ b/src/components/common/TextInput/styles.ts @@ -22,7 +22,6 @@ const styles = StyleSheet.create({ }, multilineContainer: { alignItems: 'flex-start', - // height: 90, }, gradientContainer: { borderRadius: commonBorderRadius, From 24e2083d0b6776bdf6f66ebc8b0b4df974687a2c Mon Sep 17 00:00:00 2001 From: nachosan Date: Fri, 12 Aug 2022 14:49:01 -0300 Subject: [PATCH 16/93] Changed theme color name --- src/components/common/SearchBar/styles.ts | 3 +++ src/components/tokens/TokenIcon/styles.ts | 2 +- src/constants/theme.js | 3 ++- .../Tokens/components/AddToken/steps/ReviewToken/styles.ts | 6 +++--- .../Tokens/components/AddToken/steps/TokenList/styles.ts | 2 +- 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/components/common/SearchBar/styles.ts b/src/components/common/SearchBar/styles.ts index 916d8fe4..005f72d6 100644 --- a/src/components/common/SearchBar/styles.ts +++ b/src/components/common/SearchBar/styles.ts @@ -14,8 +14,11 @@ export default StyleSheet.create({ }, input: { height: BAR_HEIGHT, + flex: 1, }, inputContent: { + borderColor: Colors.Divider[1], + borderWidth: 1, backgroundColor: Colors.Black.Pure, }, searchIcon: { diff --git a/src/components/tokens/TokenIcon/styles.ts b/src/components/tokens/TokenIcon/styles.ts index b6b09482..5e2952cc 100644 --- a/src/components/tokens/TokenIcon/styles.ts +++ b/src/components/tokens/TokenIcon/styles.ts @@ -12,7 +12,7 @@ export default StyleSheet.create({ borderRadius: 41, justifyContent: 'center', alignItems: 'center', - borderColor: Colors.Divider[16], + borderColor: Colors.Divider[1], borderWidth: 1, }, text: fontMaker({ weight: SEMIBOLD, size: 10 }), diff --git a/src/constants/theme.js b/src/constants/theme.js index ee449374..0406b1a5 100644 --- a/src/constants/theme.js +++ b/src/constants/theme.js @@ -42,7 +42,8 @@ export const Colors = { Red: '#FF453A', Transparent: '#ffffff00', Divider: { - 16: '#3D3D3D', + 1: '#3A3B40', + 2: '#737377', }, }; diff --git a/src/screens/tabs/Tokens/components/AddToken/steps/ReviewToken/styles.ts b/src/screens/tabs/Tokens/components/AddToken/steps/ReviewToken/styles.ts index 3c361d41..7cdd0589 100644 --- a/src/screens/tabs/Tokens/components/AddToken/steps/ReviewToken/styles.ts +++ b/src/screens/tabs/Tokens/components/AddToken/steps/ReviewToken/styles.ts @@ -21,14 +21,14 @@ export default StyleSheet.create({ alignItems: 'center', justifyContent: 'center', borderWidth: 1, - borderTopColor: Colors.Divider[16], - borderBottomColor: Colors.Divider[16], + borderTopColor: Colors.Divider[1], + borderBottomColor: Colors.Divider[1], padding: 14, }, logo: { height: 40, width: 40, - borderColor: Colors.Divider[16], + borderColor: Colors.Divider[1], borderWidth: 1, borderRadius: 100, marginRight: 12, diff --git a/src/screens/tabs/Tokens/components/AddToken/steps/TokenList/styles.ts b/src/screens/tabs/Tokens/components/AddToken/steps/TokenList/styles.ts index 51e791c1..455892d7 100644 --- a/src/screens/tabs/Tokens/components/AddToken/steps/TokenList/styles.ts +++ b/src/screens/tabs/Tokens/components/AddToken/steps/TokenList/styles.ts @@ -23,7 +23,7 @@ export default StyleSheet.create({ borderRadius: 100, marginRight: 12, borderWidth: 1, - borderColor: Colors.Divider[16], + borderColor: Colors.Divider[1], }, loader: { flex: 1, From 073c6c87838c7a668e594fe46573bbba3829b625 Mon Sep 17 00:00:00 2001 From: nachosan Date: Fri, 12 Aug 2022 18:02:04 -0300 Subject: [PATCH 17/93] wip: adding custom token modal --- .../buttons/RainbowButton/index.tsx | 6 +- src/components/common/ActionSheet/index.tsx | 20 +++- src/components/common/SearchBar/index.tsx | 14 ++- src/components/common/TextInput/index.tsx | 3 +- src/interfaces/dab.ts | 7 +- src/interfaces/keyring.ts | 1 + src/redux/slices/user.js | 19 ++++ .../AddToken/steps/TokenList/index.tsx | 7 +- .../Tokens/components/CustomToken/index.tsx | 104 ++++++++++++++++++ .../Tokens/components/CustomToken/styles.ts | 36 ++++++ src/translations/en/index.js | 3 + 11 files changed, 205 insertions(+), 15 deletions(-) create mode 100644 src/screens/tabs/Tokens/components/CustomToken/index.tsx create mode 100644 src/screens/tabs/Tokens/components/CustomToken/styles.ts diff --git a/src/components/buttons/RainbowButton/index.tsx b/src/components/buttons/RainbowButton/index.tsx index da77518d..b88db528 100644 --- a/src/components/buttons/RainbowButton/index.tsx +++ b/src/components/buttons/RainbowButton/index.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { StyleProp, TextProps, ViewProps } from 'react-native'; +import { StyleProp, TextStyle, ViewStyle } from 'react-native'; import LinearGradient from 'react-native-linear-gradient'; import Button from '@/buttons/Button'; @@ -11,8 +11,8 @@ import { styles } from './styles'; interface Props { text: string; onPress: () => void; - buttonStyle?: StyleProp; - textStyle?: StyleProp; + buttonStyle?: StyleProp; + textStyle?: StyleProp; disabled?: boolean; onLongPress?: () => void; loading?: boolean; diff --git a/src/components/common/ActionSheet/index.tsx b/src/components/common/ActionSheet/index.tsx index 2e1de59c..804ef724 100644 --- a/src/components/common/ActionSheet/index.tsx +++ b/src/components/common/ActionSheet/index.tsx @@ -22,13 +22,21 @@ interface Option { interface Props { modalRef: RefObject; + options: Option[]; onClose?: () => void; - title?: string; + showIcons?: boolean; subtitle?: string; - options: Option[]; + title?: string; } -function ActionSheet({ modalRef, onClose, title, subtitle, options }: Props) { +function ActionSheet({ + modalRef, + onClose, + title, + subtitle, + options, + showIcons = isAndroid, +}: Props) { const handleClose = () => { modalRef?.current?.close(); onClose?.(); @@ -59,7 +67,9 @@ function ActionSheet({ modalRef, onClose, title, subtitle, options }: Props) { key={option.id} onPress={() => handleItemPress(option)} style={[styles.item, index > 0 && styles.itemBorder]}> - {Icon && } + {showIcons && Icon && ( + + )} - {isAndroid && } + {showIcons && } {i18next.t('common.cancel')} diff --git a/src/components/common/SearchBar/index.tsx b/src/components/common/SearchBar/index.tsx index a86bdbda..5ea80500 100644 --- a/src/components/common/SearchBar/index.tsx +++ b/src/components/common/SearchBar/index.tsx @@ -13,9 +13,10 @@ interface Props { onChangeText: (text: string) => void; placeholder: string; style?: StyleProp; + onActionPress?: () => void; } -function SearchBar({ placeholder, style, onChangeText }: Props) { +function SearchBar({ placeholder, style, onChangeText, onActionPress }: Props) { return ( } /> - - - + {onActionPress && ( + + + + )} ); } diff --git a/src/components/common/TextInput/index.tsx b/src/components/common/TextInput/index.tsx index a4580ddc..64cde2a8 100644 --- a/src/components/common/TextInput/index.tsx +++ b/src/components/common/TextInput/index.tsx @@ -45,6 +45,7 @@ const TextInput = ({ contentContainerStyle, onBlur, onFocus, + blurOnSubmit = false, ...props }: Props) => { const adjustInnerHeight = useMemo( @@ -88,7 +89,7 @@ const TextInput = ({ secureTextEntry={secureTextEntry} placeholder={placeholder} onSubmitEditing={onSubmitEditing} - blurOnSubmit={false} + blurOnSubmit={blurOnSubmit} maxLength={maxLength} value={value} editable={!disabled} diff --git a/src/interfaces/dab.ts b/src/interfaces/dab.ts index cbfcde98..f49b7808 100644 --- a/src/interfaces/dab.ts +++ b/src/interfaces/dab.ts @@ -1,7 +1,7 @@ import { Principal } from '@dfinity/principal'; import { Token } from '@psychedelic/dab-js/dist/interfaces/token'; -import { Standard } from './keyring'; +import { FungibleStandard, Standard } from './keyring'; export type { BalanceResponse } from '@psychedelic/dab-js/dist/interfaces/token'; @@ -17,3 +17,8 @@ export interface DABToken extends Token { fee: number; }; } + +export interface CustomToken { + canisterId: Principal; + standard: FungibleStandard; +} diff --git a/src/interfaces/keyring.ts b/src/interfaces/keyring.ts index e6ad1ae6..8be3f4e3 100644 --- a/src/interfaces/keyring.ts +++ b/src/interfaces/keyring.ts @@ -1,3 +1,4 @@ +export type FungibleStandard = 'DIP20' | 'EXT'; export type Standard = 'DIP20' | 'XTC' | 'WICP' | 'EXT' | 'ICP'; export interface StandardToken { diff --git a/src/redux/slices/user.js b/src/redux/slices/user.js index 653ff475..62ad053e 100644 --- a/src/redux/slices/user.js +++ b/src/redux/slices/user.js @@ -398,6 +398,25 @@ export const addCustomToken = createAsyncThunk( } ); +export const getTokenInfo = createAsyncThunk( + 'keyring/getTokenInfo', + async ({ canisterId, standard }, { getState }) => { + const { keyring } = getState(); + const currentWalletId = keyring?.instance?.currentWalletId; + try { + const tokenInfo = await keyring?.instance?.getTokenInfo({ + subaccount: currentWalletId, + canisterId, + standard, + }); + return { ...tokenInfo, amount: tokenInfo.amount.toString() }; + } catch (error) { + // TODO handle error + console.log('Error while fetching token info', error); + } + } +); + export const userSlice = createSlice({ name: 'user', initialState: DEFAULT_STATE, diff --git a/src/screens/tabs/Tokens/components/AddToken/steps/TokenList/index.tsx b/src/screens/tabs/Tokens/components/AddToken/steps/TokenList/index.tsx index fe8572cf..b1c0b333 100644 --- a/src/screens/tabs/Tokens/components/AddToken/steps/TokenList/index.tsx +++ b/src/screens/tabs/Tokens/components/AddToken/steps/TokenList/index.tsx @@ -1,6 +1,7 @@ import { t } from 'i18next'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { ActivityIndicator, View } from 'react-native'; +import { Modalize } from 'react-native-modalize'; import Image from '@/components/common/Image'; import SearchBar from '@/components/common/SearchBar'; @@ -9,6 +10,7 @@ import Touchable from '@/components/common/Touchable'; import { DABToken } from '@/interfaces/dab'; import animationScales from '@/utils/animationScales'; +import CustomToken from '../../../CustomToken'; import styles from './styles'; interface Props { @@ -20,6 +22,7 @@ interface Props { export function TokenList({ onSelectedToken, tokens, loading }: Props) { const [filteredTokens, setFilteredTokens] = useState(tokens); const [search, setSearch] = useState(''); + const modalRef = useRef(null); function renderToken(token: DABToken) { return ( @@ -67,6 +70,7 @@ export function TokenList({ onSelectedToken, tokens, loading }: Props) { modalRef?.current?.open()} /> {loading ? ( @@ -78,6 +82,7 @@ export function TokenList({ onSelectedToken, tokens, loading }: Props) { : t('addToken.availableTokens')} {filteredTokens.map((token: DABToken) => renderToken(token))} + ) : ( renderEmptyState() diff --git a/src/screens/tabs/Tokens/components/CustomToken/index.tsx b/src/screens/tabs/Tokens/components/CustomToken/index.tsx new file mode 100644 index 00000000..60cab5a4 --- /dev/null +++ b/src/screens/tabs/Tokens/components/CustomToken/index.tsx @@ -0,0 +1,104 @@ +import { t } from 'i18next'; +import React, { useMemo, useRef, useState } from 'react'; +import { Keyboard, View } from 'react-native'; +import { Modalize } from 'react-native-modalize'; +import { useDispatch } from 'react-redux'; + +import Button from '@/components/buttons/Button'; +import RainbowButton from '@/components/buttons/RainbowButton'; +import ActionButton from '@/components/common/ActionButton'; +import ActionSheet from '@/components/common/ActionSheet'; +import Header from '@/components/common/Header'; +import Modal from '@/components/common/Modal'; +import Text from '@/components/common/Text'; +import TextInput from '@/components/common/TextInput'; +import { FungibleStandard } from '@/interfaces/keyring'; +import { getTokenInfo } from '@/redux/slices/user'; + +import styles from './styles'; + +interface Props { + modalRef: React.RefObject; +} + +function CustomToken({ modalRef }: Props) { + const [canisterId, setCanisterId] = useState(''); + const dispatch = useDispatch(); + const [standard, setStandard] = useState(); + const optionsRef = useRef(null); + + const clearValues = () => { + setCanisterId(''); + setStandard(undefined); + }; + + const handleClose = () => { + modalRef?.current?.close(); + clearValues(); + }; + + const handleSubmit = () => { + //validate canisterId + dispatch(getTokenInfo({ canisterId, standard })); + }; + + const standardList = useMemo( + () => [ + { + id: 1, + label: 'DIP20', + onPress: () => setStandard('DIP20'), + }, + { + id: 2, + label: 'EXT', + onPress: () => setStandard('EXT'), + }, + ], + [setStandard] + ); + + return ( + +
+ {t('addToken.customTokenTitle')} + + } + right={} + /> + + +