diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index 66ca16f382..139049865c 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -51,21 +51,17 @@ jobs: # Update the GitHub release with a checksummed archive - name: Upload archive - uses: actions/upload-release-asset@v1 + - uses: Shopify/upload-to-release@v2.0.0 with: - upload_url: ${{ github.event.release.upload_url }} - asset_path: ${{ env.ARCHIVE_NAME }}.tar.gz - asset_name: ${{ env.ARCHIVE_NAME }}.tar.gz - asset_content_type: application/gzip - env: - GITHUB_TOKEN: ${{ github.token }} + path: ${{ env.ARCHIVE_NAME }}.tar.gz + name: ${{ env.ARCHIVE_NAME }}.tar.gz + content-type: application/gzip + repo-token: ${{ github.token }} - name: Upload checksum - uses: actions/upload-release-asset@v1 + uses: Shopify/upload-to-release@v2.0.0 with: - upload_url: ${{ github.event.release.upload_url }} - asset_path: ${{ env.ARCHIVE_NAME }}-sha256-checksum.txt - asset_name: ${{ env.ARCHIVE_NAME }}-sha256-checksum.txt - asset_content_type: text/plain - env: - GITHUB_TOKEN: ${{ github.token }} + path: ${{ env.ARCHIVE_NAME }}-sha256-checksum.txt + name: ${{ env.ARCHIVE_NAME }}-sha256-checksum.txt + content-type: text/plain + repo-token: ${{ github.token }} diff --git a/README.md b/README.md index 6e426486de..9a2b592618 100644 --- a/README.md +++ b/README.md @@ -43,8 +43,6 @@ Here's the list of all the environment variables: | `NEXT_PUBLIC_FIREBASE_VAPID_KEY_PRODUCTION` | FCM vapid key on production | `NEXT_PUBLIC_FIREBASE_OPTIONS_STAGING` | FCM `initializeApp` options on staging | `NEXT_PUBLIC_FIREBASE_VAPID_KEY_STAGING` | FCM vapid key on staging -| `NEXT_PUBLIC_SOCIAL_WALLET_OPTIONS_PRODUCTION` | Web3Auth and Google credentials (production) -| `NEXT_PUBLIC_SOCIAL_WALLET_OPTIONS_STAGING` | Web3Auth and Google credentials (staging) | `NEXT_PUBLIC_SPINDL_SDK_KEY` | [Spindl](http://spindl.xyz) SDK key If you don't provide some of the variables, the corresponding features will be disabled in the UI. diff --git a/cypress/e2e/pages/create_wallet.pages.js b/cypress/e2e/pages/create_wallet.pages.js index ba859cbb02..342330fa96 100644 --- a/cypress/e2e/pages/create_wallet.pages.js +++ b/cypress/e2e/pages/create_wallet.pages.js @@ -87,22 +87,6 @@ export function verifySponsorMessageIsPresent() { cy.get(networkFeeSection).contains(sponsorStr).should('exist') } -export function verifyGoogleConnectBtnIsDisabled() { - cy.get(googleConnectBtn).should('be.disabled') -} - -export function verifyGoogleConnectBtnIsEnabled() { - cy.get(googleConnectBtn).should('not.be.disabled') -} - -export function verifyGoogleSignin() { - return cy.get(googleSignedinBtn).should('exist') -} - -export function verifyGoogleAccountInfoInHeader() { - return cy.get(accountInfoHeader).should('exist') -} - export function verifyPolicy1_1() { cy.contains(policy1_2).should('exist') // TOD: Need data-cy for containers diff --git a/cypress/e2e/pages/owners.pages.js b/cypress/e2e/pages/owners.pages.js index 9f1a38a52d..1f6a142fc1 100644 --- a/cypress/e2e/pages/owners.pages.js +++ b/cypress/e2e/pages/owners.pages.js @@ -242,5 +242,5 @@ export function verifyThreshold(startValue, endValue) { cy.get(thresholdInput).parent().click() cy.get(thresholdList).contains(endValue).should('be.visible') cy.get(thresholdList).find('li').should('have.length', endValue) - cy.get('body').click() + cy.get('body').click({ force: true }) } diff --git a/cypress/e2e/regression/create_safe_google.cy.js b/cypress/e2e/regression/create_safe_google.cy.js deleted file mode 100644 index 4a946f39ea..0000000000 --- a/cypress/e2e/regression/create_safe_google.cy.js +++ /dev/null @@ -1,53 +0,0 @@ -import * as constants from '../../support/constants' -import * as main from '../pages/main.page' -import * as createwallet from '../pages/create_wallet.pages' -import * as owner from '../pages/owners.pages' -import * as navigation from '../pages/navigation.page' - -describe('Safe creation Google tests', () => { - beforeEach(() => { - cy.visit(constants.welcomeUrl + '?chain=sep') - cy.clearLocalStorage() - main.acceptCookies() - // TODO: Need credentials to finish API Google login - // createwallet.loginGoogleAPI() - }) - - // TODO: Clarify requirements - it.skip('Verify that "Connect with Google" option is disabled for the networks without Relay on the Welcome page', () => { - owner.clickOnWalletExpandMoreIcon() - owner.clickOnDisconnectBtn() - createwallet.selectNetwork(constants.networks.polygon) - createwallet.verifyGoogleConnectBtnIsDisabled() - }) - - it.skip('Verify a successful connection with google', () => { - createwallet.verifyGoogleSignin() - }) - - it.skip('Verify Google account info in the header after account connection', () => { - createwallet.verifyGoogleAccountInfoInHeader() - }) - - it.skip('Verify a successful safe creation with a Google account', { defaultCommandTimeout: 90000 }, () => { - createwallet.verifyGoogleSignin().click() - createwallet.clickOnContinueWithWalletBtn() - createwallet.verifyOwnerInfoIsPresent() - createwallet.clickOnReviewStepNextBtn() - createwallet.verifySafeIsBeingCreated() - createwallet.verifySafeCreationIsComplete() - }) - - it.skip('Verify a successful transaction creation with Google account', { defaultCommandTimeout: 90000 }, () => { - createwallet.verifyGoogleSignin().click() - createwallet.clickOnContinueWithWalletBtn() - createwallet.clickOnReviewStepNextBtn() - createwallet.verifySafeCreationIsComplete() - navigation.clickOnSideNavigation(navigation.sideNavSettingsIcon) - owner.openAddOwnerWindow() - owner.typeOwnerAddress(constants.SEPOLIA_OWNER_2) - owner.clickOnNextBtn() - main.clickOnExecuteBtn() - owner.verifyOwnerTransactionComplted() - }) -}) diff --git a/cypress/support/constants.js b/cypress/support/constants.js index 1b9fec42e6..8b883c0224 100644 --- a/cypress/support/constants.js +++ b/cypress/support/constants.js @@ -62,7 +62,7 @@ export const getPermissionsUrl = '/get-permissions' export const appSettingsUrl = '/settings/safe-apps' export const setupUrl = '/settings/setup?safe=' export const dataSettingsUrl = '/settings/data?safe=' -export const securityUrl = '/settings/security-login?safe=' +export const securityUrl = '/settings/setup?safe=' export const invalidAppUrl = 'https://my-invalid-custom-app.com/manifest.json' export const validAppUrlJson = 'https://my-valid-custom-app.com/manifest.json' export const validAppUrl = 'https://my-valid-custom-app.com' diff --git a/package.json b/package.json index 7e7a7dce68..6c3006ceb7 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "safe-wallet-web", "homepage": "https://github.com/safe-global/safe-wallet-web", "license": "GPL-3.0", - "version": "1.36.5", + "version": "1.37.0", "type": "module", "scripts": { "dev": "next dev", @@ -62,7 +62,6 @@ "@safe-global/safe-modules-deployments": "^1.2.0", "@sentry/react": "^7.91.0", "@spindl-xyz/attribution-lite": "^1.4.0", - "@tkey-mpc/common-types": "^8.2.2", "@truffle/hdwallet-provider": "^2.1.4", "@walletconnect/utils": "^2.13.1", "@walletconnect/web3wallet": "^1.12.1", @@ -73,20 +72,17 @@ "@web3-onboard/ledger": "2.3.2", "@web3-onboard/trezor": "^2.4.2", "@web3-onboard/walletconnect": "^2.5.4", - "@web3auth/mpc-core-kit": "^1.1.3", "blo": "^1.1.1", - "bn.js": "^5.2.1", "classnames": "^2.3.1", "date-fns": "^2.30.0", "ethers": "^6.11.1", "exponential-backoff": "^3.1.0", "firebase": "^10.3.1", - "framer-motion": "^10.13.1", "fuse.js": "^6.6.2", "idb-keyval": "^6.2.1", "js-cookie": "^3.0.1", "lodash": "^4.17.21", - "next": "^14.1.0", + "next": "^14.1.1", "papaparse": "^5.3.2", "qrcode.react": "^3.1.0", "react": "^18.2.0", diff --git a/src/components/balances/AssetsHeader/index.tsx b/src/components/balances/AssetsHeader/index.tsx index 6709eb94ac..69a563a2a7 100644 --- a/src/components/balances/AssetsHeader/index.tsx +++ b/src/components/balances/AssetsHeader/index.tsx @@ -1,19 +1,24 @@ -import type { ReactElement, ReactNode } from 'react' +import { useMemo, type ReactElement, type ReactNode } from 'react' import NavTabs from '@/components/common/NavTabs' import PageHeader from '@/components/common/PageHeader' import { balancesNavItems } from '@/components/sidebar/SidebarNavigation/config' import css from '@/components/common/PageHeader/styles.module.css' +import { useCurrentChain } from '@/hooks/useChains' +import { isRouteEnabled } from '@/utils/chains' const AssetsHeader = ({ children }: { children?: ReactNode }): ReactElement => { + const chain = useCurrentChain() + const navItems = useMemo(() => balancesNavItems.filter((item) => isRouteEnabled(item.href, chain)), [chain]) + return (
- +
{children &&
{children}
} diff --git a/src/components/batch/BatchSidebar/BatchReorder.tsx b/src/components/batch/BatchSidebar/BatchReorder.tsx deleted file mode 100644 index 1f7ee41fde..0000000000 --- a/src/components/batch/BatchSidebar/BatchReorder.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Reorder } from 'framer-motion' -import type { DraftBatchItem } from '@/store/batchSlice' -import BatchTxItem from './BatchTxItem' -import { useState } from 'react' - -const BatchReorder = ({ - txItems, - onDelete, - onReorder, -}: { - txItems: DraftBatchItem[] - onDelete?: (id: string) => void - onReorder: (items: DraftBatchItem[]) => void -}) => { - const [dragging, setDragging] = useState(false) - - return ( - - {txItems.map((item, index) => ( - setDragging(true)} - onDragEnd={() => setDragging(false)} - > - - - ))} - - ) -} - -export default BatchReorder diff --git a/src/components/batch/BatchSidebar/BatchTxItem.tsx b/src/components/batch/BatchSidebar/BatchTxItem.tsx index e9161b4def..a9b27f44ca 100644 --- a/src/components/batch/BatchSidebar/BatchTxItem.tsx +++ b/src/components/batch/BatchSidebar/BatchTxItem.tsx @@ -6,7 +6,6 @@ import { type DraftBatchItem } from '@/store/batchSlice' import TxType from '@/components/transactions/TxType' import TxInfo from '@/components/transactions/TxInfo' import DeleteIcon from '@/public/images/common/delete.svg' -import DragIcon from '@/public/images/common/drag.svg' import TxData from '@/components/transactions/TxDetails/TxData' import { MethodDetails } from '@/components/transactions/TxDetails/TxData/DecodedData/MethodDetails' import { TxDataRow } from '@/components/transactions/TxDetails/Summary/TxDataRow' @@ -17,19 +16,9 @@ type BatchTxItemProps = DraftBatchItem & { id: string count: number onDelete?: (id: string) => void - draggable?: boolean - dragging?: boolean } -const BatchTxItem = ({ - id, - count, - timestamp, - txDetails, - onDelete, - dragging = false, - draggable = false, -}: BatchTxItemProps) => { +const BatchTxItem = ({ id, count, timestamp, txDetails, onDelete }: BatchTxItemProps) => { const txSummary = useMemo( () => ({ timestamp, @@ -61,18 +50,8 @@ const BatchTxItem = ({
{count}
- } disabled={dragging} className={css.accordion}> + } className={css.accordion}> - {draggable && ( - e.stopPropagation()} - /> - )} - diff --git a/src/components/batch/BatchSidebar/index.tsx b/src/components/batch/BatchSidebar/index.tsx index 5c882a2e7f..d1f89ac6d2 100644 --- a/src/components/batch/BatchSidebar/index.tsx +++ b/src/components/batch/BatchSidebar/index.tsx @@ -1,6 +1,5 @@ import { type SyntheticEvent, useEffect } from 'react' import { useCallback, useContext } from 'react' -import dynamic from 'next/dynamic' import { Button, Divider, Drawer, IconButton, SvgIcon, Typography } from '@mui/material' import CloseIcon from '@mui/icons-material/Close' import { useDraftBatch, useUpdateBatch } from '@/hooks/useDraftBatch' @@ -13,13 +12,12 @@ import { BATCH_EVENTS } from '@/services/analytics' import CheckWallet from '@/components/common/CheckWallet' import PlusIcon from '@/public/images/common/plus.svg' import EmptyBatch from './EmptyBatch' - -const BatchReorder = dynamic(() => import('./BatchReorder')) +import BatchTxList from './BatchTxList' const BatchSidebar = ({ isOpen, onToggle }: { isOpen: boolean; onToggle: (open: boolean) => void }) => { const { txFlow, setTxFlow } = useContext(TxModalContext) const batchTxs = useDraftBatch() - const [, deleteTx, onReorder] = useUpdateBatch() + const [, deleteTx] = useUpdateBatch() const closeSidebar = useCallback(() => { onToggle(false) @@ -73,7 +71,7 @@ const BatchSidebar = ({ isOpen, onToggle }: { isOpen: boolean; onToggle: (open: {batchTxs.length ? ( <>
- +
diff --git a/src/components/common/ConnectWallet/ConnectionCenter.tsx b/src/components/common/ConnectWallet/ConnectionCenter.tsx index 1af6af79d8..9c81d6f3c0 100644 --- a/src/components/common/ConnectWallet/ConnectionCenter.tsx +++ b/src/components/common/ConnectWallet/ConnectionCenter.tsx @@ -1,80 +1,14 @@ import ConnectWalletButton from '@/components/common/ConnectWallet/ConnectWalletButton' -import { useHasFeature } from '@/hooks/useChains' -import { FEATURES } from '@/utils/chains' -import madProps from '@/utils/mad-props' -import { Popover, ButtonBase, Typography, Paper, Box } from '@mui/material' -import ExpandMoreIcon from '@mui/icons-material/ExpandMore' -import ExpandLessIcon from '@mui/icons-material/ExpandLess' -import classnames from 'classnames' -import { useState, type MouseEvent, type ReactElement } from 'react' - -import KeyholeIcon from '@/components/common/icons/KeyholeIcon' -import WalletDetails from '@/components/common/ConnectWallet/WalletDetails' - +import { Box } from '@mui/material' +import { type ReactElement } from 'react' import css from '@/components/common/ConnectWallet/styles.module.css' -export const ConnectionCenter = ({ isSocialLoginEnabled }: { isSocialLoginEnabled: boolean }): ReactElement => { - const [anchorEl, setAnchorEl] = useState(null) - const open = !!anchorEl - - const handleClick = (event: MouseEvent) => { - setAnchorEl(event.currentTarget) - } - - const handleClose = () => { - setAnchorEl(null) - } - - const ExpandIcon = open ? ExpandLessIcon : ExpandMoreIcon - - if (!isSocialLoginEnabled) { - return ( - - - - ) - } - +const ConnectionCenter = (): ReactElement => { return ( - <> - - - - - Not connected - palette.error.main }}> - Connect wallet - - - - - - - - - - - - + + + ) } -const useIsSocialLoginEnabled = () => useHasFeature(FEATURES.SOCIAL_LOGIN) - -export default madProps(ConnectionCenter, { - isSocialLoginEnabled: useIsSocialLoginEnabled, -}) +export default ConnectionCenter diff --git a/src/components/common/ConnectWallet/WalletDetails.tsx b/src/components/common/ConnectWallet/WalletDetails.tsx index 414a93a4e2..8bab108de3 100644 --- a/src/components/common/ConnectWallet/WalletDetails.tsx +++ b/src/components/common/ConnectWallet/WalletDetails.tsx @@ -1,13 +1,8 @@ -import { Box, Divider, Skeleton, SvgIcon, Typography } from '@mui/material' -import dynamic from 'next/dynamic' +import { Box, Divider, SvgIcon, Typography } from '@mui/material' import type { ReactElement } from 'react' import LockIcon from '@/public/images/common/lock.svg' -const SocialSigner = dynamic(() => import('@/components/common/SocialSigner'), { - loading: () => , -}) - import WalletLogin from '@/components/welcome/WelcomeLogin/WalletLogin' const WalletDetails = ({ onConnect }: { onConnect: () => void }): ReactElement => { @@ -26,8 +21,6 @@ const WalletDetails = ({ onConnect }: { onConnect: () => void }): ReactElement = or - - ) } diff --git a/src/components/common/ConnectWallet/__tests__/ConnectionCenter.test.tsx b/src/components/common/ConnectWallet/__tests__/ConnectionCenter.test.tsx index 5ca16f4512..844eb8a112 100644 --- a/src/components/common/ConnectWallet/__tests__/ConnectionCenter.test.tsx +++ b/src/components/common/ConnectWallet/__tests__/ConnectionCenter.test.tsx @@ -1,16 +1,9 @@ -import { ConnectionCenter } from '@/components/common/ConnectWallet/ConnectionCenter' +import ConnectionCenter from '@/components/common/ConnectWallet/ConnectionCenter' import { render } from '@/tests/test-utils' describe('ConnectionCenter', () => { - it('displays a Connect wallet button if the social login feature is enabled', () => { - const { getByText, queryByText } = render() - - expect(getByText('Connect wallet')).toBeInTheDocument() - expect(queryByText('Connect')).not.toBeInTheDocument() - }) - - it('displays the ConnectWalletButton if the social login feature is disabled', () => { - const { getByText, queryByText } = render() + it('displays the ConnectWalletButton', () => { + const { getByText, queryByText } = render() expect(queryByText('Connect wallet')).not.toBeInTheDocument() expect(getByText('Connect')).toBeInTheDocument() diff --git a/src/components/common/ConnectWallet/styles.module.css b/src/components/common/ConnectWallet/styles.module.css index dcff10dfc8..483c0bf6c5 100644 --- a/src/components/common/ConnectWallet/styles.module.css +++ b/src/components/common/ConnectWallet/styles.module.css @@ -80,10 +80,6 @@ } @media (max-width: 599.95px) { - .socialLoginInfo > div > div { - display: none; - } - .notConnected { display: none; } diff --git a/src/components/common/NetworkSelector/index.tsx b/src/components/common/NetworkSelector/index.tsx index 553cd6292f..960ddc4e79 100644 --- a/src/components/common/NetworkSelector/index.tsx +++ b/src/components/common/NetworkSelector/index.tsx @@ -15,12 +15,8 @@ import { type ReactElement, useMemo } from 'react' import { useCallback } from 'react' import { AppRoutes } from '@/config/routes' import { trackEvent, OVERVIEW_EVENTS } from '@/services/analytics' -import useWallet from '@/hooks/wallets/useWallet' -import { isSocialWalletEnabled } from '@/hooks/wallets/wallets' -import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule' const NetworkSelector = (props: { onChainSelect?: () => void }): ReactElement => { - const wallet = useWallet() const isDarkMode = useDarkMode() const theme = useTheme() const { configs } = useChains() @@ -64,24 +60,17 @@ const NetworkSelector = (props: { onChainSelect?: () => void }): ReactElement => } } - const isSocialLogin = isSocialLoginWallet(wallet?.label) - const renderMenuItem = useCallback( (value: string, chain: ChainInfo) => { return ( - + ) }, - [getNetworkLink, isSocialLogin, props.onChainSelect], + [getNetworkLink, props.onChainSelect], ) return configs.length ? ( diff --git a/src/components/common/PageLayout/index.tsx b/src/components/common/PageLayout/index.tsx index a3eac5d5a7..e0f00b8e4f 100644 --- a/src/components/common/PageLayout/index.tsx +++ b/src/components/common/PageLayout/index.tsx @@ -9,7 +9,6 @@ import SideDrawer from './SideDrawer' import { useIsSidebarRoute } from '@/hooks/useIsSidebarRoute' import { TxModalContext } from '@/components/tx-flow' import BatchSidebar from '@/components/batch/BatchSidebar' -import SocialLoginDeprecation from '@/components/common/SocialLoginDeprecation' const PageLayout = ({ pathname, children }: { pathname: string; children: ReactElement }): ReactElement => { const [isSidebarRoute, isAnimated] = useIsSidebarRoute(pathname) @@ -36,8 +35,6 @@ const PageLayout = ({ pathname, children }: { pathname: string; children: ReactE })} >
- - {children}
diff --git a/src/components/common/SocialLoginDeprecation/index.tsx b/src/components/common/SocialLoginDeprecation/index.tsx deleted file mode 100644 index e484a6b7c0..0000000000 --- a/src/components/common/SocialLoginDeprecation/index.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Alert } from '@mui/material' -import Link from 'next/link' -import useWallet from '@/hooks/wallets/useWallet' -import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule' -import { AppRoutes } from '@/config/routes' -import { useRouter } from 'next/router' - -const SocialLoginDeprecation = () => { - const router = useRouter() - const wallet = useWallet() - const isSocialLogin = isSocialLoginWallet(wallet?.label) - - if (!isSocialLogin) return null - - const ownersPage = { - pathname: AppRoutes.settings.setup, - query: router.query, - } - - const settingsPage = { - pathname: AppRoutes.settings.securityLogin, - query: router.query, - } - - return ( - - The Social Login wallet is deprecated and will be removed on 01.05.2024. -
- Please{' '} - - - swap the signer - - {' '} - to a different wallet, or{' '} - - - export your private key - - {' '} - to avoid losing access to your Safe Account. -
- ) -} - -export default SocialLoginDeprecation diff --git a/src/components/common/SocialLoginInfo/index.tsx b/src/components/common/SocialLoginInfo/index.tsx deleted file mode 100644 index 00423fccba..0000000000 --- a/src/components/common/SocialLoginInfo/index.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import WalletBalance from '@/components/common/WalletBalance' -import { Badge, Box, Typography } from '@mui/material' -import css from './styles.module.css' -import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' -import { type ConnectedWallet } from '@/hooks/wallets/useOnboard' -import CopyAddressButton from '@/components/common/CopyAddressButton' -import ExplorerButton from '@/components/common/ExplorerButton' -import { getBlockExplorerLink } from '@/utils/chains' -import { useAppSelector } from '@/store' -import { selectSettings } from '@/store/settingsSlice' -import useSocialWallet from '@/hooks/wallets/mpc/useSocialWallet' - -const SocialLoginInfo = ({ - wallet, - chainInfo, - hideActions = false, - size = 28, - balance, - showBalance, -}: { - wallet: ConnectedWallet - chainInfo?: ChainInfo - hideActions?: boolean - size?: number - balance?: string - showBalance?: boolean -}) => { - const socialWalletService = useSocialWallet() - const userInfo = socialWalletService?.getUserInfo() - const prefix = chainInfo?.shortName - const link = chainInfo ? getBlockExplorerLink(chainInfo, wallet.address) : undefined - const settings = useAppSelector(selectSettings) - - if (!userInfo) return <> - - return ( - - - Profile Image - {!socialWalletService?.isMFAEnabled() && } - -
- - {userInfo.name} - - {showBalance ? ( - - - - ) : ( - - {userInfo.email} - - )} -
- {!hideActions && ( -
- - - - -
- )} -
- ) -} - -export default SocialLoginInfo diff --git a/src/components/common/SocialLoginInfo/styles.module.css b/src/components/common/SocialLoginInfo/styles.module.css deleted file mode 100644 index 0acd0bcdcf..0000000000 --- a/src/components/common/SocialLoginInfo/styles.module.css +++ /dev/null @@ -1,33 +0,0 @@ -.profileImg { - border-radius: var(--space-2); - display: block; -} - -.bubble { - content: ''; - position: absolute; - right: 3px; - bottom: 3px; -} - -.bubble span { - outline: 1px solid var(--color-background-paper); -} - -.profileData { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 2px; -} - -.text { - font-size: 12px; - line-height: 14px; -} - -.actionButtons { - display: flex; - justify-self: flex-end; - margin-left: auto; -} diff --git a/src/components/common/SocialSigner/PasswordRecovery.tsx b/src/components/common/SocialSigner/PasswordRecovery.tsx deleted file mode 100644 index d1f09b634d..0000000000 --- a/src/components/common/SocialSigner/PasswordRecovery.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { MPC_WALLET_EVENTS } from '@/services/analytics/events/mpcWallet' -import { - Typography, - FormControlLabel, - Checkbox, - Button, - Box, - Divider, - Grid, - LinearProgress, - FormControl, -} from '@mui/material' -import { useState } from 'react' -import Track from '@/components/common/Track' -import { FormProvider, useForm } from 'react-hook-form' -import PasswordInput from '@/components/settings/SecurityLogin/SocialSignerMFA/PasswordInput' -import ErrorMessage from '@/components/tx/ErrorMessage' - -import css from './styles.module.css' - -type PasswordFormData = { - password: string -} - -export const PasswordRecovery = ({ - recoverFactorWithPassword, - onSuccess, -}: { - recoverFactorWithPassword: (password: string, storeDeviceFactor: boolean) => Promise - onSuccess?: (() => void) | undefined -}) => { - const [storeDeviceFactor, setStoreDeviceFactor] = useState(false) - - const formMethods = useForm({ - mode: 'all', - defaultValues: { - password: '', - }, - }) - - const { handleSubmit, formState } = formMethods - - const [error, setError] = useState() - - const onSubmit = async (data: PasswordFormData) => { - setError(undefined) - try { - await recoverFactorWithPassword(data.password, storeDeviceFactor) - - onSuccess?.() - } catch (e) { - setError('Incorrect password') - } - } - - const isDisabled = formState.isSubmitting - - return ( - -
- - - - Verify your account - - - - - - Enter security password - - - This browser is not registered with your Account yet. Please enter your recovery password to restore - access to this Account. - - - - - - - - setStoreDeviceFactor((prev) => !prev)} /> - } - label="Do not ask again on this device" - /> - {error && {error}} - - - - - - - - - - - -
-
- ) -} diff --git a/src/components/common/SocialSigner/__tests__/PasswordRecovery.test.tsx b/src/components/common/SocialSigner/__tests__/PasswordRecovery.test.tsx deleted file mode 100644 index 488a33cccf..0000000000 --- a/src/components/common/SocialSigner/__tests__/PasswordRecovery.test.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { fireEvent, render } from '@/tests/test-utils' -import { PasswordRecovery } from '@/components/common/SocialSigner/PasswordRecovery' -import { act, waitFor } from '@testing-library/react' - -describe('PasswordRecovery', () => { - it('displays an error if password is wrong', async () => { - const mockRecoverWithPassword = jest.fn(() => Promise.reject()) - const mockOnSuccess = jest.fn() - - const { getByText, getByLabelText } = render( - , - ) - - const passwordField = getByLabelText('Recovery password') - const submitButton = getByText('Submit') - - act(() => { - fireEvent.change(passwordField, { target: { value: 'somethingwrong' } }) - submitButton.click() - }) - - await waitFor(() => { - expect(mockOnSuccess).not.toHaveBeenCalled() - expect(getByText('Incorrect password')).toBeInTheDocument() - }) - }) - - it('calls onSuccess if password is correct', async () => { - const mockRecoverWithPassword = jest.fn(() => Promise.resolve()) - const mockOnSuccess = jest.fn() - - const { getByText, getByLabelText } = render( - , - ) - - const passwordField = getByLabelText('Recovery password') - const submitButton = getByText('Submit') - - act(() => { - fireEvent.change(passwordField, { target: { value: 'somethingCorrect' } }) - submitButton.click() - }) - - await waitFor(() => { - expect(mockOnSuccess).toHaveBeenCalled() - }) - }) -}) diff --git a/src/components/common/SocialSigner/__tests__/SocialSignerLogin.test.tsx b/src/components/common/SocialSigner/__tests__/SocialSignerLogin.test.tsx deleted file mode 100644 index 6c3c33a710..0000000000 --- a/src/components/common/SocialSigner/__tests__/SocialSignerLogin.test.tsx +++ /dev/null @@ -1,172 +0,0 @@ -import { act, render, waitFor } from '@/tests/test-utils' - -import { SocialSigner, _getSupportedChains } from '@/components/common/SocialSigner' -import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/SocialLoginModule' -import { COREKIT_STATUS, type UserInfo, type Web3AuthMPCCoreKit } from '@web3auth/mpc-core-kit' -import SocialWalletService from '@/services/mpc/SocialWalletService' -import { TxModalProvider } from '@/components/tx-flow' -import { fireEvent } from '@testing-library/react' -import { type ISocialWalletService } from '@/services/mpc/interfaces' -import { connectedWalletBuilder } from '@/tests/builders/wallet' -import { chainBuilder } from '@/tests/builders/chains' -import PasswordRecoveryModal from '@/services/mpc/PasswordRecoveryModal' - -jest.mock('@/services/mpc/SocialWalletService') - -const mockWallet = connectedWalletBuilder().with({ chainId: '5', label: ONBOARD_MPC_MODULE_LABEL }).build() - -describe('SocialSignerLogin', () => { - let mockSocialWalletService: ISocialWalletService - - beforeEach(() => { - jest.resetAllMocks() - - mockSocialWalletService = new SocialWalletService({} as unknown as Web3AuthMPCCoreKit) - }) - - it('should render continue with connected account when on gnosis chain', async () => { - const mockOnLogin = jest.fn() - - const result = render( - - - - , - ) - - await waitFor(() => { - expect(result.findByText('Continue as Test Testermann')).resolves.toBeDefined() - }) - - // We do not automatically invoke the callback as the user did not actively connect - expect(mockOnLogin).not.toHaveBeenCalled() - - const button = await result.findByRole('button') - button.click() - - expect(mockOnLogin).toHaveBeenCalled() - }) - - it('should render google login button if no wallet is connected on gnosis chain', async () => { - const mockOnLogin = jest.fn() - - const result = render( - - - - , - ) - - await waitFor(async () => { - expect(result.findByText('Continue with Google')).resolves.toBeDefined() - expect(await result.findByRole('button')).toBeEnabled() - }) - }) - - it('should display a Continue as button and call onLogin when clicked', () => { - const mockOnLogin = jest.fn() - mockSocialWalletService.loginAndCreate = jest.fn(() => Promise.resolve(COREKIT_STATUS.LOGGED_IN)) - - const result = render( - - - - , - ) - - expect(result.getByText('Continue as Test Testermann')).toBeInTheDocument() - - const button = result.getByRole('button') - button.click() - - expect(mockOnLogin).toHaveBeenCalled() - }) - - it('should display Password Recovery form and display a Continue as button when login succeeds', async () => { - const mockOnLogin = jest.fn() - mockSocialWalletService.loginAndCreate = jest.fn(() => Promise.resolve(COREKIT_STATUS.REQUIRED_SHARE)) - mockSocialWalletService.getUserInfo = jest.fn().mockReturnValue(undefined) - mockSocialWalletService.recoverAccountWithPassword = jest.fn(() => Promise.resolve(true)) - - const result = render( - - - - , - ) - - await waitFor(() => { - expect(result.findByText('Continue with Google')).resolves.toBeDefined() - }) - - // We do not automatically invoke the callback as the user did not actively connect - expect(mockOnLogin).not.toHaveBeenCalled() - - const button = await result.findByRole('button') - - act(() => { - button.click() - }) - - await waitFor(() => { - expect(result.findByText('Enter security password')).resolves.toBeDefined() - }) - - const passwordField = await result.findByLabelText('Recovery password') - const submitButton = await result.findByText('Submit') - - act(() => { - fireEvent.change(passwordField, { target: { value: 'Test1234!' } }) - submitButton.click() - }) - - mockSocialWalletService.getUserInfo = jest.fn().mockReturnValue({ - email: 'test@testermann.com', - name: 'Test Testermann', - profileImage: 'test.testermann.local/profile.png', - } as unknown as UserInfo) - - result.rerender( - - - - , - ) - - await waitFor(() => { - expect(result.getByText('Continue as Test Testermann')).toBeInTheDocument() - }) - }) - - describe('getSupportedChains', () => { - const mockEthereumChain = chainBuilder() - .with({ - chainId: '1', - chainName: 'Ethereum', - disabledWallets: ['socialSigner'], - }) - .build() - const mockGnosisChain = chainBuilder() - .with({ chainId: '100', chainName: 'Gnosis Chain', disabledWallets: ['Coinbase'] }) - .build() - it('returns chain names where social login is enabled', () => { - const mockGoerliChain = chainBuilder().with({ chainId: '5', chainName: 'Goerli', disabledWallets: [] }).build() - - const mockChains = [mockEthereumChain, mockGnosisChain, mockGoerliChain] - const result = _getSupportedChains(mockChains) - - expect(result).toEqual(['Gnosis Chain', 'Goerli']) - }) - - it('returns an empty array if social login is not enabled on any chain', () => { - const mockGoerliChain = chainBuilder() - .with({ chainId: '5', chainName: 'Goerli', disabledWallets: ['socialSigner'] }) - .build() - - const mockChains = [mockEthereumChain, mockGoerliChain] - const result = _getSupportedChains(mockChains) - - expect(result).toEqual([]) - }) - }) -}) diff --git a/src/components/common/SocialSigner/index.tsx b/src/components/common/SocialSigner/index.tsx deleted file mode 100644 index 2f21e51168..0000000000 --- a/src/components/common/SocialSigner/index.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import useSocialWallet from '@/hooks/wallets/mpc/useSocialWallet' -import { type ISocialWalletService } from '@/services/mpc/interfaces' -import { Alert, Box, Button, LinearProgress, SvgIcon, Typography } from '@mui/material' -import { COREKIT_STATUS } from '@web3auth/mpc-core-kit' -import { useState } from 'react' -import GoogleLogo from '@/public/images/welcome/logo-google.svg' - -import css from './styles.module.css' -import useWallet from '@/hooks/wallets/useWallet' -import Track from '@/components/common/Track' -import { CREATE_SAFE_EVENTS } from '@/services/analytics' -import { MPC_WALLET_EVENTS } from '@/services/analytics/events/mpcWallet' -import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule' -import { CGW_NAMES } from '@/hooks/wallets/consts' -import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' -import madProps from '@/utils/mad-props' -import { asError } from '@/services/exceptions/utils' -import ErrorMessage from '@/components/tx/ErrorMessage' -import { open } from '@/services/mpc/PasswordRecoveryModal' - -export const _getSupportedChains = (chains: ChainInfo[]) => { - return chains - .filter((chain) => CGW_NAMES.SOCIAL_LOGIN && !chain.disabledWallets.includes(CGW_NAMES.SOCIAL_LOGIN)) - .map((chainConfig) => chainConfig.chainName) -} - -type SocialSignerLoginProps = { - socialWalletService: ISocialWalletService | undefined - wallet: ReturnType - onLogin?: () => void - onRequirePassword?: () => void -} - -export const SocialSigner = ({ socialWalletService, wallet, onLogin, onRequirePassword }: SocialSignerLoginProps) => { - const [loginPending, setLoginPending] = useState(false) - const [loginError, setLoginError] = useState(undefined) - const userInfo = socialWalletService?.getUserInfo() - const isDisabled = loginPending - - const isWelcomePage = !!onLogin - - const login = async () => { - if (!socialWalletService) return - - setLoginPending(true) - setLoginError(undefined) - try { - const status = await socialWalletService.loginAndCreate() - - if (status === COREKIT_STATUS.LOGGED_IN) { - setLoginPending(false) - return - } - - if (status === COREKIT_STATUS.REQUIRED_SHARE) { - onRequirePassword?.() - open(() => setLoginPending(false)) - return - } - } catch (err) { - const error = asError(err) - setLoginError(error.message) - } finally { - setLoginPending(false) - onLogin?.() - } - } - - const isSocialLogin = isSocialLoginWallet(wallet?.label) - - return ( - <> - - {isSocialLogin && userInfo ? ( - - - - ) : ( - - - - )} - {loginError && {loginError}} - - - - From 01.05.2024 we will no longer support account creation and login with Google. - - - ) -} - -export default madProps(SocialSigner, { - socialWalletService: useSocialWallet, - wallet: useWallet, -}) diff --git a/src/components/common/SocialSigner/styles.module.css b/src/components/common/SocialSigner/styles.module.css deleted file mode 100644 index 2b81df624a..0000000000 --- a/src/components/common/SocialSigner/styles.module.css +++ /dev/null @@ -1,34 +0,0 @@ -.profileImg { - border-radius: var(--space-2); - width: 32px; - height: 32px; -} - -.profileData { - display: flex; - flex-direction: column; - align-items: flex-start; -} - -.loginProgress { - margin-bottom: -16px; - top: 0; - width: 100%; - height: 2px; - position: absolute; - border-top-left-radius: 6px; - border-top-right-radius: 6px; -} - -.passwordWrapper { - padding: var(--space-4) var(--space-4) var(--space-2) var(--space-4); - display: flex; - flex-direction: column; - align-items: baseline; - gap: var(--space-1); -} - -.loginError { - width: 100%; - margin: 0; -} diff --git a/src/components/common/WalletInfo/index.test.tsx b/src/components/common/WalletInfo/index.test.tsx index 9375bb4ecc..3b435d6dd9 100644 --- a/src/components/common/WalletInfo/index.test.tsx +++ b/src/components/common/WalletInfo/index.test.tsx @@ -1,13 +1,7 @@ import { render } from '@/tests/test-utils' import { WalletInfo } from '@/components/common/WalletInfo/index' import { type EIP1193Provider, type OnboardAPI } from '@web3-onboard/core' -import { type NextRouter } from 'next/router' -import * as mpcModule from '@/services/mpc/SocialLoginModule' -import * as constants from '@/config/constants' -import { type Web3AuthMPCCoreKit } from '@web3auth/mpc-core-kit' import { act } from '@testing-library/react' -import SocialWalletService from '@/services/mpc/SocialWalletService' -import type { ISocialWalletService } from '@/services/mpc/interfaces' const mockWallet = { address: '0x1234567890123456789012345678901234567890', @@ -16,32 +10,21 @@ const mockWallet = { provider: null as unknown as EIP1193Provider, } -const mockRouter = { - query: {}, - pathname: '', -} as NextRouter - const mockOnboard = { connectWallet: jest.fn(), disconnectWallet: jest.fn(), setChain: jest.fn(), } as unknown as OnboardAPI -jest.mock('@/services/mpc/SocialWalletService') - describe('WalletInfo', () => { - let socialWalletService: ISocialWalletService beforeEach(() => { jest.resetAllMocks() - socialWalletService = new SocialWalletService({} as unknown as Web3AuthMPCCoreKit) }) it('should display the wallet address', () => { const { getByText } = render( { const { getByText } = render( { const { getByText } = render( { expect(mockOnboard.disconnectWallet).toHaveBeenCalled() }) - - it('should display a Delete Account button on dev for social login', () => { - jest.spyOn(mpcModule, 'isSocialLoginWallet').mockReturnValue(true) - jest.spyOn(constants, 'IS_PRODUCTION', 'get').mockImplementation(() => false) - - const { getByText } = render( - , - ) - - expect(getByText('Delete account')).toBeInTheDocument() - }) - - it('should not display a Delete Account on prod', () => { - jest.spyOn(mpcModule, 'isSocialLoginWallet').mockReturnValue(true) - jest.spyOn(constants, 'IS_PRODUCTION', 'get').mockImplementation(() => true) - - const { queryByText } = render( - , - ) - - expect(queryByText('Delete account')).not.toBeInTheDocument() - }) - - it('should not display a Delete Account if not social login', () => { - jest.spyOn(mpcModule, 'isSocialLoginWallet').mockReturnValue(false) - jest.spyOn(constants, 'IS_PRODUCTION', 'get').mockImplementation(() => false) - - const { queryByText } = render( - , - ) - - expect(queryByText('Delete account')).not.toBeInTheDocument() - }) - - it('should display an enable mfa button if mfa is not enabled', () => { - jest.spyOn(mpcModule, 'isSocialLoginWallet').mockReturnValue(true) - - const { getByText } = render( - , - ) - - expect(getByText('Add multifactor authentication')).toBeInTheDocument() - }) - - it('should not display an enable mfa button if mfa is already enabled', () => { - jest.spyOn(mpcModule, 'isSocialLoginWallet').mockReturnValue(true) - - // Mock that MFA is enabled - socialWalletService.enableMFA('', '') - - const { queryByText } = render( - , - ) - - expect(queryByText('Add multifactor authentication')).not.toBeInTheDocument() - }) }) diff --git a/src/components/common/WalletInfo/index.tsx b/src/components/common/WalletInfo/index.tsx index f083e57895..23c1af8cbd 100644 --- a/src/components/common/WalletInfo/index.tsx +++ b/src/components/common/WalletInfo/index.tsx @@ -2,21 +2,13 @@ import WalletBalance from '@/components/common/WalletBalance' import { WalletIdenticon } from '@/components/common/WalletOverview' import { Box, Button, Typography } from '@mui/material' import css from './styles.module.css' -import SocialLoginInfo from '@/components/common/SocialLoginInfo' -import Link from 'next/link' -import { AppRoutes } from '@/config/routes' -import LockIcon from '@/public/images/common/lock-small.svg' import EthHashInfo from '@/components/common/EthHashInfo' import ChainSwitcher from '@/components/common/ChainSwitcher' -import { IS_PRODUCTION } from '@/config/constants' -import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule' import useOnboard, { type ConnectedWallet, switchWallet } from '@/hooks/wallets/useOnboard' -import { useRouter } from 'next/router' import useAddressBook from '@/hooks/useAddressBook' import { useAppSelector } from '@/store' import { selectChainById } from '@/store/chainsSlice' import madProps from '@/utils/mad-props' -import useSocialWallet from '@/hooks/wallets/mpc/useSocialWallet' import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew' import useChainId from '@/hooks/useChainId' @@ -24,23 +16,12 @@ type WalletInfoProps = { wallet: ConnectedWallet balance?: string | bigint currentChainId: ReturnType - socialWalletService: ReturnType - router: ReturnType onboard: ReturnType addressBook: ReturnType handleClose: () => void } -export const WalletInfo = ({ - wallet, - balance, - currentChainId, - socialWalletService, - router, - onboard, - addressBook, - handleClose, -}: WalletInfoProps) => { +export const WalletInfo = ({ wallet, balance, currentChainId, onboard, addressBook, handleClose }: WalletInfoProps) => { const chainInfo = useAppSelector((state) => selectChainById(state, wallet.chainId)) const prefix = chainInfo?.shortName @@ -51,8 +32,6 @@ export const WalletInfo = ({ } } - const resetAccount = () => socialWalletService?.__deleteAccount() - const handleDisconnect = () => { onboard?.disconnectWallet({ label: wallet.label, @@ -61,48 +40,22 @@ export const WalletInfo = ({ handleClose() } - const isSocialLogin = isSocialLoginWallet(wallet.label) - return ( <> - {isSocialLogin ? ( - - - - {socialWalletService && !socialWalletService.isMFAEnabled() && ( - - - - )} - - ) : ( - <> - - - - - - )} + + + + + @@ -148,20 +101,12 @@ export const WalletInfo = ({ > Disconnect - - {!IS_PRODUCTION && isSocialLogin && ( - - )} ) } export default madProps(WalletInfo, { - socialWalletService: useSocialWallet, - router: useRouter, onboard: useOnboard, addressBook: useAddressBook, currentChainId: useChainId, diff --git a/src/components/common/WalletOverview/index.tsx b/src/components/common/WalletOverview/index.tsx index c4339f536e..f60bfc830c 100644 --- a/src/components/common/WalletOverview/index.tsx +++ b/src/components/common/WalletOverview/index.tsx @@ -11,8 +11,6 @@ import { selectChainById } from '@/store/chainsSlice' import WalletBalance from '@/components/common/WalletBalance' import css from './styles.module.css' -import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule' -import SocialLoginInfo from '@/components/common/SocialLoginInfo' export const WalletIdenticon = ({ wallet, size = 32 }: { wallet: ConnectedWallet; size?: number }) => { return ( @@ -39,22 +37,6 @@ const WalletOverview = ({ const walletChain = useAppSelector((state) => selectChainById(state, wallet.chainId)) const prefix = walletChain?.shortName - const isSocialLogin = isSocialLoginWallet(wallet.label) - - if (isSocialLogin) { - return ( -
- -
- ) - } - return ( diff --git a/src/components/common/WalletOverview/styles.module.css b/src/components/common/WalletOverview/styles.module.css index 6cf83c89fd..14dd5f668f 100644 --- a/src/components/common/WalletOverview/styles.module.css +++ b/src/components/common/WalletOverview/styles.module.css @@ -45,8 +45,4 @@ width: 22px; height: auto; } - - .socialLoginInfo > div > div:last-child { - display: none; - } } diff --git a/src/components/dashboard/index.tsx b/src/components/dashboard/index.tsx index e0a56f277a..3c72bf6be8 100644 --- a/src/components/dashboard/index.tsx +++ b/src/components/dashboard/index.tsx @@ -14,6 +14,8 @@ import { CREATION_MODAL_QUERY_PARM } from '../new-safe/create/logic' import useRecovery from '@/features/recovery/hooks/useRecovery' import { useIsRecoverySupported } from '@/features/recovery/hooks/useIsRecoverySupported' import ActivityRewardsSection from '@/components/dashboard/ActivityRewardsSection' +import { useHasFeature } from '@/hooks/useChains' +import { FEATURES } from '@/utils/chains' const RecoveryHeader = dynamic(() => import('@/features/recovery/components/RecoveryHeader')) const RecoveryWidget = dynamic(() => import('@/features/recovery/components/RecoveryWidget')) @@ -21,7 +23,7 @@ const Dashboard = (): ReactElement => { const router = useRouter() const { safe } = useSafeInfo() const { [CREATION_MODAL_QUERY_PARM]: showCreationModal = '' } = router.query - + const showSafeApps = useHasFeature(FEATURES.SAFE_APPS) const supportsRecovery = useIsRecoverySupported() const [recovery] = useRecovery() const showRecoveryWidget = supportsRecovery && !recovery @@ -53,13 +55,17 @@ const Dashboard = (): ReactElement => { ) : null} - - - + {showSafeApps && ( + + + + )} - - - + {showSafeApps && ( + + + + )} @@ -67,6 +73,7 @@ const Dashboard = (): ReactElement => { )} + {showCreationModal ? : null} ) diff --git a/src/components/new-safe/create/index.tsx b/src/components/new-safe/create/index.tsx index ba9d43b84f..9a14bad22d 100644 --- a/src/components/new-safe/create/index.tsx +++ b/src/components/new-safe/create/index.tsx @@ -18,8 +18,6 @@ import CreateSafeInfos from '@/components/new-safe/create/CreateSafeInfos' import { type ReactElement, useMemo, useState } from 'react' import ExternalLink from '@/components/common/ExternalLink' import { HelpCenterArticle } from '@/config/constants' -import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule' -import { useMnemonicSafeName } from '@/hooks/useMnemonicName' export type NewSafeFormData = { name: string @@ -151,14 +149,9 @@ const CreateSafe = () => { const staticHint = useMemo(() => staticHints[activeStep], [activeStep]) - const mnemonicSafeName = useMnemonicSafeName() - - // Jump to review screen when using social login - const isSocialLogin = isSocialLoginWallet(wallet?.label) - const initialStep = isSocialLogin ? 2 : 0 - + const initialStep = 0 const initialData: NewSafeFormData = { - name: isSocialLogin ? mnemonicSafeName : '', + name: '', owners: [], threshold: 1, saltNonce: Date.now(), diff --git a/src/components/new-safe/create/steps/ReviewStep/index.test.tsx b/src/components/new-safe/create/steps/ReviewStep/index.test.tsx index 8ebfc9bdaa..6455eef21d 100644 --- a/src/components/new-safe/create/steps/ReviewStep/index.test.tsx +++ b/src/components/new-safe/create/steps/ReviewStep/index.test.tsx @@ -7,7 +7,6 @@ import { render } from '@/tests/test-utils' import ReviewStep, { NetworkFee } from '@/components/new-safe/create/steps/ReviewStep/index' import * as useWallet from '@/hooks/wallets/useWallet' import { type ConnectedWallet } from '@/hooks/wallets/useOnboard' -import * as socialLogin from '@/services/mpc/SocialLoginModule' import { act, fireEvent } from '@testing-library/react' const mockChainInfo = { @@ -20,29 +19,13 @@ const mockChainInfo = { } as ChainInfo describe('NetworkFee', () => { - it('should display the total fee if not social login', () => { + it('should display the total fee', () => { jest.spyOn(useWallet, 'default').mockReturnValue({ label: 'MetaMask' } as unknown as ConnectedWallet) const mockTotalFee = '0.0123' const result = render() expect(result.getByText(`≈ ${mockTotalFee} ${mockChainInfo.nativeCurrency.symbol}`)).toBeInTheDocument() }) - - it('displays a sponsored by message for social login', () => { - jest.spyOn(useWallet, 'default').mockReturnValue({ label: 'Social Login' } as unknown as ConnectedWallet) - const result = render() - - expect(result.getByText(/Your account is sponsored by Gnosis/)).toBeInTheDocument() - }) - - it('displays an error message for social login if there are no relays left', () => { - jest.spyOn(useWallet, 'default').mockReturnValue({ label: 'Social Login' } as unknown as ConnectedWallet) - const result = render() - - expect( - result.getByText(/You have used up your 5 free transactions per hour. Please try again later/), - ).toBeInTheDocument() - }) }) describe('ReviewStep', () => { @@ -129,7 +112,6 @@ describe('ReviewStep', () => { } jest.spyOn(useChains, 'useHasFeature').mockReturnValue(true) jest.spyOn(relay, 'hasRemainingRelays').mockReturnValue(true) - jest.spyOn(socialLogin, 'isSocialLoginWallet').mockReturnValue(false) const { getByText } = render( , diff --git a/src/components/new-safe/create/steps/ReviewStep/index.tsx b/src/components/new-safe/create/steps/ReviewStep/index.tsx index 15801600bf..85141ffaec 100644 --- a/src/components/new-safe/create/steps/ReviewStep/index.tsx +++ b/src/components/new-safe/create/steps/ReviewStep/index.tsx @@ -14,30 +14,27 @@ import useSyncSafeCreationStep from '@/components/new-safe/create/useSyncSafeCre import ReviewRow from '@/components/new-safe/ReviewRow' import ErrorMessage from '@/components/tx/ErrorMessage' import { ExecutionMethod, ExecutionMethodSelector } from '@/components/tx/ExecutionMethodSelector' -import { RELAY_SPONSORS } from '@/components/tx/SponsoredBy' import { LATEST_SAFE_VERSION } from '@/config/constants' import PayNowPayLater, { PayMethod } from '@/features/counterfactual/PayNowPayLater' import { createCounterfactualSafe } from '@/features/counterfactual/utils' import { useCurrentChain, useHasFeature } from '@/hooks/useChains' import useGasPrice from '@/hooks/useGasPrice' import useIsWrongChain from '@/hooks/useIsWrongChain' -import { MAX_HOUR_RELAYS, useLeastRemainingRelays } from '@/hooks/useRemainingRelays' +import { useLeastRemainingRelays } from '@/hooks/useRemainingRelays' import useWalletCanPay from '@/hooks/useWalletCanPay' import useWallet from '@/hooks/wallets/useWallet' import { useWeb3 } from '@/hooks/wallets/web3' import { CREATE_SAFE_CATEGORY, CREATE_SAFE_EVENTS, OVERVIEW_EVENTS, trackEvent } from '@/services/analytics' import { gtmSetSafeAddress } from '@/services/analytics/gtm' import { getReadOnlyFallbackHandlerContract } from '@/services/contracts/safeContracts' -import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule' import { useAppDispatch } from '@/store' import { FEATURES } from '@/utils/chains' import { hasRemainingRelays } from '@/utils/relaying' import ArrowBackIcon from '@mui/icons-material/ArrowBack' -import { Alert, Box, Button, CircularProgress, Divider, Grid, Typography } from '@mui/material' +import { Box, Button, CircularProgress, Divider, Grid, Typography } from '@mui/material' import { type DeploySafeProps } from '@safe-global/protocol-kit' import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' import classnames from 'classnames' -import Image from 'next/image' import { useRouter } from 'next/router' import { useMemo, useState } from 'react' import { usePendingSafe } from '../StatusStep/usePendingSafe' @@ -55,45 +52,14 @@ export const NetworkFee = ({ }) => { const wallet = useWallet() - const isSocialLogin = isSocialLoginWallet(wallet?.label) - - if (!isSocialLogin) { - return ( - - - - ≈ {totalFee} {chain?.nativeCurrency.symbol} - - - - ) - } - - if (willRelay) { - const sponsor = RELAY_SPONSORS[chain?.chainId || ''] || RELAY_SPONSORS.default - return ( - <> - Free - - Your account is sponsored by - {sponsor.name}{' '} - {sponsor.name} - - - ) - } - return ( - - You have used up your {MAX_HOUR_RELAYS} free transactions per hour. Please try again later. - + + + + ≈ {totalFee} {chain?.nativeCurrency.symbol} + + + ) } @@ -239,8 +205,7 @@ const ReviewStep = ({ data, onSubmit, onBack, setStep }: StepRenderProps @@ -254,7 +219,7 @@ const ReviewStep = ({ data, onSubmit, onBack, setStep }: StepRenderProps - {canRelay && !isSocialLogin && payMethod === PayMethod.PayNow && ( + {canRelay && payMethod === PayMethod.PayNow && ( - {canRelay && !isSocialLogin && ( + {canRelay && ( - {!willRelay && !isSocialLogin && ( + {!willRelay && ( You will have to confirm a transaction with your connected wallet. diff --git a/src/components/new-safe/create/steps/StatusStep/useSafeCreation.ts b/src/components/new-safe/create/steps/StatusStep/useSafeCreation.ts index 47c7e4ce9d..b0fae21862 100644 --- a/src/components/new-safe/create/steps/StatusStep/useSafeCreation.ts +++ b/src/components/new-safe/create/steps/StatusStep/useSafeCreation.ts @@ -23,7 +23,7 @@ import { CREATE_SAFE_EVENTS, trackEvent } from '@/services/analytics' import { waitForCreateSafeTx } from '@/services/tx/txMonitor' import useGasPrice from '@/hooks/useGasPrice' import { hasFeature } from '@/utils/chains' -import { FEATURES } from '@safe-global/safe-gateway-typescript-sdk' +import { FEATURES } from '@/utils/chains' import type { DeploySafeProps } from '@safe-global/protocol-kit' import { usePendingSafe } from './usePendingSafe' diff --git a/src/components/notification-center/NotificationCenter/index.tsx b/src/components/notification-center/NotificationCenter/index.tsx index d756721dde..ebce3aee22 100644 --- a/src/components/notification-center/NotificationCenter/index.tsx +++ b/src/components/notification-center/NotificationCenter/index.tsx @@ -25,6 +25,8 @@ import SettingsIcon from '@/public/images/sidebar/settings.svg' import css from './styles.module.css' import { trackEvent, OVERVIEW_EVENTS } from '@/services/analytics' import SvgIcon from '@mui/icons-material/ExpandLess' +import { useHasFeature } from '@/hooks/useChains' +import { FEATURES } from '@/utils/chains' const NOTIFICATION_CENTER_LIMIT = 4 @@ -33,7 +35,7 @@ const NotificationCenter = (): ReactElement => { const [showAll, setShowAll] = useState(false) const [anchorEl, setAnchorEl] = useState(null) const open = Boolean(anchorEl) - + const hasPushNotifications = useHasFeature(FEATURES.PUSH_NOTIFICATIONS) const dispatch = useAppDispatch() const notifications = useAppSelector(selectNotifications) @@ -144,9 +146,11 @@ const NotificationCenter = (): ReactElement => { )} +
+
{canExpand && ( <> @@ -166,18 +170,21 @@ const NotificationCenter = (): ReactElement => { )} - - - Push notifications settings - - + + {hasPushNotifications && ( + + + Push notifications settings + + + )}
diff --git a/src/components/settings/ContractVersion/index.tsx b/src/components/settings/ContractVersion/index.tsx index b20683b757..334ca921d7 100644 --- a/src/components/settings/ContractVersion/index.tsx +++ b/src/components/settings/ContractVersion/index.tsx @@ -1,5 +1,5 @@ import { useContext, useMemo } from 'react' -import { Box, SvgIcon, Typography, Alert, AlertTitle, Skeleton, Button } from '@mui/material' +import { SvgIcon, Typography, Alert, AlertTitle, Skeleton, Button } from '@mui/material' import { ImplementationVersionState } from '@safe-global/safe-gateway-typescript-sdk' import { LATEST_SAFE_VERSION } from '@/config/constants' import { sameAddress } from '@/utils/addresses' @@ -25,6 +25,7 @@ export const ContractVersion = () => { const needsUpdate = safe.implementationVersionState === ImplementationVersionState.OUTDATED const showUpdateDialog = safeMasterCopy?.deployer === MasterCopyDeployer.GNOSIS && needsUpdate + const isLatestVersion = safe.version && !showUpdateDialog return ( <> @@ -32,39 +33,43 @@ export const ContractVersion = () => { Contract version - - {safeLoaded ? safe.version ?? 'Unsupported contract' : } + + {safeLoaded ? ( + <> + {safe.version ?? 'Unsupported contract'} + {isLatestVersion && ( + <> + Latest version + + )} + + ) : ( + + )} - - {safeLoaded && safe.version ? ( - showUpdateDialog ? ( - } - > - New version is available: {LATEST_SAFE_VERSION} - - Update now to take advantage of new features and the highest security standards available. You will need - to confirm this update just like any other transaction.{' '} - GitHub - + {safeLoaded && safe.version && showUpdateDialog && ( + } + > + New version is available: {LATEST_SAFE_VERSION} - - {(isOk) => ( - - )} - - - ) : ( - - Latest version - - ) - ) : null} - + + Update now to take advantage of new features and the highest security standards available. You will need to + confirm this update just like any other transaction.{' '} + GitHub + + + + {(isOk) => ( + + )} + + + )} ) } diff --git a/src/components/settings/RequiredConfirmations/index.tsx b/src/components/settings/RequiredConfirmations/index.tsx index 61db4d9900..2ccc9fc094 100644 --- a/src/components/settings/RequiredConfirmations/index.tsx +++ b/src/components/settings/RequiredConfirmations/index.tsx @@ -19,23 +19,22 @@ export const RequiredConfirmation = ({ threshold, owners }: { threshold: number;
- Any transaction requires the confirmation of: - + Any transaction requires the confirmation of: + + {threshold} out of {owners} signers. {owners > 1 && ( - - - {(isOk) => ( - - - - )} - - + + {(isOk) => ( + + + + )} + )}
diff --git a/src/components/settings/SafeModules/index.tsx b/src/components/settings/SafeModules/index.tsx index 88c85b9bea..204448ce3e 100644 --- a/src/components/settings/SafeModules/index.tsx +++ b/src/components/settings/SafeModules/index.tsx @@ -65,7 +65,7 @@ const SafeModules = () => { - Safe Account modules + Safe modules diff --git a/src/components/settings/SecurityLogin/SocialSignerExport/ExportMPCAccountModal.tsx b/src/components/settings/SecurityLogin/SocialSignerExport/ExportMPCAccountModal.tsx deleted file mode 100644 index bacce6a38f..0000000000 --- a/src/components/settings/SecurityLogin/SocialSignerExport/ExportMPCAccountModal.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import CopyButton from '@/components/common/CopyButton' -import ModalDialog from '@/components/common/ModalDialog' -import { trackEvent } from '@/services/analytics' -import { MPC_WALLET_EVENTS } from '@/services/analytics/events/mpcWallet' -import { Box, Button, DialogContent, DialogTitle, IconButton, TextField, Typography } from '@mui/material' -import { useState } from 'react' -import { useForm } from 'react-hook-form' -import { Visibility, VisibilityOff, Close } from '@mui/icons-material' -import css from '@/components/settings/SecurityLogin/SocialSignerExport/styles.module.css' -import ErrorCodes from '@/services/exceptions/ErrorCodes' -import { logError } from '@/services/exceptions' -import ErrorMessage from '@/components/tx/ErrorMessage' -import { asError } from '@/services/exceptions/utils' -import useSocialWallet from '@/hooks/wallets/mpc/useSocialWallet' - -enum ExportFieldNames { - password = 'password', - pk = 'pk', -} - -type ExportFormData = { - [ExportFieldNames.password]: string - [ExportFieldNames.pk]: string | undefined -} - -const ExportMPCAccountModal = ({ onClose, open }: { onClose: () => void; open: boolean }) => { - const socialWalletService = useSocialWallet() - const [error, setError] = useState() - - const [showPassword, setShowPassword] = useState(false) - const formMethods = useForm({ - mode: 'all', - defaultValues: { - [ExportFieldNames.password]: '', - }, - }) - const { register, formState, handleSubmit, setValue, watch, reset } = formMethods - - const exportedKey = watch(ExportFieldNames.pk) - - const onSubmit = async (data: ExportFormData) => { - if (!socialWalletService) { - return - } - try { - setError(undefined) - const pk = await socialWalletService.exportSignerKey(data[ExportFieldNames.password]) - trackEvent(MPC_WALLET_EVENTS.EXPORT_PK_SUCCESS) - setValue(ExportFieldNames.pk, pk) - } catch (err) { - logError(ErrorCodes._305, err) - trackEvent(MPC_WALLET_EVENTS.EXPORT_PK_ERROR) - setError(asError(err).message) - } - } - - const handleClose = () => { - setError(undefined) - reset() - onClose() - } - - const toggleShowPK = () => { - trackEvent(MPC_WALLET_EVENTS.SEE_PK) - setShowPassword((prev) => !prev) - } - - const onCopy = () => { - trackEvent(MPC_WALLET_EVENTS.COPY_PK) - } - - return ( - - Export your account - - - - - -
- - For security reasons you have to enter your password to reveal your account key. - - {exportedKey ? ( - - - - {showPassword ? : } - - - - ), - }} - {...register(ExportFieldNames.pk)} - /> - - ) : ( - <> - - - )} - {error && {error}} - - - - {exportedKey === undefined && ( - - )} - - -
-
-
- ) -} - -export default ExportMPCAccountModal diff --git a/src/components/settings/SecurityLogin/SocialSignerExport/index.tsx b/src/components/settings/SecurityLogin/SocialSignerExport/index.tsx deleted file mode 100644 index 088e484af2..0000000000 --- a/src/components/settings/SecurityLogin/SocialSignerExport/index.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import Track from '@/components/common/Track' -import { MPC_WALLET_EVENTS } from '@/services/analytics/events/mpcWallet' -import { Alert, Box, Button, Tooltip, Typography } from '@mui/material' -import { useState } from 'react' -import ExportMPCAccountModal from '@/components/settings/SecurityLogin/SocialSignerExport/ExportMPCAccountModal' -import useSocialWallet from '@/hooks/wallets/mpc/useSocialWallet' - -const SocialSignerExport = () => { - const [isModalOpen, setIsModalOpen] = useState(false) - - const socialWalletService = useSocialWallet() - - const isPasswordSet = socialWalletService?.isRecoveryPasswordSet() ?? false - - return ( - <> - - - Signers created via Google can be exported and imported to any non-custodial wallet outside of Safe. - - - Never disclose your keys or seed phrase to anyone. If someone gains access to them, they have full access over - your social login signer. - - - - - - - - - - - setIsModalOpen(false)} open={isModalOpen} /> - - ) -} - -export default SocialSignerExport diff --git a/src/components/settings/SecurityLogin/SocialSignerExport/styles.module.css b/src/components/settings/SecurityLogin/SocialSignerExport/styles.module.css deleted file mode 100644 index c818925ecd..0000000000 --- a/src/components/settings/SecurityLogin/SocialSignerExport/styles.module.css +++ /dev/null @@ -1,10 +0,0 @@ -.close { - position: absolute; - right: var(--space-1); - top: var(--space-1); -} - -.modalError { - width: 100%; - margin: 0; -} diff --git a/src/components/settings/SecurityLogin/SocialSignerMFA/PasswordInput.tsx b/src/components/settings/SecurityLogin/SocialSignerMFA/PasswordInput.tsx deleted file mode 100644 index 40e78010b2..0000000000 --- a/src/components/settings/SecurityLogin/SocialSignerMFA/PasswordInput.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { useState } from 'react' -import { IconButton, TextField, type TextFieldProps } from '@mui/material' -import { Visibility, VisibilityOff } from '@mui/icons-material' -import { useFormContext, type Validate } from 'react-hook-form' - -const PasswordInput = ({ - name, - validate, - required = false, - ...props -}: Omit & { - name: string - validate?: Validate - required?: boolean -}) => { - const [showPassword, setShowPassword] = useState(false) - - const { register, formState } = useFormContext() || {} - - return ( - setShowPassword((prev) => !prev)} - edge="end" - > - {showPassword ? : } - - ), - }} - {...register(name, { - required, - validate, - })} - /> - ) -} - -export default PasswordInput diff --git a/src/components/settings/SecurityLogin/SocialSignerMFA/index.test.tsx b/src/components/settings/SecurityLogin/SocialSignerMFA/index.test.tsx deleted file mode 100644 index 4a2bde113d..0000000000 --- a/src/components/settings/SecurityLogin/SocialSignerMFA/index.test.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { _getPasswordStrength, PasswordStrength } from '@/components/settings/SecurityLogin/SocialSignerMFA/index' - -describe('_getPasswordStrength', () => { - it('should return weak if the value has fewer than 9 characters', () => { - const result = _getPasswordStrength('Testpw1!') - - expect(result).toEqual(PasswordStrength.weak) - }) - - it('should return weak if the value has no uppercase letter', () => { - const result = _getPasswordStrength('testpassword1!') - - expect(result).toEqual(PasswordStrength.weak) - }) - - it('should return weak if the value has no number', () => { - const result = _getPasswordStrength('Testpassword!') - - expect(result).toEqual(PasswordStrength.weak) - }) - - it('should return weak if the value has no special character', () => { - const result = _getPasswordStrength('Testpassword123') - - expect(result).toEqual(PasswordStrength.weak) - }) - - it('should return weak if the value has 12 or more characters but no uppercase letter', () => { - const result = _getPasswordStrength('testpassword123!') - - expect(result).toEqual(PasswordStrength.weak) - }) - - it('should return medium if the value has 9 or more characters, uppercase, number and special character', () => { - const result = _getPasswordStrength('Testpw123!') - - expect(result).toEqual(PasswordStrength.medium) - }) - - it('should return strong if the value has 12 or more characters, uppercase, number and special character', () => { - const result = _getPasswordStrength('Testpassword123!') - - expect(result).toEqual(PasswordStrength.strong) - }) -}) diff --git a/src/components/settings/SecurityLogin/SocialSignerMFA/index.tsx b/src/components/settings/SecurityLogin/SocialSignerMFA/index.tsx deleted file mode 100644 index cfb4ec0464..0000000000 --- a/src/components/settings/SecurityLogin/SocialSignerMFA/index.tsx +++ /dev/null @@ -1,298 +0,0 @@ -import Track from '@/components/common/Track' -import { - Typography, - Button, - Box, - Accordion, - AccordionSummary, - AccordionDetails, - Grid, - FormControl, - SvgIcon, - Divider, - Alert, -} from '@mui/material' -import { MPC_WALLET_EVENTS } from '@/services/analytics/events/mpcWallet' -import { useRouter } from 'next/router' -import { useState, useMemo, type ChangeEvent } from 'react' -import { FormProvider, useForm } from 'react-hook-form' -import CheckIcon from '@/public/images/common/check-filled.svg' -import LockWarningIcon from '@/public/images/common/lock-warning.svg' -import PasswordInput from '@/components/settings/SecurityLogin/SocialSignerMFA/PasswordInput' -import css from '@/components/settings/SecurityLogin/SocialSignerMFA/styles.module.css' -import BarChartIcon from '@/public/images/common/bar-chart.svg' -import ShieldIcon from '@/public/images/common/shield.svg' -import ShieldOffIcon from '@/public/images/common/shield-off.svg' -import ExpandMoreIcon from '@mui/icons-material/ExpandMore' -import useSocialWallet from '@/hooks/wallets/mpc/useSocialWallet' - -enum PasswordFieldNames { - currentPassword = 'currentPassword', - newPassword = 'newPassword', - confirmPassword = 'confirmPassword', -} - -type PasswordFormData = { - [PasswordFieldNames.currentPassword]: string | undefined - [PasswordFieldNames.newPassword]: string - [PasswordFieldNames.confirmPassword]: string -} - -export enum PasswordStrength { - strong, - medium, - weak, -} - -// At least 12 characters, one lowercase, one uppercase, one number, one symbol -const strongPassword = new RegExp('(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^A-Za-z0-9])(?=.{12,})') -// At least 9 characters, one lowercase, one uppercase, one number, one symbol -const mediumPassword = new RegExp('((?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^A-Za-z0-9])(?=.{9,}))') - -export const _getPasswordStrength = (value: string): PasswordStrength | undefined => { - if (value === '') return undefined - - if (strongPassword.test(value)) { - return PasswordStrength.strong - } - - if (mediumPassword.test(value)) { - return PasswordStrength.medium - } - - return PasswordStrength.weak -} - -const passwordStrengthMap = { - [PasswordStrength.strong]: { - label: 'Strong', - className: 'strongPassword', - }, - [PasswordStrength.medium]: { - label: 'Medium', - className: 'mediumPassword', - }, - [PasswordStrength.weak]: { - label: 'Weak', - className: 'weakPassword', - }, -} as const - -const SocialSignerMFA = () => { - const router = useRouter() - const socialWalletService = useSocialWallet() - const [passwordStrength, setPasswordStrength] = useState() - const [submitError, setSubmitError] = useState() - const [open, setOpen] = useState(false) - - const formMethods = useForm({ - mode: 'all', - defaultValues: { - [PasswordFieldNames.confirmPassword]: '', - [PasswordFieldNames.currentPassword]: undefined, - [PasswordFieldNames.newPassword]: '', - }, - }) - - const { formState, handleSubmit, reset, watch } = formMethods - - const isPasswordSet = useMemo(() => { - if (!socialWalletService) { - return false - } - return socialWalletService.isRecoveryPasswordSet() - }, [socialWalletService]) - - const onSubmit = async (data: PasswordFormData) => { - if (!socialWalletService) return - - try { - await socialWalletService.enableMFA( - data[PasswordFieldNames.currentPassword], - data[PasswordFieldNames.newPassword], - ) - onReset() - setOpen(false) - - // This is a workaround so that the isPasswordSet and isMFAEnabled state update - router.reload() - } catch (e) { - setSubmitError('The password you entered is incorrect. Please try again.') - } - } - - const onReset = () => { - reset() - setPasswordStrength(undefined) - setSubmitError(undefined) - } - - const toggleAccordion = () => { - setOpen((prev) => !prev) - } - - const confirmPassword = watch(PasswordFieldNames.confirmPassword) - const newPassword = watch(PasswordFieldNames.newPassword) - const passwordsEmpty = confirmPassword === '' && newPassword === '' - const passwordsMatch = newPassword === confirmPassword - - const isSubmitDisabled = - !passwordsMatch || - passwordStrength === PasswordStrength.weak || - formState.isSubmitting || - !formMethods.formState.isValid - - return ( - -
- - - Protect your social login signer with a password. It will be used to restore access in another browser or on - another device. - - - }> - - - Password - - - - - - {isPasswordSet && ( - <> - - - - - - )} - - - ) => { - const value = event.target.value - setPasswordStrength(_getPasswordStrength(value)) - }, - }} - /> - - - {passwordStrength !== undefined - ? `${passwordStrengthMap[passwordStrength].label} password` - : 'Password strength'} - - - Include at least 9 or more characters, a number, an uppercase letter and a symbol - - - - - - - {passwordsEmpty ? ( - <> - Passwords should match - - ) : passwordsMatch ? ( - <> - Passwords match - - ) : ( - <> - Passwords don't match - - )} - - - - {submitError && {submitError}} - - - - - - - - - `1px solid ${theme.palette.border.light}` }} - > - - - - You won't be able to restore this password - -
    - - You will have to input this password if you login with this social login signer in another - browser or on another device. - - - We suggest to use a password manager or to write the password down on a piece of paper and - secure it. - -
-
-
-
-
-
-
-
-
- ) -} - -export default SocialSignerMFA diff --git a/src/components/settings/SecurityLogin/SocialSignerMFA/styles.module.css b/src/components/settings/SecurityLogin/SocialSignerMFA/styles.module.css deleted file mode 100644 index b41b19e58f..0000000000 --- a/src/components/settings/SecurityLogin/SocialSignerMFA/styles.module.css +++ /dev/null @@ -1,76 +0,0 @@ -.list { - list-style: none; - counter-reset: item; - padding: 0; - margin: var(--space-2) 0; -} - -.list li { - counter-increment: item; - margin-bottom: var(--space-2); - display: flex; -} - -.list li:before { - margin-right: var(--space-1); - content: counter(item); - background: var(--color-primary-main); - border-radius: 100%; - color: var(--color-background-paper); - width: 20px; - height: 20px; - line-height: 20px; - font-size: 12px; - text-align: center; - flex-shrink: 0; -} - -.list li:last-child { - margin-bottom: 0; -} - -.defaultPassword, -.passwordsShouldMatch { - color: var(--color-border-main); -} - -.passwordsShouldMatch svg path { - stroke: var(--color-border-main); -} - -.defaultPassword svg path { - stroke: var(--color-border-main); -} - -.weakPassword { - color: var(--color-error-main); -} - -.weakPassword svg path:first-child { - stroke: var(--color-error-main); -} - -.mediumPassword { - color: var(--color-warning-main); -} - -.mediumPassword svg path:first-child, -.mediumPassword svg path:nth-child(2) { - stroke: var(--color-warning-main); -} - -.strongPassword { - color: var(--color-success-main); -} - -.strongPassword svg path { - stroke: var(--color-success-main); -} - -.passwordsMatch { - color: var(--color-success-main); -} - -.passwordsNoMatch { - color: var(--color-error-main); -} diff --git a/src/components/settings/SecurityLogin/index.tsx b/src/components/settings/SecurityLogin/index.tsx index 944df3bac7..0fb33dc324 100644 --- a/src/components/settings/SecurityLogin/index.tsx +++ b/src/components/settings/SecurityLogin/index.tsx @@ -1,9 +1,4 @@ -import { Box, Grid, Paper, Typography } from '@mui/material' -import SocialSignerMFA from './SocialSignerMFA' -import SocialSignerExport from './SocialSignerExport' -import useWallet from '@/hooks/wallets/useWallet' -import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule' -import SpendingLimits from '../SpendingLimits' +import { Box } from '@mui/material' import dynamic from 'next/dynamic' import { useIsRecoverySupported } from '@/features/recovery/hooks/useIsRecoverySupported' import SecuritySettings from '../SecuritySettings' @@ -12,45 +7,11 @@ const RecoverySettings = dynamic(() => import('@/features/recovery/components/Re const SecurityLogin = () => { const isRecoverySupported = useIsRecoverySupported() - const wallet = useWallet() - const isSocialLogin = isSocialLoginWallet(wallet?.label) return ( {isRecoverySupported && } - {isSocialLogin && ( - <> - - - - - Multi-factor Authentication - - - - - - - - - - - - - Social login signer export - - - - - - - - - )} - - - ) diff --git a/src/components/settings/SettingsHeader/index.test.tsx b/src/components/settings/SettingsHeader/index.test.tsx index f50dc917fc..32574c41fb 100644 --- a/src/components/settings/SettingsHeader/index.test.tsx +++ b/src/components/settings/SettingsHeader/index.test.tsx @@ -1,12 +1,11 @@ -import SettingsHeader from '@/components/settings/SettingsHeader/index' +import { SettingsHeader } from '@/components/settings/SettingsHeader/index' +import { CONFIG_SERVICE_CHAINS } from '@/tests/mocks/chains' import * as safeAddress from '@/hooks/useSafeAddress' -import * as feature from '@/hooks/useChains' -import * as wallet from '@/hooks/wallets/useWallet' -import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/SocialLoginModule' -import { connectedWalletBuilder } from '@/tests/builders/wallet' import { render } from '@/tests/test-utils' import { faker } from '@faker-js/faker' +import { FEATURES } from '@/utils/chains' +import type { ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' describe('SettingsHeader', () => { beforeEach(() => { @@ -20,27 +19,24 @@ describe('SettingsHeader', () => { }) it('displays safe specific preferences if on a safe', () => { - const result = render() + const result = render() expect(result.getByText('Setup')).toBeInTheDocument() }) it('displays Notifications if feature is enabled', () => { - jest.spyOn(feature, 'useHasFeature').mockReturnValue(true) - - const result = render() + const result = render( + , + ) expect(result.getByText('Notifications')).toBeInTheDocument() }) - - it('displays Security & Login if connected wallet is a social signer', () => { - const mockWallet = connectedWalletBuilder().with({ label: ONBOARD_MPC_MODULE_LABEL }).build() - jest.spyOn(wallet, 'default').mockReturnValue(mockWallet) - - const result = render() - - expect(result.getByText('Security & Login')).toBeInTheDocument() - }) }) describe('No safe is open', () => { @@ -50,7 +46,7 @@ describe('SettingsHeader', () => { }) it('displays general preferences if no safe is open', () => { - const result = render() + const result = render() expect(result.getByText('Cookies')).toBeInTheDocument() expect(result.getByText('Appearance')).toBeInTheDocument() @@ -59,20 +55,17 @@ describe('SettingsHeader', () => { }) it('displays Notifications if feature is enabled', () => { - jest.spyOn(feature, 'useHasFeature').mockReturnValue(true) - - const result = render() + const result = render( + , + ) expect(result.getByText('Notifications')).toBeInTheDocument() }) - - it('displays Security & Login if connected wallet is a social signer', () => { - const mockWallet = connectedWalletBuilder().with({ label: ONBOARD_MPC_MODULE_LABEL }).build() - jest.spyOn(wallet, 'default').mockReturnValue(mockWallet) - - const result = render() - - expect(result.getByText('Security & Login')).toBeInTheDocument() - }) }) }) diff --git a/src/components/settings/SettingsHeader/index.tsx b/src/components/settings/SettingsHeader/index.tsx index 5bb856f21a..f20def13f1 100644 --- a/src/components/settings/SettingsHeader/index.tsx +++ b/src/components/settings/SettingsHeader/index.tsx @@ -5,11 +5,20 @@ import PageHeader from '@/components/common/PageHeader' import { generalSettingsNavItems, settingsNavItems } from '@/components/sidebar/SidebarNavigation/config' import css from '@/components/common/PageHeader/styles.module.css' import useSafeAddress from '@/hooks/useSafeAddress' +import { useCurrentChain } from '@/hooks/useChains' +import { isRouteEnabled } from '@/utils/chains' +import madProps from '@/utils/mad-props' -const SettingsHeader = (): ReactElement => { - const safeAddress = useSafeAddress() - - const navItems = safeAddress ? settingsNavItems : generalSettingsNavItems +export const SettingsHeader = ({ + safeAddress, + chain, +}: { + safeAddress: ReturnType + chain: ReturnType +}): ReactElement => { + const navItems = safeAddress + ? settingsNavItems.filter((route) => isRouteEnabled(route.href, chain)) + : generalSettingsNavItems return ( { ) } -export default SettingsHeader +export default madProps(SettingsHeader, { + safeAddress: useSafeAddress, + chain: useCurrentChain, +}) diff --git a/src/components/sidebar/SidebarNavigation/config.tsx b/src/components/sidebar/SidebarNavigation/config.tsx index 27cc371aeb..64ed08785d 100644 --- a/src/components/sidebar/SidebarNavigation/config.tsx +++ b/src/components/sidebar/SidebarNavigation/config.tsx @@ -9,11 +9,13 @@ import AppsIcon from '@/public/images/apps/apps-icon.svg' import SettingsIcon from '@/public/images/sidebar/settings.svg' import SwapIcon from '@/public/images/common/swap.svg' import { SvgIcon } from '@mui/material' +import Chip from '@mui/material/Chip' export type NavItem = { label: string icon?: ReactElement href: string + tag?: ReactElement } export const navItems: NavItem[] = [ @@ -31,6 +33,7 @@ export const navItems: NavItem[] = [ label: 'Swap', icon: , href: AppRoutes.swap, + tag: , }, { label: 'Transactions', @@ -90,8 +93,8 @@ export const settingsNavItems = [ href: AppRoutes.settings.appearance, }, { - label: 'Security & Login', - href: AppRoutes.settings.securityLogin, + label: 'Security', + href: AppRoutes.settings.security, }, { label: 'Notifications', @@ -129,8 +132,8 @@ export const generalSettingsNavItems = [ href: AppRoutes.settings.notifications, }, { - label: 'Security & Login', - href: AppRoutes.settings.securityLogin, + label: 'Security', + href: AppRoutes.settings.security, }, { label: 'Data', diff --git a/src/components/sidebar/SidebarNavigation/index.tsx b/src/components/sidebar/SidebarNavigation/index.tsx index b247e31b6a..7c6fd9eba1 100644 --- a/src/components/sidebar/SidebarNavigation/index.tsx +++ b/src/components/sidebar/SidebarNavigation/index.tsx @@ -1,7 +1,7 @@ import React, { useMemo, type ReactElement } from 'react' import { useRouter } from 'next/router' import ListItem from '@mui/material/ListItem' -import { type ChainInfo, ImplementationVersionState } from '@safe-global/safe-gateway-typescript-sdk' +import { ImplementationVersionState } from '@safe-global/safe-gateway-typescript-sdk' import { SidebarList, @@ -15,7 +15,7 @@ import useSafeInfo from '@/hooks/useSafeInfo' import { AppRoutes } from '@/config/routes' import { useQueuedTxsLength } from '@/hooks/useTxQueue' import { useCurrentChain } from '@/hooks/useChains' -import { FeatureRoutes, hasFeature } from '@/utils/chains' +import { isRouteEnabled } from '@/utils/chains' import { trackEvent } from '@/services/analytics' import { SWAP_EVENTS, SWAP_LABELS } from '@/services/analytics/events/swaps' import useIsCounterfactualSafe from '@/features/counterfactual/hooks/useIsCounterfactualSafe' @@ -24,13 +24,6 @@ const getSubdirectory = (pathname: string): string => { return pathname.split('/')[1] } -const isRouteEnabled = (route: string, chain?: ChainInfo) => { - if (!chain) return false - - const featureRoute = FeatureRoutes[route] - return !featureRoute || hasFeature(chain, featureRoute) -} - const Navigation = (): ReactElement => { const chain = useCurrentChain() const router = useRouter() @@ -56,13 +49,6 @@ const Navigation = (): ReactElement => { } } - const getCounter = (item: NavItem) => { - // Indicate qeueued txs - if (item.href === AppRoutes.transactions.history) { - return queueSize - } - } - // Route Transactions to Queue if there are queued txs, otherwise to History const getRoute = (href: string) => { if (href === AppRoutes.transactions.history && queueSize) { @@ -82,6 +68,12 @@ const Navigation = (): ReactElement => { {enabledNavItems.map((item) => { const isSelected = currentSubdirectory === getSubdirectory(item.href) + let ItemTag = item.tag ? item.tag : null + + if (item.href === AppRoutes.transactions.history) { + ItemTag = queueSize ? : null + } + return ( { {item.label} - + {ItemTag} diff --git a/src/components/theme/darkPalette.ts b/src/components/theme/darkPalette.ts index 8ecf64ed9b..46818fd506 100644 --- a/src/components/theme/darkPalette.ts +++ b/src/components/theme/darkPalette.ts @@ -12,7 +12,7 @@ const darkPalette = { secondary: { dark: '#636669', main: '#FFFFFF', - light: '#12FF80', + light: '#B0FFC9', background: '#1B2A22', }, border: { diff --git a/src/components/theme/safeTheme.ts b/src/components/theme/safeTheme.ts index 766b4c96c7..e9108c3258 100644 --- a/src/components/theme/safeTheme.ts +++ b/src/components/theme/safeTheme.ts @@ -17,6 +17,7 @@ declare module '@mui/material/styles' { backdrop: Palette['primary'] static: Palette['primary'] } + export interface PaletteOptions { border: PaletteOptions['primary'] logo: PaletteOptions['primary'] @@ -33,6 +34,7 @@ declare module '@mui/material/styles' { export interface PaletteColor { background?: string } + export interface SimplePaletteColorOptions { background?: string } @@ -52,6 +54,7 @@ declare module '@mui/material/Button' { export interface ButtonPropsColorOverrides { background: true } + export interface ButtonPropsVariantOverrides { danger: true } @@ -279,6 +282,14 @@ const createSafeTheme = (mode: PaletteMode): Theme => { }, }, }, + MuiChip: { + styleOverrides: { + colorSuccess: ({ theme }) => ({ + backgroundColor: theme.palette.secondary.light, + height: '24px', + }), + }, + }, MuiAlert: { styleOverrides: { standardError: ({ theme }) => ({ diff --git a/src/components/transactions/TxFilterForm/TxFilterForm.test.tsx b/src/components/transactions/TxFilterForm/TxFilterForm.test.tsx new file mode 100644 index 0000000000..fc9a83e9ae --- /dev/null +++ b/src/components/transactions/TxFilterForm/TxFilterForm.test.tsx @@ -0,0 +1,227 @@ +import React from 'react' +import { screen, fireEvent } from '@testing-library/react' +import { act, render } from '@/tests/test-utils' +import '@testing-library/jest-dom/extend-expect' +import TxFilterForm from './index' +import { useRouter } from 'next/router' + +jest.mock('next/router', () => ({ + useRouter: jest.fn(), +})) + +const mockRouter = { + query: {}, + pathname: '', + push: jest.fn(), +} + +const toggleFilter = jest.fn() + +const fromDate = '20/01/2021' +const toDate = '20/01/2020' +const placeholder = 'dd/mm/yyyy' +const errorMsgFormat = 'Invalid address format' + +describe('TxFilterForm Component Tests', () => { + beforeEach(() => { + ;(useRouter as jest.Mock).mockReturnValue(mockRouter) + }) + + const renderComponent = () => render() + + it('Verify that when an end date is behind a start date, there are validation rules applied', async () => { + renderComponent() + + const errorMsgEndDate = 'Must be after "From" date' + const errorMsgStartDate = 'Must be before "To" date' + + const fromDateInput = screen.getAllByPlaceholderText(placeholder)[0] + const toDateInput = screen.getAllByPlaceholderText(placeholder)[1] + + await act(async () => { + fireEvent.change(fromDateInput, { target: { value: fromDate } }) + fireEvent.change(toDateInput, { target: { value: toDate } }) + }) + + expect(fromDateInput).toHaveValue(fromDate) + expect(toDateInput).toHaveValue(toDate) + + expect(await screen.findByText(errorMsgEndDate, { selector: 'label' })).toBeInTheDocument() + + await act(async () => { + fireEvent.change(fromDateInput, { target: { value: '' } }) + fireEvent.change(toDateInput, { target: { value: '' } }) + fireEvent.change(toDateInput, { target: { value: toDate } }) + fireEvent.change(fromDateInput, { target: { value: fromDate } }) + }) + + expect(toDateInput).toHaveValue(toDate) + expect(fromDateInput).toHaveValue(fromDate) + + expect(await screen.findByText(errorMsgStartDate, { selector: 'label' })).toBeInTheDocument() + }) + + it('Verify there is error when start and end date contain far future dates', async () => { + renderComponent() + + const futureDate = '20/01/2036' + const errorMsgFutureDate = 'Date cannot be in the future' + + const fromDateInput = screen.getAllByPlaceholderText(placeholder)[0] + const toDateInput = screen.getAllByPlaceholderText(placeholder)[1] + + await act(async () => { + fireEvent.change(fromDateInput, { target: { value: fromDate } }) + fireEvent.change(toDateInput, { target: { value: futureDate } }) + }) + + expect(await screen.findByText(errorMsgFutureDate, { selector: 'label' })).toBeInTheDocument() + + await act(async () => { + fireEvent.change(fromDateInput, { target: { value: futureDate } }) + fireEvent.change(toDateInput, { target: { value: toDate } }) + }) + + expect(await screen.findByText(errorMsgFutureDate, { selector: 'label' })).toBeInTheDocument() + }) + + it('Verify that when entering invalid characters in token filed shows an error message', async () => { + renderComponent() + + const token = '694urt5' + + const tokenInput = screen.getByTestId('token-input').querySelector('input') as HTMLInputElement + + expect(tokenInput).toBeInTheDocument() + + await act(async () => { + fireEvent.change(tokenInput, { target: { value: token } }) + }) + + expect(await screen.findByText(errorMsgFormat, { selector: 'label' })).toBeInTheDocument() + }) + + it('Verify there is error when 0 is entered in amount field', async () => { + renderComponent() + + const errorMsgZero = 'The value must be greater than 0' + const amountInput = screen.getByTestId('amount-input').querySelector('input') as HTMLInputElement + + expect(amountInput).toBeInTheDocument() + + await act(async () => { + fireEvent.change(amountInput, { target: { value: '0' } }) + }) + + expect(await screen.findByText(errorMsgZero, { selector: 'label' })).toBeInTheDocument() + }) + + it('Verify that entering negative numbers and a non-numeric value in the amount filter is not allowed', async () => { + renderComponent() + + const amountInput = screen.getByTestId('amount-input').querySelector('input') as HTMLInputElement + + expect(amountInput).toBeInTheDocument() + + await act(async () => { + fireEvent.change(amountInput, { target: { value: '-1' } }) + }) + expect(amountInput).toHaveValue('1') + await act(async () => { + fireEvent.change(amountInput, { target: { value: 'hrtyu' } }) + }) + expect(amountInput).toHaveValue('') + }) + + it('Verify that characters and negative numbers cannot be entered in nonce filed', async () => { + renderComponent() + + const outgoingRadio = screen.getByLabelText('Outgoing') + fireEvent.click(outgoingRadio) + + const nonceInput = screen.getByTestId('nonce-input').querySelector('input') as HTMLInputElement + + expect(nonceInput).toBeInTheDocument() + + await act(async () => { + fireEvent.change(nonceInput, { target: { value: '-1' } }) + }) + expect(nonceInput).toHaveValue('1') + await act(async () => { + fireEvent.change(nonceInput, { target: { value: 'hrtyu' } }) + }) + expect(nonceInput).toHaveValue('') + }) + + it('Verify that entering random characters in module field shows error', async () => { + renderComponent() + + const outgoingRadio = screen.getByLabelText('Module-based') + fireEvent.click(outgoingRadio) + + const addressInput = screen.getByTestId('address-item').querySelector('input') as HTMLInputElement + + expect(addressInput).toBeInTheDocument() + + await act(async () => { + fireEvent.change(addressInput, { target: { value: 'hrtyu' } }) + }) + expect(await screen.findByText(errorMsgFormat, { selector: 'label' })).toBeInTheDocument() + }) + + it('Verify when filter is cleared, the filter modal is still displayed', async () => { + renderComponent() + + const fromDate1 = '20/01/2021' + const toDate1 = '20/01/2022' + + const clearButton = screen.getByTestId('clear-btn') + const modal = screen.getByTestId('filter-modal') + + const fromDateInput = screen.getAllByPlaceholderText(placeholder)[0] + const toDateInput = screen.getAllByPlaceholderText(placeholder)[1] + + await act(async () => { + fireEvent.change(fromDateInput, { target: { value: fromDate1 } }) + fireEvent.change(toDateInput, { target: { value: toDate1 } }) + }) + + expect(fromDateInput).toHaveValue(fromDate1) + expect(toDateInput).toHaveValue(toDate1) + + await act(async () => { + fireEvent.click(clearButton) + }) + + expect(fromDateInput).toHaveValue('') + expect(toDateInput).toHaveValue('') + expect(modal).toBeInTheDocument() + }) + + it('Verify when filter is applied, it disappears from the view', async () => { + renderComponent() + + const fromDate = '20/01/2020' + const toDate = '20/01/2021' + + const applyButton = screen.getByTestId('apply-btn') + + const fromDateInput = screen.getAllByPlaceholderText(placeholder)[0] + const toDateInput = screen.getAllByPlaceholderText(placeholder)[1] + + await act(async () => { + fireEvent.change(fromDateInput, { target: { value: fromDate } }) + fireEvent.change(toDateInput, { target: { value: toDate } }) + }) + + expect(fromDateInput).toHaveValue(fromDate) + expect(toDateInput).toHaveValue(toDate) + + await act(async () => { + fireEvent.click(applyButton) + }) + + // Check that toggleFilter hide filter trigger has been called + expect(toggleFilter).toHaveBeenCalled() + }) +}) diff --git a/src/components/transactions/TxFilterForm/index.tsx b/src/components/transactions/TxFilterForm/index.tsx index dc07cb4c9e..2d31eb0232 100644 --- a/src/components/transactions/TxFilterForm/index.tsx +++ b/src/components/transactions/TxFilterForm/index.tsx @@ -120,7 +120,7 @@ const TxFilterForm = ({ toggleFilter }: { toggleFilter: () => void }): ReactElem
- + palette.primary.light }}>Transaction type @@ -185,6 +185,7 @@ const TxFilterForm = ({ toggleFilter }: { toggleFilter: () => void }): ReactElem }} render={({ field, fieldState }) => ( void }): ReactElem {isIncomingFilter && ( void }): ReactElem }} render={({ field, fieldState }) => ( void }): ReactElem - - diff --git a/src/components/transactions/TxList/index.tsx b/src/components/transactions/TxList/index.tsx index 1b92a9860b..ac816b689b 100644 --- a/src/components/transactions/TxList/index.tsx +++ b/src/components/transactions/TxList/index.tsx @@ -1,10 +1,10 @@ -import type { ReactElement, ReactNode } from 'react' -import { useMemo } from 'react' +import GroupedTxListItems from '@/components/transactions/GroupedTxListItems' +import { groupConflictingTxs } from '@/utils/tx-list' import { Box } from '@mui/material' import type { TransactionListPage } from '@safe-global/safe-gateway-typescript-sdk' +import type { ReactElement, ReactNode } from 'react' +import { useMemo } from 'react' import TxListItem from '../TxListItem' -import GroupedTxListItems from '@/components/transactions/GroupedTxListItems' -import { groupConflictingTxs } from '@/utils/tx-list' import css from './styles.module.css' type TxListProps = { diff --git a/src/components/tx-flow/common/TxButton.tsx b/src/components/tx-flow/common/TxButton.tsx index 214f796660..265b1a0e7d 100644 --- a/src/components/tx-flow/common/TxButton.tsx +++ b/src/components/tx-flow/common/TxButton.tsx @@ -8,6 +8,8 @@ import Track from '@/components/common/Track' import { MODALS_EVENTS } from '@/services/analytics' import { useContext } from 'react' import { TxModalContext } from '..' +import { useHasFeature } from '@/hooks/useChains' +import { FEATURES } from '@/utils/chains' const buttonSx = { height: '58px', @@ -27,6 +29,9 @@ export const SendTokensButton = ({ onClick, sx }: { onClick: () => void; sx?: Bu export const SendNFTsButton = () => { const router = useRouter() const { setTxFlow } = useContext(TxModalContext) + const isEnabled = useHasFeature(FEATURES.ERC721) + + if (!isEnabled) return null const isNftPage = router.pathname === AppRoutes.balances.nfts const onClick = isNftPage ? () => setTxFlow(undefined) : undefined diff --git a/src/components/tx-flow/common/TxLayout/index.tsx b/src/components/tx-flow/common/TxLayout/index.tsx index 872f5e6a9c..de243b6d85 100644 --- a/src/components/tx-flow/common/TxLayout/index.tsx +++ b/src/components/tx-flow/common/TxLayout/index.tsx @@ -1,6 +1,7 @@ import useSafeInfo from '@/hooks/useSafeInfo' import { type ComponentType, type ReactElement, type ReactNode, useContext, useEffect, useState } from 'react' import { Box, Container, Grid, Typography, Button, Paper, SvgIcon, IconButton, useMediaQuery } from '@mui/material' +import ArrowBackIcon from '@mui/icons-material/ArrowBack' import { useTheme } from '@mui/material/styles' import type { TransactionSummary } from '@safe-global/safe-gateway-typescript-sdk' import classnames from 'classnames' @@ -146,9 +147,10 @@ const TxLayout = ({ {onBack && step > 0 && ( diff --git a/src/components/tx-flow/flows/ExecuteBatch/ReviewBatch.tsx b/src/components/tx-flow/flows/ExecuteBatch/ReviewBatch.tsx index fb47366b97..e4fd4c17b6 100644 --- a/src/components/tx-flow/flows/ExecuteBatch/ReviewBatch.tsx +++ b/src/components/tx-flow/flows/ExecuteBatch/ReviewBatch.tsx @@ -1,6 +1,6 @@ import { CircularProgress, Typography, Button, CardActions, Divider, Alert } from '@mui/material' import useAsync from '@/hooks/useAsync' -import { FEATURES } from '@safe-global/safe-gateway-typescript-sdk' +import { FEATURES } from '@/utils/chains' import type { TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk' import { getReadOnlyMultiSendCallOnlyContract } from '@/services/contracts/safeContracts' import { useCurrentChain } from '@/hooks/useChains' diff --git a/src/components/tx-flow/flows/SignMessage/SignMessage.tsx b/src/components/tx-flow/flows/SignMessage/SignMessage.tsx index e0aebcf1cb..7531d2a3dc 100644 --- a/src/components/tx-flow/flows/SignMessage/SignMessage.tsx +++ b/src/components/tx-flow/flows/SignMessage/SignMessage.tsx @@ -166,7 +166,7 @@ const BlindSigningWarning = ({ return ( This request involves{' '} - + blind signing , which can lead to unpredictable outcomes. @@ -176,7 +176,7 @@ const BlindSigningWarning = ({ ) : ( <> If you wish to proceed, you must first{' '} - + enable blind signing . diff --git a/src/components/welcome/MyAccounts/index.tsx b/src/components/welcome/MyAccounts/index.tsx index 717de2940a..6bf3e19f3e 100644 --- a/src/components/welcome/MyAccounts/index.tsx +++ b/src/components/welcome/MyAccounts/index.tsx @@ -28,7 +28,7 @@ const AccountsList = ({ safes, onLinkClick }: AccountsListProps) => { const ownedSafes = useMemo(() => safes?.filter(({ isWatchlist }) => !isWatchlist), [safes]) const watchlistSafes = useMemo(() => safes?.filter(({ isWatchlist }) => isWatchlist), [safes]) - useTrackSafesCount(ownedSafes, watchlistSafes) + useTrackSafesCount(ownedSafes, watchlistSafes, wallet) const isLoginPage = router.pathname === AppRoutes.welcome.accounts const trackingLabel = isLoginPage ? OVERVIEW_LABELS.login_page : OVERVIEW_LABELS.sidebar diff --git a/src/components/welcome/MyAccounts/styles.module.css b/src/components/welcome/MyAccounts/styles.module.css index 5aa0edf2b1..60854d02b8 100644 --- a/src/components/welcome/MyAccounts/styles.module.css +++ b/src/components/welcome/MyAccounts/styles.module.css @@ -64,10 +64,18 @@ 'a c d'; } - .safeLink :nth-child(1) { grid-area: a; } - .safeLink :nth-child(2) { grid-area: b; } - .safeLink :nth-child(3) { grid-area: c; } - .safeLink :nth-child(4) { grid-area: d; } + .safeLink :nth-child(1) { + grid-area: a; + } + .safeLink :nth-child(2) { + grid-area: b; + } + .safeLink :nth-child(3) { + grid-area: c; + } + .safeLink :nth-child(4) { + grid-area: d; + } } .safeAddress { diff --git a/src/components/welcome/MyAccounts/useAllOwnedSafes.ts b/src/components/welcome/MyAccounts/useAllOwnedSafes.ts index a14c362eff..b96e7007cb 100644 --- a/src/components/welcome/MyAccounts/useAllOwnedSafes.ts +++ b/src/components/welcome/MyAccounts/useAllOwnedSafes.ts @@ -7,17 +7,32 @@ import { useEffect } from 'react' const CACHE_KEY = 'ownedSafesCache_' +type OwnedSafesPerAddress = { + address: string | undefined + ownedSafes: AllOwnedSafes +} + const useAllOwnedSafes = (address: string): AsyncResult => { const [cache, setCache] = useLocalStorage(CACHE_KEY + address) - const [data, error, isLoading] = useAsync(async () => { - if (!address) return {} - return getAllOwnedSafes(address) + const [data, error, isLoading] = useAsync(async () => { + if (!address) + return { + ownedSafes: {}, + address: undefined, + } + const ownedSafes = await getAllOwnedSafes(address) + return { + ownedSafes, + address, + } }, [address]) useEffect(() => { - if (data != undefined) setCache(data) - }, [data, setCache]) + if (data?.ownedSafes != undefined && data.address === address) { + setCache(data.ownedSafes) + } + }, [address, cache, data, setCache]) return [cache, error, isLoading] } diff --git a/src/components/welcome/MyAccounts/useAllSafes.ts b/src/components/welcome/MyAccounts/useAllSafes.ts index c5dc62f6a5..84c3a40020 100644 --- a/src/components/welcome/MyAccounts/useAllSafes.ts +++ b/src/components/welcome/MyAccounts/useAllSafes.ts @@ -8,7 +8,6 @@ import useChains from '@/hooks/useChains' import useWallet from '@/hooks/wallets/useWallet' import { selectUndeployedSafes } from '@/store/slices' import { sameAddress } from '@/utils/addresses' - export type SafeItem = { chainId: string address: string @@ -37,12 +36,15 @@ export const useHasSafes = () => { const useAllSafes = (): SafeItems | undefined => { const { address: walletAddress = '' } = useWallet() || {} - const [allOwned] = useAllOwnedSafes(walletAddress) + const [allOwned, , allOwnedLoading] = useAllOwnedSafes(walletAddress) const allAdded = useAddedSafes() const { configs } = useChains() const undeployedSafes = useAppSelector(selectUndeployedSafes) return useMemo(() => { + if (walletAddress && (allOwned === undefined || allOwnedLoading)) { + return undefined + } const chains = uniq(Object.keys(allAdded).concat(Object.keys(allOwned || {}))) return chains.flatMap((chainId) => { @@ -64,7 +66,7 @@ const useAllSafes = (): SafeItems | undefined => { } }) }) - }, [allAdded, allOwned, configs, undeployedSafes, walletAddress]) + }, [allAdded, allOwned, allOwnedLoading, configs, undeployedSafes, walletAddress]) } export default useAllSafes diff --git a/src/components/welcome/MyAccounts/useSafeOverviews.ts b/src/components/welcome/MyAccounts/useSafeOverviews.ts index bfe33c3eb4..3101b54c48 100644 --- a/src/components/welcome/MyAccounts/useSafeOverviews.ts +++ b/src/components/welcome/MyAccounts/useSafeOverviews.ts @@ -1,26 +1,44 @@ +import { useMemo } from 'react' import { useTokenListSetting } from '@/hooks/loadables/useLoadBalances' -import useAsync from '@/hooks/useAsync' +import useAsync, { type AsyncResult } from '@/hooks/useAsync' import useWallet from '@/hooks/wallets/useWallet' import { useAppSelector } from '@/store' import { selectCurrency } from '@/store/settingsSlice' -import { getSafeOverviews } from '@safe-global/safe-gateway-typescript-sdk' +import { type SafeOverview, getSafeOverviews } from '@safe-global/safe-gateway-typescript-sdk' -function useSafeOverviews(safes: Array<{ address: string; chainId: string }>) { +const _cache: Record = {} + +type SafeParams = { + address: string + chainId: string +} + +// EIP155 address format +const makeSafeId = ({ chainId, address }: SafeParams) => `${chainId}:${address}` as `${number}:0x${string}` + +function useSafeOverviews(safes: Array): AsyncResult { const excludeSpam = useTokenListSetting() || false const currency = useAppSelector(selectCurrency) const wallet = useWallet() const walletAddress = wallet?.address + const safesIds = useMemo(() => safes.map(makeSafeId), [safes]) - return useAsync(async () => { - const safesStrings = safes.map((safe) => `${safe.chainId}:${safe.address}` as `${number}:0x${string}`) - - return await getSafeOverviews(safesStrings, { + const [data, error, isLoading] = useAsync(async () => { + return await getSafeOverviews(safesIds, { trusted: true, exclude_spam: excludeSpam, currency, wallet_address: walletAddress, }) - }, [safes, excludeSpam, currency, walletAddress]) + }, [safesIds, excludeSpam, currency, walletAddress]) + + const cacheKey = safesIds.join() + const result = data ?? _cache[cacheKey] + + // Cache until the next page load + _cache[cacheKey] = result + + return useMemo(() => [result, error, isLoading], [result, error, isLoading]) } export default useSafeOverviews diff --git a/src/components/welcome/MyAccounts/useTrackedSafesCount.ts b/src/components/welcome/MyAccounts/useTrackedSafesCount.ts index 69bd25229b..e22d788aca 100644 --- a/src/components/welcome/MyAccounts/useTrackedSafesCount.ts +++ b/src/components/welcome/MyAccounts/useTrackedSafesCount.ts @@ -3,20 +3,37 @@ import { OVERVIEW_EVENTS, trackEvent } from '@/services/analytics' import { useRouter } from 'next/router' import { useEffect } from 'react' import type { SafeItems } from './useAllSafes' +import type { ConnectedWallet } from '@/hooks/wallets/useOnboard' -let isTracked = false +let isOwnedSafesTracked = false +let isWatchlistTracked = false -const useTrackSafesCount = (ownedSafes: SafeItems | undefined, watchlistSafes: SafeItems | undefined) => { +const useTrackSafesCount = ( + ownedSafes: SafeItems | undefined, + watchlistSafes: SafeItems | undefined, + wallet: ConnectedWallet | null, +) => { const router = useRouter() const isLoginPage = router.pathname === AppRoutes.welcome.accounts + // Reset tracking for new wallet useEffect(() => { - if (watchlistSafes && ownedSafes && isLoginPage && !isTracked) { + isOwnedSafesTracked = false + }, [wallet?.address]) + + useEffect(() => { + if (!isOwnedSafesTracked && ownedSafes && ownedSafes.length > 0 && isLoginPage) { trackEvent({ ...OVERVIEW_EVENTS.TOTAL_SAFES_OWNED, label: ownedSafes.length }) + isOwnedSafesTracked = true + } + }, [isLoginPage, ownedSafes]) + + useEffect(() => { + if (watchlistSafes && isLoginPage && watchlistSafes.length > 0 && !isWatchlistTracked) { trackEvent({ ...OVERVIEW_EVENTS.TOTAL_SAFES_WATCHLIST, label: watchlistSafes.length }) - isTracked = true + isWatchlistTracked = true } - }, [isLoginPage, ownedSafes, watchlistSafes]) + }, [isLoginPage, watchlistSafes]) } export default useTrackSafesCount diff --git a/src/components/welcome/NewSafeSocial.tsx b/src/components/welcome/NewSafeSocial.tsx deleted file mode 100644 index b74e4501a7..0000000000 --- a/src/components/welcome/NewSafeSocial.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import React from 'react' -import { Box, Button, Grid, Typography } from '@mui/material' -import css from './styles.module.css' -import Link from 'next/link' - -import ChevronLeftIcon from '@mui/icons-material/ChevronLeft' -import WelcomeLogin from './WelcomeLogin' -import GnosisChainLogo from '@/public/images/common/gnosis-chain-logo.png' -import Image from 'next/image' - -const BulletListItem = ({ text }: { text: string }) => ( -
  • - - {text} - -
  • -) - -const MarqueeItem = () => { - return ( -
  • - Free on - Gnosis Chain logo - Gnosis Chain -
  • - ) -} - -const NewSafeSocial = () => { - return ( - <> - - - - - -
    - - - Get the most secure web3 account in {'<'}30 seconds - - -
      - - - -
    - - - - -
    - -
    -
      - - - -
    - -
    -
    -
    -
    - - ) -} - -export default NewSafeSocial diff --git a/src/components/welcome/WelcomeLogin/WalletLogin.tsx b/src/components/welcome/WelcomeLogin/WalletLogin.tsx index cec4771cea..97a5d78e01 100644 --- a/src/components/welcome/WelcomeLogin/WalletLogin.tsx +++ b/src/components/welcome/WelcomeLogin/WalletLogin.tsx @@ -1,6 +1,5 @@ import useConnectWallet from '@/components/common/ConnectWallet/useConnectWallet' import useWallet from '@/hooks/wallets/useWallet' -import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule' import { Box, Button, Typography } from '@mui/material' import EthHashInfo from '@/components/common/EthHashInfo' import WalletIcon from '@/components/common/WalletIcon' @@ -14,9 +13,7 @@ const WalletLogin = ({ onLogin }: { onLogin: () => void }) => { onLogin() } - const isSocialLogin = isSocialLoginWallet(wallet?.label) - - if (wallet !== null && !isSocialLogin) { + if (wallet !== null) { return (