Skip to content

Commit

Permalink
Feat: My Accounts page
Browse files Browse the repository at this point in the history
  • Loading branch information
katspaugh committed Feb 9, 2024
1 parent 88c5b56 commit 3573daf
Show file tree
Hide file tree
Showing 12 changed files with 256 additions and 18 deletions.
2 changes: 1 addition & 1 deletion src/components/common/EthHashInfo/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const EthHashInfo = ({
const currentChainId = useChainId()
const chain = useAppSelector((state) => selectChainById(state, props.chainId || currentChainId))
const addressBook = useAddressBook()
const link = chain ? getBlockExplorerLink(chain, props.address) : undefined
const link = chain && getBlockExplorerLink(chain, props.address) : undefined
const name = showName ? addressBook[props.address] || props.name : undefined

return (
Expand Down
63 changes: 63 additions & 0 deletions src/components/welcome/MyAccounts/AccountItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useMemo } from 'react'
import { ListItemButton, Box, Typography } from '@mui/material'
import Link from 'next/link'
import SafeIcon from '@/components/common/SafeIcon'
import Track from '@/components/common/Track'
import { OPEN_SAFE_LABELS, OVERVIEW_EVENTS } from '@/services/analytics'
import { AppRoutes } from '@/config/routes'
import { useAppSelector } from '@/store'
import { selectChainById } from '@/store/chainsSlice'
import ChainIndicator from '@/components/common/ChainIndicator'
import css from './styles.module.css'
import { selectAllAddressBooks } from '@/store/addressBookSlice'
import { shortenAddress } from '@/utils/formatters'

type AccountItemProps = {
chainId: string
address: string
threshold?: number
owners?: number
}

const getSafeHref = (prefix: string, address: string) => ({
pathname: AppRoutes.home,
query: { safe: `${prefix}:${address}` },
})

const AccountItem = ({ chainId, address, ...rest }: AccountItemProps) => {
const chain = useAppSelector((state) => selectChainById(state, chainId))

const href = useMemo(() => {
return chain ? getSafeHref(chain.shortName, address) : ''
}, [chain, address])

const name = useAppSelector(selectAllAddressBooks)[chainId]?.[address]

return (
<Track {...OVERVIEW_EVENTS.OPEN_SAFE} label={OPEN_SAFE_LABELS.login_page}>
<Link href={href}>
<ListItemButton className={css.listItem}>
<SafeIcon address={address} {...rest} />

<Typography variant="body2" component="div">
{name && (
<Typography fontWeight="bold" fontSize="inherit">
{name}
</Typography>
)}
<b>{chain?.shortName}: </b>
<Typography color="text.secondary" fontSize="inherit" component="span">
{shortenAddress(address)}
</Typography>
</Typography>

<Box flex={1} />

<ChainIndicator chainId={chainId} />
</ListItemButton>
</Link>
</Track>
)
}

export default AccountItem
18 changes: 18 additions & 0 deletions src/components/welcome/MyAccounts/CreateButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Button } from '@mui/material'
import Link from 'next/link'
import { AppRoutes } from '@/config/routes'
import { useCurrentChain } from '@/hooks/useChains'

const CreateButton = () => {
const currentChain = useCurrentChain()

return (
<Link href={{ pathname: AppRoutes.newSafe.create, query: { chain: currentChain?.shortName } }}>
<Button disableElevation size="small" variant="contained">
Create account
</Button>
</Link>
)
}

export default CreateButton
65 changes: 65 additions & 0 deletions src/components/welcome/MyAccounts/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { useMemo, useState } from 'react'
import { Button, Box, Paper, Typography } from '@mui/material'
import madProps from '@/utils/mad-props'
import AccountItem from './AccountItem'
import CreateButton from './CreateButton'
import useAllSafes, { type SafeItems } from './useAllSafes'

type AccountsListProps = {
safes: SafeItems
}

const DEFAULT_SHOWN = 5
const MAX_DEFAULT_SHOWN = 7

const AccountsList = ({ safes }: AccountsListProps) => {
const [maxShown, setMaxShown] = useState<number>(DEFAULT_SHOWN)

const shownSafes = useMemo(() => {
if (safes.length <= MAX_DEFAULT_SHOWN) {
return safes
}
return safes.slice(0, maxShown)
}, [safes, maxShown])

const onShowMore = () => {
const pageSize = 100 // DEFAULT_SHOWN
setMaxShown((prev) => prev + pageSize)
}

return (
<Box display="flex" justifyContent="center">
<Box width={600} m={2}>
<Box display="flex" justifyContent="space-between" py={3}>
<Typography variant="h1" fontWeight={700}>
My Safe accounts
<Typography component="span" color="text.secondary" fontSize="inherit" fontWeight="normal">
{' '}
({safes.length})
</Typography>
</Typography>

<CreateButton />
</Box>

<Paper sx={{ p: 3, pb: 2 }}>
{shownSafes.map((item) => (
<AccountItem {...item} key={item.chainId + item.address} />
))}

{safes.length > shownSafes.length && (
<Box display="flex" justifyContent="center">
<Button onClick={onShowMore}>Show more</Button>
</Box>
)}
</Paper>
</Box>
</Box>
)
}

const MyAccounts = madProps(AccountsList, {
safes: useAllSafes,
})

export default MyAccounts
8 changes: 8 additions & 0 deletions src/components/welcome/MyAccounts/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.listItem {
gap: var(--space-2);
border: 1px solid var(--color-border-light);
border-radius: var(--space-1);
margin-bottom: 12px;
padding-top: var(--space-2);
padding-bottom: var(--space-2);
}
14 changes: 14 additions & 0 deletions src/components/welcome/MyAccounts/useAllOwnedSafes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { getAllOwnedSafes } from '@safe-global/safe-gateway-typescript-sdk'
import useAsync from '@/hooks/useAsync'
import useWallet from '@/hooks/wallets/useWallet'

const useAllOwnedSafes = () => {
const { address = '' } = useWallet() || {}

return useAsync(() => {
if (!address) return
return getAllOwnedSafes(address)
}, [address])
}

export default useAllOwnedSafes
46 changes: 46 additions & 0 deletions src/components/welcome/MyAccounts/useAllSafes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { useMemo } from 'react'
import uniq from 'lodash/uniq'
import { useAppSelector } from '@/store'
import { selectAllAddedSafes } from '@/store/addedSafesSlice'
import useAllOwnedSafes from './useAllOwnedSafes'
import useChains from '@/hooks/useChains'
import useChainId from '@/hooks/useChainId'

export type SafeItems = Array<{
chainId: string
address: string
threshold?: number
owners?: number
}>

const useAddedSafes = () => {
const allAdded = useAppSelector(selectAllAddedSafes)
return allAdded
}

const useAllSafes = (): SafeItems => {
const [allOwned = {}] = useAllOwnedSafes()
const allAdded = useAddedSafes()
const { configs } = useChains()
const currentChainId = useChainId()

return useMemo<SafeItems>(() => {
const chains = uniq([currentChainId].concat(Object.keys(allAdded)).concat(Object.keys(allOwned)))

return chains.flatMap((chainId) => {
if (!configs.some((item) => item.chainId === chainId)) return []
const addedOnChain = Object.keys(allAdded[chainId] || {})
const ownedOnChain = allOwned[chainId]
const uniqueAddresses = uniq(addedOnChain.concat(ownedOnChain)).filter(Boolean)

return uniqueAddresses.map((address) => ({
address,
chainId,
threshold: allAdded[chainId]?.[address]?.threshold,
owners: allAdded[chainId]?.[address]?.owners.length,
}))
})
}, [configs, allAdded, allOwned, currentChainId])
}

export default useAllSafes
36 changes: 20 additions & 16 deletions src/components/welcome/WelcomeLogin/index.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,37 @@
import isEmpty from 'lodash/isEmpty'
import { AppRoutes } from '@/config/routes'
import { useHasFeature } from '@/hooks/useChains'
import { FEATURES } from '@/utils/chains'
import { Paper, SvgIcon, Typography, Divider, Link, Box, Skeleton } from '@mui/material'
import { Paper, SvgIcon, Typography, Divider, Box, Skeleton } from '@mui/material'
import SafeLogo from '@/public/images/logo-text.svg'
import dynamic from 'next/dynamic'
import css from './styles.module.css'
import { useRouter } from 'next/router'
import WalletLogin from './WalletLogin'
import { LOAD_SAFE_EVENTS, CREATE_SAFE_EVENTS } from '@/services/analytics/events/createLoadSafe'
import Track from '@/components/common/Track'
import { CREATE_SAFE_EVENTS } from '@/services/analytics/events/createLoadSafe'
import { trackEvent } from '@/services/analytics'
import { useAppSelector } from '@/store'
import { selectAllAddedSafes } from '@/store/addedSafesSlice'
import useWallet from '@/hooks/wallets/useWallet'

const SocialSigner = dynamic(() => import('@/components/common/SocialSigner'), {
loading: () => <Skeleton variant="rounded" height={42} width="100%" />,
})

const WelcomeLogin = () => {
const router = useRouter()
const wallet = useWallet()
const isSocialLoginEnabled = useHasFeature(FEATURES.SOCIAL_LOGIN)
const addedSafes = useAppSelector(selectAllAddedSafes)
const hasAddedSafes = !isEmpty(addedSafes)

const continueToCreation = () => {
trackEvent(CREATE_SAFE_EVENTS.OPEN_SAFE_CREATION)
router.push({ pathname: AppRoutes.newSafe.create, query: router.query })
if (hasAddedSafes) {
router.push({ pathname: AppRoutes.welcome.accounts, query: router.query })
} else {
trackEvent(CREATE_SAFE_EVENTS.OPEN_SAFE_CREATION)
router.push({ pathname: AppRoutes.newSafe.create, query: router.query })
}
}

return (
Expand All @@ -30,12 +40,15 @@ const WelcomeLogin = () => {
<SvgIcon component={SafeLogo} inheritViewBox sx={{ height: '24px', width: '80px', ml: '-8px' }} />

<Typography variant="h6" mt={6} fontWeight={700}>
Create Account
Get started
</Typography>

<Typography mb={2} textAlign="center">
Choose how you would like to create your Safe Account
{wallet
? 'Open your existing Safe Accounts or create a new one'
: 'Connect your wallet to create a new Safe Account or open an existing one'}
</Typography>

<WalletLogin onLogin={continueToCreation} />

{isSocialLoginEnabled && (
Expand All @@ -49,15 +62,6 @@ const WelcomeLogin = () => {
<SocialSigner onLogin={continueToCreation} />
</>
)}

<Typography mt={2} textAlign="center">
Already have a Safe Account?
</Typography>
<Track {...LOAD_SAFE_EVENTS.LOAD_BUTTON}>
<Link color="primary" href={AppRoutes.newSafe.load}>
Add existing one
</Link>
</Track>
</Box>
</Paper>
)
Expand Down
3 changes: 2 additions & 1 deletion src/config/routes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
export const AppRoutes = {
'404': '/404',
_offline: '/_offline',
wc: '/wc',
terms: '/terms',
privacy: '/privacy',
Expand All @@ -11,6 +10,7 @@ export const AppRoutes = {
cookie: '/cookie',
addressBook: '/address-book',
addOwner: '/addOwner',
_offline: '/_offline',
apps: {
open: '/apps/open',
index: '/apps',
Expand Down Expand Up @@ -52,5 +52,6 @@ export const AppRoutes = {
welcome: {
socialLogin: '/welcome/social-login',
index: '/welcome',
accounts: '/welcome/accounts',
},
}
1 change: 1 addition & 0 deletions src/hooks/useIsSidebarRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const NO_SIDEBAR_ROUTES = [
AppRoutes.index,
AppRoutes.welcome.index,
AppRoutes.welcome.socialLogin,
AppRoutes.welcome.accounts,
AppRoutes.imprint,
AppRoutes.privacy,
AppRoutes.cookie,
Expand Down
17 changes: 17 additions & 0 deletions src/pages/welcome/accounts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { NextPage } from 'next'
import Head from 'next/head'
import MyAccounts from '@/components/welcome/MyAccounts'

const Accounts: NextPage = () => {
return (
<>
<Head>
<title>{'Safe{Wallet} – My accounts'}</title>
</Head>

<MyAccounts />
</>
)
}

export default Accounts
1 change: 1 addition & 0 deletions src/services/analytics/events/overview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,5 @@ export enum OPEN_SAFE_LABELS {
sidebar = 'sidebar',
after_create = 'after_create',
after_add = 'after_add',
login_page = 'login_page',
}

0 comments on commit 3573daf

Please sign in to comment.