diff --git a/locales/en.json b/locales/en.json index eaeb0914..ccb7d919 100644 --- a/locales/en.json +++ b/locales/en.json @@ -20,7 +20,7 @@ "bodyExplainMeTrustedSomeone": "You trusted @{actor} which means that @{actor} can send you their tokens.", "bodyExplainMeUntrustedSomeone": "You untrusted @{actor} which means that you do not accept any tokens from them anymore.", "bodyExplainReceivedCircles": "You received {value} {denominator} from @{actor} transitively.", - "bodyExplainReceivedUBI": "This is your Universal Basic Income, issued as your own personal currency. You receive circa {rate} per month.", + "bodyExplainReceivedUBI": "This is your Universal Basic Income, issued as your own personal currency. You receive {rate} per day.", "bodyExplainSecondary": "All activities carry a small transaction fee used to maintain the Circles network.", "bodyExplainSentCircles": "You sent {value} {denominator} to @{actor} transitively.", "bodyExplainTrustedBySomeone": "@{actor} trusts you. You can send your token to them now.", @@ -44,7 +44,7 @@ }, "BalanceDisplay": { "tooltipSharedWalletBalance": "This is the shared wallet balance. Shared wallets do not receive basic income.", - "tooltipYourBalance": "Your basic income is issued continuously at the rate of {rate} Circles a month." + "tooltipYourBalance": "Your basic income is issued continuously at the rate of {rate} circles per day." }, "ButtonClipboard": { "buttonCopy": "Copy to clipboard", @@ -63,7 +63,7 @@ "bodyCriticalErrorTryAgain": "Oh no! Something went wrong while loading your account. Please reload this page and try again.", "buttonBurnWallet": "Reset Account", "buttonReload": "Refresh Page", - "dialogAreYouSure": "Do you really want to reset this wallet? You will not be able to recover without a seed phrase.", + "dialogAreYouSure": "Do you really want to reset this wallet? You will not be able to recover without your magic words.", "infoCopiedMessage": "Text copied to clipboard" }, "Dashboard": { @@ -85,7 +85,7 @@ "DialogBurn": { "dialogBurnCancel": "Cancel", "dialogBurnConfirm": "End Session", - "dialogBurnDescription": "Are you sure you want to end this session? You will not be able to restore your account unless you have your Seed Phrase!", + "dialogBurnDescription": "Are you sure you want to end this session? You will not be able to restore your account unless you have your Magic Words!", "dialogBurnTitle": "Warning!" }, "DialogTrust": { @@ -191,12 +191,12 @@ "es": "ES" }, "Login": { - "bodyEnterYourSeedPhrase": "Enter your seed phrase below to restore your wallet:", - "bodyLostYourSeedPhrase": "Lost your seed phrase?", + "bodyEnterYourSeedPhrase": "Enter your magic words below to restore your wallet:", + "bodyLostYourSeedPhrase": "Lost your magic words?", "bodyNeedHelp": "Questions?", "buttonCreateNewWallet": "Sign Up Here", "buttonSubmit": "Submit", - "errorRestoreFailedInvalidSeedphrase": "Your seed phrase is invalid.", + "errorRestoreFailedInvalidSeedPhrase": "Your magic words are invalid.", "errorRestoreFailedUnknown": "Could not restore account due to an unknown error.", "errorRestoreFailedUnknownSafe": "The account you tried to restore is not registered.", "headingLogin": "Login", @@ -211,8 +211,9 @@ }, "Navigation": { "buttonActivityLog": "Activity Log", + "buttonBalanceTokens": "Balance Breakdown", "buttonSettings": "Settings", - "buttonExportSeedPhrase": "Export Seed Phrase", + "buttonExportSeedPhrase": "Export Magic Words", "buttonMyQR": "My QR", "buttonDoublePeople": "Trust People", "buttonSendCircles": "Send Circles", @@ -247,7 +248,7 @@ "bodySecureWalletP1B": " in a place you trust.", "bodySecureWalletP2": "This is the only way to recover your wallet if you get locked out of the app, or get a new device.", "bodySeedPhrase": "This is your account password. You can use this to login into your account on other devices.", - "bodySeedPhraseChallenge": "Please enter word {wordIndex} in your seed phrase:", + "bodySeedPhraseChallenge": "Please enter word {wordIndex} in your magic words:", "bodyUsername": "Your username is how your friends can search for you on the Circles App.", "bodyGuidelinesUsername": "Only basic characters and numbers (A-Z, 0-9) are allowed, no symbols or whitespace, 3-24 characters.", "buttonCopyToClipboard": "Copy to clipboard", @@ -362,7 +363,7 @@ "SeedPhrase": { "bodyDescription": "Print this and store it safely:", "buttonCopyToClipboard": "Copy to clipboard", - "headingExportSeedPhrase": "Export Seed Phrase" + "headingExportSeedPhrase": "Export Magic Words" }, "Send": { "headingSendCircles": "Send Circles" @@ -416,6 +417,10 @@ "TextVerification": { "noteInvalid": "You cannot write special symbols like {}
^ or more than 100 characters." }, + "Tokens": { + "headingTokens": "Balance Breakdown", + "descriptionTokens": "All circles you hold were 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" @@ -449,7 +454,7 @@ "infoUbiPayoutReceived": "You just received your UBI of
{payout}circles for the month!" }, "Validation": { - "bodyDoNotReset": "Do not reset your browser local storage or you will need to recover your data with your seed phrase.", + "bodyDoNotReset": "Do not reset your browser local storage or you will need to recover your data with your magic words.", "bodyTrustDescription": "To receive your basic income,", "bodyTrustDescriptionEmphasize": "three people must trust your profile.", "buttonShareProfileLink": "Share Profile Link", diff --git a/src/components/ActivityStreamItem.js b/src/components/ActivityStreamItem.js index f6f6aab6..28a9d79e 100644 --- a/src/components/ActivityStreamItem.js +++ b/src/components/ActivityStreamItem.js @@ -217,7 +217,7 @@ const ActivityStreamExplained = ({ return translate(`ActivityStream.bodyExplain${messageId}`, { ...data, actor, - rate: ISSUANCE_RATE_MONTH, + rate: ISSUANCE_RATE_MONTH / 30, }); }, [actor, data, messageId]); diff --git a/src/components/BalanceDisplay.js b/src/components/BalanceDisplay.js index fd24e2c1..221798ec 100644 --- a/src/components/BalanceDisplay.js +++ b/src/components/BalanceDisplay.js @@ -1,8 +1,12 @@ import { CircularProgress } from '@mui/material'; import { Box, Tooltip, Typography } from '@mui/material'; import makeStyles from '@mui/styles/makeStyles'; +import PropTypes from 'prop-types'; 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'; @@ -26,9 +30,15 @@ const useStyles = makeStyles((theme) => ({ marginRight: theme.spacing(0.5), fontSize: '2.3rem', }, + balanceUnderline: { + '&:hover': { + textDecoration: 'underline', + textDecorationColor: theme.custom.colors.violet, + }, + }, })); -const BalanceDisplay = () => { +const BalanceDisplay = ({ underlineAtHover = true }) => { const dispatch = useDispatch(); const classes = useStyles(); const { token, safe } = useSelector((state) => state); @@ -46,7 +56,7 @@ const BalanceDisplay = () => { const tooltipText = safe.isOrganization ? translate('BalanceDisplay.tooltipSharedWalletBalance') : translate('BalanceDisplay.tooltipYourBalance', { - rate: ISSUANCE_RATE_MONTH, + rate: ISSUANCE_RATE_MONTH / 30, }); return ( @@ -55,9 +65,14 @@ const BalanceDisplay = () => { {isLoading ? ( ) : ( - - - {tokenBalance} + + + + {tokenBalance} + )} @@ -65,4 +80,8 @@ const BalanceDisplay = () => { ); }; +BalanceDisplay.propTypes = { + underlineAtHover: PropTypes.bool, +}; + export default BalanceDisplay; 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/Navigation.js b/src/components/Navigation.js index fd666b39..d6aea925 100644 --- a/src/components/Navigation.js +++ b/src/components/Navigation.js @@ -15,6 +15,7 @@ import { SEND_PATH, SETTINGS_PATH, SHARE_PATH, + TOKENS_PATH, } from '~/routes'; import AvatarWithQR from '~/components/AvatarWithQR'; @@ -134,6 +135,9 @@ const NavigationMain = ({ onClick }) => { {translate('Navigation.buttonActivityLog')} + + {translate('Navigation.buttonBalanceTokens')} + {!safe.isOrganization && ( {translate('Navigation.buttonOrganization')} 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..1dda999f 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 = '/balance'; 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/Login.js b/src/views/Login.js index c72159ca..1a7a480b 100644 --- a/src/views/Login.js +++ b/src/views/Login.js @@ -92,7 +92,7 @@ const Login = () => { } catch (error) { let translationId = 'Login.errorRestoreFailedUnknown'; if (error.message === RESTORE_ACCOUNT_INVALID_SEED_PHRASE) { - translationId = 'Login.errorRestoreFailedInvalidSeedphrase'; + translationId = 'Login.errorRestoreFailedInvalidSeedPhrase'; } else if (error.message === RESTORE_ACCOUNT_UNKNOWN_SAFE) { translationId = 'Login.errorRestoreFailedUnknownSafe'; } diff --git a/src/views/Tokens.js b/src/views/Tokens.js new file mode 100644 index 00000000..026c4dd3 --- /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; diff --git a/src/views/Welcome.js b/src/views/Welcome.js index 8fd3a23a..84f229a1 100644 --- a/src/views/Welcome.js +++ b/src/views/Welcome.js @@ -91,7 +91,7 @@ const Welcome = () => { - + {translate('Welcome.linkFAQ')}