diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 51882e3db..feb1582c5 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -419,5 +419,22 @@ "ownerNotManager": "You must be connected as the Manager of this name to set the verification record. You can view and update the Manager under the Ownership tab.", "wrongAccount": "You must be connected as {{ nameOrAddress }} to set the verification record.", "default": "We could't verify your account. Please return to Dentity and try again." + }, + "networkNotifications": { + "Ethereum": { + "title": "Switch to Ethereum?", + "description": "You've selected Ethereum (mainnet) in your wallet.", + "action": "Go to app.ens.domains" + }, + "Sepolia": { + "title": "Switch to Sepolia?", + "description": "You've selected Sepolia (testnet) in your wallet.", + "action": "Go to sepolia.app.ens.domains" + }, + "Holesky": { + "title": "Switch to Holesky?", + "description": "You've selected Holesky (testnet) in your wallet.", + "action": "Go to holesky.app.ens.domains" + } } } diff --git a/src/components/NetworkNotifications.test.tsx b/src/components/NetworkNotifications.test.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/src/components/NetworkNotifications.tsx b/src/components/NetworkNotifications.tsx new file mode 100644 index 000000000..3a728a105 --- /dev/null +++ b/src/components/NetworkNotifications.tsx @@ -0,0 +1,54 @@ +import { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled, { css } from 'styled-components' +import { useAccount } from 'wagmi' + +import { Button, Toast } from '@ensdomains/thorin' + +import { getChainsFromUrl, getSupportedChainById } from '@app/constants/chains' + +const appLinks = { + ethereum: 'app.ens.domains', + sepolia: 'sepolia.app.ens.domains', + holesky: 'holesky.app.ens.domains', +} + +export const NetworkNotifications = () => { + const { t } = useTranslation() + const account = useAccount() + const [open, setOpen] = useState(false) + + const connectedChainName = account?.chain?.name + const connectedChainId = account?.chain?.id + + console.log('connectedChainName: ', connectedChainName) + + useEffect(() => { + if (!connectedChainName) return + if (!getSupportedChainById(connectedChainId)) return + + const currentChain = getChainsFromUrl()?.[0] + if (currentChain?.id !== connectedChainId) { + setOpen(true) + } + }, [connectedChainName, connectedChainId]) + + return ( + setOpen(false)} + > + + + ) +} diff --git a/src/components/TestnetWarning.tsx b/src/components/TestnetWarning.tsx index a94727886..647096330 100644 --- a/src/components/TestnetWarning.tsx +++ b/src/components/TestnetWarning.tsx @@ -1,8 +1,7 @@ -// import { useSearchParams } from 'next/dist/client/components/navigation' import { useEffect, useState } from 'react' import styled, { css } from 'styled-components' -import { getChainFromSubdomain, getChainFromUrl } from '@app/utils/utils' +import { getChainsFromUrl } from '@app/constants/chains' const Container = styled.div( ({ theme }) => css` @@ -15,41 +14,15 @@ const Container = styled.div( `, ) -/* -const getChainFromQueryString = (searchParams: URLSearchParams) => { - // Query param only possible in test/dev - if ( - !( - window.location.hostname.endsWith('localhost') || - window.location.hostname.endsWith('ens.pages.dev') - ) - ) - return - - // If requested chain id is supported return it, otherwise default to mainnet - searchParams.get('chainId') -} - -const getChainFromSubdomain = (subdomain: string) => { - // if (subdomain === 'testnet') return chains[0] - // if (subdomain === 'beta') return chains[1] - // return chains[2] -} - -const useGetConifguredChain = () => { - const searchParams = useSearchParams() -} - */ - export const TestnetWarning = () => { - const chain = getChainFromUrl() + const chains = getChainsFromUrl() const [isClient, setIsClient] = useState(false) useEffect(() => { setIsClient(true) }, []) - if (isClient && chain && chain.id !== 1) - return You are viewing the ENS app on {chain.name} testnet. + if (isClient && chains && chains[0].id !== 1) + return You are viewing the ENS app on {chains[0].name} testnet. return null } diff --git a/src/components/Notifications.test.tsx b/src/components/TransactionNotifications.test.tsx similarity index 86% rename from src/components/Notifications.test.tsx rename to src/components/TransactionNotifications.test.tsx index 8c329f18a..af6fcd843 100644 --- a/src/components/Notifications.test.tsx +++ b/src/components/TransactionNotifications.test.tsx @@ -8,7 +8,7 @@ import type { Transaction } from '@app/hooks/transactions/transactionStore' import { useBreakpoint } from '@app/utils/BreakpointProvider' import { UpdateCallback, useCallbackOnTransaction } from '@app/utils/SyncProvider/SyncProvider' -import { Notifications } from './Notifications' +import { TransactionNotifications } from './TransactionNotifications' vi.mock('@app/hooks/chain/useChainName') vi.mock('@app/utils/SyncProvider/SyncProvider') @@ -45,15 +45,15 @@ describe('Notifications', () => { }) mockUseChainName.mockReturnValue('mainnet') it('should not render a toast if there is no transactions', () => { - render() + render() expect(screen.queryByTestId('toast-desktop')).not.toBeInTheDocument() }) it('should render a toast when a pending transaction is confirmed', async () => { - const { rerender } = render() + const { rerender } = render() expect(screen.queryByTestId('toast-desktop')).not.toBeInTheDocument() cb(makeRecentTransaction('confirmed')(null, 0)) - rerender() + rerender() await waitFor(() => screen.queryByTestId('toast-desktop'), { timeout: 500, @@ -62,13 +62,13 @@ describe('Notifications', () => { it('should show a new notification on dismiss if there is one queued', async () => { const mockData = Array.from({ length: 2 }, makeRecentTransaction('pending')) - const { rerender } = render() + const { rerender } = render() expect(screen.queryByTestId('toast-desktop')).not.toBeInTheDocument() cb({ ...mockData[0], status: 'confirmed1' as any }) cb({ ...mockData[1], status: 'confirmed2' as any }) - rerender() + rerender() await waitFor(() => screen.queryByText('transaction.status.confirmed1.notifyTitle'), { timeout: 500, @@ -83,12 +83,12 @@ describe('Notifications', () => { }).then((el) => expect(el).toBeInTheDocument()) }) it('should show the correct title and description for a notification', async () => { - const { rerender } = render() + const { rerender } = render() const mockData = makeRecentTransaction('confirmed')(null, 0) cb(mockData) - rerender() + rerender() await waitFor(() => screen.queryByTestId('toast-desktop'), { timeout: 500, @@ -98,12 +98,12 @@ describe('Notifications', () => { expect(screen.getByText('transaction.status.confirmed.notifyMessage')).toBeInTheDocument() }) it('should not show a notification for a repriced transaction', async () => { - const { rerender } = render() + const { rerender } = render() const mockData = makeRecentTransaction('repriced')(null, 0) cb(mockData) - rerender() + rerender() await waitFor(() => screen.queryByTestId('toast-desktop'), { timeout: 500, diff --git a/src/components/Notifications.tsx b/src/components/TransactionNotifications.tsx similarity index 98% rename from src/components/Notifications.tsx rename to src/components/TransactionNotifications.tsx index 811050a68..44eb73292 100644 --- a/src/components/Notifications.tsx +++ b/src/components/TransactionNotifications.tsx @@ -28,7 +28,7 @@ const ButtonContainer = styled.div( `, ) -export const Notifications = () => { +export const TransactionNotifications = () => { const { t } = useTranslation() const breakpoints = useBreakpoint() diff --git a/src/constants/chains.ts b/src/constants/chains.ts index 4bb193d91..9316bf212 100644 --- a/src/constants/chains.ts +++ b/src/constants/chains.ts @@ -1,3 +1,4 @@ +import { match } from 'ts-pattern' import { holesky } from 'viem/chains' import { localhost, mainnet, sepolia } from 'wagmi/chains' @@ -6,6 +7,8 @@ import { addEnsContracts } from '@ensdomains/ensjs' import type { Register } from '@app/local-contracts' import { makeLocalhostChainWithEns } from '@app/utils/chains/makeLocalhostChainWithEns' +const isLocalProvider = !!process.env.NEXT_PUBLIC_PROVIDER + export const deploymentAddresses = JSON.parse( process.env.NEXT_PUBLIC_DEPLOYMENT_ADDRESSES || '{}', ) as Register['deploymentAddresses'] @@ -43,3 +46,46 @@ export type SupportedChain = | typeof sepoliaWithEns | typeof holeskyWithEns | typeof localhostWithEns + +export const getChainsFromUrl = () => { + if (typeof window === 'undefined') + return [ + ...(isLocalProvider ? ([localhostWithEns] as const) : ([] as const)), + sepoliaWithEns, + mainnetWithEns, + holeskyWithEns, + ] + + const { hostname, search } = window.location + const params = new URLSearchParams(search) + const chainParam = params.get('chain') + const segments = hostname.split('.') + + if (segments.length === 4 && segments.slice(1).join('.') === 'ens-app-v3.pages.dev') { + if (chainParam === 'sepolia') return sepoliaWithEns + if (chainParam === 'holesky') return holeskyWithEns + } + + if (!hostname.includes('app.ens.domains')) return mainnetWithEns + if (segments.length !== 4) return mainnetWithEns + + return match(segments[0]) + .with('sepolia', () => [ + ...(isLocalProvider ? ([localhostWithEns] as const) : ([] as const)), + sepoliaWithEns, + mainnetWithEns, + holeskyWithEns, + ]) + .with('holesky', () => [ + ...(isLocalProvider ? ([localhostWithEns] as const) : ([] as const)), + holeskyWithEns, + sepoliaWithEns, + mainnetWithEns, + ]) + .otherwise(() => [ + ...(isLocalProvider ? ([localhostWithEns] as const) : ([] as const)), + mainnetWithEns, + holeskyWithEns, + sepoliaWithEns, + ]) +} diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 9f91affa6..dc0d9a0e6 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -2,15 +2,21 @@ import '@splidejs/react-splide/css' import { NextPage } from 'next' import type { AppProps } from 'next/app' -import { ReactElement, ReactNode } from 'react' +import { ReactElement, ReactNode, useState } from 'react' import { I18nextProvider } from 'react-i18next' import { IntercomProvider } from 'react-use-intercom' import { createGlobalStyle, keyframes, ThemeProvider } from 'styled-components' -import { ThorinGlobalStyles, lightTheme as thorinLightTheme } from '@ensdomains/thorin' +import { + Button, + ThorinGlobalStyles, + lightTheme as thorinLightTheme, + Toast, +} from '@ensdomains/thorin' -import { Notifications } from '@app/components/Notifications' +import { NetworkNotifications } from '@app/components/NetworkNotifications' import { TestnetWarning } from '@app/components/TestnetWarning' +import { TransactionNotifications } from '@app/components/TransactionNotifications' import { TransactionStoreProvider } from '@app/hooks/transactions/TransactionStoreContext' import { Basic } from '@app/layouts/Basic' import { TransactionFlowProvider } from '@app/transaction-flow/TransactionFlowProvider' @@ -145,7 +151,8 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) { - + + {getLayout()} diff --git a/src/utils/query/wagmi.ts b/src/utils/query/wagmi.ts index 7857374c8..461f26266 100644 --- a/src/utils/query/wagmi.ts +++ b/src/utils/query/wagmi.ts @@ -4,9 +4,8 @@ import { holesky, localhost, mainnet, sepolia } from 'wagmi/chains' import { ccipRequest } from '@ensdomains/ensjs/utils' -import { localhostWithEns } from '@app/constants/chains' +import { getChainsFromUrl } from '@app/constants/chains' -import { getChainFromUrl } from '../utils' import { rainbowKitConnectors } from './wallets' const isLocalProvider = !!process.env.NEXT_PUBLIC_PROVIDER @@ -73,12 +72,6 @@ const localStorageWithInvertMiddleware = (): Storage | undefined => { } } -const getChain = () => { - if (isLocalProvider) return localhostWithEns - const chain = getChainFromUrl() - return chain -} - const transports = { ...(isLocalProvider ? ({ @@ -99,7 +92,7 @@ const wagmiConfig_ = createConfig({ ssr: true, multiInjectedProviderDiscovery: true, storage: createStorage({ storage: localStorageWithInvertMiddleware(), key: prefix }), - chains: [getChain()], + chains: [...getChainsFromUrl()], client: ({ chain }) => { const chainId = chain.id diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 69bfbfdea..13c41f7d6 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,12 +1,10 @@ import type { TFunction } from 'react-i18next' -import { match } from 'ts-pattern' import { toBytes, type Address } from 'viem' import { Eth2ldName } from '@ensdomains/ensjs/dist/types/types' import { GetPriceReturnType } from '@ensdomains/ensjs/public' import { DecodedFuses } from '@ensdomains/ensjs/utils' -import { holeskyWithEns, mainnetWithEns, sepoliaWithEns } from '@app/constants/chains' import { KNOWN_RESOLVER_DATA } from '@app/constants/resolverAddressData' import { CURRENCY_FLUCTUATION_BUFFER_PERCENTAGE } from './constants' @@ -222,25 +220,3 @@ export const hslToHex = (hsl: string) => { } return `#${f(0)}${f(8)}${f(4)}` } - -export const getChainFromUrl = () => { - if (typeof window === 'undefined') return mainnetWithEns - - const { hostname, search } = window.location - const params = new URLSearchParams(search) - const chainParam = params.get('chain') - const segments = hostname.split('.') - - if (segments.length === 4 && segments.slice(1).join('.') === 'ens-app-v3.pages.dev') { - if (chainParam === 'sepolia') return sepoliaWithEns - if (chainParam === 'holesky') return holeskyWithEns - } - - if (!hostname.includes('app.ens.domains')) return mainnetWithEns - if (segments.length !== 4) return mainnetWithEns - - return match(segments[0]) - .with('sepolia', () => sepoliaWithEns) - .with('holesky', () => holeskyWithEns) - .otherwise(() => mainnetWithEns) -}