From 454212b9afb9b0c928dca681f400590c250ff77c Mon Sep 17 00:00:00 2001 From: Rachel Ho Date: Thu, 7 Oct 2021 18:44:30 +0800 Subject: [PATCH 01/12] token amount & usd balance display for starred accounts --- components/Layout/LeftMenu.tsx | 32 ++--------- components/Layout/StarredAccount.tsx | 86 ++++++++++++++++++++++++++++ components/Layout/styles.ts | 2 +- 3 files changed, 91 insertions(+), 29 deletions(-) create mode 100644 components/Layout/StarredAccount.tsx diff --git a/components/Layout/LeftMenu.tsx b/components/Layout/LeftMenu.tsx index dbd7e006..a028af25 100644 --- a/components/Layout/LeftMenu.tsx +++ b/components/Layout/LeftMenu.tsx @@ -1,7 +1,6 @@ import React from 'react' import useTranslation from 'next-translate/useTranslation' import { - Avatar, Box, Button, List, @@ -20,11 +19,11 @@ import AddressBookIcon from '../../assets/images/icons/icon_address_book.svg' import Logo from '../../assets/images/logo.svg' import LogoExpended from '../../assets/images/logo_expended.svg' import useStyles from './styles' +import StarredAccount from './StarredAccount' import useIconProps from '../../misc/useIconProps' import { MenuWidth } from '.' import { useGeneralContext } from '../../contexts/GeneralContext' import { CustomTheme } from '../../misc/theme' -import cryptocurrencies from '../../misc/cryptocurrencies' import { useWalletsContext } from '../../contexts/WalletsContext' interface LeftMenuProps { @@ -179,32 +178,9 @@ const LeftMenu: React.FC = ({ activeItem, isMenuExpanded, setIsMe {accounts .filter((account) => account.fav) - .map((account) => { - const crypto = cryptocurrencies[account.crypto] - return ( - - - - - - - - - ) - })} + .map((account) => ( + + ))} diff --git a/components/Layout/StarredAccount.tsx b/components/Layout/StarredAccount.tsx new file mode 100644 index 00000000..96fcea58 --- /dev/null +++ b/components/Layout/StarredAccount.tsx @@ -0,0 +1,86 @@ +import React from 'react' +import Link from 'next/link' +import useTranslation from 'next-translate/useTranslation' +import { gql, useSubscription } from '@apollo/client' +import { + Avatar, + Box, + ListItem, + ListItemIcon, + ListItemText, + useTheme, + CircularProgress, + Typography, +} from '@material-ui/core' +import { useGeneralContext } from '../../contexts/GeneralContext' +import { + formatCurrency, + formatTokenAmount, + getTotalBalance, + getTotalTokenAmount, + transformGqlAcountBalance, +} from '../../misc/utils' +import { CustomTheme } from '../../misc/theme' +import cryptocurrencies from '../../misc/cryptocurrencies' +import useStyles from './styles' +import { getLatestAccountBalance } from '../../graphql/queries/accountBalances' + +interface StarredAccountProps { + account: Account +} + +const StarredAccount: React.FC = ({ account }) => { + const themeStyle: CustomTheme = useTheme() + const { lang } = useTranslation('common') + const { currency } = useGeneralContext() + const classes = useStyles() + const crypto = cryptocurrencies[account.crypto] + // Latest data + const { data: balanceData, loading } = useSubscription( + gql` + ${getLatestAccountBalance(account.crypto)} + `, + { variables: { address: account.address } } + ) + const { tokenAmounts, usdBalance } = React.useMemo(() => { + const accountBalance = transformGqlAcountBalance(balanceData, Date.now()) + return { + tokenAmounts: getTotalTokenAmount(accountBalance).amount, + usdBalance: getTotalBalance(accountBalance).balance, + } + }, [balanceData]) + return ( + + + + + + + + {loading ? ( + + + + ) : ( + + {formatTokenAmount(tokenAmounts, account.crypto, lang)} + {/* after USD balance is added */} + {/* {formatCurrency(usdBalance, currency, lang)} */} + + )} + + + + ) +} + +export default StarredAccount diff --git a/components/Layout/styles.ts b/components/Layout/styles.ts index 647acb44..1b1bdd34 100644 --- a/components/Layout/styles.ts +++ b/components/Layout/styles.ts @@ -67,7 +67,7 @@ const useStyles = makeStyles( width: 'fit-content', }, favMenuItem: { - height: theme.spacing(6), + height: theme.spacing(8), borderRadius: theme.spacing(1), whiteSpace: 'nowrap', paddingLeft: theme.spacing(1.5), From be6cb3aadf748e50509046992e9c2cbeba443d2e Mon Sep 17 00:00:00 2001 From: Calvin Kei Date: Fri, 8 Oct 2021 23:19:31 +0800 Subject: [PATCH 02/12] persisted password for 15 min --- components/Layout/index.tsx | 22 +++++++++++++++++++- components/UnlockPasswordDialog/index.tsx | 12 +---------- contexts/WalletsContext.tsx | 16 ++++++++++++--- misc/usePersistedState.ts | 25 ++++++++++++++++++----- tests/components/Layout/index.test.tsx | 1 + tests/contexts/WalletsContext.test.tsx | 1 + 6 files changed, 57 insertions(+), 20 deletions(-) diff --git a/components/Layout/index.tsx b/components/Layout/index.tsx index b44af7ff..140494e0 100644 --- a/components/Layout/index.tsx +++ b/components/Layout/index.tsx @@ -35,9 +35,11 @@ const Layout: React.FC = ({ const classes = useStyles() const theme = useTheme() const [isMenuExpanded, setIsMenuExpanded, loaded] = usePersistedState('isMenuExpanded', true) - const { isFirstTimeUser, isUnlocked, isChromeExtInstalled } = useWalletsContext() + const { isFirstTimeUser, isUnlocked, isChromeExtInstalled, unlockWallets, password } = + useWalletsContext() // Hide menu for chrome extension const router = useRouter() + const defaultPassword = router.query.password const { isChromeExt } = useIsChromeExt() // Open ConfirmTransactionDialog with correct query params @@ -57,6 +59,24 @@ const Layout: React.FC = ({ } }, [router]) + const initPassword = React.useCallback(async () => { + try { + if (!isUnlocked) { + if (defaultPassword) { + await unlockWallets(String(defaultPassword)) + } else if (password) { + await unlockWallets(password) + } + } + } catch (err) { + console.log(err) + } + }, [defaultPassword, password, isUnlocked, unlockWallets]) + + React.useEffect(() => { + initPassword() + }, [initPassword]) + return loaded ? ( <> { const { t } = useTranslation('common') const classes = useStyles() const iconProps = useIconProps() - const { wallets, reset, unlockWallets } = useWalletsContext() + const { wallets, reset } = useWalletsContext() const isMobile = useIsMobile() - const { - query: { password: defaultPassword }, - } = useRouter() const [isReset, setReset] = React.useState(false) const [stage, setStage, toPrevStage, isPrevStageAvailable] = useStateHistory( UnlockPasswordStage.UnlockPasswordStage @@ -76,12 +72,6 @@ const UnlockPasswordDialog: React.FC = () => { } }, [stage, t]) - React.useEffect(() => { - if (defaultPassword) { - unlockWallets(String(defaultPassword)) - } - }, [defaultPassword, unlockWallets]) - return ( {isPrevStageAvailable && stage !== 'unlock' ? ( diff --git a/contexts/WalletsContext.tsx b/contexts/WalletsContext.tsx index 8061e69f..5a9a6091 100644 --- a/contexts/WalletsContext.tsx +++ b/contexts/WalletsContext.tsx @@ -1,5 +1,8 @@ import React from 'react' import sendMsgToChromeExt from '../misc/sendMsgToChromeExt' +import usePersistedState from '../misc/usePersistedState' + +const PASSWORD_EXPIRES_IN_MIN = 15 interface WalletsState { isFirstTimeUser: boolean @@ -39,9 +42,16 @@ const WalletsContext = React.createContext(initialState) const WalletsProvider: React.FC = ({ children }) => { const [wallets, setWallets] = React.useState([]) const [accounts, setAccounts] = React.useState([]) - const [isFirstTimeUser, setIsFirstTimeUser] = React.useState(false) - const [isChromeExtInstalled, setIsChromeExtInstalled] = React.useState(false) - const [password, setPassword] = React.useState('') + const [isFirstTimeUser, setIsFirstTimeUser] = usePersistedState('isFirstTimeUser', false) + const [isChromeExtInstalled, setIsChromeExtInstalled] = usePersistedState( + 'isChromeExtInstalled', + false + ) + const [password, setPassword] = usePersistedState( + 'password', + '', + PASSWORD_EXPIRES_IN_MIN * 60 * 1000 + ) const reset = React.useCallback(async () => { await sendMsgToChromeExt({ diff --git a/misc/usePersistedState.ts b/misc/usePersistedState.ts index ca36578a..17d58e33 100644 --- a/misc/usePersistedState.ts +++ b/misc/usePersistedState.ts @@ -1,11 +1,19 @@ import React from 'react' -const retrievePersistedValue =

(key: string, initialValue: P) => { +const retrievePersistedValue =

(key: string, initialValue: P, expireWithinMs?: number) => { try { const persistedString = localStorage.getItem(key) if (!persistedString) { return initialValue } + if (expireWithinMs) { + const lastUpdatedAt = localStorage.getItem(`${key}-last-updated-at`) + if (Number(lastUpdatedAt) + expireWithinMs < Date.now()) { + localStorage.removeItem(key) + localStorage.removeItem(`${key}-last-updated-at`) + return initialValue + } + } const persistedValue = JSON.parse(persistedString) return persistedValue } catch (err) { @@ -15,13 +23,20 @@ const retrievePersistedValue =

(key: string, initialValue: P) => { const usePersistedState =

( key: string, - initialValue: P + initialValue: P, + expireWithinMs?: number ): [P, React.Dispatch>, boolean] => { - const [value, setValue] = React.useState(retrievePersistedValue(key, initialValue)) + const persistedValue = retrievePersistedValue(key, initialValue, expireWithinMs) + const [value, setValue] = React.useState(persistedValue) const [loaded, setLoaded] = React.useState(false) React.useEffect(() => { - localStorage.setItem(key, JSON.stringify(value)) - }, [value]) + if (persistedValue !== value) { + localStorage.setItem(key, JSON.stringify(value)) + if (expireWithinMs) { + localStorage.setItem(`${key}-last-updated-at`, String(Date.now())) + } + } + }, [value, persistedValue, expireWithinMs]) // for conditional rendering in SSR React.useEffect(() => { setLoaded(true) diff --git a/tests/components/Layout/index.test.tsx b/tests/components/Layout/index.test.tsx index a0449a8f..4dba1b65 100644 --- a/tests/components/Layout/index.test.tsx +++ b/tests/components/Layout/index.test.tsx @@ -10,6 +10,7 @@ const mockWalletsContext = { jest.mock('next/router', () => ({ useRouter: () => ({ asPath: 'https://localhost:3000', + query: {}, }), })) jest.mock('../../../contexts/WalletsContext', () => ({ diff --git a/tests/contexts/WalletsContext.test.tsx b/tests/contexts/WalletsContext.test.tsx index d4791e44..4667de32 100644 --- a/tests/contexts/WalletsContext.test.tsx +++ b/tests/contexts/WalletsContext.test.tsx @@ -421,6 +421,7 @@ describe('context: WalletsContext', () => { }) afterEach(() => { + localStorage.clear() cleanup() jest.clearAllMocks() }) From 6f39e2a5a127b41eb538cae8d468ef4a8e172d9c Mon Sep 17 00:00:00 2001 From: Calvin Kei Date: Fri, 8 Oct 2021 23:26:58 +0800 Subject: [PATCH 03/12] added loading state for unlocking wallets --- components/Layout/index.tsx | 13 ++++++++----- contexts/WalletsContext.tsx | 9 ++++++--- custom.d.ts | 2 ++ 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/components/Layout/index.tsx b/components/Layout/index.tsx index 140494e0..dc804207 100644 --- a/components/Layout/index.tsx +++ b/components/Layout/index.tsx @@ -35,7 +35,7 @@ const Layout: React.FC = ({ const classes = useStyles() const theme = useTheme() const [isMenuExpanded, setIsMenuExpanded, loaded] = usePersistedState('isMenuExpanded', true) - const { isFirstTimeUser, isUnlocked, isChromeExtInstalled, unlockWallets, password } = + const { isFirstTimeUser, appUnlockState, isChromeExtInstalled, unlockWallets, password } = useWalletsContext() // Hide menu for chrome extension const router = useRouter() @@ -61,7 +61,7 @@ const Layout: React.FC = ({ const initPassword = React.useCallback(async () => { try { - if (!isUnlocked) { + if (appUnlockState !== 'unlocked') { if (defaultPassword) { await unlockWallets(String(defaultPassword)) } else if (password) { @@ -71,7 +71,7 @@ const Layout: React.FC = ({ } catch (err) { console.log(err) } - }, [defaultPassword, password, isUnlocked, unlockWallets]) + }, [defaultPassword, password, appUnlockState, unlockWallets]) React.useEffect(() => { initPassword() @@ -100,9 +100,12 @@ const Layout: React.FC = ({ }} > {passwordRequired && isFirstTimeUser ? : null} - {!passwordRequired || isUnlocked ? children : null} + {!passwordRequired || appUnlockState === 'unlocked' ? children : null} - {passwordRequired && !isFirstTimeUser && isChromeExtInstalled ? ( + {passwordRequired && + !isFirstTimeUser && + isChromeExtInstalled && + appUnlockState === 'locked' ? ( ) : null} {!isChromeExtInstalled ? : null} diff --git a/contexts/WalletsContext.tsx b/contexts/WalletsContext.tsx index 5a9a6091..53919760 100644 --- a/contexts/WalletsContext.tsx +++ b/contexts/WalletsContext.tsx @@ -6,7 +6,7 @@ const PASSWORD_EXPIRES_IN_MIN = 15 interface WalletsState { isFirstTimeUser: boolean - isUnlocked: boolean + appUnlockState: AppUnlockState isChromeExtInstalled: boolean wallets: Wallet[] accounts: Account[] @@ -30,7 +30,7 @@ interface WalletsState { const initialState: WalletsState = { isFirstTimeUser: false, - isUnlocked: false, + appUnlockState: 'locked', isChromeExtInstalled: false, wallets: [], accounts: [], @@ -42,6 +42,7 @@ const WalletsContext = React.createContext(initialState) const WalletsProvider: React.FC = ({ children }) => { const [wallets, setWallets] = React.useState([]) const [accounts, setAccounts] = React.useState([]) + const [appUnlockState, setAppUnlockState] = React.useState(initialState.appUnlockState) const [isFirstTimeUser, setIsFirstTimeUser] = usePersistedState('isFirstTimeUser', false) const [isChromeExtInstalled, setIsChromeExtInstalled] = usePersistedState( 'isChromeExtInstalled', @@ -78,6 +79,7 @@ const WalletsProvider: React.FC = ({ children }) => { const unlockWallets = React.useCallback( async (pw: string) => { if (!isFirstTimeUser) { + setAppUnlockState('unlocking') const walletaResponse = await sendMsgToChromeExt({ event: 'getWallets', data: { @@ -92,6 +94,7 @@ const WalletsProvider: React.FC = ({ children }) => { }) setWallets(walletaResponse.wallets) setAccounts(accountsResponse.accounts) + setAppUnlockState('unlocked') } setPassword(pw) }, @@ -261,7 +264,7 @@ const WalletsProvider: React.FC = ({ children }) => { Date: Fri, 8 Oct 2021 23:27:35 +0800 Subject: [PATCH 04/12] update test --- tests/components/Layout/__snapshots__/index.test.tsx.snap | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/components/Layout/__snapshots__/index.test.tsx.snap b/tests/components/Layout/__snapshots__/index.test.tsx.snap index 18a6c479..593f16aa 100644 --- a/tests/components/Layout/__snapshots__/index.test.tsx.snap +++ b/tests/components/Layout/__snapshots__/index.test.tsx.snap @@ -67,7 +67,6 @@ Array [ } } />, - , ] `; @@ -114,6 +113,5 @@ Array [ } } />, - , ] `; From 54feb38d91750b609d90517be21204fbf819a090 Mon Sep 17 00:00:00 2001 From: Calvin Kei Date: Sat, 9 Oct 2021 00:44:28 +0800 Subject: [PATCH 05/12] fix e2e test --- components/Layout/index.tsx | 5 +- .../UnlockPasswordDialog/UnlockPassword.tsx | 9 +++- components/UnlockPasswordDialog/index.tsx | 4 +- contexts/WalletsContext.tsx | 50 +++++++++++-------- .../onboarding/createWallet.spec.ts | 4 ++ ...Phrase.ts => importMnemonicPhrase.spec.ts} | 4 ++ .../{unlockApp.ts => unlockApp.spec.ts} | 0 locales/en/common.json | 1 + .../Layout/__snapshots__/index.test.tsx.snap | 2 + .../__snapshots__/index.test.tsx.snap | 33 ++++++++++-- 10 files changed, 79 insertions(+), 33 deletions(-) rename cypress/integration/onboarding/{importMnemonicPhrase.ts => importMnemonicPhrase.spec.ts} (91%) rename cypress/integration/onboarding/{unlockApp.ts => unlockApp.spec.ts} (100%) diff --git a/components/Layout/index.tsx b/components/Layout/index.tsx index dc804207..174ce877 100644 --- a/components/Layout/index.tsx +++ b/components/Layout/index.tsx @@ -102,10 +102,7 @@ const Layout: React.FC = ({ {passwordRequired && isFirstTimeUser ? : null} {!passwordRequired || appUnlockState === 'unlocked' ? children : null} - {passwordRequired && - !isFirstTimeUser && - isChromeExtInstalled && - appUnlockState === 'locked' ? ( + {passwordRequired && !isFirstTimeUser && isChromeExtInstalled ? ( ) : null} {!isChromeExtInstalled ? : null} diff --git a/components/UnlockPasswordDialog/UnlockPassword.tsx b/components/UnlockPasswordDialog/UnlockPassword.tsx index 026e56bc..9c073d61 100644 --- a/components/UnlockPasswordDialog/UnlockPassword.tsx +++ b/components/UnlockPasswordDialog/UnlockPassword.tsx @@ -1,4 +1,10 @@ -import { Button, DialogActions, DialogContent, DialogContentText } from '@material-ui/core' +import { + Button, + DialogActions, + DialogContent, + DialogContentText, + Typography, +} from '@material-ui/core' import useTranslation from 'next-translate/useTranslation' import React from 'react' import useStyles from './styles' @@ -44,6 +50,7 @@ const UnlockPassword: React.FC = ({ onForgot }) => { helperText={error} onChange={(e) => setPassword(e.target.value)} /> + {t('unlock pasword helper text')}