diff --git a/cypress/e2e/pages/create_wallet.pages.js b/cypress/e2e/pages/create_wallet.pages.js index 4d458ddd43..77c4e945ef 100644 --- a/cypress/e2e/pages/create_wallet.pages.js +++ b/cypress/e2e/pages/create_wallet.pages.js @@ -10,7 +10,8 @@ const ownerAddress = 'input[name^="owners"][name$="address"]' const thresholdInput = 'input[name="threshold"]' export const removeOwnerBtn = 'button[aria-label="Remove owner"]' const connectingContainer = 'div[class*="connecting-container"]' -const createNewSafeBtn = 'span[data-track="create-safe: Continue to creation"]' +const createNewSafeBtn = '[data-testid="create-safe-btn"]' +const continueWithWalletBtn = 'span[data-track="create-safe: Continue to my accounts"]' const connectWalletBtn = 'Connect wallet' const googleConnectBtn = '[data-testid="google-connect-btn"]' const googleSignedinBtn = '[data-testid="signed-in-account-btn"]' @@ -110,6 +111,10 @@ export function clickOnCreateNewSafeBtn() { cy.get(createNewSafeBtn).click().wait(1000) } +export function clickOnContinueWithWalletBtn() { + cy.get(continueWithWalletBtn).click().wait(1000) +} + export function clickOnConnectWalletBtn() { cy.get(welcomeLoginScreen).within(() => { cy.get('button').contains(connectWalletBtn).should('be.visible').should('be.enabled').click().wait(1000) diff --git a/cypress/e2e/regression/create_safe_simple.cy.js b/cypress/e2e/regression/create_safe_simple.cy.js index f8627e89d9..327179d93e 100644 --- a/cypress/e2e/regression/create_safe_simple.cy.js +++ b/cypress/e2e/regression/create_safe_simple.cy.js @@ -14,6 +14,7 @@ describe('Safe creation tests', () => { it('Verify Next button is disabled until switching to network is done', () => { owner.waitForConnectionStatus() createwallet.selectNetwork(constants.networks.ethereum) + createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() createwallet.checkNetworkChangeWarningMsg() createwallet.verifyNextBtnIsDisabled() @@ -24,6 +25,7 @@ describe('Safe creation tests', () => { // TODO: Check unit tests it('Verify error message is displayed if wallet name input exceeds 50 characters', () => { owner.waitForConnectionStatus() + createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() createwallet.typeWalletName(main.generateRandomString(51)) owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.exceedChars) @@ -34,6 +36,7 @@ describe('Safe creation tests', () => { // TODO: Check unit tests it('Verify there is no error message is displayed if wallet name input contains less than 50 characters', () => { owner.waitForConnectionStatus() + createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() createwallet.typeWalletName(main.generateRandomString(50)) owner.verifyValidWalletName(constants.addressBookErrrMsg.exceedChars) @@ -41,6 +44,7 @@ describe('Safe creation tests', () => { it('Verify current connected account is shown as default owner', () => { owner.waitForConnectionStatus() + createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() createwallet.clickOnNextBtn() owner.verifyExistingOwnerAddress(0, constants.DEFAULT_OWNER_ADDRESS) @@ -49,6 +53,7 @@ describe('Safe creation tests', () => { // TODO: Check unit tests it('Verify error message is displayed if owner name input exceeds 50 characters', () => { owner.waitForConnectionStatus() + createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() owner.typeExistingOwnerName(main.generateRandomString(51)) owner.verifyErrorMsgInvalidAddress(constants.addressBookErrrMsg.exceedChars) @@ -57,6 +62,7 @@ describe('Safe creation tests', () => { // TODO: Check unit tests it('Verify there is no error message is displayed if owner name input contains less than 50 characters', () => { owner.waitForConnectionStatus() + createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() owner.typeExistingOwnerName(main.generateRandomString(50)) owner.verifyValidWalletName(constants.addressBookErrrMsg.exceedChars) @@ -65,6 +71,7 @@ describe('Safe creation tests', () => { it('Verify data persistence', () => { const ownerName = 'David' owner.waitForConnectionStatus() + createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() createwallet.clickOnNextBtn() createwallet.clickOnAddNewOwnerBtn() @@ -97,6 +104,7 @@ describe('Safe creation tests', () => { it('Verify tip is displayed on right side for threshold 1/1', () => { owner.waitForConnectionStatus() + createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() createwallet.clickOnNextBtn() createwallet.verifyPolicy1_1() @@ -105,6 +113,7 @@ describe('Safe creation tests', () => { // TODO: Check unit tests it('Verify address input validation rules', () => { owner.waitForConnectionStatus() + createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() createwallet.clickOnNextBtn() createwallet.clickOnAddNewOwnerBtn() diff --git a/cypress/e2e/safe-apps/tx-builder.spec.cy.js b/cypress/e2e/safe-apps/tx-builder.spec.cy.js index c021504610..4179b8bf46 100644 --- a/cypress/e2e/safe-apps/tx-builder.spec.cy.js +++ b/cypress/e2e/safe-apps/tx-builder.spec.cy.js @@ -197,7 +197,7 @@ describe('Transaction Builder tests', { defaultCommandTimeout: 20000 }, () => { }) }) - it('Verify a batch cannot be created without asset amount', () => { + it.skip('Verify a batch cannot be created without asset amount', () => { cy.enter(iframeSelector).then((getBody) => { getBody().findByLabelText(safeapps.enterAddressStr).type(constants.SEPOLIA_TEST_SAFE_10) getBody().findByText(safeapps.addTransactionStr).click() @@ -266,7 +266,7 @@ describe('Transaction Builder tests', { defaultCommandTimeout: 20000 }, () => { }) }) - it('Verify a valid batch as successful can be simulated', () => { + it.skip('Verify a valid batch as successful can be simulated', () => { cy.enter(iframeSelector).then((getBody) => { getBody().findByLabelText(safeapps.enterAddressStr).type(constants.SEPOLIA_TEST_SAFE_10) getBody().findByLabelText(safeapps.tokenAmount).type('0') @@ -278,7 +278,7 @@ describe('Transaction Builder tests', { defaultCommandTimeout: 20000 }, () => { }) }) - it('Verify an invalid batch as failed can be simulated', () => { + it.skip('Verify an invalid batch as failed can be simulated', () => { cy.enter(iframeSelector).then((getBody) => { getBody().findByLabelText(safeapps.enterAddressStr).type(constants.SEPOLIA_TEST_SAFE_10) getBody().findByLabelText(safeapps.tokenAmount).type('100') diff --git a/cypress/e2e/smoke/create_safe_simple.cy.js b/cypress/e2e/smoke/create_safe_simple.cy.js index 3578417fce..54e1491739 100644 --- a/cypress/e2e/smoke/create_safe_simple.cy.js +++ b/cypress/e2e/smoke/create_safe_simple.cy.js @@ -11,6 +11,7 @@ describe('[SMOKE] Safe creation tests', () => { main.acceptCookies() }) it('[SMOKE] Verify a Wallet can be connected', () => { + createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() owner.clickOnWalletExpandMoreIcon() owner.clickOnDisconnectBtn() @@ -20,12 +21,14 @@ describe('[SMOKE] Safe creation tests', () => { it('[SMOKE] Verify that a new Wallet has default name related to the selected network', () => { owner.waitForConnectionStatus() + createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() createwallet.verifyDefaultWalletName(createwallet.defaltSepoliaPlaceholder) }) it('[SMOKE] Verify Add and Remove Owner Row works as expected', () => { owner.waitForConnectionStatus() + createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() createwallet.clickOnNextBtn() createwallet.clickOnAddNewOwnerBtn() @@ -40,6 +43,7 @@ describe('[SMOKE] Safe creation tests', () => { it('[SMOKE] Verify Threshold Setup', () => { owner.waitForConnectionStatus() + createwallet.clickOnContinueWithWalletBtn() createwallet.clickOnCreateNewSafeBtn() createwallet.clickOnNextBtn() createwallet.clickOnAddNewOwnerBtn() diff --git a/src/components/common/ChainIndicator/index.tsx b/src/components/common/ChainIndicator/index.tsx index ab84e47a71..f113146f16 100644 --- a/src/components/common/ChainIndicator/index.tsx +++ b/src/components/common/ChainIndicator/index.tsx @@ -15,6 +15,7 @@ type ChainIndicatorProps = { className?: string showUnknown?: boolean showLogo?: boolean + responsive?: boolean } const fallbackChainConfig = { @@ -32,6 +33,7 @@ const ChainIndicator = ({ inline = false, showUnknown = true, showLogo = true, + responsive = false, }: ChainIndicatorProps): ReactElement | null => { const currentChainId = useChainId() const id = chainId || currentChainId @@ -56,7 +58,12 @@ const ChainIndicator = ({ {showLogo && ( )} - {chainConfig.chainName} + {chainConfig.chainName} ) : null } diff --git a/src/components/common/ChainIndicator/styles.module.css b/src/components/common/ChainIndicator/styles.module.css index fee2c496dc..42f18a2504 100644 --- a/src/components/common/ChainIndicator/styles.module.css +++ b/src/components/common/ChainIndicator/styles.module.css @@ -1,9 +1,9 @@ .indicator { display: flex; align-items: center; - justify-content: center; min-width: 70px; font-size: 12px; + justify-content: center; } .inlineIndicator { @@ -21,5 +21,16 @@ align-items: center; gap: var(--space-1); padding: 0; + min-width: 115px; font-size: 14px; + justify-content: start; +} + +@media (max-width: 899.95px) { + .responsive .name { + display: none; + } +.indicator { + min-width: 35px; +} } diff --git a/src/components/common/EthHashInfo/index.tsx b/src/components/common/EthHashInfo/index.tsx index 7d6821f2ba..18559af22e 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 && props.hasExplorer ? getBlockExplorerLink(chain, props.address) : undefined const name = showName ? addressBook[props.address] || props.name : undefined return ( diff --git a/src/components/common/NetworkSelector/index.tsx b/src/components/common/NetworkSelector/index.tsx index 60d45f68bf..34981d95fc 100644 --- a/src/components/common/NetworkSelector/index.tsx +++ b/src/components/common/NetworkSelector/index.tsx @@ -19,8 +19,6 @@ import useWallet from '@/hooks/wallets/useWallet' import { isSocialWalletEnabled } from '@/hooks/wallets/wallets' import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule' -const keepPathRoutes = [AppRoutes.welcome.index, AppRoutes.newSafe.create, AppRoutes.newSafe.load] - const MenuWithTooltip = forwardRef(function MenuWithTooltip(props: any, ref) { return ( @@ -43,10 +41,10 @@ const NetworkSelector = (props: { onChainSelect?: () => void }): ReactElement => const getNetworkLink = useCallback( (shortName: string) => { - const shouldKeepPath = keepPathRoutes.includes(router.pathname) + const shouldKeepPath = !router.query.safe const route = { - pathname: shouldKeepPath ? router.pathname : '/', + pathname: shouldKeepPath ? router.pathname : AppRoutes.index, query: { chain: shortName, } as { diff --git a/src/components/welcome/MyAccounts/AccountItem.tsx b/src/components/welcome/MyAccounts/AccountItem.tsx new file mode 100644 index 0000000000..06ef5412b3 --- /dev/null +++ b/src/components/welcome/MyAccounts/AccountItem.tsx @@ -0,0 +1,66 @@ +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' +import SafeListContextMenu from '@/components/sidebar/SafeListContextMenu' + +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..6cef3dae4e --- /dev/null +++ b/src/components/welcome/MyAccounts/CreateButton.tsx @@ -0,0 +1,28 @@ +import { Button } from '@mui/material' +import Link from 'next/link' +import { AppRoutes } from '@/config/routes' +import { OVERVIEW_EVENTS, trackEvent } from '@/services/analytics' + +const buttonSx = { width: ['100%', 'auto'] } + +const onClick = () => { + trackEvent(OVERVIEW_EVENTS.CREATE_NEW_SAFE) +} + +const CreateButton = () => ( + + + +) + +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..2a19f8d582 --- /dev/null +++ b/src/components/welcome/MyAccounts/index.tsx @@ -0,0 +1,77 @@ +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' +import Track from '@/components/common/Track' +import { OVERVIEW_EVENTS } from '@/services/analytics' +import { DataWidget } from '@/components/welcome/SafeListDrawer/DataWidget' +import css from './styles.module.css' + +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.length ? ( + shownSafes.map((item) => ) + ) : ( + + You don't have any Safe Accounts yet + + )} + + {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..df7742fec5 --- /dev/null +++ b/src/components/welcome/MyAccounts/styles.module.css @@ -0,0 +1,37 @@ +.container { + width: 600px; +} + +.listItem { + 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); +} + +.listItem > :first-child { + flex: 1; +} + +.safeLink { + display: flex; + align-items: center; + padding-right: var(--space-1); +} + +.safeAddress { + flex: 1; + white-space: nowrap; + padding-left: var(--space-2); +} + +@media (max-width: 899.95px) { + .container { + width: auto; + } + + .safeLink { + padding-right: 0; + } +} diff --git a/src/components/welcome/MyAccounts/useAllOwnedSafes.ts b/src/components/welcome/MyAccounts/useAllOwnedSafes.ts new file mode 100644 index 0000000000..023bc00b91 --- /dev/null +++ b/src/components/welcome/MyAccounts/useAllOwnedSafes.ts @@ -0,0 +1,11 @@ +import { getAllOwnedSafes } from '@safe-global/safe-gateway-typescript-sdk' +import useAsync from '@/hooks/useAsync' + +const useAllOwnedSafes = (address: string) => { + 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..4b064818c3 --- /dev/null +++ b/src/components/welcome/MyAccounts/useAllSafes.ts @@ -0,0 +1,57 @@ +import { useMemo } from 'react' +import uniq from 'lodash/uniq' +import isEmpty from 'lodash/isEmpty' +import { useAppSelector } from '@/store' +import { selectAllAddedSafes } from '@/store/addedSafesSlice' +import useAllOwnedSafes from './useAllOwnedSafes' +import useChains from '@/hooks/useChains' +import useChainId from '@/hooks/useChainId' +import useWallet from '@/hooks/wallets/useWallet' + +export type SafeItems = Array<{ + chainId: string + address: string + threshold?: number + owners?: number +}> + +const useAddedSafes = () => { + const allAdded = useAppSelector(selectAllAddedSafes) + return allAdded +} + +export const useHasSafes = () => { + const { address = '' } = useWallet() || {} + const allAdded = useAddedSafes() + const hasAdded = isEmpty(allAdded) + const [allOwned] = useAllOwnedSafes(!hasAdded ? address : '') // pass an empty string to not fetch owned safes + return hasAdded || !isEmpty(allOwned) +} + +const useAllSafes = (): SafeItems => { + const { address = '' } = useWallet() || {} + const [allOwned = {}] = useAllOwnedSafes(address) + 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/WalletLogin.tsx b/src/components/welcome/WelcomeLogin/WalletLogin.tsx index 8add385375..fa8138974f 100644 --- a/src/components/welcome/WelcomeLogin/WalletLogin.tsx +++ b/src/components/welcome/WelcomeLogin/WalletLogin.tsx @@ -2,9 +2,9 @@ import useConnectWallet from '@/components/common/ConnectWallet/useConnectWallet import Track from '@/components/common/Track' import useWallet from '@/hooks/wallets/useWallet' import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule' -import { CREATE_SAFE_EVENTS } from '@/services/analytics' import { Box, Button, Typography } from '@mui/material' import EthHashInfo from '@/components/common/EthHashInfo' +import { CREATE_SAFE_EVENTS } from '@/services/analytics' const WalletLogin = ({ onLogin }: { onLogin: () => void }) => { const wallet = useWallet() @@ -15,7 +15,7 @@ const WalletLogin = ({ onLogin }: { onLogin: () => void }) => { if (wallet !== null && !isSocialLogin) { return ( - +