diff --git a/src/components/common/EthHashInfo/index.tsx b/src/components/common/EthHashInfo/index.tsx index 7d6821f2ba..f5f66a60aa 100644 --- a/src/components/common/EthHashInfo/index.tsx +++ b/src/components/common/EthHashInfo/index.tsx @@ -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 ( diff --git a/src/components/welcome/MyAccounts/AccountItem.tsx b/src/components/welcome/MyAccounts/AccountItem.tsx new file mode 100644 index 0000000000..21b0ecc271 --- /dev/null +++ b/src/components/welcome/MyAccounts/AccountItem.tsx @@ -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 ( + + + + + + + {name && ( + + {name} + + )} + {chain?.shortName}: + + {shortenAddress(address)} + + + + + + + + + + ) +} + +export default AccountItem diff --git a/src/components/welcome/MyAccounts/CreateButton.tsx b/src/components/welcome/MyAccounts/CreateButton.tsx new file mode 100644 index 0000000000..fe7112f511 --- /dev/null +++ b/src/components/welcome/MyAccounts/CreateButton.tsx @@ -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 ( + + + + ) +} + +export default CreateButton diff --git a/src/components/welcome/MyAccounts/index.tsx b/src/components/welcome/MyAccounts/index.tsx new file mode 100644 index 0000000000..b4fe580e97 --- /dev/null +++ b/src/components/welcome/MyAccounts/index.tsx @@ -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(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 ( + + + + + My Safe accounts + + {' '} + ({safes.length}) + + + + + + + + {shownSafes.map((item) => ( + + ))} + + {safes.length > shownSafes.length && ( + + + + )} + + + + ) +} + +const MyAccounts = madProps(AccountsList, { + safes: useAllSafes, +}) + +export default MyAccounts diff --git a/src/components/welcome/MyAccounts/styles.module.css b/src/components/welcome/MyAccounts/styles.module.css new file mode 100644 index 0000000000..a391717b4c --- /dev/null +++ b/src/components/welcome/MyAccounts/styles.module.css @@ -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); +} diff --git a/src/components/welcome/MyAccounts/useAllOwnedSafes.ts b/src/components/welcome/MyAccounts/useAllOwnedSafes.ts new file mode 100644 index 0000000000..4762bad957 --- /dev/null +++ b/src/components/welcome/MyAccounts/useAllOwnedSafes.ts @@ -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 diff --git a/src/components/welcome/MyAccounts/useAllSafes.ts b/src/components/welcome/MyAccounts/useAllSafes.ts new file mode 100644 index 0000000000..ad4df13909 --- /dev/null +++ b/src/components/welcome/MyAccounts/useAllSafes.ts @@ -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(() => { + 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 diff --git a/src/components/welcome/WelcomeLogin/index.tsx b/src/components/welcome/WelcomeLogin/index.tsx index 7cdb42ae16..9d3bbc7cb4 100644 --- a/src/components/welcome/WelcomeLogin/index.tsx +++ b/src/components/welcome/WelcomeLogin/index.tsx @@ -1,15 +1,18 @@ +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: () => , @@ -17,11 +20,18 @@ const SocialSigner = dynamic(() => import('@/components/common/SocialSigner'), { 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 ( @@ -30,12 +40,15 @@ const WelcomeLogin = () => { - Create Account + Get started - 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'} + {isSocialLoginEnabled && ( @@ -49,15 +62,6 @@ const WelcomeLogin = () => { )} - - - Already have a Safe Account? - - - - Add existing one - - ) diff --git a/src/config/routes.ts b/src/config/routes.ts index 4ea91c3249..fbc4c0f96e 100644 --- a/src/config/routes.ts +++ b/src/config/routes.ts @@ -1,6 +1,5 @@ export const AppRoutes = { '404': '/404', - _offline: '/_offline', wc: '/wc', terms: '/terms', privacy: '/privacy', @@ -11,6 +10,7 @@ export const AppRoutes = { cookie: '/cookie', addressBook: '/address-book', addOwner: '/addOwner', + _offline: '/_offline', apps: { open: '/apps/open', index: '/apps', @@ -52,5 +52,6 @@ export const AppRoutes = { welcome: { socialLogin: '/welcome/social-login', index: '/welcome', + accounts: '/welcome/accounts', }, } diff --git a/src/hooks/useIsSidebarRoute.ts b/src/hooks/useIsSidebarRoute.ts index 287fc689cd..1b89b2f606 100644 --- a/src/hooks/useIsSidebarRoute.ts +++ b/src/hooks/useIsSidebarRoute.ts @@ -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, diff --git a/src/pages/welcome/accounts.tsx b/src/pages/welcome/accounts.tsx new file mode 100644 index 0000000000..b85f0a906e --- /dev/null +++ b/src/pages/welcome/accounts.tsx @@ -0,0 +1,17 @@ +import type { NextPage } from 'next' +import Head from 'next/head' +import MyAccounts from '@/components/welcome/MyAccounts' + +const Accounts: NextPage = () => { + return ( + <> + + {'Safe{Wallet} – My accounts'} + + + + + ) +} + +export default Accounts diff --git a/src/services/analytics/events/overview.ts b/src/services/analytics/events/overview.ts index 514a3b7286..4d5ed9030b 100644 --- a/src/services/analytics/events/overview.ts +++ b/src/services/analytics/events/overview.ts @@ -114,4 +114,5 @@ export enum OPEN_SAFE_LABELS { sidebar = 'sidebar', after_create = 'after_create', after_add = 'after_add', + login_page = 'login_page', }