diff --git a/locales/en.json b/locales/en.json index 7b14560e..593bdb7f 100644 --- a/locales/en.json +++ b/locales/en.json @@ -397,6 +397,10 @@ "TextVerification": { "noteInvalid": "You cannot write special symbols like {}
^ or more than 100 characters." }, + "Tokens": { + "headingTokens": "Your Circles", + "descriptionTokens": "All circles you hold was originally issued as someone's basic income. These users are the original issuers of the circles you hold. Each of these circles can be sent directly to other wallets that trust these users." + }, "TransferInfoBalanceCard": { "bodyTotalBalance": "Your total balance: {balance}", "tooltipTotalBalance": "This is the current balance of all Circles you have on your account" diff --git a/src/components/BalanceDisplay.js b/src/components/BalanceDisplay.js index fd24e2c1..ef3b18a6 100644 --- a/src/components/BalanceDisplay.js +++ b/src/components/BalanceDisplay.js @@ -3,6 +3,9 @@ import { Box, Tooltip, Typography } from '@mui/material'; import makeStyles from '@mui/styles/makeStyles'; import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import { Link } from 'react-router-dom'; + +import { TOKENS_PATH } from '~/routes'; import { useUpdateLoop } from '~/hooks/update'; import translate from '~/services/locale'; @@ -56,8 +59,10 @@ const BalanceDisplay = () => { ) : ( - - {tokenBalance} + + + {tokenBalance} + )} diff --git a/src/components/Finder.js b/src/components/Finder.js index 4be83f9e..583db8d7 100644 --- a/src/components/Finder.js +++ b/src/components/Finder.js @@ -62,7 +62,6 @@ function filterToQuery(filterName) { const useStyles = makeStyles((theme) => ({ searchItem: { cursor: 'pointer', - boxShadow: theme.custom.shadows.gray, }, noSearchResultContainer: { marginTop: '80px', diff --git a/src/components/ProfileMini.js b/src/components/ProfileMini.js index 46c5f934..f08f5f0b 100644 --- a/src/components/ProfileMini.js +++ b/src/components/ProfileMini.js @@ -7,6 +7,7 @@ import { Typography, } from '@mui/material'; import makeStyles from '@mui/styles/makeStyles'; +import clsx from 'clsx'; import PropTypes from 'prop-types'; import React, { Fragment, useState } from 'react'; import { useDispatch } from 'react-redux'; @@ -28,6 +29,16 @@ import { addSafeOwner } from '~/store/safe/actions'; import { IconFriends, IconSend, IconTrust } from '~/styles/icons'; const useStyles = makeStyles((theme) => ({ + cardContainer: { + boxShadow: theme.custom.shadows.navigationFloating, + '&:hover': { + background: theme.custom.colors.blackSqueeze, + + '& .MuiCardHeader-root': { + background: theme.custom.colors.blackSqueeze, + }, + }, + }, cardHeader: { padding: theme.spacing(1), }, @@ -64,6 +75,7 @@ const ProfileMini = ({ className, hasActions = false, isSharedWalletCreation, + value, ...props }) => { const classes = useStyles(); @@ -160,10 +172,10 @@ const ProfileMini = ({ onConfirm={handleClose} /> )} - + - ) + )) || + (value && ( + + {value} + + )) } avatar={} classes={{ @@ -258,6 +279,7 @@ ProfileMini.propTypes = { className: PropTypes.string, hasActions: PropTypes.bool, isSharedWalletCreation: PropTypes.bool, + value: PropTypes.string, }; ProfileMiniActions.propTypes = { diff --git a/src/routes.js b/src/routes.js index c3f6f5c8..d11d67ae 100644 --- a/src/routes.js +++ b/src/routes.js @@ -23,6 +23,7 @@ import Send from '~/views/Send'; import SendConfirm from '~/views/SendConfirm'; import Settings from '~/views/Settings'; import Share from '~/views/Share'; +import Tokens from '~/views/Tokens'; import TutorialOnboarding from '~/views/TutorialOnboarding'; import Validation from '~/views/Validation'; import ValidationLock from '~/views/ValidationLock'; @@ -43,6 +44,7 @@ export const ORGANIZATION_PATH = '/organization'; export const PROFILE_PATH = '/profile/:address'; export const QR_GENERATOR_PATH = '/sharedwallet/qr'; export const SEARCH_PATH = '/search'; +export const TOKENS_PATH = '/tokens'; export const SEED_PHRASE_PATH = '/seedphrase'; export const SEND_CONFIRM_PATH = '/send/:address(0x[0-9a-fA-f]{40})'; export const SEND_PATH = '/send'; @@ -265,6 +267,7 @@ const Routes = () => { + { - const { safe, token } = getState(); + const { token, safe } = getState(); // No token address given yet if (!token.address && !safe.isOrganization) { @@ -167,6 +170,67 @@ export function checkCurrentBalance() { }; } +export function checkOtherTokens() { + return async (dispatch, getState) => { + const { safe } = getState(); + + try { + const otherTokens = await core.token.listAllTokens(safe.currentAccount); + + const filterOrderedOtherTokens = otherTokens + .filter( + (item) => + formatCirclesValue(item.amount, Date.now(), 2, false) > 0.005, + ) + .reverse(); + dispatch({ + type: ActionTypes.TOKEN_UPDATE_OTHER_TOKENS, + meta: { + otherTokens: filterOrderedOtherTokens, + }, + }); + dispatch({ + type: ActionTypes.TOKEN_UPDATE_OTHER_TOKENS_SUCCESS, + }); + } catch (error) { + dispatch({ + type: ActionTypes.TOKEN_UPDATE_OTHER_TOKENS_LOADING, + meta: { + isLoading: false, + }, + }); + dispatch({ + type: ActionTypes.TOKEN_UPDATE_OTHER_TOKENS_ERROR, + meta: { + isError: true, + }, + }); + logError(error); + dispatch( + notify({ + text: ( + + {translateErrorForUser(error)} + + ), + type: NotificationsTypes.ERROR, + }), + ); + } + }; +} + +export function checkOtherTokensLoading(isLoading) { + return async (dispatch) => { + dispatch({ + type: ActionTypes.TOKEN_UPDATE_OTHER_TOKENS_LOADING, + meta: { + isLoading, + }, + }); + }; +} + export function requestUBIPayout(payout) { return async (dispatch, getState) => { const { safe, token } = getState(); diff --git a/src/store/token/reducers.js b/src/store/token/reducers.js index 42dcf768..e94d3355 100644 --- a/src/store/token/reducers.js +++ b/src/store/token/reducers.js @@ -10,6 +10,11 @@ const initialState = { isLoading: false, lastPayoutAt: null, lastUpdateAt: null, + otherTokens: { + isLoading: true, + isError: false, + otherTokens: [], + }, }; const tokenReducer = (state = initialState, action) => { @@ -23,6 +28,24 @@ const tokenReducer = (state = initialState, action) => { address: { $set: action.meta.address }, lastPayoutAt: { $set: action.meta.lastPayoutAt }, }); + case ActionTypes.TOKEN_UPDATE_OTHER_TOKENS: + return update(state, { + otherTokens: { + otherTokens: { $set: action.meta.otherTokens }, + }, + }); + case ActionTypes.TOKEN_UPDATE_OTHER_TOKENS_LOADING: + return update(state, { + otherTokens: { + isLoading: { $set: action.meta.isLoading }, + }, + }); + case ActionTypes.TOKEN_UPDATE_OTHER_TOKENS_ERROR: + return update(state, { + otherTokens: { + isError: { $set: action.meta.isError }, + }, + }); case ActionTypes.TOKEN_BALANCE_UPDATE: return update(state, { isLoading: { $set: true }, diff --git a/src/store/token/types.js b/src/store/token/types.js index 27e9be17..77ce43b4 100644 --- a/src/store/token/types.js +++ b/src/store/token/types.js @@ -18,4 +18,8 @@ export default createTypes( 'TOKEN_UPDATE', 'TOKEN_UPDATE_ERROR', 'TOKEN_UPDATE_SUCCESS', + 'TOKEN_UPDATE_OTHER_TOKENS', + 'TOKEN_UPDATE_OTHER_TOKENS_LOADING', + 'TOKEN_UPDATE_OTHER_TOKENS_ERROR', + 'TOKEN_UPDATE_OTHER_TOKENS_SUCCESS', ); diff --git a/src/styles/theme.js b/src/styles/theme.js index e3b75c2f..299a743c 100644 --- a/src/styles/theme.js +++ b/src/styles/theme.js @@ -321,6 +321,12 @@ export default createTheme({ background: gradients.violetCurved, backgroundClip: 'text', textFillColor: 'transparent', + '& svg': { + fill: colors.violet, + }, + '& a': { + textDecoration: 'none', + }, }, poster: { fontSize: '4rem', diff --git a/src/views/Tokens.js b/src/views/Tokens.js new file mode 100644 index 00000000..01d4e92e --- /dev/null +++ b/src/views/Tokens.js @@ -0,0 +1,154 @@ +import { + Box, + CircularProgress, + Container, + Link as MuiLink, + Typography, +} from '@mui/material'; +import makeStyles from '@mui/styles/makeStyles'; +import PropTypes from 'prop-types'; +import React, { Fragment } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { Link } from 'react-router-dom'; + +import BalanceDisplay from '~/components/BalanceDisplay'; +import ButtonBack from '~/components/ButtonBack'; +import CenteredHeading from '~/components/CenteredHeading'; +import Header from '~/components/Header'; +import ProfileMini from '~/components/ProfileMini'; +import View from '~/components/View'; +import { useUpdateLoop } from '~/hooks/update'; +import { useRelativeProfileLink } from '~/hooks/url'; +import translate from '~/services/locale'; +import { + checkOtherTokens, + checkOtherTokensLoading, +} from '~/store/token/actions'; +import { formatCirclesValue } from '~/utils/format'; + +const useStyles = makeStyles(() => ({ + tokenContainer: { + paddingTop: '32px', + }, + tokenListContainer: { + paddingTop: '32px', + paddingBottom: '16px', + }, + tokenItem: { + marginBottom: '16px', + cursor: 'pointer', + }, + link: { + '&:hover': { + textDecoration: 'none', + }, + }, + balanceContainer: { + margin: '0 auto', + textAlign: 'center', + }, +})); + +const Tokens = () => { + const classes = useStyles(); + const dispatch = useDispatch(); + + const otherTokens = useSelector((state) => state.token.otherTokens); + + useUpdateLoop( + async () => { + if (otherTokens.length === 0) { + await dispatch(checkOtherTokensLoading(false)); + } else { + await dispatch(checkOtherTokensLoading(true)); + } + await dispatch(checkOtherTokens()); + }, + { + frequency: 1000 * 10, + }, + ); + + return ( + <> +
+ + {translate('Tokens.headingTokens')} +
+ + + + + + {translate('Tokens.descriptionTokens')} + + + + + ); +}; + +const TokensList = ({ otherTokens, isLoading }) => { + const classes = useStyles(); + + return ( + + {(!isLoading || otherTokens?.length !== 0) && + otherTokens?.map((item, index) => { + const circlesValue = formatCirclesValue( + item.amount, + Date.now(), + 2, + false, + ); + return ( + + ); + })} + {isLoading && otherTokens?.length === 0 && ( + + + + )} + + ); +}; + +const TokenItem = ({ ownerAddress, value }) => { + const classes = useStyles(); + const profilePath = useRelativeProfileLink(ownerAddress); + + return ( + + + + ); +}; + +TokensList.propTypes = { + isLoading: PropTypes.bool, + otherTokens: PropTypes.array, +}; + +TokenItem.propTypes = { + ownerAddress: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, +}; + +export default Tokens;